@hunterchen/canvas 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/components/canvas/backgrounds.js +67 -38
  2. package/dist/components/canvas/backgrounds.js.map +1 -1
  3. package/dist/components/canvas/canvas.js +451 -387
  4. package/dist/components/canvas/canvas.js.map +1 -1
  5. package/dist/components/canvas/component.js +108 -174
  6. package/dist/components/canvas/component.js.map +1 -1
  7. package/dist/components/canvas/draggable.js +168 -151
  8. package/dist/components/canvas/draggable.js.map +1 -1
  9. package/dist/components/canvas/navbar/index.js +164 -142
  10. package/dist/components/canvas/navbar/index.js.map +1 -1
  11. package/dist/components/canvas/navbar/single-button.js +176 -149
  12. package/dist/components/canvas/navbar/single-button.js.map +1 -1
  13. package/dist/components/canvas/toolbar.js +121 -82
  14. package/dist/components/canvas/toolbar.js.map +1 -1
  15. package/dist/components/canvas/wrapper.js +127 -99
  16. package/dist/components/canvas/wrapper.js.map +1 -1
  17. package/dist/contexts/CanvasContext.js +25 -17
  18. package/dist/contexts/CanvasContext.js.map +1 -1
  19. package/dist/contexts/PerformanceContext.js +51 -50
  20. package/dist/contexts/PerformanceContext.js.map +1 -1
  21. package/dist/hooks/usePerformanceMode.js +36 -37
  22. package/dist/hooks/usePerformanceMode.js.map +1 -1
  23. package/dist/hooks/useWindowDimensions.js +22 -18
  24. package/dist/hooks/useWindowDimensions.js.map +1 -1
  25. package/dist/index.js +17 -21
  26. package/dist/lib/canvas.js +65 -72
  27. package/dist/lib/canvas.js.map +1 -1
  28. package/dist/lib/constants.js +78 -92
  29. package/dist/lib/constants.js.map +1 -1
  30. package/dist/lib/utils.js +10 -5
  31. package/dist/lib/utils.js.map +1 -1
  32. package/dist/utils/performance.js +18 -23
  33. package/dist/utils/performance.js.map +1 -1
  34. package/package.json +7 -21
  35. package/dist/components/canvas/offest.js +0 -12
  36. package/dist/components/canvas/offest.js.map +0 -1
  37. package/dist/index.js.map +0 -1
  38. package/dist/types/index.js +0 -6
  39. package/dist/types/index.js.map +0 -1
@@ -1,391 +1,455 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { motion, useMotionValue, animate, useTransform, } from "framer-motion";
3
- import { useState, useRef, useEffect, useCallback, useMemo, } from "react";
4
- import { CanvasProvider } from "../../contexts/CanvasContext";
5
- import { calcInitialBoxWidth, getDistance, getMidpoint, getScreenSizeEnum, getSectionPanCoordinates, INTERACTIVE_SELECTOR, MAX_ZOOM, MIN_ZOOMS, panToOffsetScene, ZOOM_BOUND, } from "../../lib/canvas";
6
- import { STAGE2_TRANSITION, MOUSE_WHEEL_ZOOM_SENSITIVITY, TRACKPAD_ZOOM_SENSITIVITY, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, } from "../../lib/constants";
7
- import useWindowDimensions from "../../hooks/useWindowDimensions";
8
- import Navbar from "./navbar";
9
- import Toolbar from "./toolbar";
10
- import { CanvasWrapper } from "./wrapper";
11
- import { usePerformanceMode } from "../../hooks/usePerformanceMode";
12
- import { DefaultCanvasBackground } from "./backgrounds";
1
+ import { CanvasProvider } from "../../contexts/CanvasContext.js";
2
+ import { DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, INTERACTIVE_SELECTOR, MAX_ZOOM, MIN_ZOOMS, MOUSE_WHEEL_ZOOM_SENSITIVITY, STAGE2_TRANSITION, TRACKPAD_ZOOM_SENSITIVITY, ZOOM_BOUND } from "../../lib/constants.js";
3
+ import { calcInitialBoxWidth, getDistance, getMidpoint, getScreenSizeEnum, getSectionPanCoordinates, panToOffsetScene } from "../../lib/canvas.js";
4
+ import useWindowDimensions_default from "../../hooks/useWindowDimensions.js";
5
+ import { usePerformanceMode } from "../../hooks/usePerformanceMode.js";
6
+ import Navbar from "./navbar/index.js";
7
+ import toolbar_default from "./toolbar.js";
8
+ import { CanvasWrapper } from "./wrapper.js";
9
+ import { DefaultCanvasBackground } from "./backgrounds.js";
10
+ import { animate, motion, useMotionValue, useTransform } from "framer-motion";
11
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
+
14
+ //#region src/components/canvas/canvas.tsx
13
15
  const stopAllMotion = (x, y, scale) => {
14
- x.stop();
15
- y.stop();
16
- scale.stop();
16
+ x.stop();
17
+ y.stop();
18
+ scale.stop();
17
19
  };
