feat(web): animate zoom toggle with cubicOut easing

This commit is contained in:
midzelis
2026-03-06 03:12:25 +00:00
parent 6e9a425592
commit 6fe63b70a6
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)), zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)),
]; ];
const stopIfDisabled = (event: Event) => { const onInteractionStart = (event: Event) => {
if (options?.disabled) { if (options?.disabled) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} }
assetViewerManager.cancelZoomAnimation();
}; };
node.addEventListener('wheel', stopIfDisabled, { capture: true }); node.addEventListener('wheel', onInteractionStart, { capture: true });
node.addEventListener('pointerdown', stopIfDisabled, { capture: true }); node.addEventListener('pointerdown', onInteractionStart, { capture: true });
node.style.overflow = 'visible'; node.style.overflow = 'visible';
return { return {
@@ -27,8 +28,8 @@ export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolea
for (const unsubscribe of unsubscribes) { for (const unsubscribe of unsubscribes) {
unsubscribe(); unsubscribe();
} }
node.removeEventListener('wheel', stopIfDisabled, { capture: true }); node.removeEventListener('wheel', onInteractionStart, { capture: true });
node.removeEventListener('pointerdown', stopIfDisabled, { capture: true }); node.removeEventListener('pointerdown', onInteractionStart, { capture: true });
zoomInstance.cleanup(); zoomInstance.cleanup();
}, },
}; };

View File

@@ -103,7 +103,8 @@
}; };
const onZoom = () => { const onZoom = () => {
assetViewerManager.zoom = assetViewerManager.zoom > 1 ? 1 : 2; const targetZoom = assetViewerManager.zoom > 1 ? 1 : 2;
assetViewerManager.animatedZoom(targetZoom);
}; };
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow); 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 { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
import { PersistedLocalStorage } from '$lib/utils/persisted'; import { PersistedLocalStorage } from '$lib/utils/persisted';
import type { ZoomImageWheelState } from '@zoom-image/core'; import type { ZoomImageWheelState } from '@zoom-image/core';
import { cubicOut } from 'svelte/easing';
const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false); const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false);
@@ -21,6 +22,7 @@ export type Events = {
export class AssetViewerManager extends BaseEventManager<Events> { export class AssetViewerManager extends BaseEventManager<Events> {
#zoomState = $state(createDefaultZoomState()); #zoomState = $state(createDefaultZoomState());
#animationFrameId: number | null = null;
imgRef = $state<HTMLImageElement | undefined>(); imgRef = $state<HTMLImageElement | undefined>();
isShowActivityPanel = $state(false); isShowActivityPanel = $state(false);
@@ -45,6 +47,7 @@ export class AssetViewerManager extends BaseEventManager<Events> {
} }
set zoom(zoom: number) { set zoom(zoom: number) {
this.cancelZoomAnimation();
this.zoomState = { ...this.zoomState, currentZoom: zoom }; this.zoomState = { ...this.zoomState, currentZoom: zoom };
} }
@@ -69,7 +72,35 @@ export class AssetViewerManager extends BaseEventManager<Events> {
this.#zoomState = state; 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() { resetZoomState() {
this.cancelZoomAnimation();
this.zoomState = createDefaultZoomState(); this.zoomState = createDefaultZoomState();
} }