feat(calendar-lab): switch zoom scene to panzoom engine
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Panzoom from "@panzoom/panzoom";
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
|
|
||||||
type Level = "year" | "month" | "week" | "day";
|
type Level = "year" | "month" | "week" | "day";
|
||||||
@@ -24,12 +25,12 @@ const VIEWPORT_PADDING = 20;
|
|||||||
|
|
||||||
const viewportRef = ref<HTMLDivElement | null>(null);
|
const viewportRef = ref<HTMLDivElement | null>(null);
|
||||||
const sceneRef = 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 resizeObserver = ref<ResizeObserver | null>(null);
|
||||||
|
|
||||||
const currentLevel = ref<Level>("year");
|
const currentLevel = ref<Level>("year");
|
||||||
const transitionTarget = ref<Level | null>(null);
|
const transitionTarget = ref<Level | null>(null);
|
||||||
const isAnimating = ref(false);
|
const isAnimating = ref(false);
|
||||||
const useTransition = ref(false);
|
|
||||||
|
|
||||||
const selectedMonth = ref(0);
|
const selectedMonth = ref(0);
|
||||||
const selectedWeek = ref(0);
|
const selectedWeek = ref(0);
|
||||||
@@ -41,11 +42,6 @@ const hoveredDay = ref(0);
|
|||||||
|
|
||||||
const primeFocusId = ref("");
|
const primeFocusId = ref("");
|
||||||
|
|
||||||
const cameraX = ref(0);
|
|
||||||
const cameraY = ref(0);
|
|
||||||
const cameraScaleX = ref(1);
|
|
||||||
const cameraScaleY = ref(1);
|
|
||||||
|
|
||||||
const wheelPrimeDirection = ref<"" | Direction>("");
|
const wheelPrimeDirection = ref<"" | Direction>("");
|
||||||
const wheelPrimeTicks = ref(0);
|
const wheelPrimeTicks = ref(0);
|
||||||
|
|
||||||
@@ -59,12 +55,6 @@ const displayLevelIndex = computed(() => LEVELS.indexOf(displayLevel.value));
|
|||||||
const canZoomIn = computed(() => currentLevelIndex.value < LEVELS.length - 1);
|
const canZoomIn = computed(() => currentLevelIndex.value < LEVELS.length - 1);
|
||||||
const canZoomOut = computed(() => currentLevelIndex.value > 0);
|
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) {
|
function getFocusId(level: Level) {
|
||||||
if (level === "year") return "focus-year";
|
if (level === "year") return "focus-year";
|
||||||
if (level === "month") return `focus-month-${selectedMonth.value}`;
|
if (level === "month") return `focus-month-${selectedMonth.value}`;
|
||||||
@@ -79,30 +69,44 @@ function findFocusElement(level: Level) {
|
|||||||
return scene.querySelector<HTMLElement>(`[data-focus-id="${id}"]`);
|
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) {
|
function applyCameraToElement(element: HTMLElement, animate: boolean) {
|
||||||
const viewport = viewportRef.value;
|
const viewport = viewportRef.value;
|
||||||
const scene = sceneRef.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 viewportWidth = Math.max(1, viewport.clientWidth);
|
||||||
const viewportHeight = Math.max(1, viewport.clientHeight);
|
const viewportHeight = Math.max(1, viewport.clientHeight);
|
||||||
const safeWidth = Math.max(1, viewportWidth - VIEWPORT_PADDING * 2);
|
const safeWidth = Math.max(1, viewportWidth - VIEWPORT_PADDING * 2);
|
||||||
const safeHeight = Math.max(1, viewportHeight - 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 x = VIEWPORT_PADDING + (safeWidth - targetRect.width * scale) / 2 - targetRect.x * scale;
|
||||||
const elementRect = element.getBoundingClientRect();
|
const y = VIEWPORT_PADDING + (safeHeight - targetRect.height * scale) / 2 - targetRect.y * scale;
|
||||||
|
|
||||||
const localX = elementRect.left - sceneRect.left;
|
panzoom.zoom(scale, { animate, force: true });
|
||||||
const localY = elementRect.top - sceneRect.top;
|
panzoom.pan(x, y, { animate, force: true });
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCameraToLevel(level: Level, animate: boolean) {
|
function applyCameraToLevel(level: Level, animate: boolean) {
|
||||||
@@ -190,7 +194,6 @@ async function animateToLevel(level: Level) {
|
|||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
animationTimer = setTimeout(() => {
|
animationTimer = setTimeout(() => {
|
||||||
useTransition.value = false;
|
|
||||||
currentLevel.value = level;
|
currentLevel.value = level;
|
||||||
transitionTarget.value = null;
|
transitionTarget.value = null;
|
||||||
isAnimating.value = false;
|
isAnimating.value = false;
|
||||||
@@ -281,6 +284,19 @@ function showDayContent(index: number) {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
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);
|
applyCameraToLevel("year", false);
|
||||||
|
|
||||||
resizeObserver.value = new ResizeObserver(() => {
|
resizeObserver.value = new ResizeObserver(() => {
|
||||||
@@ -296,6 +312,8 @@ onBeforeUnmount(() => {
|
|||||||
if (animationTimer) clearTimeout(animationTimer);
|
if (animationTimer) clearTimeout(animationTimer);
|
||||||
if (primeTimer) clearTimeout(primeTimer);
|
if (primeTimer) clearTimeout(primeTimer);
|
||||||
resizeObserver.value?.disconnect();
|
resizeObserver.value?.disconnect();
|
||||||
|
panzoomRef.value?.destroy();
|
||||||
|
panzoomRef.value = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -334,7 +352,7 @@ onBeforeUnmount(() => {
|
|||||||
:class="canZoomIn ? 'cursor-zoom-in' : 'cursor-zoom-out'"
|
:class="canZoomIn ? 'cursor-zoom-in' : 'cursor-zoom-out'"
|
||||||
@wheel.prevent="onWheel"
|
@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">
|
<article class="calendar-year" data-focus-id="focus-year">
|
||||||
<div class="calendar-year-grid">
|
<div class="calendar-year-grid">
|
||||||
<div
|
<div
|
||||||
|
|||||||
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@langchain/openai": "^0.6.9",
|
"@langchain/openai": "^0.6.9",
|
||||||
"@nuxt/eslint": "^1.15.1",
|
"@nuxt/eslint": "^1.15.1",
|
||||||
"@nuxtjs/apollo": "^5.0.0-alpha.15",
|
"@nuxtjs/apollo": "^5.0.0-alpha.15",
|
||||||
|
"@panzoom/panzoom": "^4.6.1",
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tiptap/extension-collaboration": "^2.27.2",
|
"@tiptap/extension-collaboration": "^2.27.2",
|
||||||
@@ -6476,6 +6477,12 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@panzoom/panzoom": {
|
||||||
|
"version": "4.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@panzoom/panzoom/-/panzoom-4.6.1.tgz",
|
||||||
|
"integrity": "sha512-ogf/KhHHjj+DYAvHfaf3TXMQ8OE36pJtKpabLlx1OmpjcgtpCvkUoCiNONA8kuVRPGJdLiqMf0n8LRFXj1OyuA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@parcel/watcher": {
|
"node_modules/@parcel/watcher": {
|
||||||
"version": "2.5.6",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@langchain/openai": "^0.6.9",
|
"@langchain/openai": "^0.6.9",
|
||||||
"@nuxt/eslint": "^1.15.1",
|
"@nuxt/eslint": "^1.15.1",
|
||||||
"@nuxtjs/apollo": "^5.0.0-alpha.15",
|
"@nuxtjs/apollo": "^5.0.0-alpha.15",
|
||||||
|
"@panzoom/panzoom": "^4.6.1",
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tiptap/extension-collaboration": "^2.27.2",
|
"@tiptap/extension-collaboration": "^2.27.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user