fix(calendar-lab): stabilize center fit and wheel direction

This commit is contained in:
Ruslan Bakiev
2026-02-23 18:26:07 +07:00
parent 179cc39d53
commit 0ed2a6b353

View File

@@ -45,8 +45,9 @@ const primeFocusId = ref("");
const wheelPrimeDirection = ref<"" | Direction>("");
const wheelPrimeTicks = ref(0);
let animationTimer: ReturnType<typeof setTimeout> | null = null;
let primeTimer: ReturnType<typeof setTimeout> | null = null;
let animationFrameId: number | null = null;
let animationToken = 0;
const currentLevelIndex = computed(() => LEVELS.indexOf(currentLevel.value));
const displayLevel = computed(() => transitionTarget.value ?? currentLevel.value);
@@ -105,11 +106,22 @@ function getRectInScene(element: HTMLElement, scene: HTMLElement) {
};
}
function applyCameraToElement(element: HTMLElement, animate: boolean) {
function easing(t: number) {
return 1 - (1 - t) ** 3;
}
function stopCameraAnimation() {
animationToken += 1;
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
}
function computeTransformForElement(element: HTMLElement) {
const viewport = viewportRef.value;
const scene = sceneRef.value;
const panzoom = panzoomRef.value;
if (!viewport || !scene || !panzoom) return;
if (!viewport || !scene) return null;
const targetRect = getRectInScene(element, scene);
@@ -122,14 +134,68 @@ function applyCameraToElement(element: HTMLElement, animate: boolean) {
const x = VIEWPORT_PADDING + (safeWidth - targetRect.width * scale) / 2 - targetRect.x * scale;
const y = VIEWPORT_PADDING + (safeHeight - targetRect.height * scale) / 2 - targetRect.y * scale;
panzoom.zoom(scale, { animate, force: true });
panzoom.pan(x, y, { animate, force: true });
return { x, y, scale };
}
function applyCameraToLevel(level: Level, animate: boolean) {
function applyTransform(transform: { x: number; y: number; scale: number }) {
const panzoom = panzoomRef.value;
if (!panzoom) return;
panzoom.zoom(transform.scale, { animate: false, force: true });
panzoom.pan(transform.x, transform.y, { animate: false, force: true });
}
async function applyCameraToElement(element: HTMLElement, animate: boolean) {
const panzoom = panzoomRef.value;
if (!panzoom) return;
const target = computeTransformForElement(element);
if (!target) return;
if (!animate) {
stopCameraAnimation();
applyTransform(target);
return;
}
stopCameraAnimation();
const localToken = animationToken;
const startPan = panzoom.getPan();
const startScale = panzoom.getScale();
const startAt = performance.now();
await new Promise<void>((resolve) => {
const step = (now: number) => {
if (localToken !== animationToken) {
resolve();
return;
}
const elapsed = now - startAt;
const t = Math.max(0, Math.min(1, elapsed / ZOOM_ANIMATION_MS));
const k = easing(t);
applyTransform({
x: startPan.x + (target.x - startPan.x) * k,
y: startPan.y + (target.y - startPan.y) * k,
scale: startScale + (target.scale - startScale) * k,
});
if (t < 1) {
animationFrameId = requestAnimationFrame(step);
return;
}
animationFrameId = null;
resolve();
};
animationFrameId = requestAnimationFrame(step);
});
}
async function applyCameraToLevel(level: Level, animate: boolean) {
const element = findFocusElement(level);
if (!element) return;
applyCameraToElement(element, animate);
await applyCameraToElement(element, animate);
}
function startPrime(focusId: string) {
@@ -202,21 +268,11 @@ async function animateToLevel(level: Level) {
transitionTarget.value = level;
await nextTick();
applyCameraToLevel(level, true);
await applyCameraToLevel(level, true);
if (animationTimer) {
clearTimeout(animationTimer);
animationTimer = null;
}
await new Promise<void>((resolve) => {
animationTimer = setTimeout(() => {
currentLevel.value = level;
transitionTarget.value = null;
isAnimating.value = false;
resolve();
}, ZOOM_ANIMATION_MS + 40);
});
currentLevel.value = level;
transitionTarget.value = null;
isAnimating.value = false;
}
async function zoom(direction: Direction) {
@@ -232,7 +288,7 @@ async function zoom(direction: Direction) {
function onWheel(event: WheelEvent) {
if (isAnimating.value) return;
const direction: Direction = event.deltaY > 0 ? "in" : "out";
const direction: Direction = event.deltaY < 0 ? "in" : "out";
const target = prepareZoomTarget(direction);
if (!target) return;
@@ -304,9 +360,7 @@ onMounted(async () => {
if (sceneRef.value) {
panzoomRef.value = Panzoom(sceneRef.value, {
animate: true,
duration: ZOOM_ANIMATION_MS,
easing: "cubic-bezier(0.2, 0.8, 0.2, 1)",
animate: false,
maxScale: 24,
minScale: 0.08,
disablePan: true,
@@ -326,8 +380,8 @@ onMounted(async () => {
});
onBeforeUnmount(() => {
if (animationTimer) clearTimeout(animationTimer);
if (primeTimer) clearTimeout(primeTimer);
stopCameraAnimation();
resizeObserver.value?.disconnect();
panzoomRef.value?.destroy();
panzoomRef.value = null;