Compare commits

...

1 Commits

Author SHA1 Message Date
midzelis
6fe63b70a6 feat(web): animate zoom toggle with cubicOut easing 2026-03-07 14:11:31 +00:00
3 changed files with 39 additions and 6 deletions

View File

@@ -9,14 +9,15 @@ export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolea
zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)),
];
const stopIfDisabled = (event: Event) => {
const onInteractionStart = (event: Event) => {
if (options?.disabled) {
event.stopImmediatePropagation();
}
assetViewerManager.cancelZoomAnimation();
};
node.addEventListener('wheel', stopIfDisabled, { capture: true });
node.addEventListener('pointerdown', stopIfDisabled, { capture: true });
node.addEventListener('wheel', onInteractionStart, { capture: true });
node.addEventListener('pointerdown', onInteractionStart, { capture: true });
node.style.overflow = 'visible';
return {
@@ -27,8 +28,8 @@ export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolea
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
node.removeEventListener('wheel', stopIfDisabled, { capture: true });
node.removeEventListener('pointerdown', stopIfDisabled, { capture: true });
node.removeEventListener('wheel', onInteractionStart, { capture: true });
node.removeEventListener('pointerdown', onInteractionStart, { capture: true });
zoomInstance.cleanup();
},
};

View File

@@ -103,7 +103,8 @@
};
const onZoom = () => {
assetViewerManager.zoom = assetViewerManager.zoom > 1 ? 1 : 2;
const targetZoom = assetViewerManager.zoom > 1 ? 1 : 2;
assetViewerManager.animatedZoom(targetZoom);
};
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);

View File

@@ -2,6 +2,7 @@ import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
import { PersistedLocalStorage } from '$lib/utils/persisted';
import type { ZoomImageWheelState } from '@zoom-image/core';
import { cubicOut } from 'svelte/easing';
const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false);
@@ -21,6 +22,7 @@ export type Events = {
export class AssetViewerManager extends BaseEventManager<Events> {
#zoomState = $state(createDefaultZoomState());
#animationFrameId: number | null = null;
imgRef = $state<HTMLImageElement | undefined>();
isShowActivityPanel = $state(false);
@@ -45,6 +47,7 @@ export class AssetViewerManager extends BaseEventManager<Events> {
}
set zoom(zoom: number) {
this.cancelZoomAnimation();
this.zoomState = { ...this.zoomState, currentZoom: zoom };
}
@@ -69,7 +72,35 @@ export class AssetViewerManager extends BaseEventManager<Events> {
this.#zoomState = state;
}
cancelZoomAnimation() {
if (this.#animationFrameId !== null) {
cancelAnimationFrame(this.#animationFrameId);
this.#animationFrameId = null;
}
}
animatedZoom(targetZoom: number, duration = 300) {
this.cancelZoomAnimation();
const startZoom = this.#zoomState.currentZoom;
const startTime = performance.now();
const frame = (currentTime: number) => {
const elapsed = currentTime - startTime;
const linearProgress = Math.min(elapsed / duration, 1);
const easedProgress = cubicOut(linearProgress);
const interpolatedZoom = startZoom + (targetZoom - startZoom) * easedProgress;
this.zoomState = { ...this.#zoomState, currentZoom: interpolatedZoom };
this.#animationFrameId = linearProgress < 1 ? requestAnimationFrame(frame) : null;
};
this.#animationFrameId = requestAnimationFrame(frame);
}
resetZoomState() {
this.cancelZoomAnimation();
this.zoomState = createDefaultZoomState();
}