feat(calendar-lab): switch zoom scene to panzoom engine
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import Panzoom from "@panzoom/panzoom";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
|
||||
type Level = "year" | "month" | "week" | "day";
|
||||
@@ -24,12 +25,12 @@ const VIEWPORT_PADDING = 20;
|
||||
|
||||
const viewportRef = ref<HTMLDivElement | null>(null);
|
||||
const sceneRef = ref<HTMLDivElement | null>(null);
|
||||
const panzoomRef = ref<ReturnType<typeof Panzoom> | null>(null);
|
||||
const resizeObserver = ref<ResizeObserver | null>(null);
|
||||
|
||||
const currentLevel = ref<Level>("year");
|
||||
const transitionTarget = ref<Level | null>(null);
|
||||
const isAnimating = ref(false);
|
||||
const useTransition = ref(false);
|
||||
|
||||
const selectedMonth = ref(0);
|
||||
const selectedWeek = ref(0);
|
||||
@@ -41,11 +42,6 @@ const hoveredDay = ref(0);
|
||||
|
||||
const primeFocusId = ref("");
|
||||
|
||||
const cameraX = ref(0);
|
||||
const cameraY = ref(0);
|
||||
const cameraScaleX = ref(1);
|
||||
const cameraScaleY = ref(1);
|
||||
|
||||
const wheelPrimeDirection = ref<"" | Direction>("");
|
||||
const wheelPrimeTicks = ref(0);
|
||||
|
||||
@@ -59,12 +55,6 @@ const displayLevelIndex = computed(() => LEVELS.indexOf(displayLevel.value));
|
||||
const canZoomIn = computed(() => currentLevelIndex.value < LEVELS.length - 1);
|
||||
const canZoomOut = computed(() => currentLevelIndex.value > 0);
|
||||
|
||||
const sceneStyle = computed(() => ({
|
||||
transform: `translate3d(${cameraX.value}px, ${cameraY.value}px, 0) scale(${cameraScaleX.value}, ${cameraScaleY.value})`,
|
||||
transformOrigin: "0 0",
|
||||
transition: useTransition.value ? `transform ${ZOOM_ANIMATION_MS}ms cubic-bezier(0.2, 0.8, 0.2, 1)` : "none",
|
||||
}));
|
||||
|
||||
function getFocusId(level: Level) {
|
||||
if (level === "year") return "focus-year";
|
||||
if (level === "month") return `focus-month-${selectedMonth.value}`;
|
||||
@@ -79,30 +69,44 @@ function findFocusElement(level: Level) {
|
||||
return scene.querySelector<HTMLElement>(`[data-focus-id="${id}"]`);
|
||||
}
|
||||
|
||||
function getRectInScene(element: HTMLElement, scene: HTMLElement) {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let node: HTMLElement | null = element;
|
||||
|
||||
while (node && node !== scene) {
|
||||
x += node.offsetLeft;
|
||||
y += node.offsetTop;
|
||||
node = node.offsetParent as HTMLElement | null;
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width: Math.max(1, element.offsetWidth),
|
||||
height: Math.max(1, element.offsetHeight),
|
||||
};
|
||||
}
|
||||
|
||||
function applyCameraToElement(element: HTMLElement, animate: boolean) {
|
||||
const viewport = viewportRef.value;
|
||||
const scene = sceneRef.value;
|
||||
if (!viewport || !scene) return;
|
||||
const panzoom = panzoomRef.value;
|
||||
if (!viewport || !scene || !panzoom) return;
|
||||
|
||||
const targetRect = getRectInScene(element, scene);
|
||||
|
||||
const viewportWidth = Math.max(1, viewport.clientWidth);
|
||||
const viewportHeight = Math.max(1, viewport.clientHeight);
|
||||
const safeWidth = Math.max(1, viewportWidth - VIEWPORT_PADDING * 2);
|
||||
const safeHeight = Math.max(1, viewportHeight - VIEWPORT_PADDING * 2);
|
||||
const scale = Math.min(safeWidth / targetRect.width, safeHeight / targetRect.height);
|
||||
|
||||
const sceneRect = scene.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
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 localX = elementRect.left - sceneRect.left;
|
||||
const localY = elementRect.top - sceneRect.top;
|
||||
|
||||
const scaleX = safeWidth / Math.max(1, elementRect.width);
|
||||
const scaleY = safeHeight / Math.max(1, elementRect.height);
|
||||
|
||||
useTransition.value = animate;
|
||||
cameraScaleX.value = scaleX;
|
||||
cameraScaleY.value = scaleY;
|
||||
cameraX.value = VIEWPORT_PADDING - localX * scaleX;
|
||||
cameraY.value = VIEWPORT_PADDING - localY * scaleY;
|
||||
panzoom.zoom(scale, { animate, force: true });
|
||||
panzoom.pan(x, y, { animate, force: true });
|
||||
}
|
||||
|
||||
function applyCameraToLevel(level: Level, animate: boolean) {
|
||||
@@ -190,7 +194,6 @@ async function animateToLevel(level: Level) {
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
animationTimer = setTimeout(() => {
|
||||
useTransition.value = false;
|
||||
currentLevel.value = level;
|
||||
transitionTarget.value = null;
|
||||
isAnimating.value = false;
|
||||
@@ -281,6 +284,19 @@ function showDayContent(index: number) {
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
|
||||
if (sceneRef.value) {
|
||||
panzoomRef.value = Panzoom(sceneRef.value, {
|
||||
animate: true,
|
||||
duration: ZOOM_ANIMATION_MS,
|
||||
easing: "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||
maxScale: 24,
|
||||
minScale: 0.08,
|
||||
disablePan: true,
|
||||
origin: "0 0",
|
||||
});
|
||||
}
|
||||
|
||||
applyCameraToLevel("year", false);
|
||||
|
||||
resizeObserver.value = new ResizeObserver(() => {
|
||||
@@ -296,6 +312,8 @@ onBeforeUnmount(() => {
|
||||
if (animationTimer) clearTimeout(animationTimer);
|
||||
if (primeTimer) clearTimeout(primeTimer);
|
||||
resizeObserver.value?.disconnect();
|
||||
panzoomRef.value?.destroy();
|
||||
panzoomRef.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -334,7 +352,7 @@ onBeforeUnmount(() => {
|
||||
:class="canZoomIn ? 'cursor-zoom-in' : 'cursor-zoom-out'"
|
||||
@wheel.prevent="onWheel"
|
||||
>
|
||||
<div ref="sceneRef" class="calendar-lab-scene" :style="sceneStyle">
|
||||
<div ref="sceneRef" class="calendar-lab-scene">
|
||||
<article class="calendar-year" data-focus-id="focus-year">
|
||||
<div class="calendar-year-grid">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user