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 wheelPrimeDirection = ref<"" | Direction>("");
|
||||||
const wheelPrimeTicks = ref(0);
|
const wheelPrimeTicks = ref(0);
|
||||||
|
|
||||||
let animationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let primeTimer: 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 currentLevelIndex = computed(() => LEVELS.indexOf(currentLevel.value));
|
||||||
const displayLevel = computed(() => transitionTarget.value ?? 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 viewport = viewportRef.value;
|
||||||
const scene = sceneRef.value;
|
const scene = sceneRef.value;
|
||||||
const panzoom = panzoomRef.value;
|
if (!viewport || !scene) return null;
|
||||||
if (!viewport || !scene || !panzoom) return;
|
|
||||||
|
|
||||||
const targetRect = getRectInScene(element, scene);
|
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 x = VIEWPORT_PADDING + (safeWidth - targetRect.width * scale) / 2 - targetRect.x * scale;
|
||||||
const y = VIEWPORT_PADDING + (safeHeight - targetRect.height * scale) / 2 - targetRect.y * scale;
|
const y = VIEWPORT_PADDING + (safeHeight - targetRect.height * scale) / 2 - targetRect.y * scale;
|
||||||
|
|
||||||
panzoom.zoom(scale, { animate, force: true });
|
return { x, y, scale };
|
||||||
panzoom.pan(x, y, { animate, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
const element = findFocusElement(level);
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
applyCameraToElement(element, animate);
|
await applyCameraToElement(element, animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPrime(focusId: string) {
|
function startPrime(focusId: string) {
|
||||||
@@ -202,21 +268,11 @@ async function animateToLevel(level: Level) {
|
|||||||
transitionTarget.value = level;
|
transitionTarget.value = level;
|
||||||
|
|
||||||
await nextTick();
|
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;
|
currentLevel.value = level;
|
||||||
transitionTarget.value = null;
|
transitionTarget.value = null;
|
||||||
isAnimating.value = false;
|
isAnimating.value = false;
|
||||||
resolve();
|
|
||||||
}, ZOOM_ANIMATION_MS + 40);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function zoom(direction: Direction) {
|
async function zoom(direction: Direction) {
|
||||||
@@ -232,7 +288,7 @@ async function zoom(direction: Direction) {
|
|||||||
function onWheel(event: WheelEvent) {
|
function onWheel(event: WheelEvent) {
|
||||||
if (isAnimating.value) return;
|
if (isAnimating.value) return;
|
||||||
|
|
||||||
const direction: Direction = event.deltaY > 0 ? "in" : "out";
|
const direction: Direction = event.deltaY < 0 ? "in" : "out";
|
||||||
const target = prepareZoomTarget(direction);
|
const target = prepareZoomTarget(direction);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
@@ -304,9 +360,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
if (sceneRef.value) {
|
if (sceneRef.value) {
|
||||||
panzoomRef.value = Panzoom(sceneRef.value, {
|
panzoomRef.value = Panzoom(sceneRef.value, {
|
||||||
animate: true,
|
animate: false,
|
||||||
duration: ZOOM_ANIMATION_MS,
|
|
||||||
easing: "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
|
||||||
maxScale: 24,
|
maxScale: 24,
|
||||||
minScale: 0.08,
|
minScale: 0.08,
|
||||||
disablePan: true,
|
disablePan: true,
|
||||||
@@ -326,8 +380,8 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (animationTimer) clearTimeout(animationTimer);
|
|
||||||
if (primeTimer) clearTimeout(primeTimer);
|
if (primeTimer) clearTimeout(primeTimer);
|
||||||
|
stopCameraAnimation();
|
||||||
resizeObserver.value?.disconnect();
|
resizeObserver.value?.disconnect();
|
||||||
panzoomRef.value?.destroy();
|
panzoomRef.value?.destroy();
|
||||||
panzoomRef.value = null;
|
panzoomRef.value = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user