fix(calendar-lab): stabilize center fit and wheel direction
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user