18
- const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introContent, loadingText, introBackgroundGradient, canvasBoxGradient, growTransition, blurTransition, canvasBackground, wrapperBackground, toolbarConfig, navbarConfig, canvasHeight, canvasWidth, }) => {
19
- const { height: windowHeight, width: windowWidth } = useWindowDimensions();
20
- const { mode } = usePerformanceMode();
21
- const hasNavbar = Boolean(navItems && navItems.length > 0);
22
- const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;
23
- const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;
24
- const MIN_ZOOM = MIN_ZOOMS[getScreenSizeEnum(windowWidth)];
25
- // tracks if user is panning the screen
26
- const [isPanning, setIsPanning] = useState(false);
27
- // this one is moving from scene control, not from user
28
- const [isSceneMoving, setIsSceneMoving] = useState(false);
29
- const [panStartPoint, setPanStartPoint] = useState({ x: 0, y: 0 });
30
- const [initialPanOffsetOnDrag, setInitialPanOffsetOnDrag] = useState({
31
- x: 0,
32
- y: 0,
33
- });
34
- const [isResetting, setIsResetting] = useState(false);
35
- const [maxZIndex, setMaxZIndex] = useState(50);
36
- const [animationStage, setAnimationStage] = useState(0); // 0: initial, 1: finish grow, 2: pan to home
37
- const [nextTargetSection, setNextTargetSection] = useState(null);
38
- // Track if the intro (stage1 + stage2) is still running, to avoid accidental cancellation
39
- const isIntroAnimatingRef = useRef(true);
40
- const initialBoxWidth = useMemo(() => calcInitialBoxWidth(windowWidth, windowHeight), [windowWidth, windowHeight]);
41
- // somewhere near the middle-ish
42
- const x = useMotionValue(0);
43
- const y = useMotionValue(0);
44
- const scale = useMotionValue(initialBoxWidth);
45
- const offsetHomeCoordinates = useMemo(() => getSectionPanCoordinates({
46
- windowDimensions: { width: windowWidth, height: windowHeight },
47
- coords: homeCoordinates,
48
- targetZoom: 1,
49
- }), [homeCoordinates, windowWidth, windowHeight]);
50
- const onResetViewAndItems = useCallback((onComplete) => {
51
- setIsResetting(true);
52
- void panToOffsetScene(offsetHomeCoordinates, x, y, scale, 1).then(() => {
53
- setIsResetting(false);
54
- if (onComplete)
55
- onComplete();
56
- });
57
- }, [offsetHomeCoordinates, x, y, scale]);
58
- // Shared intro progress (0->1) driven by CanvasWrapper
59
- const introProgress = useMotionValue(0);
60
- // Precompute final stage1 scale and offsets (snapshot dimensions once on mount)
61
- const stage1Targets = useMemo(() => {
62
- const finalScale = Math.max((windowWidth || 0) / sceneWidth, (windowHeight || 0) / sceneHeight);
63
- const endX = (windowWidth - sceneWidth * finalScale) / 2;
64
- const endY = (windowHeight - sceneHeight * finalScale) / 2;
65
- return { finalScale, endX, endY };
66
- }, [windowWidth, windowHeight, sceneWidth, sceneHeight]);
67
- // Replace direct motion values with derived transforms during stage1
68
- const derivedScale = useTransform(introProgress, [0, 1], [initialBoxWidth, stage1Targets.finalScale]);
69
- const derivedX = useTransform(introProgress, [0, 1], [0, stage1Targets.endX]);
70
- const derivedY = useTransform(introProgress, [0, 1], [0, stage1Targets.endY]);
71
- // While intro (stage1) is running, bind x/y/scale to derived versions.
72
- useEffect(() => {
73
- const unsubscribeScale = derivedScale.on("change", (v) => {
74
- if (animationStage === 0)
75
- scale.set(v);
76
- });
77
- const unsubscribeX = derivedX.on("change", (v) => {
78
- if (animationStage === 0)
79
- x.set(v);
80
- });
81
- const unsubscribeY = derivedY.on("change", (v) => {
82
- if (animationStage === 0)
83
- y.set(v);
84
- });
85
- return () => {
86
- unsubscribeScale();
87
- unsubscribeX();
88
- unsubscribeY();
89
- };
90
- }, [derivedScale, derivedX, derivedY, animationStage, scale, x, y]);
91
- // Kick off stage2 (pan to home) when grow completes (introProgress hits 1)
92
- const startStage2 = useCallback(() => {
93
- setAnimationStage(1);
94
- Promise.all([
95
- animate(x, offsetHomeCoordinates.x, STAGE2_TRANSITION),
96
- animate(y, offsetHomeCoordinates.y, STAGE2_TRANSITION),
97
- animate(scale, 1, STAGE2_TRANSITION),
98
- ])
99
- .then(() => {
100
- setAnimationStage(2);
101
- isIntroAnimatingRef.current = false;
102
- })
103
- .catch(() => {
104
- isIntroAnimatingRef.current = false;
105
- });
106
- }, [offsetHomeCoordinates, x, y, scale]);
107
- const viewportRef = useRef(null);
108
- const sceneRef = useRef(null);
109
- // Stable wheel listener wrapper that always calls the latest handler via ref
110
- const wheelHandlerRef = useRef(null);
111
- const wheelWrapper = useCallback((e) => {
112
- wheelHandlerRef.current?.(e);
113
- }, []);
114
- // Ensure wheel listener attaches when the element actually mounts (wrapper delays child mount)
115
- const setViewportRef = useCallback((node) => {
116
- // Clean up old listener if ref changes/unmounts
117
- if (viewportRef.current) {
118
- viewportRef.current.removeEventListener("wheel", wheelWrapper);
119
- }
120
- viewportRef.current = node;
121
- if (node) {
122
- node.addEventListener("wheel", wheelWrapper, { passive: false });
123
- }
124
- }, [wheelWrapper]);
125
- const activePointersRef = useRef(new Map());
126
- const initialPinchStateRef = useRef(null);
127
- const panToOffset = useCallback((offset, viewportRef, onComplete, zoom) => {
128
- if (!viewportRef.current)
129
- return;
130
- setIsSceneMoving(true);
131
- // Calculate bounds based on scene and viewport dimensions
132
- const viewportWidth = viewportRef.current.offsetWidth;
133
- const viewportHeight = viewportRef.current.offsetHeight;
134
- const minPanX = viewportWidth - sceneWidth * (zoom ?? 1);
135
- const maxPanX = 0;
136
- const minPanY = viewportHeight - sceneHeight * (zoom ?? 1);
137
- const maxPanY = 0;
138
- // Clamp the offset to keep the scene within bounds, shouldn't be needed but still implemented
139
- const clampedX = Math.min(Math.max(offset.x, minPanX), maxPanX);
140
- const clampedY = Math.min(Math.max(offset.y, minPanY), maxPanY);
141
- void panToOffsetScene({ x: clampedX, y: clampedY }, x, y, scale, zoom).then(() => {
142
- setIsSceneMoving(false);
143
- if (onComplete)
144
- onComplete();
145
- });
146
- }, [sceneWidth, sceneHeight, x, y, scale]);
147
- // Guarded stop that ignores attempts during intro animations
148
- const stopAllSceneMotion = useCallback(() => {
149
- if (isIntroAnimatingRef.current)
150
- return; // ignore stops while intro runs
151
- stopAllMotion(x, y, scale);
152
- }, [x, y, scale]);
153
- const handlePointerDown = useCallback((event) => {
154
- if (animationStage < 2)
155
- return; // ignore during intro animations
156
- activePointersRef.current.set(event.pointerId, event);
157
- event.target.setPointerCapture(event.pointerId);
158
- if (isResetting || isSceneMoving)
159
- return;
160
- stopAllSceneMotion();
161
- // pan with 1 pointer, pinch with 2 pointers
162
- if (activePointersRef.current.size === 1) {
163
- // do not pan from interactive elements
164
- const targetElement = event.target;
165
- if (targetElement.closest(INTERACTIVE_SELECTOR)) {
166
- activePointersRef.current.delete(event.pointerId);
167
- event.target.releasePointerCapture(event.pointerId);
168
- return;
169
- }
170
- setIsPanning(true);
171
- setPanStartPoint({ x: event.clientX, y: event.clientY });
172
- setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });
173
- if (viewportRef.current)
174
- viewportRef.current.style.cursor = "grabbing";
175
- }
176
- else if (activePointersRef.current.size === 2) {
177
- setIsPanning(false);
178
- const pointers = Array.from(activePointersRef.current.values());
179
- initialPinchStateRef.current = {
180
- distance: getDistance(pointers[0], pointers[1]),
181
- midpoint: getMidpoint(pointers[0], pointers[1]),
182
- zoom: scale.get(),
183
- panOffset: { x: x.get(), y: y.get() },
184
- };
185
- }
186
- }, [
187
- isResetting,
188
- isSceneMoving,
189
- setIsPanning,
190
- setPanStartPoint,
191
- setInitialPanOffsetOnDrag,
192
- x,
193
- y,
194
- scale,
195
- viewportRef,
196
- animationStage,
197
- stopAllSceneMotion,
198
- ]);
199
- const handlePointerMove = useCallback((event) => {
200
- if (animationStage < 2)
201
- return;
202
- if (isPanning || activePointersRef.current.size >= 2) {
203
- stopAllSceneMotion();
204
- }
205
- if (!activePointersRef.current.has(event.pointerId) || isResetting)
206
- return;
207
- activePointersRef.current.set(event.pointerId, event);
208
- if (isPanning && activePointersRef.current.size === 1) {
209
- event.preventDefault();
210
- const deltaX = event.clientX - panStartPoint.x;
211
- const deltaY = event.clientY - panStartPoint.y;
212
- // UPDATE to use motion value
213
- const minPanX = windowWidth - sceneWidth * scale.get();
214
- const maxPanX = 0;
215
- const minPanY = windowHeight - sceneHeight * scale.get();
216
- const maxPanY = 0;
217
- const newX = Math.min(Math.max(initialPanOffsetOnDrag.x + deltaX, minPanX), maxPanX);
218
- const newY = Math.min(Math.max(initialPanOffsetOnDrag.y + deltaY, minPanY), maxPanY);
219
- x.set(newX);
220
- y.set(newY);
221
- }
222
- else if (activePointersRef.current.size >= 2 &&
223
- initialPinchStateRef.current) {
224
- event.preventDefault();
225
- const pointers = Array.from(activePointersRef.current.values());
226
- const p1 = pointers[0];
227
- const p2 = pointers[1];
228
- const currentDistance = getDistance(p1, p2);
229
- const currentMidpoint = getMidpoint(p1, p2);
230
- const { distance: initialDistance, zoom: initialZoom, panOffset: initialPanOffsetPinch, } = initialPinchStateRef.current;
231
- if (initialDistance === 0)
232
- return;
233
- let newZoom = initialZoom * (currentDistance / initialDistance);
234
- newZoom = Math.max((window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas
235
- (window.innerHeight / sceneHeight) * ZOOM_BOUND, // Ensure zoom is at least the height of the canvas
236
- Math.min(newZoom, 10), MIN_ZOOM // Ensure zoom is not less than MIN_ZOOM
237
- );
238
- const mx = currentMidpoint.x;
239
- const my = currentMidpoint.y;
240
- const minPanX = windowWidth - sceneWidth * newZoom;
241
- const maxPanX = 0;
242
- const minPanY = windowHeight - sceneHeight * newZoom;
243
- const maxPanY = 0;
244
- let newPanX = mx - ((mx - initialPanOffsetPinch.x) / initialZoom) * newZoom;
245
- let newPanY = my - ((my - initialPanOffsetPinch.y) / initialZoom) * newZoom;
246
- // Clamp pan to prevent leaving bounds
247
- newPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);
248
- newPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);
249
- scale.set(newZoom);
250
- x.set(newPanX);
251
- y.set(newPanY);
252
- }
253
- }, [
254
- isPanning,
255
- isResetting,
256
- x,
257
- y,
258
- scale,
259
- panStartPoint.x,
260
- panStartPoint.y,
261
- windowWidth,
262
- sceneWidth,
263
- windowHeight,
264
- sceneHeight,
265
- initialPanOffsetOnDrag.x,
266
- initialPanOffsetOnDrag.y,
267
- MIN_ZOOM,
268
- animationStage,
269
- stopAllSceneMotion,
270
- ]);
271
- const handlePointerUpOrCancel = useCallback((event) => {
272
- if (animationStage < 2) {
273
- event.preventDefault();
274
- return; // ignore pointer up during intro
275
- }
276
- stopAllSceneMotion();
277
- event.preventDefault();
278
- if (event.target.hasPointerCapture(event.pointerId)) {
279
- event.target.releasePointerCapture(event.pointerId);
280
- }
281
- activePointersRef.current.delete(event.pointerId);
282
- if (isPanning && activePointersRef.current.size < 1) {
283
- setIsPanning(false);
284
- if (viewportRef.current)
285
- viewportRef.current.style.cursor = "url('/customcursor.svg'), grab";
286
- }
287
- if (initialPinchStateRef.current && activePointersRef.current.size < 2) {
288
- initialPinchStateRef.current = null;
289
- }
290
- if (!isPanning &&
291
- activePointersRef.current.size === 1 &&
292
- !initialPinchStateRef.current) {
293
- const lastPointer = Array.from(activePointersRef.current.values())[0];
294
- setIsPanning(true);
295
- setPanStartPoint({ x: lastPointer.clientX, y: lastPointer.clientY });
296
- setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });
297
- }
298
- }, [x, y, isPanning, animationStage, stopAllSceneMotion]);
299
- const handleWheelZoom = useCallback((event) => {
300
- if (animationStage < 2) {
301
- event.preventDefault();
302
- return; // block wheel interaction during intro animations
303
- }
304
- event.preventDefault();
305
- // pinch gesture on track
306
- const isPinch = event.ctrlKey || event.metaKey;
307
- const isMouseWheelZoom = event.deltaMode === WheelEvent.DOM_DELTA_LINE ||
308
- Math.abs(event.deltaY) >= 100;
309
- // mouse wheel zoom and track pad zoom have different sensitivities
310
- const ZOOM_SENSITIVITY = isMouseWheelZoom
311
- ? MOUSE_WHEEL_ZOOM_SENSITIVITY
312
- : TRACKPAD_ZOOM_SENSITIVITY;
313
- if (isPinch) {
314
- const currentZoom = scale.get();
315
- const nextZoom = Math.max(Math.min(currentZoom * (1 - event.deltaY * ZOOM_SENSITIVITY), MAX_ZOOM), MIN_ZOOM, (window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas
316
- (window.innerHeight / sceneHeight) * ZOOM_BOUND // Ensure zoom is at least the height of the canvas
317
- );
318
- const rect = viewportRef.current?.getBoundingClientRect();
319
- if (!rect)
320
- return;
321
- const vpLeft = rect.left;
322
- const vpTop = rect.top;
323
- const viewportWidth = rect.width;
324
- const viewportHeight = rect.height;
325
- const cursorSceneX = (event.clientX - vpLeft - x.get()) / currentZoom;
326
- const cursorSceneY = (event.clientY - vpTop - y.get()) / currentZoom;
327
- let newPanX = event.clientX - vpLeft - cursorSceneX * nextZoom;
328
- let newPanY = event.clientY - vpTop - cursorSceneY * nextZoom;
329
- const minPanX = viewportWidth - sceneWidth * nextZoom;
330
- const minPanY = viewportHeight - sceneHeight * nextZoom;
331
- const maxPanX = 0;
332
- const maxPanY = 0;
333
- newPanX = Math.min(maxPanX, Math.max(minPanX, newPanX));
334
- newPanY = Math.min(maxPanY, Math.max(minPanY, newPanY));
335
- x.set(newPanX);
336
- y.set(newPanY);
337
- scale.set(nextZoom);
338
- }
339
- else {
340
- stopAllSceneMotion();
341
- const scrollSpeed = 1;
342
- const newPanX = x.get() - event.deltaX * scrollSpeed;
343
- const newPanY = y.get() - event.deltaY * scrollSpeed;
344
- const minPanX = windowWidth - sceneWidth * scale.get();
345
- const maxPanX = 0;
346
- const minPanY = windowHeight - sceneHeight * scale.get();
347
- const maxPanY = 0;
348
- const clampedPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);
349
- const clampedPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);
350
- x.set(clampedPanX);
351
- y.set(clampedPanY);
352
- }
353
- }, [
354
- scale,
355
- MIN_ZOOM,
356
- x,
357
- y,
358
- sceneWidth,
359
- sceneHeight,
360
- windowWidth,
361
- windowHeight,
362
- animationStage,
363
- stopAllSceneMotion,
364
- ]);
365
- // Keep the wheel handler ref pointing to the latest implementation
366
- useEffect(() => {
367
- wheelHandlerRef.current = handleWheelZoom;
368
- }, [handleWheelZoom]);
369
- const handlePanToOffset = useCallback((offset, onComplete, zoom) => {
370
- panToOffset({
371
- x: -offset.x,
372
- y: -offset.y,
373
- }, viewportRef, onComplete, zoom);
374
- }, [panToOffset, viewportRef]);
375
- return (_jsx(CanvasWrapper, { introProgress: introProgress, onIntroGrowComplete: startStage2, skipIntro: skipIntro, introContent: introContent, loadingText: loadingText, introBackgroundGradient: introBackgroundGradient, wrapperBackground: wrapperBackground, canvasBoxGradient: canvasBoxGradient, growTransition: growTransition, blurTransition: blurTransition, children: _jsxs(CanvasProvider, { x: x, y: y, scale: scale, isResetting: isResetting, maxZIndex: maxZIndex, setMaxZIndex: setMaxZIndex, animationStage: animationStage, nextTargetSection: nextTargetSection, setNextTargetSection: setNextTargetSection, children: [animationStage >= 2 && (_jsxs(_Fragment, { children: [!toolbarConfig?.hidden && (_jsx(Toolbar, { homeCoordinates: offsetHomeCoordinates, config: toolbarConfig })), hasNavbar && navItems && !navbarConfig?.hidden && (_jsx(Navbar, { panToOffset: handlePanToOffset, onReset: onResetViewAndItems, items: navItems, config: navbarConfig }))] })), _jsx("div", { ref: setViewportRef, className: "relative h-full w-full touch-none select-none overflow-hidden", style: {
376
- touchAction: "none",
377
- pointerEvents: animationStage >= 2 ? "auto" : "none",
378
- overscrollBehavior: "contain",
379
- }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUpOrCancel, onPointerLeave: handlePointerUpOrCancel, onPointerCancel: handlePointerUpOrCancel, children: _jsxs(motion.div, { ref: sceneRef, className: "absolute z-20 origin-top-left", initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.3, ease: "easeIn" }, style: {
380
- width: `${sceneWidth}px`,
381
- height: `${sceneHeight}px`,
382
- x,
383
- y,
384
- scale,
385
- willChange: mode !== "high" && (animationStage < 2 || isPanning)
386
- ? "transform"
387
- : "auto",
388
- }, children: [canvasBackground !== undefined ? (canvasBackground) : (_jsx(_Fragment, { children: animationStage >= 1 && mode === "high" ? (_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.5, ease: "easeIn" }, children: _jsx(DefaultCanvasBackground, {}) })) : (_jsx(DefaultCanvasBackground, {})) })), children] }) })] }) }));
20
+ const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introContent, loadingText, introBackgroundGradient, canvasBoxGradient, growTransition, blurTransition, canvasBackground, wrapperBackground, toolbarConfig, navbarConfig, canvasHeight, canvasWidth }) => {
21
+ const { height: windowHeight, width: windowWidth } = useWindowDimensions_default();
22
+ const { mode } = usePerformanceMode();
23
+ const hasNavbar = Boolean(navItems && navItems.length > 0);
24
+ const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;
25
+ const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;
26
+ const MIN_ZOOM = MIN_ZOOMS[getScreenSizeEnum(windowWidth)];
27
+ const [isPanning, setIsPanning] = useState(false);
28
+ const [isSceneMoving, setIsSceneMoving] = useState(false);
29
+ const [panStartPoint, setPanStartPoint] = useState({
30
+ x: 0,
31
+ y: 0
32
+ });
33
+ const [initialPanOffsetOnDrag, setInitialPanOffsetOnDrag] = useState({
34
+ x: 0,
35
+ y: 0
36
+ });
37
+ const [isResetting, setIsResetting] = useState(false);
38
+ const [maxZIndex, setMaxZIndex] = useState(50);
39
+ const [animationStage, setAnimationStage] = useState(0);
40
+ const [nextTargetSection, setNextTargetSection] = useState(null);
41
+ const isIntroAnimatingRef = useRef(true);
42
+ const initialBoxWidth = useMemo(() => calcInitialBoxWidth(windowWidth, windowHeight), [windowWidth, windowHeight]);
43
+ const x = useMotionValue(0);
44
+ const y = useMotionValue(0);
45
+ const scale = useMotionValue(initialBoxWidth);
46
+ const offsetHomeCoordinates = useMemo(() => getSectionPanCoordinates({
47
+ windowDimensions: {
48
+ width: windowWidth,
49
+ height: windowHeight
50
+ },
51
+ coords: homeCoordinates,
52
+ targetZoom: 1
53
+ }), [
54
+ homeCoordinates,
55
+ windowWidth,
56
+ windowHeight
57
+ ]);
58
+ const onResetViewAndItems = useCallback((onComplete) => {
59
+ setIsResetting(true);
60
+ panToOffsetScene(offsetHomeCoordinates, x, y, scale, 1).then(() => {
61
+ setIsResetting(false);
62
+ if (onComplete) onComplete();
63
+ });
64
+ }, [
65
+ offsetHomeCoordinates,
66
+ x,
67
+ y,
68
+ scale
69
+ ]);
70
+ const introProgress = useMotionValue(0);
71
+ const stage1Targets = useMemo(() => {
72
+ const finalScale = Math.max((windowWidth || 0) / sceneWidth, (windowHeight || 0) / sceneHeight);
73
+ return {
74
+ finalScale,
75
+ endX: (windowWidth - sceneWidth * finalScale) / 2,
76
+ endY: (windowHeight - sceneHeight * finalScale) / 2
77
+ };
78
+ }, [
79
+ windowWidth,
80
+ windowHeight,
81
+ sceneWidth,
82
+ sceneHeight
83
+ ]);
84
+ const derivedScale = useTransform(introProgress, [0, 1], [initialBoxWidth, stage1Targets.finalScale]);
85
+ const derivedX = useTransform(introProgress, [0, 1], [0, stage1Targets.endX]);
86
+ const derivedY = useTransform(introProgress, [0, 1], [0, stage1Targets.endY]);
87
+ useEffect(() => {
88
+ const unsubscribeScale = derivedScale.on("change", (v) => {
89
+ if (animationStage === 0) scale.set(v);
90
+ });
91
+ const unsubscribeX = derivedX.on("change", (v) => {
92
+ if (animationStage === 0) x.set(v);
93
+ });
94
+ const unsubscribeY = derivedY.on("change", (v) => {
95
+ if (animationStage === 0) y.set(v);
96
+ });
97
+ return () => {
98
+ unsubscribeScale();
99
+ unsubscribeX();
100
+ unsubscribeY();
101
+ };
102
+ }, [
103
+ derivedScale,
104
+ derivedX,
105
+ derivedY,
106
+ animationStage,
107
+ scale,
108
+ x,
109
+ y
110
+ ]);
111
+ const startStage2 = useCallback(() => {
112
+ setAnimationStage(1);
113
+ Promise.all([
114
+ animate(x, offsetHomeCoordinates.x, STAGE2_TRANSITION),
115
+ animate(y, offsetHomeCoordinates.y, STAGE2_TRANSITION),
116
+ animate(scale, 1, STAGE2_TRANSITION)
117
+ ]).then(() => {
118
+ setAnimationStage(2);
119
+ isIntroAnimatingRef.current = false;
120
+ }).catch(() => {
121
+ isIntroAnimatingRef.current = false;
122
+ });
123
+ }, [
124
+ offsetHomeCoordinates,
125
+ x,
126
+ y,
127
+ scale
128
+ ]);
129
+ const viewportRef = useRef(null);
130
+ const sceneRef = useRef(null);
131
+ const wheelHandlerRef = useRef(null);
132
+ const wheelWrapper = useCallback((e) => {
133
+ wheelHandlerRef.current?.(e);
134
+ }, []);
135
+ const setViewportRef = useCallback((node) => {
136
+ if (viewportRef.current) viewportRef.current.removeEventListener("wheel", wheelWrapper);
137
+ viewportRef.current = node;
138
+ if (node) node.addEventListener("wheel", wheelWrapper, { passive: false });
139
+ }, [wheelWrapper]);
140
+ const activePointersRef = useRef(/* @__PURE__ */ new Map());
141
+ const initialPinchStateRef = useRef(null);
142
+ const panToOffset = useCallback((offset, viewportRef, onComplete, zoom) => {
143
+ if (!viewportRef.current) return;
144
+ setIsSceneMoving(true);
145
+ const viewportWidth = viewportRef.current.offsetWidth;
146
+ const viewportHeight = viewportRef.current.offsetHeight;
147
+ const minPanX = viewportWidth - sceneWidth * (zoom ?? 1);
148
+ const maxPanX = 0;
149
+ const minPanY = viewportHeight - sceneHeight * (zoom ?? 1);
150
+ panToOffsetScene({
151
+ x: Math.min(Math.max(offset.x, minPanX), maxPanX),
152
+ y: Math.min(Math.max(offset.y, minPanY), 0)
153
+ }, x, y, scale, zoom).then(() => {
154
+ setIsSceneMoving(false);
155
+ if (onComplete) onComplete();
156
+ });
157
+ }, [
158
+ sceneWidth,
159
+ sceneHeight,
160
+ x,
161
+ y,
162
+ scale
163
+ ]);
164
+ const stopAllSceneMotion = useCallback(() => {
165
+ if (isIntroAnimatingRef.current) return;
166
+ stopAllMotion(x, y, scale);
167
+ }, [
168
+ x,
169
+ y,
170
+ scale
171
+ ]);
172
+ const handlePointerDown = useCallback((event) => {
173
+ if (animationStage < 2) return;
174
+ activePointersRef.current.set(event.pointerId, event);
175
+ event.target.setPointerCapture(event.pointerId);
176
+ if (isResetting || isSceneMoving) return;
177
+ stopAllSceneMotion();
178
+ if (activePointersRef.current.size === 1) {
179
+ if (event.target.closest(INTERACTIVE_SELECTOR)) {
180
+ activePointersRef.current.delete(event.pointerId);
181
+ event.target.releasePointerCapture(event.pointerId);
182
+ return;
183
+ }
184
+ setIsPanning(true);
185
+ setPanStartPoint({
186
+ x: event.clientX,
187
+ y: event.clientY
188
+ });
189
+ setInitialPanOffsetOnDrag({
190
+ x: x.get(),
191
+ y: y.get()
192
+ });
193
+ if (viewportRef.current) viewportRef.current.style.cursor = "grabbing";
194
+ } else if (activePointersRef.current.size === 2) {
195
+ setIsPanning(false);
196
+ const pointers = Array.from(activePointersRef.current.values());
197
+ initialPinchStateRef.current = {
198
+ distance: getDistance(pointers[0], pointers[1]),
199
+ midpoint: getMidpoint(pointers[0], pointers[1]),
200
+ zoom: scale.get(),
201
+ panOffset: {
202
+ x: x.get(),
203
+ y: y.get()
204
+ }
205
+ };
206
+ }
207
+ }, [
208
+ isResetting,
209
+ isSceneMoving,
210
+ setIsPanning,
211
+ setPanStartPoint,
212
+ setInitialPanOffsetOnDrag,
213
+ x,
214
+ y,
215
+ scale,
216
+ viewportRef,
217
+ animationStage,
218
+ stopAllSceneMotion
219
+ ]);
220
+ const handlePointerMove = useCallback((event) => {
221
+ if (animationStage < 2) return;
222
+ if (isPanning || activePointersRef.current.size >= 2) stopAllSceneMotion();
223
+ if (!activePointersRef.current.has(event.pointerId) || isResetting) return;
224
+ activePointersRef.current.set(event.pointerId, event);
225
+ if (isPanning && activePointersRef.current.size === 1) {
226
+ event.preventDefault();
227
+ const deltaX = event.clientX - panStartPoint.x;
228
+ const deltaY = event.clientY - panStartPoint.y;
229
+ const minPanX = windowWidth - sceneWidth * scale.get();
230
+ const maxPanX = 0;
231
+ const minPanY = windowHeight - sceneHeight * scale.get();
232
+ const maxPanY = 0;
233
+ const newX = Math.min(Math.max(initialPanOffsetOnDrag.x + deltaX, minPanX), maxPanX);
234
+ const newY = Math.min(Math.max(initialPanOffsetOnDrag.y + deltaY, minPanY), maxPanY);
235
+ x.set(newX);
236
+ y.set(newY);
237
+ } else if (activePointersRef.current.size >= 2 && initialPinchStateRef.current) {
238
+ event.preventDefault();
239
+ const pointers = Array.from(activePointersRef.current.values());
240
+ const p1 = pointers[0];
241
+ const p2 = pointers[1];
242
+ const currentDistance = getDistance(p1, p2);
243
+ const currentMidpoint = getMidpoint(p1, p2);
244
+ const { distance: initialDistance, zoom: initialZoom, panOffset: initialPanOffsetPinch } = initialPinchStateRef.current;
245
+ if (initialDistance === 0) return;
246
+ let newZoom = initialZoom * (currentDistance / initialDistance);
247
+ newZoom = Math.max(window.innerWidth / sceneWidth * ZOOM_BOUND, window.innerHeight / sceneHeight * ZOOM_BOUND, Math.min(newZoom, 10), MIN_ZOOM);
248
+ const mx = currentMidpoint.x;
249
+ const my = currentMidpoint.y;
250
+ const minPanX = windowWidth - sceneWidth * newZoom;
251
+ const maxPanX = 0;
252
+ const minPanY = windowHeight - sceneHeight * newZoom;
253
+ const maxPanY = 0;
254
+ let newPanX = mx - (mx - initialPanOffsetPinch.x) / initialZoom * newZoom;
255
+ let newPanY = my - (my - initialPanOffsetPinch.y) / initialZoom * newZoom;
256
+ newPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);
257
+ newPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);
258
+ scale.set(newZoom);
259
+ x.set(newPanX);
260
+ y.set(newPanY);
261
+ }
262
+ }, [
263
+ isPanning,
264
+ isResetting,
265
+ x,
266
+ y,
267
+ scale,
268
+ panStartPoint.x,
269
+ panStartPoint.y,
270
+ windowWidth,
271
+ sceneWidth,
272
+ windowHeight,
273
+ sceneHeight,
274
+ initialPanOffsetOnDrag.x,
275
+ initialPanOffsetOnDrag.y,
276
+ MIN_ZOOM,
277
+ animationStage,
278
+ stopAllSceneMotion
279
+ ]);
280
+ const handlePointerUpOrCancel = useCallback((event) => {
281
+ if (animationStage < 2) {
282
+ event.preventDefault();
283
+ return;
284
+ }
285
+ stopAllSceneMotion();
286
+ event.preventDefault();
287
+ if (event.target.hasPointerCapture(event.pointerId)) event.target.releasePointerCapture(event.pointerId);
288
+ activePointersRef.current.delete(event.pointerId);
289
+ if (isPanning && activePointersRef.current.size < 1) {
290
+ setIsPanning(false);
291
+ if (viewportRef.current) viewportRef.current.style.cursor = "url('/customcursor.svg'), grab";
292
+ }
293
+ if (initialPinchStateRef.current && activePointersRef.current.size < 2) initialPinchStateRef.current = null;
294
+ if (!isPanning && activePointersRef.current.size === 1 && !initialPinchStateRef.current) {
295
+ const lastPointer = Array.from(activePointersRef.current.values())[0];
296
+ setIsPanning(true);
297
+ setPanStartPoint({
298
+ x: lastPointer.clientX,
299
+ y: lastPointer.clientY
300
+ });
301
+ setInitialPanOffsetOnDrag({
302
+ x: x.get(),
303
+ y: y.get()
304
+ });
305
+ }
306
+ }, [
307
+ x,
308
+ y,
309
+ isPanning,
310
+ animationStage,
311
+ stopAllSceneMotion
312
+ ]);
313
+ const handleWheelZoom = useCallback((event) => {
314
+ if (animationStage < 2) {
315
+ event.preventDefault();
316
+ return;
317
+ }
318
+ event.preventDefault();
319
+ const isPinch = event.ctrlKey || event.metaKey;
320
+ const ZOOM_SENSITIVITY = event.deltaMode === WheelEvent.DOM_DELTA_LINE || Math.abs(event.deltaY) >= 100 ? MOUSE_WHEEL_ZOOM_SENSITIVITY : TRACKPAD_ZOOM_SENSITIVITY;
321
+ if (isPinch) {
322
+ const currentZoom = scale.get();
323
+ const nextZoom = Math.max(Math.min(currentZoom * (1 - event.deltaY * ZOOM_SENSITIVITY), MAX_ZOOM), MIN_ZOOM, window.innerWidth / sceneWidth * ZOOM_BOUND, window.innerHeight / sceneHeight * ZOOM_BOUND);
324
+ const rect = viewportRef.current?.getBoundingClientRect();
325
+ if (!rect) return;
326
+ const vpLeft = rect.left;
327
+ const vpTop = rect.top;
328
+ const viewportWidth = rect.width;
329
+ const viewportHeight = rect.height;
330
+ const cursorSceneX = (event.clientX - vpLeft - x.get()) / currentZoom;
331
+ const cursorSceneY = (event.clientY - vpTop - y.get()) / currentZoom;
332
+ let newPanX = event.clientX - vpLeft - cursorSceneX * nextZoom;
333
+ let newPanY = event.clientY - vpTop - cursorSceneY * nextZoom;
334
+ const minPanX = viewportWidth - sceneWidth * nextZoom;
335
+ const minPanY = viewportHeight - sceneHeight * nextZoom;
336
+ const maxPanX = 0;
337
+ const maxPanY = 0;
338
+ newPanX = Math.min(maxPanX, Math.max(minPanX, newPanX));
339
+ newPanY = Math.min(maxPanY, Math.max(minPanY, newPanY));
340
+ x.set(newPanX);
341
+ y.set(newPanY);
342
+ scale.set(nextZoom);
343
+ } else {
344
+ stopAllSceneMotion();
345
+ const scrollSpeed = 1;
346
+ const newPanX = x.get() - event.deltaX * scrollSpeed;
347
+ const newPanY = y.get() - event.deltaY * scrollSpeed;
348
+ const minPanX = windowWidth - sceneWidth * scale.get();
349
+ const maxPanX = 0;
350
+ const minPanY = windowHeight - sceneHeight * scale.get();
351
+ const maxPanY = 0;
352
+ const clampedPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);
353
+ const clampedPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);
354
+ x.set(clampedPanX);
355
+ y.set(clampedPanY);
356
+ }
357
+ }, [
358
+ scale,
359
+ MIN_ZOOM,
360
+ x,
361
+ y,
362
+ sceneWidth,
363
+ sceneHeight,
364
+ windowWidth,
365
+ windowHeight,
366
+ animationStage,
367
+ stopAllSceneMotion
368
+ ]);
369
+ useEffect(() => {
370
+ wheelHandlerRef.current = handleWheelZoom;
371
+ }, [handleWheelZoom]);
372
+ const handlePanToOffset = useCallback((offset, onComplete, zoom) => {
373
+ panToOffset({
374
+ x: -offset.x,
375
+ y: -offset.y
376
+ }, viewportRef, onComplete, zoom);
377
+ }, [panToOffset, viewportRef]);
378
+ return /* @__PURE__ */ jsx(CanvasWrapper, {
379
+ introProgress,
380
+ onIntroGrowComplete: startStage2,
381
+ skipIntro,
382
+ introContent,
383
+ loadingText,
384
+ introBackgroundGradient,
385
+ wrapperBackground,
386
+ canvasBoxGradient,
387
+ growTransition,
388
+ blurTransition,
389
+ children: /* @__PURE__ */ jsxs(CanvasProvider, {
390
+ x,
391
+ y,
392
+ scale,
393
+ isResetting,
394
+ maxZIndex,
395
+ setMaxZIndex,
396
+ animationStage,
397
+ nextTargetSection,
398
+ setNextTargetSection,
399
+ children: [animationStage >= 2 && /* @__PURE__ */ jsxs(Fragment, { children: [!toolbarConfig?.hidden && /* @__PURE__ */ jsx(toolbar_default, {
400
+ homeCoordinates: offsetHomeCoordinates,
401
+ config: toolbarConfig
402
+ }), hasNavbar && navItems && !navbarConfig?.hidden && /* @__PURE__ */ jsx(Navbar, {
403
+ panToOffset: handlePanToOffset,
404
+ onReset: onResetViewAndItems,
405
+ items: navItems,
406
+ config: navbarConfig
407
+ })] }), /* @__PURE__ */ jsx("div", {
408
+ ref: setViewportRef,
409
+ className: "relative h-full w-full touch-none select-none overflow-hidden",
410
+ style: {
411
+ touchAction: "none",
412
+ pointerEvents: animationStage >= 2 ? "auto" : "none",
413
+ overscrollBehavior: "contain"
414
+ },
415
+ onPointerDown: handlePointerDown,
416
+ onPointerMove: handlePointerMove,
417
+ onPointerUp: handlePointerUpOrCancel,
418
+ onPointerLeave: handlePointerUpOrCancel,
419
+ onPointerCancel: handlePointerUpOrCancel,
420
+ children: /* @__PURE__ */ jsxs(motion.div, {
421
+ ref: sceneRef,
422
+ className: "absolute z-20 origin-top-left",
423
+ initial: { opacity: 0 },
424
+ animate: { opacity: 1 },
425
+ transition: {
426
+ duration: .3,
427
+ ease: "easeIn"
428
+ },
429
+ style: {
430
+ width: `${sceneWidth}px`,
431
+ height: `${sceneHeight}px`,
432
+ x,
433
+ y,
434
+ scale,
435
+ willChange: mode !== "high" && (animationStage < 2 || isPanning) ? "transform" : "auto"
436
+ },
437
+ children: [canvasBackground !== void 0 ? canvasBackground : /* @__PURE__ */ jsx(Fragment, { children: animationStage >= 1 && mode === "high" ? /* @__PURE__ */ jsx(motion.div, {
438
+ initial: { opacity: 0 },
439
+ animate: { opacity: 1 },
440
+ transition: {
441
+ duration: .5,
442
+ ease: "easeIn"
443
+ },
444
+ children: /* @__PURE__ */ jsx(DefaultCanvasBackground, {})
445
+ }) : /* @__PURE__ */ jsx(DefaultCanvasBackground, {}) }), children]
446
+ })
447
+ })]
448
+ })
449
+ });
389
450
  };
390
- export default Canvas;
451
+ var canvas_default = Canvas;
452
+
453
+ //#endregion
454
+ export { canvas_default as default };
391
455
  //# sourceMappingURL=canvas.js.map