@hunterchen/canvas 0.11.0 → 0.12.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.
- package/dist/components/canvas/canvas.d.ts +3 -1
- package/dist/components/canvas/canvas.d.ts.map +1 -1
- package/dist/components/canvas/canvas.js +15 -5
- package/dist/components/canvas/canvas.js.map +1 -1
- package/dist/components/canvas/draggable.d.ts.map +1 -1
- package/dist/components/canvas/draggable.js +4 -2
- package/dist/components/canvas/draggable.js.map +1 -1
- package/dist/components/canvas/navbar/index.d.ts +4 -1
- package/dist/components/canvas/navbar/index.d.ts.map +1 -1
- package/dist/components/canvas/navbar/index.js +3 -3
- package/dist/components/canvas/navbar/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/canvas/canvas.tsx +19 -2
- package/src/components/canvas/draggable.tsx +4 -2
- package/src/components/canvas/navbar/index.tsx +5 -2
- package/src/index.ts +1 -0
- package/src/types/index.ts +12 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Transition } from "framer-motion";
|
|
2
2
|
import React, { type FC } from "react";
|
|
3
|
-
import type { NavItem, NavbarConfig, SectionCoordinates, ToolbarConfig } from "../../types";
|
|
3
|
+
import type { NavItem, NavbarConfig, SectionCoordinates, ToolbarConfig, ZoomConfig } from "../../types";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
interface Props {
|
|
6
6
|
homeCoordinates: SectionCoordinates;
|
|
@@ -35,6 +35,8 @@ interface Props {
|
|
|
35
35
|
toolbarConfig?: ToolbarConfig;
|
|
36
36
|
/** Navbar customization options */
|
|
37
37
|
navbarConfig?: NavbarConfig;
|
|
38
|
+
/** Zoom level overrides per screen size */
|
|
39
|
+
zoomConfig?: ZoomConfig;
|
|
38
40
|
}
|
|
39
41
|
declare const Canvas: FC<Props>;
|
|
40
42
|
export default Canvas;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../../../src/components/canvas/canvas.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,EAIZ,KAAK,EAAE,EAIR,MAAM,OAAO,CAAC;AA4Bf,OAAO,KAAK,EAEV,OAAO,EACP,YAAY,EACZ,kBAAkB,EAClB,aAAa,
|
|
1
|
+
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../../../src/components/canvas/canvas.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,EAIZ,KAAK,EAAE,EAIR,MAAM,OAAO,CAAC;AA4Bf,OAAO,KAAK,EAEV,OAAO,EACP,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACb,UAAU,EACX,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,KAAK;IACb,eAAe,EAAE,kBAAkB,CAAC;IACpC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAG1B,WAAW,CAAC,EAAC,MAAM,CAAC;IACpB,YAAY,CAAC,EAAC,MAAM,CAAC;IAGrB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IAGrB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,0BAA0B;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6BAA6B;IAC7B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,6BAA6B;IAC7B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,8CAA8C;IAC9C,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,qDAAqD;IACrD,cAAc,CAAC,EAAE,UAAU,CAAC;IAG5B,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAG9B,oCAAoC;IACpC,aAAa,CAAC,EAAE,aAAa,CAAC;IAG9B,mCAAmC;IACnC,YAAY,CAAC,EAAE,YAAY,CAAC;IAG5B,2CAA2C;IAC3C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAYD,QAAA,MAAM,MAAM,EAAE,EAAE,CAAC,KAAK,CAyrBrB,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -17,13 +17,21 @@ const stopAllMotion = (x, y, scale) => {
|
|
|
17
17
|
y.stop();
|
|
18
18
|
scale.stop();
|
|
19
19
|
};
|
|
20
|
-
const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introContent, loadingText, introBackgroundGradient, canvasBoxGradient, growTransition, blurTransition, panTransition, fadeTransition, canvasBackground, wrapperBackground, toolbarConfig, navbarConfig, canvasHeight, canvasWidth }) => {
|
|
20
|
+
const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introContent, loadingText, introBackgroundGradient, canvasBoxGradient, growTransition, blurTransition, panTransition, fadeTransition, canvasBackground, wrapperBackground, toolbarConfig, navbarConfig, canvasHeight, canvasWidth, zoomConfig }) => {
|
|
21
21
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions_default();
|
|
22
22
|
const { mode } = usePerformanceMode();
|
|
23
23
|
const hasNavbar = Boolean(navItems && navItems.length > 0);
|
|
24
24
|
const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;
|
|
25
25
|
const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;
|
|
26
|
-
const
|
|
26
|
+
const resolvedMinZooms = useMemo(() => zoomConfig?.minZooms ? {
|
|
27
|
+
...MIN_ZOOMS,
|
|
28
|
+
...zoomConfig.minZooms
|
|
29
|
+
} : MIN_ZOOMS, [zoomConfig?.minZooms]);
|
|
30
|
+
const resolvedResponsiveZoomMap = useMemo(() => zoomConfig?.responsiveZoomMap ? {
|
|
31
|
+
...RESPONSIVE_ZOOM_MAP,
|
|
32
|
+
...zoomConfig.responsiveZoomMap
|
|
33
|
+
} : RESPONSIVE_ZOOM_MAP, [zoomConfig?.responsiveZoomMap]);
|
|
34
|
+
const MIN_ZOOM = resolvedMinZooms[getScreenSizeEnum(windowWidth)];
|
|
27
35
|
const [isPanning, setIsPanning] = useState(false);
|
|
28
36
|
const [isSceneMoving, setIsSceneMoving] = useState(false);
|
|
29
37
|
const [panStartPoint, setPanStartPoint] = useState({
|
|
@@ -425,7 +433,7 @@ const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introC
|
|
|
425
433
|
setNextTargetSection(sectionId);
|
|
426
434
|
if (item.isHome) onResetViewAndItems();
|
|
427
435
|
else {
|
|
428
|
-
const zoom =
|
|
436
|
+
const zoom = resolvedResponsiveZoomMap[getScreenSizeEnum(windowWidth)];
|
|
429
437
|
handlePanToOffset(getSectionPanCoordinates({
|
|
430
438
|
windowDimensions: {
|
|
431
439
|
width: windowWidth,
|
|
@@ -448,7 +456,8 @@ const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introC
|
|
|
448
456
|
windowHeight,
|
|
449
457
|
onResetViewAndItems,
|
|
450
458
|
handlePanToOffset,
|
|
451
|
-
setNextTargetSection
|
|
459
|
+
setNextTargetSection,
|
|
460
|
+
resolvedResponsiveZoomMap
|
|
452
461
|
]),
|
|
453
462
|
children: [animationStage >= 2 && /* @__PURE__ */ jsxs(Fragment, { children: [!toolbarConfig?.hidden && /* @__PURE__ */ jsx(toolbar_default, {
|
|
454
463
|
homeCoordinates: offsetHomeCoordinates,
|
|
@@ -458,7 +467,8 @@ const Canvas = ({ children, homeCoordinates, navItems, skipIntro = false, introC
|
|
|
458
467
|
onReset: onResetViewAndItems,
|
|
459
468
|
items: navItems,
|
|
460
469
|
config: navbarConfig,
|
|
461
|
-
onRegisterNavigate: registerNavbarNavigate
|
|
470
|
+
onRegisterNavigate: registerNavbarNavigate,
|
|
471
|
+
responsiveZoomMap: resolvedResponsiveZoomMap
|
|
462
472
|
})] }), /* @__PURE__ */ jsx("div", {
|
|
463
473
|
ref: setViewportRef,
|
|
464
474
|
className: "relative h-full w-full touch-none select-none overflow-hidden",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","names":["useWindowDimensions","Toolbar"],"sources":["../../../src/components/canvas/canvas.tsx"],"sourcesContent":["import {\n motion,\n type MotionValue,\n type Point,\n useMotionValue,\n animate,\n useTransform,\n type Transition,\n} from \"framer-motion\";\nimport React, {\n useState,\n useRef,\n type PointerEvent,\n type FC,\n useEffect,\n useCallback,\n useMemo,\n} from \"react\";\nimport { CanvasProvider } from \"../../contexts/CanvasContext\";\nimport {\n calcInitialBoxWidth,\n canvasHeight,\n canvasWidth,\n getDistance,\n getMidpoint,\n getScreenSizeEnum,\n getSectionPanCoordinates,\n INTERACTIVE_SELECTOR,\n MAX_ZOOM,\n MIN_ZOOMS,\n panToOffsetScene,\n ZOOM_BOUND,\n} from \"../../lib/canvas\";\nimport {\n STAGE2_TRANSITION,\n FADE_TRANSITION,\n MOUSE_WHEEL_ZOOM_SENSITIVITY,\n TRACKPAD_ZOOM_SENSITIVITY,\n DEFAULT_CANVAS_WIDTH,\n DEFAULT_CANVAS_HEIGHT,\n RESPONSIVE_ZOOM_MAP,\n} from \"../../lib/constants\";\nimport useWindowDimensions from \"../../hooks/useWindowDimensions\";\nimport Navbar from \"./navbar\";\nimport Toolbar from \"./toolbar\";\nimport type {\n CanvasSection,\n NavItem,\n NavbarConfig,\n SectionCoordinates,\n ToolbarConfig,\n} from \"../../types\";\nimport { CanvasWrapper } from \"./wrapper\";\nimport { usePerformanceMode } from \"../../hooks/usePerformanceMode\";\nimport type { ReactNode } from \"react\";\nimport { DefaultCanvasBackground } from \"./backgrounds\";\n\ninterface Props {\n homeCoordinates: SectionCoordinates;\n children: React.ReactNode;\n\n // Optional height and with params, if omitted sizing will be 6000x4000\n canvasWidth?:number;\n canvasHeight?:number;\n\n // Navbar data (optional). If omitted, navbar is hidden.\n /** Array of navigation items for the navbar. If omitted, navbar is hidden. */\n navItems?: NavItem[];\n\n // ============== Intro Animation Customization ==============\n /** Disable intro animation entirely */\n skipIntro?: boolean;\n /** Custom intro content during loading */\n introContent?: ReactNode;\n /** Custom loading text */\n loadingText?: string;\n /** Background gradient for intro */\n introBackgroundGradient?: string;\n /** Canvas box gradient */\n canvasBoxGradient?: string;\n /** Custom grow transition */\n growTransition?: Transition;\n /** Custom blur transition */\n blurTransition?: Transition;\n /** Custom pan-to-home transition (stage 2) */\n panTransition?: Transition;\n /** Custom fade-in transition for the canvas scene */\n fadeTransition?: Transition;\n\n // ============== Background Customization ==============\n /** Custom canvas background. If not provided, uses DefaultCanvasBackground. */\n canvasBackground?: ReactNode;\n /** Custom wrapper/intro background. If not provided, uses introBackgroundGradient. */\n wrapperBackground?: ReactNode;\n\n // ============== Toolbar Customization ==============\n /** Toolbar customization options */\n toolbarConfig?: ToolbarConfig;\n\n // ============== Navbar Customization ==============\n /** Navbar customization options */\n navbarConfig?: NavbarConfig;\n}\n\nconst stopAllMotion = (\n x: MotionValue<number>,\n y: MotionValue<number>,\n scale: MotionValue<number>\n) => {\n x.stop();\n y.stop();\n scale.stop();\n};\n\nconst Canvas: FC<Props> = ({\n children,\n homeCoordinates,\n navItems,\n skipIntro = false,\n introContent,\n loadingText,\n introBackgroundGradient,\n canvasBoxGradient,\n growTransition,\n blurTransition,\n panTransition,\n fadeTransition,\n canvasBackground,\n wrapperBackground,\n toolbarConfig,\n navbarConfig,\n canvasHeight,\n canvasWidth,\n}) => {\n const { height: windowHeight, width: windowWidth } = useWindowDimensions();\n\n const { mode } = usePerformanceMode();\n\n const hasNavbar = Boolean(navItems && navItems.length > 0);\n\n const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;\n const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;\n\n const MIN_ZOOM = MIN_ZOOMS[getScreenSizeEnum(windowWidth)];\n\n // tracks if user is panning the screen\n const [isPanning, setIsPanning] = useState<boolean>(false);\n // this one is moving from scene control, not from user\n const [isSceneMoving, setIsSceneMoving] = useState<boolean>(false);\n const [panStartPoint, setPanStartPoint] = useState<Point>({ x: 0, y: 0 });\n const [initialPanOffsetOnDrag, setInitialPanOffsetOnDrag] = useState<Point>({\n x: 0,\n y: 0,\n });\n const [isResetting, setIsResetting] = useState<boolean>(false);\n const [maxZIndex, setMaxZIndex] = useState<number>(50);\n const [animationStage, setAnimationStage] = useState<number>(\n skipIntro ? 2 : 0\n ); // 0: initial, 1: finish grow, 2: pan to home\n const [nextTargetSection, setNextTargetSection] =\n useState<CanvasSection | null>(null);\n // Track if the intro (stage1 + stage2) is still running, to avoid accidental cancellation\n const isIntroAnimatingRef = useRef(!skipIntro);\n\n const initialBoxWidth = useMemo(\n () => calcInitialBoxWidth(windowWidth, windowHeight),\n [windowWidth, windowHeight]\n );\n\n const offsetHomeCoordinates = useMemo(\n () =>\n getSectionPanCoordinates({\n windowDimensions: { width: windowWidth, height: windowHeight },\n coords: homeCoordinates,\n targetZoom: 1,\n }),\n [homeCoordinates, windowWidth, windowHeight]\n );\n\n // When skipIntro, initialize at home position; otherwise start at origin for intro animation\n const x = useMotionValue(skipIntro ? offsetHomeCoordinates.x : 0);\n const y = useMotionValue(skipIntro ? offsetHomeCoordinates.y : 0);\n const scale = useMotionValue(skipIntro ? 1 : initialBoxWidth);\n\n const onResetViewAndItems = useCallback(\n (onComplete?: () => void): void => {\n setIsResetting(true);\n\n void panToOffsetScene(offsetHomeCoordinates, x, y, scale, 1).then(() => {\n setIsResetting(false);\n if (onComplete) onComplete();\n });\n },\n [offsetHomeCoordinates, x, y, scale]\n );\n\n // Shared intro progress (0->1) driven by CanvasWrapper\n const introProgress = useMotionValue(0);\n\n // Precompute final stage1 scale and offsets (snapshot dimensions once on mount)\n const stage1Targets = useMemo(() => {\n const finalScale = Math.max(\n (windowWidth || 0) / sceneWidth,\n (windowHeight || 0) / sceneHeight\n );\n const endX = (windowWidth - sceneWidth * finalScale) / 2;\n const endY = (windowHeight - sceneHeight * finalScale) / 2;\n return { finalScale, endX, endY };\n }, [windowWidth, windowHeight, sceneWidth, sceneHeight]);\n\n // Replace direct motion values with derived transforms during stage1\n const derivedScale = useTransform(\n introProgress,\n [0, 1],\n [initialBoxWidth, stage1Targets.finalScale]\n );\n const derivedX = useTransform(introProgress, [0, 1], [0, stage1Targets.endX]);\n const derivedY = useTransform(introProgress, [0, 1], [0, stage1Targets.endY]);\n\n // While intro (stage1) is running, bind x/y/scale to derived versions.\n useEffect(() => {\n const unsubscribeScale = derivedScale.on(\"change\", (v) => {\n if (animationStage === 0) scale.set(v);\n });\n const unsubscribeX = derivedX.on(\"change\", (v) => {\n if (animationStage === 0) x.set(v);\n });\n const unsubscribeY = derivedY.on(\"change\", (v) => {\n if (animationStage === 0) y.set(v);\n });\n return () => {\n unsubscribeScale();\n unsubscribeX();\n unsubscribeY();\n };\n }, [derivedScale, derivedX, derivedY, animationStage, scale, x, y]);\n\n // Merge custom panTransition with defaults\n const effectivePanTransition: Transition = useMemo(() => {\n if (!panTransition) return STAGE2_TRANSITION;\n return {\n ...STAGE2_TRANSITION,\n ...panTransition,\n };\n }, [panTransition]);\n\n // Kick off stage2 (pan to home) when grow completes (introProgress hits 1)\n const startStage2 = useCallback(() => {\n if (skipIntro) {\n x.set(offsetHomeCoordinates.x);\n y.set(offsetHomeCoordinates.y);\n scale.set(1);\n setAnimationStage(2);\n isIntroAnimatingRef.current = false;\n return;\n }\n\n setAnimationStage(1);\n\n Promise.all([\n animate(x, offsetHomeCoordinates.x, effectivePanTransition),\n animate(y, offsetHomeCoordinates.y, effectivePanTransition),\n animate(scale, 1, effectivePanTransition),\n ])\n .then(() => {\n setAnimationStage(2);\n isIntroAnimatingRef.current = false;\n })\n .catch(() => {\n isIntroAnimatingRef.current = false;\n });\n }, [offsetHomeCoordinates, x, y, scale, effectivePanTransition, skipIntro]);\n\n const viewportRef = useRef<HTMLDivElement>(null);\n const sceneRef = useRef<HTMLDivElement>(null);\n\n // Stable wheel listener wrapper that always calls the latest handler via ref\n const wheelHandlerRef = useRef<((e: WheelEvent) => void) | null>(null);\n const wheelWrapper = useCallback((e: WheelEvent) => {\n wheelHandlerRef.current?.(e);\n }, []);\n\n // Ensure wheel listener attaches when the element actually mounts (wrapper delays child mount)\n const setViewportRef = useCallback(\n (node: HTMLDivElement | null) => {\n // Clean up old listener if ref changes/unmounts\n if (viewportRef.current) {\n viewportRef.current.removeEventListener(\"wheel\", wheelWrapper);\n }\n viewportRef.current = node;\n if (node) {\n node.addEventListener(\"wheel\", wheelWrapper, { passive: false });\n }\n },\n [wheelWrapper]\n );\n\n const activePointersRef = useRef<Map<number, PointerEvent<HTMLDivElement>>>(\n new Map()\n );\n const initialPinchStateRef = useRef<{\n distance: number;\n midpoint: Point;\n zoom: number;\n panOffset: Point;\n } | null>(null);\n\n const panToOffset = useCallback(\n (\n offset: Point,\n viewportRef: React.RefObject<HTMLDivElement | null>,\n onComplete?: () => void,\n zoom?: number\n ): void => {\n if (!viewportRef.current) return;\n setIsSceneMoving(true);\n\n // Calculate bounds based on scene and viewport dimensions\n const viewportWidth = viewportRef.current.offsetWidth;\n const viewportHeight = viewportRef.current.offsetHeight;\n\n const minPanX = viewportWidth - sceneWidth * (zoom ?? 1);\n const maxPanX = 0;\n const minPanY = viewportHeight - sceneHeight * (zoom ?? 1);\n const maxPanY = 0;\n\n // Clamp the offset to keep the scene within bounds, shouldn't be needed but still implemented\n const clampedX = Math.min(Math.max(offset.x, minPanX), maxPanX);\n const clampedY = Math.min(Math.max(offset.y, minPanY), maxPanY);\n\n void panToOffsetScene(\n { x: clampedX, y: clampedY },\n x,\n y,\n scale,\n zoom\n ).then(() => {\n setIsSceneMoving(false);\n if (onComplete) onComplete();\n });\n },\n [sceneWidth, sceneHeight, x, y, scale]\n );\n\n // Guarded stop that ignores attempts during intro animations\n const stopAllSceneMotion = useCallback(() => {\n if (isIntroAnimatingRef.current) return; // ignore stops while intro runs\n stopAllMotion(x, y, scale);\n }, [x, y, scale]);\n\n const handlePointerDown = useCallback(\n (event: PointerEvent<HTMLDivElement>): void => {\n if (animationStage < 2) return; // ignore during intro animations\n activePointersRef.current.set(event.pointerId, event);\n (event.target as HTMLElement).setPointerCapture(event.pointerId);\n if (isResetting || isSceneMoving) return;\n stopAllSceneMotion();\n // pan with 1 pointer, pinch with 2 pointers\n if (activePointersRef.current.size === 1) {\n // do not pan from interactive elements\n const targetElement = event.target as HTMLElement;\n if (targetElement.closest(INTERACTIVE_SELECTOR)) {\n activePointersRef.current.delete(event.pointerId);\n (event.target as HTMLElement).releasePointerCapture(event.pointerId);\n return;\n }\n\n setIsPanning(true);\n setPanStartPoint({ x: event.clientX, y: event.clientY });\n setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });\n if (viewportRef.current) viewportRef.current.style.cursor = \"grabbing\";\n } else if (activePointersRef.current.size === 2) {\n setIsPanning(false);\n const pointers = Array.from(activePointersRef.current.values());\n initialPinchStateRef.current = {\n distance: getDistance(pointers[0]!, pointers[1]!),\n midpoint: getMidpoint(pointers[0]!, pointers[1]!),\n zoom: scale.get(),\n panOffset: { x: x.get(), y: y.get() },\n };\n }\n },\n [\n isResetting,\n isSceneMoving,\n setIsPanning,\n setPanStartPoint,\n setInitialPanOffsetOnDrag,\n x,\n y,\n scale,\n viewportRef,\n animationStage,\n stopAllSceneMotion,\n ]\n );\n\n const handlePointerMove = useCallback(\n (event: PointerEvent<HTMLDivElement>): void => {\n if (animationStage < 2) return;\n if (isPanning || activePointersRef.current.size >= 2) {\n stopAllSceneMotion();\n }\n if (!activePointersRef.current.has(event.pointerId) || isResetting)\n return;\n activePointersRef.current.set(event.pointerId, event);\n\n if (isPanning && activePointersRef.current.size === 1) {\n event.preventDefault();\n const deltaX = event.clientX - panStartPoint.x;\n const deltaY = event.clientY - panStartPoint.y;\n\n // UPDATE to use motion value\n const minPanX = windowWidth - sceneWidth * scale.get();\n const maxPanX = 0;\n const minPanY = windowHeight - sceneHeight * scale.get();\n const maxPanY = 0;\n\n const newX = Math.min(\n Math.max(initialPanOffsetOnDrag.x + deltaX, minPanX),\n maxPanX\n );\n const newY = Math.min(\n Math.max(initialPanOffsetOnDrag.y + deltaY, minPanY),\n maxPanY\n );\n x.set(newX);\n y.set(newY);\n } else if (\n activePointersRef.current.size >= 2 &&\n initialPinchStateRef.current\n ) {\n event.preventDefault();\n const pointers = Array.from(activePointersRef.current.values());\n const p1 = pointers[0]!;\n const p2 = pointers[1]!;\n\n const currentDistance = getDistance(p1, p2);\n const currentMidpoint = getMidpoint(p1, p2);\n\n const {\n distance: initialDistance,\n zoom: initialZoom,\n panOffset: initialPanOffsetPinch,\n } = initialPinchStateRef.current;\n\n if (initialDistance === 0) return;\n\n let newZoom = initialZoom * (currentDistance / initialDistance);\n newZoom = Math.max(\n (window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas\n (window.innerHeight / sceneHeight) * ZOOM_BOUND, // Ensure zoom is at least the height of the canvas\n Math.min(newZoom, 10),\n MIN_ZOOM // Ensure zoom is not less than MIN_ZOOM\n );\n\n const mx = currentMidpoint.x;\n const my = currentMidpoint.y;\n\n const minPanX = windowWidth - sceneWidth * newZoom;\n const maxPanX = 0;\n const minPanY = windowHeight - sceneHeight * newZoom;\n const maxPanY = 0;\n\n let newPanX =\n mx - ((mx - initialPanOffsetPinch.x) / initialZoom) * newZoom;\n let newPanY =\n my - ((my - initialPanOffsetPinch.y) / initialZoom) * newZoom;\n\n // Clamp pan to prevent leaving bounds\n newPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);\n newPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);\n\n scale.set(newZoom);\n x.set(newPanX);\n y.set(newPanY);\n }\n },\n [\n isPanning,\n isResetting,\n x,\n y,\n scale,\n panStartPoint.x,\n panStartPoint.y,\n windowWidth,\n sceneWidth,\n windowHeight,\n sceneHeight,\n initialPanOffsetOnDrag.x,\n initialPanOffsetOnDrag.y,\n MIN_ZOOM,\n animationStage,\n stopAllSceneMotion,\n ]\n );\n\n const handlePointerUpOrCancel = useCallback(\n (event: PointerEvent<HTMLDivElement>): void => {\n if (animationStage < 2) {\n event.preventDefault();\n return; // ignore pointer up during intro\n }\n stopAllSceneMotion();\n event.preventDefault();\n if ((event.target as HTMLElement).hasPointerCapture(event.pointerId)) {\n (event.target as HTMLElement).releasePointerCapture(event.pointerId);\n }\n activePointersRef.current.delete(event.pointerId);\n\n if (isPanning && activePointersRef.current.size < 1) {\n setIsPanning(false);\n if (viewportRef.current)\n viewportRef.current.style.cursor = \"url('/customcursor.svg'), grab\";\n }\n\n if (initialPinchStateRef.current && activePointersRef.current.size < 2) {\n initialPinchStateRef.current = null;\n }\n\n if (\n !isPanning &&\n activePointersRef.current.size === 1 &&\n !initialPinchStateRef.current\n ) {\n const lastPointer = Array.from(activePointersRef.current.values())[0]!;\n setIsPanning(true);\n setPanStartPoint({ x: lastPointer.clientX, y: lastPointer.clientY });\n setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });\n }\n },\n [x, y, isPanning, animationStage, stopAllSceneMotion]\n );\n\n const handleWheelZoom = useCallback(\n (event: WheelEvent) => {\n if (animationStage < 2) {\n event.preventDefault();\n return; // block wheel interaction during intro animations\n }\n event.preventDefault();\n // pinch gesture on track\n const isPinch = event.ctrlKey || event.metaKey;\n const isMouseWheelZoom =\n event.deltaMode === WheelEvent.DOM_DELTA_LINE ||\n Math.abs(event.deltaY) >= 100;\n\n // mouse wheel zoom and track pad zoom have different sensitivities\n const ZOOM_SENSITIVITY = isMouseWheelZoom\n ? MOUSE_WHEEL_ZOOM_SENSITIVITY\n : TRACKPAD_ZOOM_SENSITIVITY;\n\n if (isPinch) {\n const currentZoom = scale.get();\n const nextZoom = Math.max(\n Math.min(\n currentZoom * (1 - event.deltaY * ZOOM_SENSITIVITY),\n MAX_ZOOM\n ),\n MIN_ZOOM,\n (window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas\n (window.innerHeight / sceneHeight) * ZOOM_BOUND // Ensure zoom is at least the height of the canvas\n );\n\n const rect = viewportRef.current?.getBoundingClientRect();\n\n if (!rect) return;\n\n const vpLeft = rect.left;\n const vpTop = rect.top;\n const viewportWidth = rect.width;\n const viewportHeight = rect.height;\n\n const cursorSceneX = (event.clientX - vpLeft - x.get()) / currentZoom;\n const cursorSceneY = (event.clientY - vpTop - y.get()) / currentZoom;\n\n let newPanX = event.clientX - vpLeft - cursorSceneX * nextZoom;\n let newPanY = event.clientY - vpTop - cursorSceneY * nextZoom;\n\n const minPanX = viewportWidth - sceneWidth * nextZoom;\n const minPanY = viewportHeight - sceneHeight * nextZoom;\n const maxPanX = 0;\n const maxPanY = 0;\n\n newPanX = Math.min(maxPanX, Math.max(minPanX, newPanX));\n newPanY = Math.min(maxPanY, Math.max(minPanY, newPanY));\n\n x.set(newPanX);\n y.set(newPanY);\n scale.set(nextZoom);\n } else {\n stopAllSceneMotion();\n\n const scrollSpeed = 1;\n const newPanX = x.get() - event.deltaX * scrollSpeed;\n const newPanY = y.get() - event.deltaY * scrollSpeed;\n\n const minPanX = windowWidth - sceneWidth * scale.get();\n const maxPanX = 0;\n const minPanY = windowHeight - sceneHeight * scale.get();\n const maxPanY = 0;\n\n const clampedPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);\n const clampedPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);\n\n x.set(clampedPanX);\n y.set(clampedPanY);\n }\n },\n [\n scale,\n MIN_ZOOM,\n x,\n y,\n sceneWidth,\n sceneHeight,\n windowWidth,\n windowHeight,\n animationStage,\n stopAllSceneMotion,\n ]\n );\n\n // Keep the wheel handler ref pointing to the latest implementation\n useEffect(() => {\n wheelHandlerRef.current = handleWheelZoom;\n }, [handleWheelZoom]);\n\n const handlePanToOffset = useCallback(\n (\n offset: { x: number; y: number },\n onComplete?: () => void,\n zoom?: number\n ) => {\n panToOffset(\n {\n x: -offset.x,\n y: -offset.y,\n },\n viewportRef,\n onComplete,\n zoom\n );\n },\n [panToOffset, viewportRef]\n );\n\n // --- navigateToSection: delegates to Navbar when mounted, falls back to basic pan ---\n const navbarNavigateRef = useRef<((sectionId: string) => void) | null>(null);\n\n const registerNavbarNavigate = useCallback(\n (handler: ((sectionId: string) => void) | null) => {\n navbarNavigateRef.current = handler;\n },\n []\n );\n\n const navigateToSection = useCallback(\n (sectionId: string) => {\n if (navbarNavigateRef.current) {\n // Navbar is mounted — delegate for full behavior (highlight, pre-render, etc.)\n navbarNavigateRef.current(sectionId);\n } else if (navItems) {\n // Fallback: no navbar, do basic pan\n const item = navItems.find((i) => i.id === sectionId);\n if (!item) return;\n\n setNextTargetSection(sectionId);\n\n if (item.isHome) {\n onResetViewAndItems();\n } else {\n const zoom = RESPONSIVE_ZOOM_MAP[getScreenSizeEnum(windowWidth)];\n const panCoords = getSectionPanCoordinates({\n windowDimensions: { width: windowWidth, height: windowHeight },\n coords: {\n x: item.x,\n y: item.y,\n width: item.width,\n height: item.height,\n },\n targetZoom: zoom,\n negative: true,\n });\n handlePanToOffset(panCoords, undefined, zoom);\n }\n }\n },\n [\n navItems,\n windowWidth,\n windowHeight,\n onResetViewAndItems,\n handlePanToOffset,\n setNextTargetSection,\n ]\n );\n\n return (\n <CanvasWrapper\n introProgress={introProgress}\n onIntroGrowComplete={startStage2}\n skipIntro={skipIntro}\n introContent={introContent}\n loadingText={loadingText}\n introBackgroundGradient={introBackgroundGradient}\n wrapperBackground={wrapperBackground}\n canvasBoxGradient={canvasBoxGradient}\n growTransition={growTransition}\n blurTransition={blurTransition}\n >\n <CanvasProvider\n x={x}\n y={y}\n scale={scale}\n isResetting={isResetting}\n maxZIndex={maxZIndex}\n setMaxZIndex={setMaxZIndex}\n animationStage={animationStage}\n nextTargetSection={nextTargetSection}\n setNextTargetSection={setNextTargetSection}\n navigateToSection={navigateToSection}\n >\n {animationStage >= 2 && (\n <>\n {!toolbarConfig?.hidden && (\n <Toolbar\n homeCoordinates={offsetHomeCoordinates}\n config={toolbarConfig}\n />\n )}\n {hasNavbar && navItems && !navbarConfig?.hidden && (\n <Navbar\n panToOffset={handlePanToOffset}\n onReset={onResetViewAndItems}\n items={navItems}\n config={navbarConfig}\n onRegisterNavigate={registerNavbarNavigate}\n />\n )}\n </>\n )}\n <div\n ref={setViewportRef}\n className=\"relative h-full w-full touch-none select-none overflow-hidden\"\n style={{\n touchAction: \"none\",\n pointerEvents: animationStage >= 2 ? \"auto\" : \"none\",\n overscrollBehavior: \"contain\",\n }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUpOrCancel}\n onPointerLeave={handlePointerUpOrCancel}\n onPointerCancel={handlePointerUpOrCancel}\n >\n <motion.div\n ref={sceneRef}\n className=\"absolute z-20 origin-top-left\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={fadeTransition ?? FADE_TRANSITION}\n style={{\n width: `${sceneWidth}px`,\n height: `${sceneHeight}px`,\n x,\n y,\n scale,\n willChange:\n mode !== \"high\" && (animationStage < 2 || isPanning)\n ? \"transform\"\n : \"auto\",\n }}\n >\n {/* Canvas Background - customizable or default */}\n {canvasBackground !== undefined ? (\n canvasBackground\n ) : (\n <>\n {animationStage >= 1 && mode === \"high\" ? (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={{ duration: 0.5, ease: \"easeIn\" }}\n >\n <DefaultCanvasBackground />\n </motion.div>\n ) : (\n <DefaultCanvasBackground />\n )}\n </>\n )}\n {children}\n </motion.div>\n </div>\n </CanvasProvider>\n </CanvasWrapper>\n );\n};\n\nexport default Canvas;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"canvas.js","names":["useWindowDimensions","Toolbar"],"sources":["../../../src/components/canvas/canvas.tsx"],"sourcesContent":["import {\n motion,\n type MotionValue,\n type Point,\n useMotionValue,\n animate,\n useTransform,\n type Transition,\n} from \"framer-motion\";\nimport React, {\n useState,\n useRef,\n type PointerEvent,\n type FC,\n useEffect,\n useCallback,\n useMemo,\n} from \"react\";\nimport { CanvasProvider } from \"../../contexts/CanvasContext\";\nimport {\n calcInitialBoxWidth,\n canvasHeight,\n canvasWidth,\n getDistance,\n getMidpoint,\n getScreenSizeEnum,\n getSectionPanCoordinates,\n INTERACTIVE_SELECTOR,\n MAX_ZOOM,\n MIN_ZOOMS,\n panToOffsetScene,\n ZOOM_BOUND,\n} from \"../../lib/canvas\";\nimport {\n STAGE2_TRANSITION,\n FADE_TRANSITION,\n MOUSE_WHEEL_ZOOM_SENSITIVITY,\n TRACKPAD_ZOOM_SENSITIVITY,\n DEFAULT_CANVAS_WIDTH,\n DEFAULT_CANVAS_HEIGHT,\n RESPONSIVE_ZOOM_MAP,\n} from \"../../lib/constants\";\nimport useWindowDimensions from \"../../hooks/useWindowDimensions\";\nimport Navbar from \"./navbar\";\nimport Toolbar from \"./toolbar\";\nimport type {\n CanvasSection,\n NavItem,\n NavbarConfig,\n SectionCoordinates,\n ToolbarConfig,\n ZoomConfig,\n} from \"../../types\";\nimport { CanvasWrapper } from \"./wrapper\";\nimport { usePerformanceMode } from \"../../hooks/usePerformanceMode\";\nimport type { ReactNode } from \"react\";\nimport { DefaultCanvasBackground } from \"./backgrounds\";\n\ninterface Props {\n homeCoordinates: SectionCoordinates;\n children: React.ReactNode;\n\n // Optional height and with params, if omitted sizing will be 6000x4000\n canvasWidth?:number;\n canvasHeight?:number;\n\n // Navbar data (optional). If omitted, navbar is hidden.\n /** Array of navigation items for the navbar. If omitted, navbar is hidden. */\n navItems?: NavItem[];\n\n // ============== Intro Animation Customization ==============\n /** Disable intro animation entirely */\n skipIntro?: boolean;\n /** Custom intro content during loading */\n introContent?: ReactNode;\n /** Custom loading text */\n loadingText?: string;\n /** Background gradient for intro */\n introBackgroundGradient?: string;\n /** Canvas box gradient */\n canvasBoxGradient?: string;\n /** Custom grow transition */\n growTransition?: Transition;\n /** Custom blur transition */\n blurTransition?: Transition;\n /** Custom pan-to-home transition (stage 2) */\n panTransition?: Transition;\n /** Custom fade-in transition for the canvas scene */\n fadeTransition?: Transition;\n\n // ============== Background Customization ==============\n /** Custom canvas background. If not provided, uses DefaultCanvasBackground. */\n canvasBackground?: ReactNode;\n /** Custom wrapper/intro background. If not provided, uses introBackgroundGradient. */\n wrapperBackground?: ReactNode;\n\n // ============== Toolbar Customization ==============\n /** Toolbar customization options */\n toolbarConfig?: ToolbarConfig;\n\n // ============== Navbar Customization ==============\n /** Navbar customization options */\n navbarConfig?: NavbarConfig;\n\n // ============== Zoom Customization ==============\n /** Zoom level overrides per screen size */\n zoomConfig?: ZoomConfig;\n}\n\nconst stopAllMotion = (\n x: MotionValue<number>,\n y: MotionValue<number>,\n scale: MotionValue<number>\n) => {\n x.stop();\n y.stop();\n scale.stop();\n};\n\nconst Canvas: FC<Props> = ({\n children,\n homeCoordinates,\n navItems,\n skipIntro = false,\n introContent,\n loadingText,\n introBackgroundGradient,\n canvasBoxGradient,\n growTransition,\n blurTransition,\n panTransition,\n fadeTransition,\n canvasBackground,\n wrapperBackground,\n toolbarConfig,\n navbarConfig,\n canvasHeight,\n canvasWidth,\n zoomConfig,\n}) => {\n const { height: windowHeight, width: windowWidth } = useWindowDimensions();\n\n const { mode } = usePerformanceMode();\n\n const hasNavbar = Boolean(navItems && navItems.length > 0);\n\n const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;\n const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;\n\n const resolvedMinZooms = useMemo(\n () => zoomConfig?.minZooms ? { ...MIN_ZOOMS, ...zoomConfig.minZooms } : MIN_ZOOMS,\n [zoomConfig?.minZooms]\n );\n const resolvedResponsiveZoomMap = useMemo(\n () => zoomConfig?.responsiveZoomMap ? { ...RESPONSIVE_ZOOM_MAP, ...zoomConfig.responsiveZoomMap } : RESPONSIVE_ZOOM_MAP,\n [zoomConfig?.responsiveZoomMap]\n );\n\n const MIN_ZOOM = resolvedMinZooms[getScreenSizeEnum(windowWidth)];\n\n // tracks if user is panning the screen\n const [isPanning, setIsPanning] = useState<boolean>(false);\n // this one is moving from scene control, not from user\n const [isSceneMoving, setIsSceneMoving] = useState<boolean>(false);\n const [panStartPoint, setPanStartPoint] = useState<Point>({ x: 0, y: 0 });\n const [initialPanOffsetOnDrag, setInitialPanOffsetOnDrag] = useState<Point>({\n x: 0,\n y: 0,\n });\n const [isResetting, setIsResetting] = useState<boolean>(false);\n const [maxZIndex, setMaxZIndex] = useState<number>(50);\n const [animationStage, setAnimationStage] = useState<number>(\n skipIntro ? 2 : 0\n ); // 0: initial, 1: finish grow, 2: pan to home\n const [nextTargetSection, setNextTargetSection] =\n useState<CanvasSection | null>(null);\n // Track if the intro (stage1 + stage2) is still running, to avoid accidental cancellation\n const isIntroAnimatingRef = useRef(!skipIntro);\n\n const initialBoxWidth = useMemo(\n () => calcInitialBoxWidth(windowWidth, windowHeight),\n [windowWidth, windowHeight]\n );\n\n const offsetHomeCoordinates = useMemo(\n () =>\n getSectionPanCoordinates({\n windowDimensions: { width: windowWidth, height: windowHeight },\n coords: homeCoordinates,\n targetZoom: 1,\n }),\n [homeCoordinates, windowWidth, windowHeight]\n );\n\n // When skipIntro, initialize at home position; otherwise start at origin for intro animation\n const x = useMotionValue(skipIntro ? offsetHomeCoordinates.x : 0);\n const y = useMotionValue(skipIntro ? offsetHomeCoordinates.y : 0);\n const scale = useMotionValue(skipIntro ? 1 : initialBoxWidth);\n\n const onResetViewAndItems = useCallback(\n (onComplete?: () => void): void => {\n setIsResetting(true);\n\n void panToOffsetScene(offsetHomeCoordinates, x, y, scale, 1).then(() => {\n setIsResetting(false);\n if (onComplete) onComplete();\n });\n },\n [offsetHomeCoordinates, x, y, scale]\n );\n\n // Shared intro progress (0->1) driven by CanvasWrapper\n const introProgress = useMotionValue(0);\n\n // Precompute final stage1 scale and offsets (snapshot dimensions once on mount)\n const stage1Targets = useMemo(() => {\n const finalScale = Math.max(\n (windowWidth || 0) / sceneWidth,\n (windowHeight || 0) / sceneHeight\n );\n const endX = (windowWidth - sceneWidth * finalScale) / 2;\n const endY = (windowHeight - sceneHeight * finalScale) / 2;\n return { finalScale, endX, endY };\n }, [windowWidth, windowHeight, sceneWidth, sceneHeight]);\n\n // Replace direct motion values with derived transforms during stage1\n const derivedScale = useTransform(\n introProgress,\n [0, 1],\n [initialBoxWidth, stage1Targets.finalScale]\n );\n const derivedX = useTransform(introProgress, [0, 1], [0, stage1Targets.endX]);\n const derivedY = useTransform(introProgress, [0, 1], [0, stage1Targets.endY]);\n\n // While intro (stage1) is running, bind x/y/scale to derived versions.\n useEffect(() => {\n const unsubscribeScale = derivedScale.on(\"change\", (v) => {\n if (animationStage === 0) scale.set(v);\n });\n const unsubscribeX = derivedX.on(\"change\", (v) => {\n if (animationStage === 0) x.set(v);\n });\n const unsubscribeY = derivedY.on(\"change\", (v) => {\n if (animationStage === 0) y.set(v);\n });\n return () => {\n unsubscribeScale();\n unsubscribeX();\n unsubscribeY();\n };\n }, [derivedScale, derivedX, derivedY, animationStage, scale, x, y]);\n\n // Merge custom panTransition with defaults\n const effectivePanTransition: Transition = useMemo(() => {\n if (!panTransition) return STAGE2_TRANSITION;\n return {\n ...STAGE2_TRANSITION,\n ...panTransition,\n };\n }, [panTransition]);\n\n // Kick off stage2 (pan to home) when grow completes (introProgress hits 1)\n const startStage2 = useCallback(() => {\n if (skipIntro) {\n x.set(offsetHomeCoordinates.x);\n y.set(offsetHomeCoordinates.y);\n scale.set(1);\n setAnimationStage(2);\n isIntroAnimatingRef.current = false;\n return;\n }\n\n setAnimationStage(1);\n\n Promise.all([\n animate(x, offsetHomeCoordinates.x, effectivePanTransition),\n animate(y, offsetHomeCoordinates.y, effectivePanTransition),\n animate(scale, 1, effectivePanTransition),\n ])\n .then(() => {\n setAnimationStage(2);\n isIntroAnimatingRef.current = false;\n })\n .catch(() => {\n isIntroAnimatingRef.current = false;\n });\n }, [offsetHomeCoordinates, x, y, scale, effectivePanTransition, skipIntro]);\n\n const viewportRef = useRef<HTMLDivElement>(null);\n const sceneRef = useRef<HTMLDivElement>(null);\n\n // Stable wheel listener wrapper that always calls the latest handler via ref\n const wheelHandlerRef = useRef<((e: WheelEvent) => void) | null>(null);\n const wheelWrapper = useCallback((e: WheelEvent) => {\n wheelHandlerRef.current?.(e);\n }, []);\n\n // Ensure wheel listener attaches when the element actually mounts (wrapper delays child mount)\n const setViewportRef = useCallback(\n (node: HTMLDivElement | null) => {\n // Clean up old listener if ref changes/unmounts\n if (viewportRef.current) {\n viewportRef.current.removeEventListener(\"wheel\", wheelWrapper);\n }\n viewportRef.current = node;\n if (node) {\n node.addEventListener(\"wheel\", wheelWrapper, { passive: false });\n }\n },\n [wheelWrapper]\n );\n\n const activePointersRef = useRef<Map<number, PointerEvent<HTMLDivElement>>>(\n new Map()\n );\n const initialPinchStateRef = useRef<{\n distance: number;\n midpoint: Point;\n zoom: number;\n panOffset: Point;\n } | null>(null);\n\n const panToOffset = useCallback(\n (\n offset: Point,\n viewportRef: React.RefObject<HTMLDivElement | null>,\n onComplete?: () => void,\n zoom?: number\n ): void => {\n if (!viewportRef.current) return;\n setIsSceneMoving(true);\n\n // Calculate bounds based on scene and viewport dimensions\n const viewportWidth = viewportRef.current.offsetWidth;\n const viewportHeight = viewportRef.current.offsetHeight;\n\n const minPanX = viewportWidth - sceneWidth * (zoom ?? 1);\n const maxPanX = 0;\n const minPanY = viewportHeight - sceneHeight * (zoom ?? 1);\n const maxPanY = 0;\n\n // Clamp the offset to keep the scene within bounds, shouldn't be needed but still implemented\n const clampedX = Math.min(Math.max(offset.x, minPanX), maxPanX);\n const clampedY = Math.min(Math.max(offset.y, minPanY), maxPanY);\n\n void panToOffsetScene(\n { x: clampedX, y: clampedY },\n x,\n y,\n scale,\n zoom\n ).then(() => {\n setIsSceneMoving(false);\n if (onComplete) onComplete();\n });\n },\n [sceneWidth, sceneHeight, x, y, scale]\n );\n\n // Guarded stop that ignores attempts during intro animations\n const stopAllSceneMotion = useCallback(() => {\n if (isIntroAnimatingRef.current) return; // ignore stops while intro runs\n stopAllMotion(x, y, scale);\n }, [x, y, scale]);\n\n const handlePointerDown = useCallback(\n (event: PointerEvent<HTMLDivElement>): void => {\n if (animationStage < 2) return; // ignore during intro animations\n activePointersRef.current.set(event.pointerId, event);\n (event.target as HTMLElement).setPointerCapture(event.pointerId);\n if (isResetting || isSceneMoving) return;\n stopAllSceneMotion();\n // pan with 1 pointer, pinch with 2 pointers\n if (activePointersRef.current.size === 1) {\n // do not pan from interactive elements\n const targetElement = event.target as HTMLElement;\n if (targetElement.closest(INTERACTIVE_SELECTOR)) {\n activePointersRef.current.delete(event.pointerId);\n (event.target as HTMLElement).releasePointerCapture(event.pointerId);\n return;\n }\n\n setIsPanning(true);\n setPanStartPoint({ x: event.clientX, y: event.clientY });\n setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });\n if (viewportRef.current) viewportRef.current.style.cursor = \"grabbing\";\n } else if (activePointersRef.current.size === 2) {\n setIsPanning(false);\n const pointers = Array.from(activePointersRef.current.values());\n initialPinchStateRef.current = {\n distance: getDistance(pointers[0]!, pointers[1]!),\n midpoint: getMidpoint(pointers[0]!, pointers[1]!),\n zoom: scale.get(),\n panOffset: { x: x.get(), y: y.get() },\n };\n }\n },\n [\n isResetting,\n isSceneMoving,\n setIsPanning,\n setPanStartPoint,\n setInitialPanOffsetOnDrag,\n x,\n y,\n scale,\n viewportRef,\n animationStage,\n stopAllSceneMotion,\n ]\n );\n\n const handlePointerMove = useCallback(\n (event: PointerEvent<HTMLDivElement>): void => {\n if (animationStage < 2) return;\n if (isPanning || activePointersRef.current.size >= 2) {\n stopAllSceneMotion();\n }\n if (!activePointersRef.current.has(event.pointerId) || isResetting)\n return;\n activePointersRef.current.set(event.pointerId, event);\n\n if (isPanning && activePointersRef.current.size === 1) {\n event.preventDefault();\n const deltaX = event.clientX - panStartPoint.x;\n const deltaY = event.clientY - panStartPoint.y;\n\n // UPDATE to use motion value\n const minPanX = windowWidth - sceneWidth * scale.get();\n const maxPanX = 0;\n const minPanY = windowHeight - sceneHeight * scale.get();\n const maxPanY = 0;\n\n const newX = Math.min(\n Math.max(initialPanOffsetOnDrag.x + deltaX, minPanX),\n maxPanX\n );\n const newY = Math.min(\n Math.max(initialPanOffsetOnDrag.y + deltaY, minPanY),\n maxPanY\n );\n x.set(newX);\n y.set(newY);\n } else if (\n activePointersRef.current.size >= 2 &&\n initialPinchStateRef.current\n ) {\n event.preventDefault();\n const pointers = Array.from(activePointersRef.current.values());\n const p1 = pointers[0]!;\n const p2 = pointers[1]!;\n\n const currentDistance = getDistance(p1, p2);\n const currentMidpoint = getMidpoint(p1, p2);\n\n const {\n distance: initialDistance,\n zoom: initialZoom,\n panOffset: initialPanOffsetPinch,\n } = initialPinchStateRef.current;\n\n if (initialDistance === 0) return;\n\n let newZoom = initialZoom * (currentDistance / initialDistance);\n newZoom = Math.max(\n (window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas\n (window.innerHeight / sceneHeight) * ZOOM_BOUND, // Ensure zoom is at least the height of the canvas\n Math.min(newZoom, 10),\n MIN_ZOOM // Ensure zoom is not less than MIN_ZOOM\n );\n\n const mx = currentMidpoint.x;\n const my = currentMidpoint.y;\n\n const minPanX = windowWidth - sceneWidth * newZoom;\n const maxPanX = 0;\n const minPanY = windowHeight - sceneHeight * newZoom;\n const maxPanY = 0;\n\n let newPanX =\n mx - ((mx - initialPanOffsetPinch.x) / initialZoom) * newZoom;\n let newPanY =\n my - ((my - initialPanOffsetPinch.y) / initialZoom) * newZoom;\n\n // Clamp pan to prevent leaving bounds\n newPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);\n newPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);\n\n scale.set(newZoom);\n x.set(newPanX);\n y.set(newPanY);\n }\n },\n [\n isPanning,\n isResetting,\n x,\n y,\n scale,\n panStartPoint.x,\n panStartPoint.y,\n windowWidth,\n sceneWidth,\n windowHeight,\n sceneHeight,\n initialPanOffsetOnDrag.x,\n initialPanOffsetOnDrag.y,\n MIN_ZOOM,\n animationStage,\n stopAllSceneMotion,\n ]\n );\n\n const handlePointerUpOrCancel = useCallback(\n (event: PointerEvent<HTMLDivElement>): void => {\n if (animationStage < 2) {\n event.preventDefault();\n return; // ignore pointer up during intro\n }\n stopAllSceneMotion();\n event.preventDefault();\n if ((event.target as HTMLElement).hasPointerCapture(event.pointerId)) {\n (event.target as HTMLElement).releasePointerCapture(event.pointerId);\n }\n activePointersRef.current.delete(event.pointerId);\n\n if (isPanning && activePointersRef.current.size < 1) {\n setIsPanning(false);\n if (viewportRef.current)\n viewportRef.current.style.cursor = \"url('/customcursor.svg'), grab\";\n }\n\n if (initialPinchStateRef.current && activePointersRef.current.size < 2) {\n initialPinchStateRef.current = null;\n }\n\n if (\n !isPanning &&\n activePointersRef.current.size === 1 &&\n !initialPinchStateRef.current\n ) {\n const lastPointer = Array.from(activePointersRef.current.values())[0]!;\n setIsPanning(true);\n setPanStartPoint({ x: lastPointer.clientX, y: lastPointer.clientY });\n setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });\n }\n },\n [x, y, isPanning, animationStage, stopAllSceneMotion]\n );\n\n const handleWheelZoom = useCallback(\n (event: WheelEvent) => {\n if (animationStage < 2) {\n event.preventDefault();\n return; // block wheel interaction during intro animations\n }\n event.preventDefault();\n // pinch gesture on track\n const isPinch = event.ctrlKey || event.metaKey;\n const isMouseWheelZoom =\n event.deltaMode === WheelEvent.DOM_DELTA_LINE ||\n Math.abs(event.deltaY) >= 100;\n\n // mouse wheel zoom and track pad zoom have different sensitivities\n const ZOOM_SENSITIVITY = isMouseWheelZoom\n ? MOUSE_WHEEL_ZOOM_SENSITIVITY\n : TRACKPAD_ZOOM_SENSITIVITY;\n\n if (isPinch) {\n const currentZoom = scale.get();\n const nextZoom = Math.max(\n Math.min(\n currentZoom * (1 - event.deltaY * ZOOM_SENSITIVITY),\n MAX_ZOOM\n ),\n MIN_ZOOM,\n (window.innerWidth / sceneWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas\n (window.innerHeight / sceneHeight) * ZOOM_BOUND // Ensure zoom is at least the height of the canvas\n );\n\n const rect = viewportRef.current?.getBoundingClientRect();\n\n if (!rect) return;\n\n const vpLeft = rect.left;\n const vpTop = rect.top;\n const viewportWidth = rect.width;\n const viewportHeight = rect.height;\n\n const cursorSceneX = (event.clientX - vpLeft - x.get()) / currentZoom;\n const cursorSceneY = (event.clientY - vpTop - y.get()) / currentZoom;\n\n let newPanX = event.clientX - vpLeft - cursorSceneX * nextZoom;\n let newPanY = event.clientY - vpTop - cursorSceneY * nextZoom;\n\n const minPanX = viewportWidth - sceneWidth * nextZoom;\n const minPanY = viewportHeight - sceneHeight * nextZoom;\n const maxPanX = 0;\n const maxPanY = 0;\n\n newPanX = Math.min(maxPanX, Math.max(minPanX, newPanX));\n newPanY = Math.min(maxPanY, Math.max(minPanY, newPanY));\n\n x.set(newPanX);\n y.set(newPanY);\n scale.set(nextZoom);\n } else {\n stopAllSceneMotion();\n\n const scrollSpeed = 1;\n const newPanX = x.get() - event.deltaX * scrollSpeed;\n const newPanY = y.get() - event.deltaY * scrollSpeed;\n\n const minPanX = windowWidth - sceneWidth * scale.get();\n const maxPanX = 0;\n const minPanY = windowHeight - sceneHeight * scale.get();\n const maxPanY = 0;\n\n const clampedPanX = Math.min(Math.max(newPanX, minPanX), maxPanX);\n const clampedPanY = Math.min(Math.max(newPanY, minPanY), maxPanY);\n\n x.set(clampedPanX);\n y.set(clampedPanY);\n }\n },\n [\n scale,\n MIN_ZOOM,\n x,\n y,\n sceneWidth,\n sceneHeight,\n windowWidth,\n windowHeight,\n animationStage,\n stopAllSceneMotion,\n ]\n );\n\n // Keep the wheel handler ref pointing to the latest implementation\n useEffect(() => {\n wheelHandlerRef.current = handleWheelZoom;\n }, [handleWheelZoom]);\n\n const handlePanToOffset = useCallback(\n (\n offset: { x: number; y: number },\n onComplete?: () => void,\n zoom?: number\n ) => {\n panToOffset(\n {\n x: -offset.x,\n y: -offset.y,\n },\n viewportRef,\n onComplete,\n zoom\n );\n },\n [panToOffset, viewportRef]\n );\n\n // --- navigateToSection: delegates to Navbar when mounted, falls back to basic pan ---\n const navbarNavigateRef = useRef<((sectionId: string) => void) | null>(null);\n\n const registerNavbarNavigate = useCallback(\n (handler: ((sectionId: string) => void) | null) => {\n navbarNavigateRef.current = handler;\n },\n []\n );\n\n const navigateToSection = useCallback(\n (sectionId: string) => {\n if (navbarNavigateRef.current) {\n // Navbar is mounted — delegate for full behavior (highlight, pre-render, etc.)\n navbarNavigateRef.current(sectionId);\n } else if (navItems) {\n // Fallback: no navbar, do basic pan\n const item = navItems.find((i) => i.id === sectionId);\n if (!item) return;\n\n setNextTargetSection(sectionId);\n\n if (item.isHome) {\n onResetViewAndItems();\n } else {\n const zoom = resolvedResponsiveZoomMap[getScreenSizeEnum(windowWidth)];\n const panCoords = getSectionPanCoordinates({\n windowDimensions: { width: windowWidth, height: windowHeight },\n coords: {\n x: item.x,\n y: item.y,\n width: item.width,\n height: item.height,\n },\n targetZoom: zoom,\n negative: true,\n });\n handlePanToOffset(panCoords, undefined, zoom);\n }\n }\n },\n [\n navItems,\n windowWidth,\n windowHeight,\n onResetViewAndItems,\n handlePanToOffset,\n setNextTargetSection,\n resolvedResponsiveZoomMap,\n ]\n );\n\n return (\n <CanvasWrapper\n introProgress={introProgress}\n onIntroGrowComplete={startStage2}\n skipIntro={skipIntro}\n introContent={introContent}\n loadingText={loadingText}\n introBackgroundGradient={introBackgroundGradient}\n wrapperBackground={wrapperBackground}\n canvasBoxGradient={canvasBoxGradient}\n growTransition={growTransition}\n blurTransition={blurTransition}\n >\n <CanvasProvider\n x={x}\n y={y}\n scale={scale}\n isResetting={isResetting}\n maxZIndex={maxZIndex}\n setMaxZIndex={setMaxZIndex}\n animationStage={animationStage}\n nextTargetSection={nextTargetSection}\n setNextTargetSection={setNextTargetSection}\n navigateToSection={navigateToSection}\n >\n {animationStage >= 2 && (\n <>\n {!toolbarConfig?.hidden && (\n <Toolbar\n homeCoordinates={offsetHomeCoordinates}\n config={toolbarConfig}\n />\n )}\n {hasNavbar && navItems && !navbarConfig?.hidden && (\n <Navbar\n panToOffset={handlePanToOffset}\n onReset={onResetViewAndItems}\n items={navItems}\n config={navbarConfig}\n onRegisterNavigate={registerNavbarNavigate}\n responsiveZoomMap={resolvedResponsiveZoomMap}\n />\n )}\n </>\n )}\n <div\n ref={setViewportRef}\n className=\"relative h-full w-full touch-none select-none overflow-hidden\"\n style={{\n touchAction: \"none\",\n pointerEvents: animationStage >= 2 ? \"auto\" : \"none\",\n overscrollBehavior: \"contain\",\n }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUpOrCancel}\n onPointerLeave={handlePointerUpOrCancel}\n onPointerCancel={handlePointerUpOrCancel}\n >\n <motion.div\n ref={sceneRef}\n className=\"absolute z-20 origin-top-left\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={fadeTransition ?? FADE_TRANSITION}\n style={{\n width: `${sceneWidth}px`,\n height: `${sceneHeight}px`,\n x,\n y,\n scale,\n willChange:\n mode !== \"high\" && (animationStage < 2 || isPanning)\n ? \"transform\"\n : \"auto\",\n }}\n >\n {/* Canvas Background - customizable or default */}\n {canvasBackground !== undefined ? (\n canvasBackground\n ) : (\n <>\n {animationStage >= 1 && mode === \"high\" ? (\n <motion.div\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={{ duration: 0.5, ease: \"easeIn\" }}\n >\n <DefaultCanvasBackground />\n </motion.div>\n ) : (\n <DefaultCanvasBackground />\n )}\n </>\n )}\n {children}\n </motion.div>\n </div>\n </CanvasProvider>\n </CanvasWrapper>\n );\n};\n\nexport default Canvas;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"draggable.d.ts","sourceRoot":"","sources":["../../../src/components/canvas/draggable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAMN,MAAM,OAAO,CAAC;AACf,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,eAAe,CAAC;AAGvB,UAAU,KAAK;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,cAAe,SAAQ,eAAe,CAAC,KAAK,CAAC;IAC5D,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,KAAK,OAAO,CAAC;CAC5D;AAID,eAAO,MAAM,SAAS,oGAoGrB,CAAC;AAIF,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"draggable.d.ts","sourceRoot":"","sources":["../../../src/components/canvas/draggable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAMN,MAAM,OAAO,CAAC;AACf,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,eAAe,CAAC;AAGvB,UAAU,KAAK;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,cAAe,SAAQ,eAAe,CAAC,KAAK,CAAC;IAC5D,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,KAAK,OAAO,CAAC;CAC5D;AAID,eAAO,MAAM,SAAS,oGAoGrB,CAAC;AAIF,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyDD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,2CAwGxD"}
|
|
@@ -85,6 +85,8 @@ const Draggable = forwardRef((props, ref) => {
|
|
|
85
85
|
});
|
|
86
86
|
Draggable.displayName = "Draggable";
|
|
87
87
|
function drawImageToCanvas(img, canvas) {
|
|
88
|
+
canvas.width = img.naturalWidth;
|
|
89
|
+
canvas.height = img.naturalHeight;
|
|
88
90
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
89
91
|
if (!ctx) return;
|
|
90
92
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
@@ -95,8 +97,8 @@ function getAlphaAtCoords(clientX, clientY, canvas, img) {
|
|
|
95
97
|
const ctx = canvas.getContext("2d");
|
|
96
98
|
if (!ctx) return 0;
|
|
97
99
|
const rect = img.getBoundingClientRect();
|
|
98
|
-
const x = (clientX - rect.left) / rect.width *
|
|
99
|
-
const y = (clientY - rect.top) / rect.height *
|
|
100
|
+
const x = (clientX - rect.left) / rect.width * canvas.width;
|
|
101
|
+
const y = (clientY - rect.top) / rect.height * canvas.height;
|
|
100
102
|
return ctx.getImageData(x, y, 1, 1).data[3] ?? 0;
|
|
101
103
|
}
|
|
102
104
|
function isMouseOverImage(clientX, clientY, img) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"draggable.js","names":[],"sources":["../../../src/components/canvas/draggable.tsx"],"sourcesContent":["import React, {\n useRef,\n useEffect,\n forwardRef,\n useState,\n useCallback,\n} from \"react\";\nimport {\n animate,\n motion,\n useAnimationControls,\n useMotionValue,\n type HTMLMotionProps,\n type PanInfo,\n} from \"framer-motion\";\nimport { useCanvasContext } from \"../../contexts/CanvasContext\";\n\ninterface Point {\n x: number;\n y: number;\n}\n\nexport interface DraggableProps extends HTMLMotionProps<\"div\"> {\n initialPos?: Point;\n shouldStopPropagation?: (e: React.PointerEvent) => boolean;\n}\n\nconst defaultPos = { x: 0, y: 0 };\n\nexport const Draggable = forwardRef<HTMLDivElement, DraggableProps>(\n (props, ref) => {\n const {\n initialPos: passedPos,\n children,\n style,\n shouldStopPropagation = () => true,\n ...restProps\n } = props;\n\n const {\n scale: parentZoom,\n isResetting,\n maxZIndex,\n setMaxZIndex,\n } = useCanvasContext();\n\n const initialPos = passedPos ?? defaultPos;\n\n const x = useMotionValue(initialPos.x);\n const y = useMotionValue(initialPos.y);\n\n const logicalPositionRef = useRef<Point>({ ...initialPos });\n const controls = useAnimationControls();\n\n const [zIndex, setZIndex] = useState(1);\n\n useEffect(() => {\n if (isResetting) {\n logicalPositionRef.current = { ...initialPos };\n void animate(x, initialPos.x, {\n duration: 0.3,\n type: \"spring\",\n damping: 14,\n stiffness: 120,\n mass: 1,\n });\n void animate(y, initialPos.y, {\n duration: 0.3,\n type: \"spring\",\n damping: 14,\n stiffness: 120,\n mass: 1,\n });\n }\n }, [initialPos, controls, isResetting, x, y]);\n\n const handleDrag = (\n _event: MouseEvent | TouchEvent | PointerEvent,\n info: PanInfo,\n ) => {\n controls.stop();\n const deltaParentX = info.delta.x / parentZoom.get();\n const deltaParentY = info.delta.y / parentZoom.get();\n\n logicalPositionRef.current.x += deltaParentX;\n logicalPositionRef.current.y += deltaParentY;\n\n x.set(logicalPositionRef.current.x);\n y.set(logicalPositionRef.current.y);\n\n if (zIndex < maxZIndex) {\n setZIndex(maxZIndex + 1);\n setMaxZIndex(maxZIndex + 1);\n }\n };\n\n return (\n <motion.div\n ref={ref}\n dragMomentum={false}\n drag\n animate={controls}\n onDrag={handleDrag}\n style={{\n ...style,\n x,\n y,\n zIndex,\n }}\n initial={{\n scale: 1,\n filter: \"drop-shadow(0 0px 0px rgba(0, 0, 0, 0)) brightness(1)\",\n position: \"relative\",\n }}\n onPointerDown={(e: React.PointerEvent) => {\n if (shouldStopPropagation?.(e)) {\n e.stopPropagation();\n }\n }}\n transition={{\n duration: 0.1,\n ease: \"easeOut\",\n }}\n {...restProps}\n >\n {children}\n </motion.div>\n );\n },\n);\n\nDraggable.displayName = \"Draggable\";\n\nexport interface DraggableImageProps extends DraggableProps {\n src: string;\n alt?: string;\n width?: string | number;\n height?: string | number;\n scale?: number;\n hoverScale?: number;\n}\n\nfunction drawImageToCanvas(img: HTMLImageElement, canvas: HTMLCanvasElement) {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(img, 0, 0);\n}\n\nfunction getAlphaAtCoords(\n clientX: number,\n clientY: number,\n canvas: HTMLCanvasElement | null,\n img: HTMLImageElement | null,\n): number {\n if (!canvas || !img) return 0;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return 0;\n\n const rect = img.getBoundingClientRect();\n\n const x = ((clientX - rect.left) / rect.width) * img.naturalWidth;\n const y = ((clientY - rect.top) / rect.height) * img.naturalHeight;\n\n const alpha = ctx.getImageData(x, y, 1, 1).data[3] ?? 0;\n return alpha;\n}\n\nfunction isMouseOverImage(\n clientX: number,\n clientY: number,\n img: HTMLImageElement | null,\n) {\n if (!img) return false;\n const rect = img.getBoundingClientRect();\n return (\n clientX >= rect.left &&\n clientX <= rect.right &&\n clientY >= rect.top &&\n clientY <= rect.bottom\n );\n}\n\nfunction updateCursor(\n opaque: boolean,\n isMouseDown: boolean,\n img: HTMLImageElement | null,\n) {\n let cursor = \"url('customcursor.svg'), auto\"; // default\n if (opaque) cursor = \"grab\";\n if (isMouseDown) cursor = \"grabbing\";\n if (img) img.style.cursor = cursor;\n}\n\nexport function DraggableImage(props: DraggableImageProps) {\n const {\n src,\n alt,\n width,\n height,\n initialPos,\n animate,\n className,\n scale,\n hoverScale,\n ...restProps\n } = props;\n const imgRef = useRef<HTMLImageElement>(null);\n const [isOpaque, setIsOpaque] = useState(true); // default to true for better UX\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const isMouseDown = useRef(false);\n\n // create a invisible canvas element to check the alpha value of the image\n useEffect(() => {\n if (typeof window !== \"undefined\" && !canvasRef.current) {\n canvasRef.current = document.createElement(\"canvas\");\n }\n const img = imgRef.current;\n const canvas = canvasRef.current;\n if (!img || !canvas) return;\n if (!img.complete) {\n img.onload = () => drawImageToCanvas(img, canvas);\n } else {\n drawImageToCanvas(img, canvas);\n }\n return () => {\n if (img) img.onload = null;\n };\n }, []);\n\n // handle global mouse move to update cursor and opacity\n useEffect(() => {\n const handleGlobalMouseMove = (e: MouseEvent) => {\n if (\n !isMouseDown.current &&\n isMouseOverImage(e.clientX, e.clientY, imgRef.current)\n ) {\n const alpha = getAlphaAtCoords(\n e.clientX,\n e.clientY,\n canvasRef.current,\n imgRef.current,\n );\n\n // checking alpha > n rather than 0 to not trigger on shadows and such\n const opaque = alpha > 128;\n\n setIsOpaque(opaque);\n updateCursor(opaque, false, imgRef.current);\n }\n };\n window.addEventListener(\"mousemove\", handleGlobalMouseMove);\n return () => {\n window.removeEventListener(\"mousemove\", handleGlobalMouseMove);\n };\n }, []);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n isMouseDown.current = true;\n e.stopPropagation(); // Prevents the event from bubbling up\n updateCursor(true, true, imgRef.current);\n }, []);\n\n const handlePointerUp = () => {\n isMouseDown.current = false;\n updateCursor(isOpaque, false, imgRef.current);\n };\n\n const hoverScaleValue = isOpaque ? (hoverScale ?? (scale ?? 1)) : (scale ?? 1);\n\n return (\n <Draggable\n initialPos={initialPos}\n className={className}\n drag={isOpaque}\n style={{\n height: 0,\n }}\n {...restProps}\n >\n <motion.img\n ref={imgRef}\n src={src}\n alt={alt}\n width={width}\n height={height}\n animate={animate}\n draggable=\"false\"\n whileHover={{ scale: hoverScaleValue }}\n style={{\n scale: scale ?? 1,\n pointerEvents: isOpaque ? \"auto\" : \"none\",\n }}\n onPointerDown={handlePointerDown}\n onPointerUp={handlePointerUp}\n />\n </Draggable>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"draggable.js","names":[],"sources":["../../../src/components/canvas/draggable.tsx"],"sourcesContent":["import React, {\n useRef,\n useEffect,\n forwardRef,\n useState,\n useCallback,\n} from \"react\";\nimport {\n animate,\n motion,\n useAnimationControls,\n useMotionValue,\n type HTMLMotionProps,\n type PanInfo,\n} from \"framer-motion\";\nimport { useCanvasContext } from \"../../contexts/CanvasContext\";\n\ninterface Point {\n x: number;\n y: number;\n}\n\nexport interface DraggableProps extends HTMLMotionProps<\"div\"> {\n initialPos?: Point;\n shouldStopPropagation?: (e: React.PointerEvent) => boolean;\n}\n\nconst defaultPos = { x: 0, y: 0 };\n\nexport const Draggable = forwardRef<HTMLDivElement, DraggableProps>(\n (props, ref) => {\n const {\n initialPos: passedPos,\n children,\n style,\n shouldStopPropagation = () => true,\n ...restProps\n } = props;\n\n const {\n scale: parentZoom,\n isResetting,\n maxZIndex,\n setMaxZIndex,\n } = useCanvasContext();\n\n const initialPos = passedPos ?? defaultPos;\n\n const x = useMotionValue(initialPos.x);\n const y = useMotionValue(initialPos.y);\n\n const logicalPositionRef = useRef<Point>({ ...initialPos });\n const controls = useAnimationControls();\n\n const [zIndex, setZIndex] = useState(1);\n\n useEffect(() => {\n if (isResetting) {\n logicalPositionRef.current = { ...initialPos };\n void animate(x, initialPos.x, {\n duration: 0.3,\n type: \"spring\",\n damping: 14,\n stiffness: 120,\n mass: 1,\n });\n void animate(y, initialPos.y, {\n duration: 0.3,\n type: \"spring\",\n damping: 14,\n stiffness: 120,\n mass: 1,\n });\n }\n }, [initialPos, controls, isResetting, x, y]);\n\n const handleDrag = (\n _event: MouseEvent | TouchEvent | PointerEvent,\n info: PanInfo,\n ) => {\n controls.stop();\n const deltaParentX = info.delta.x / parentZoom.get();\n const deltaParentY = info.delta.y / parentZoom.get();\n\n logicalPositionRef.current.x += deltaParentX;\n logicalPositionRef.current.y += deltaParentY;\n\n x.set(logicalPositionRef.current.x);\n y.set(logicalPositionRef.current.y);\n\n if (zIndex < maxZIndex) {\n setZIndex(maxZIndex + 1);\n setMaxZIndex(maxZIndex + 1);\n }\n };\n\n return (\n <motion.div\n ref={ref}\n dragMomentum={false}\n drag\n animate={controls}\n onDrag={handleDrag}\n style={{\n ...style,\n x,\n y,\n zIndex,\n }}\n initial={{\n scale: 1,\n filter: \"drop-shadow(0 0px 0px rgba(0, 0, 0, 0)) brightness(1)\",\n position: \"relative\",\n }}\n onPointerDown={(e: React.PointerEvent) => {\n if (shouldStopPropagation?.(e)) {\n e.stopPropagation();\n }\n }}\n transition={{\n duration: 0.1,\n ease: \"easeOut\",\n }}\n {...restProps}\n >\n {children}\n </motion.div>\n );\n },\n);\n\nDraggable.displayName = \"Draggable\";\n\nexport interface DraggableImageProps extends DraggableProps {\n src: string;\n alt?: string;\n width?: string | number;\n height?: string | number;\n scale?: number;\n hoverScale?: number;\n}\n\nfunction drawImageToCanvas(img: HTMLImageElement, canvas: HTMLCanvasElement) {\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return;\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(img, 0, 0);\n}\n\nfunction getAlphaAtCoords(\n clientX: number,\n clientY: number,\n canvas: HTMLCanvasElement | null,\n img: HTMLImageElement | null,\n): number {\n if (!canvas || !img) return 0;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return 0;\n\n const rect = img.getBoundingClientRect();\n\n const x = ((clientX - rect.left) / rect.width) * canvas.width;\n const y = ((clientY - rect.top) / rect.height) * canvas.height;\n\n const alpha = ctx.getImageData(x, y, 1, 1).data[3] ?? 0;\n return alpha;\n}\n\nfunction isMouseOverImage(\n clientX: number,\n clientY: number,\n img: HTMLImageElement | null,\n) {\n if (!img) return false;\n const rect = img.getBoundingClientRect();\n return (\n clientX >= rect.left &&\n clientX <= rect.right &&\n clientY >= rect.top &&\n clientY <= rect.bottom\n );\n}\n\nfunction updateCursor(\n opaque: boolean,\n isMouseDown: boolean,\n img: HTMLImageElement | null,\n) {\n let cursor = \"url('customcursor.svg'), auto\"; // default\n if (opaque) cursor = \"grab\";\n if (isMouseDown) cursor = \"grabbing\";\n if (img) img.style.cursor = cursor;\n}\n\nexport function DraggableImage(props: DraggableImageProps) {\n const {\n src,\n alt,\n width,\n height,\n initialPos,\n animate,\n className,\n scale,\n hoverScale,\n ...restProps\n } = props;\n const imgRef = useRef<HTMLImageElement>(null);\n const [isOpaque, setIsOpaque] = useState(true); // default to true for better UX\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const isMouseDown = useRef(false);\n\n // create a invisible canvas element to check the alpha value of the image\n useEffect(() => {\n if (typeof window !== \"undefined\" && !canvasRef.current) {\n canvasRef.current = document.createElement(\"canvas\");\n }\n const img = imgRef.current;\n const canvas = canvasRef.current;\n if (!img || !canvas) return;\n if (!img.complete) {\n img.onload = () => drawImageToCanvas(img, canvas);\n } else {\n drawImageToCanvas(img, canvas);\n }\n return () => {\n if (img) img.onload = null;\n };\n }, []);\n\n // handle global mouse move to update cursor and opacity\n useEffect(() => {\n const handleGlobalMouseMove = (e: MouseEvent) => {\n if (\n !isMouseDown.current &&\n isMouseOverImage(e.clientX, e.clientY, imgRef.current)\n ) {\n const alpha = getAlphaAtCoords(\n e.clientX,\n e.clientY,\n canvasRef.current,\n imgRef.current,\n );\n\n // checking alpha > n rather than 0 to not trigger on shadows and such\n const opaque = alpha > 128;\n\n setIsOpaque(opaque);\n updateCursor(opaque, false, imgRef.current);\n }\n };\n window.addEventListener(\"mousemove\", handleGlobalMouseMove);\n return () => {\n window.removeEventListener(\"mousemove\", handleGlobalMouseMove);\n };\n }, []);\n\n const handlePointerDown = useCallback((e: React.PointerEvent) => {\n isMouseDown.current = true;\n e.stopPropagation(); // Prevents the event from bubbling up\n updateCursor(true, true, imgRef.current);\n }, []);\n\n const handlePointerUp = () => {\n isMouseDown.current = false;\n updateCursor(isOpaque, false, imgRef.current);\n };\n\n const hoverScaleValue = isOpaque ? (hoverScale ?? (scale ?? 1)) : (scale ?? 1);\n\n return (\n <Draggable\n initialPos={initialPos}\n className={className}\n drag={isOpaque}\n style={{\n height: 0,\n }}\n {...restProps}\n >\n <motion.img\n ref={imgRef}\n src={src}\n alt={alt}\n width={width}\n height={height}\n animate={animate}\n draggable=\"false\"\n whileHover={{ scale: hoverScaleValue }}\n style={{\n scale: scale ?? 1,\n pointerEvents: isOpaque ? \"auto\" : \"none\",\n }}\n onPointerDown={handlePointerDown}\n onPointerUp={handlePointerUp}\n />\n </Draggable>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NavItem, NavbarConfig } from "../../../types";
|
|
2
|
+
import { type ScreenSizeEnum } from "../../../lib/constants";
|
|
2
3
|
interface NavbarProps {
|
|
3
4
|
panToOffset: (offset: {
|
|
4
5
|
x: number;
|
|
@@ -11,7 +12,9 @@ interface NavbarProps {
|
|
|
11
12
|
config?: NavbarConfig;
|
|
12
13
|
/** Register a handler so external code can trigger navigation via navigateToSection */
|
|
13
14
|
onRegisterNavigate?: (handler: ((sectionId: string) => void) | null) => void;
|
|
15
|
+
/** Resolved responsive zoom map (defaults merged with user overrides) */
|
|
16
|
+
responsiveZoomMap: Record<ScreenSizeEnum, number>;
|
|
14
17
|
}
|
|
15
|
-
export default function Navbar({ panToOffset, onReset, items, config, onRegisterNavigate, }: NavbarProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export default function Navbar({ panToOffset, onReset, items, config, onRegisterNavigate, responsiveZoomMap, }: NavbarProps): import("react/jsx-runtime").JSX.Element;
|
|
16
19
|
export {};
|
|
17
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/canvas/navbar/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAkB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/canvas/navbar/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAkB,MAAM,gBAAgB,CAAC;AAQ5E,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAGhC,UAAU,WAAW;IACnB,WAAW,EAAE,CACX,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAChC,UAAU,CAAC,EAAE,MAAM,IAAI,EACvB,IAAI,CAAC,EAAE,MAAM,KACV,IAAI,CAAC;IACV,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gFAAgF;IAChF,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,mCAAmC;IACnC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7E,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;CACnD;AAqCD,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,EAC7B,WAAW,EACX,OAAO,EACP,KAAK,EACL,MAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,EAAE,WAAW,2CA2Mb"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCanvasContext } from "../../../contexts/CanvasContext.js";
|
|
2
|
-
import { NAVBAR_DEBOUNCE_MS
|
|
2
|
+
import { NAVBAR_DEBOUNCE_MS } from "../../../lib/constants.js";
|
|
3
3
|
import { getScreenSizeEnum, getSectionPanCoordinates } from "../../../lib/canvas.js";
|
|
4
4
|
import useWindowDimensions_default from "../../../hooks/useWindowDimensions.js";
|
|
5
5
|
import { cn } from "../../../lib/utils.js";
|
|
@@ -42,7 +42,7 @@ const responsivePositionClasses = {
|
|
|
42
42
|
left: "left-4",
|
|
43
43
|
right: "right-4"
|
|
44
44
|
};
|
|
45
|
-
function Navbar({ panToOffset, onReset, items, config = {}, onRegisterNavigate }) {
|
|
45
|
+
function Navbar({ panToOffset, onReset, items, config = {}, onRegisterNavigate, responsiveZoomMap }) {
|
|
46
46
|
const { x, y, scale, animationStage, setNextTargetSection } = useCanvasContext();
|
|
47
47
|
const [expandedButton, setExpandedButton] = useState(null);
|
|
48
48
|
const activePans = useRef(0);
|
|
@@ -51,7 +51,7 @@ function Navbar({ panToOffset, onReset, items, config = {}, onRegisterNavigate }
|
|
|
51
51
|
const debounceCooldownTimeout = useRef(null);
|
|
52
52
|
const { height, width } = useWindowDimensions_default();
|
|
53
53
|
const { mode } = usePerformanceMode();
|
|
54
|
-
const defaultZoom =
|
|
54
|
+
const defaultZoom = responsiveZoomMap[getScreenSizeEnum(width)];
|
|
55
55
|
const debounceMs = NAVBAR_DEBOUNCE_MS[mode] ?? 0;
|
|
56
56
|
const { display = "icons", position = "bottom", className, style, buttonConfig, tooltipConfig, gap, padding } = config;
|
|
57
57
|
const isVertical = position === "left" || position === "right";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["useWindowDimensions"],"sources":["../../../../src/components/canvas/navbar/index.tsx"],"sourcesContent":["import { motion, useMotionValueEvent } from \"framer-motion\";\nimport { useState, useRef, useEffect, useCallback, useMemo } from \"react\";\nimport SingleButton from \"./single-button\";\nimport type { NavItem, NavbarConfig, NavbarPosition } from \"../../../types\";\nimport { useCanvasContext } from \"../../../contexts/CanvasContext\";\nimport useWindowDimensions from \"../../../hooks/useWindowDimensions\";\nimport { usePerformanceMode } from \"../../../hooks/usePerformanceMode\";\nimport {\n getScreenSizeEnum,\n getSectionPanCoordinates,\n} from \"../../../lib/canvas\";\nimport {\n RESPONSIVE_ZOOM_MAP,\n NAVBAR_DEBOUNCE_MS,\n} from \"../../../lib/constants\";\nimport { cn } from \"../../../lib/utils\";\n\ninterface NavbarProps {\n panToOffset: (\n offset: { x: number; y: number },\n onComplete?: () => void,\n zoom?: number,\n ) => void;\n onReset: () => void;\n /** Array of navigation items defining sections, their icons, and coordinates */\n items: NavItem[];\n /** Navbar configuration options */\n config?: NavbarConfig;\n /** Register a handler so external code can trigger navigation via navigateToSection */\n onRegisterNavigate?: (handler: ((sectionId: string) => void) | null) => void;\n}\n\nconst positionStyles: Record<NavbarPosition, React.CSSProperties> = {\n top: {\n top: \"1rem\",\n bottom: \"auto\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n },\n bottom: {\n bottom: \"1rem\",\n top: \"auto\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n },\n left: {\n left: \"1rem\",\n right: \"auto\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n },\n right: {\n right: \"1rem\",\n left: \"auto\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n },\n};\n\n// Responsive position adjustments (mobile vs desktop)\nconst responsivePositionClasses: Record<NavbarPosition, string> = {\n top: \"top-12 md:top-4\",\n bottom: \"bottom-12 md:bottom-4\",\n left: \"left-4\",\n right: \"right-4\",\n};\n\nexport default function Navbar({\n panToOffset,\n onReset,\n items,\n config = {},\n onRegisterNavigate,\n}: NavbarProps) {\n const { x, y, scale, animationStage, setNextTargetSection } =\n useCanvasContext();\n const [expandedButton, setExpandedButton] = useState<string | null>(null);\n const activePans = useRef(0);\n const panTimeout = useRef<NodeJS.Timeout | null>(null);\n\n // Debounce state\n const debounceBlocked = useRef(false);\n const debounceCooldownTimeout = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n\n const { height, width } = useWindowDimensions();\n const { mode } = usePerformanceMode();\n\n const defaultZoom = RESPONSIVE_ZOOM_MAP[getScreenSizeEnum(width)];\n\n // Derive debounce duration from performance mode\n const debounceMs = NAVBAR_DEBOUNCE_MS[mode] ?? 0;\n\n // Extract config values\n const {\n display = \"icons\",\n position = \"bottom\",\n className,\n style,\n buttonConfig,\n tooltipConfig,\n gap,\n padding,\n } = config;\n\n const isVertical = position === \"left\" || position === \"right\";\n\n // Find the home section from items\n const homeItem = useMemo(() => items.find((item) => item.isHome), [items]);\n\n // Leading-edge debounce handler\n const handleDebouncedClick = useCallback(\n (callback: () => void) => {\n if (debounceMs === 0) {\n callback();\n return;\n }\n\n if (debounceBlocked.current) {\n // We're in the cooldown window; ignore this click\n return;\n }\n\n // Enter cooldown and perform the click immediately\n debounceBlocked.current = true;\n callback();\n\n if (debounceCooldownTimeout.current) {\n clearTimeout(debounceCooldownTimeout.current);\n }\n\n debounceCooldownTimeout.current = setTimeout(() => {\n debounceBlocked.current = false;\n debounceCooldownTimeout.current = null;\n }, debounceMs);\n },\n [debounceMs],\n );\n\n const updateExpandedButton = () => {\n // reset activePans if no movement has occurred recently\n if (panTimeout.current) clearTimeout(panTimeout.current);\n panTimeout.current = setTimeout(() => {\n activePans.current = 0;\n }, 500);\n\n if (activePans.current == 0) setExpandedButton(null);\n };\n\n useMotionValueEvent(x, \"change\", updateExpandedButton);\n useMotionValueEvent(y, \"change\", updateExpandedButton);\n useMotionValueEvent(scale, \"change\", updateExpandedButton);\n\n const handlePan = useCallback(\n function handlePan(item: NavItem) {\n setExpandedButton(item.id);\n activePans.current++;\n\n // Predictive pre-render hint: mark the target section so its CanvasComponent can\n // render even before it comes fully into view.\n setNextTargetSection(item.id);\n\n if (item.isHome) {\n onReset();\n return;\n }\n\n const panCoords = getSectionPanCoordinates({\n windowDimensions: { width, height },\n coords: { x: item.x, y: item.y, width: item.width, height: item.height },\n targetZoom: defaultZoom,\n negative: true,\n });\n\n panToOffset(\n panCoords,\n () => {\n activePans.current--;\n },\n defaultZoom,\n );\n },\n [panToOffset, onReset, width, height, defaultZoom, setNextTargetSection],\n );\n\n // Register handlePan for external navigation via navigateToSection\n const handlePanRef = useRef(handlePan);\n useEffect(() => {\n handlePanRef.current = handlePan;\n }, [handlePan]);\n\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n }, [items]);\n\n useEffect(() => {\n if (!onRegisterNavigate) return;\n const handler = (sectionId: string) => {\n const item = itemsRef.current.find((i) => i.id === sectionId);\n if (item) handlePanRef.current(item);\n };\n onRegisterNavigate(handler);\n return () => onRegisterNavigate(null);\n }, [onRegisterNavigate]);\n\n // Clean up timers on unmount and pan to home on animation complete\n useEffect(() => {\n if (animationStage < 2) return;\n if (homeItem) {\n handlePan(homeItem);\n }\n return () => {\n if (panTimeout.current) clearTimeout(panTimeout.current);\n if (debounceCooldownTimeout.current)\n clearTimeout(debounceCooldownTimeout.current);\n };\n }, [handlePan, animationStage, homeItem]);\n\n // Compute container styles (positioning only)\n const containerStyle: React.CSSProperties = {\n position: \"fixed\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n display: \"flex\",\n justifyContent: \"center\",\n alignItems: \"center\",\n ...positionStyles[position],\n };\n\n // Compute inner container styles (visual styling)\n const innerStyle: React.CSSProperties = {\n ...(gap !== undefined && { gap: `${gap}px` }),\n ...(padding !== undefined && { padding: `${padding}px` }),\n ...(isVertical && { flexDirection: \"column\" }),\n ...style,\n };\n\n return (\n <div\n className={responsivePositionClasses[position]}\n style={containerStyle}\n >\n {/* padding to prevent edge bug */}\n <div className={isVertical ? \"py-4 md:py-8\" : \"px-4 md:px-8\"}>\n <motion.div\n className={cn(\n \"flex select-none items-center justify-center gap-1 rounded-[10px] border p-1 shadow-[0_6px_12px_rgba(0,0,0,0.08)]\",\n !style?.backgroundColor && \"bg-white\",\n !style?.borderColor && \"border-neutral-200\",\n isVertical && \"flex-col\",\n className,\n )}\n style={innerStyle}\n >\n <div className={cn(\"flex items-center gap-1\", isVertical && \"flex-col\")}>\n {items.map((item) => (\n <SingleButton\n key={item.id}\n label={item.label}\n icon={item.icon}\n onClick={() => handlePan(item)}\n isPushed={expandedButton === item.id}\n onDebouncedClick={handleDebouncedClick}\n displayMode={display}\n buttonConfig={buttonConfig}\n tooltipConfig={tooltipConfig}\n isVertical={isVertical}\n />\n ))}\n </div>\n </motion.div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["useWindowDimensions"],"sources":["../../../../src/components/canvas/navbar/index.tsx"],"sourcesContent":["import { motion, useMotionValueEvent } from \"framer-motion\";\nimport { useState, useRef, useEffect, useCallback, useMemo } from \"react\";\nimport SingleButton from \"./single-button\";\nimport type { NavItem, NavbarConfig, NavbarPosition } from \"../../../types\";\nimport { useCanvasContext } from \"../../../contexts/CanvasContext\";\nimport useWindowDimensions from \"../../../hooks/useWindowDimensions\";\nimport { usePerformanceMode } from \"../../../hooks/usePerformanceMode\";\nimport {\n getScreenSizeEnum,\n getSectionPanCoordinates,\n} from \"../../../lib/canvas\";\nimport {\n NAVBAR_DEBOUNCE_MS,\n type ScreenSizeEnum,\n} from \"../../../lib/constants\";\nimport { cn } from \"../../../lib/utils\";\n\ninterface NavbarProps {\n panToOffset: (\n offset: { x: number; y: number },\n onComplete?: () => void,\n zoom?: number,\n ) => void;\n onReset: () => void;\n /** Array of navigation items defining sections, their icons, and coordinates */\n items: NavItem[];\n /** Navbar configuration options */\n config?: NavbarConfig;\n /** Register a handler so external code can trigger navigation via navigateToSection */\n onRegisterNavigate?: (handler: ((sectionId: string) => void) | null) => void;\n /** Resolved responsive zoom map (defaults merged with user overrides) */\n responsiveZoomMap: Record<ScreenSizeEnum, number>;\n}\n\nconst positionStyles: Record<NavbarPosition, React.CSSProperties> = {\n top: {\n top: \"1rem\",\n bottom: \"auto\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n },\n bottom: {\n bottom: \"1rem\",\n top: \"auto\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n },\n left: {\n left: \"1rem\",\n right: \"auto\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n },\n right: {\n right: \"1rem\",\n left: \"auto\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n },\n};\n\n// Responsive position adjustments (mobile vs desktop)\nconst responsivePositionClasses: Record<NavbarPosition, string> = {\n top: \"top-12 md:top-4\",\n bottom: \"bottom-12 md:bottom-4\",\n left: \"left-4\",\n right: \"right-4\",\n};\n\nexport default function Navbar({\n panToOffset,\n onReset,\n items,\n config = {},\n onRegisterNavigate,\n responsiveZoomMap,\n}: NavbarProps) {\n const { x, y, scale, animationStage, setNextTargetSection } =\n useCanvasContext();\n const [expandedButton, setExpandedButton] = useState<string | null>(null);\n const activePans = useRef(0);\n const panTimeout = useRef<NodeJS.Timeout | null>(null);\n\n // Debounce state\n const debounceBlocked = useRef(false);\n const debounceCooldownTimeout = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n );\n\n const { height, width } = useWindowDimensions();\n const { mode } = usePerformanceMode();\n\n const defaultZoom = responsiveZoomMap[getScreenSizeEnum(width)];\n\n // Derive debounce duration from performance mode\n const debounceMs = NAVBAR_DEBOUNCE_MS[mode] ?? 0;\n\n // Extract config values\n const {\n display = \"icons\",\n position = \"bottom\",\n className,\n style,\n buttonConfig,\n tooltipConfig,\n gap,\n padding,\n } = config;\n\n const isVertical = position === \"left\" || position === \"right\";\n\n // Find the home section from items\n const homeItem = useMemo(() => items.find((item) => item.isHome), [items]);\n\n // Leading-edge debounce handler\n const handleDebouncedClick = useCallback(\n (callback: () => void) => {\n if (debounceMs === 0) {\n callback();\n return;\n }\n\n if (debounceBlocked.current) {\n // We're in the cooldown window; ignore this click\n return;\n }\n\n // Enter cooldown and perform the click immediately\n debounceBlocked.current = true;\n callback();\n\n if (debounceCooldownTimeout.current) {\n clearTimeout(debounceCooldownTimeout.current);\n }\n\n debounceCooldownTimeout.current = setTimeout(() => {\n debounceBlocked.current = false;\n debounceCooldownTimeout.current = null;\n }, debounceMs);\n },\n [debounceMs],\n );\n\n const updateExpandedButton = () => {\n // reset activePans if no movement has occurred recently\n if (panTimeout.current) clearTimeout(panTimeout.current);\n panTimeout.current = setTimeout(() => {\n activePans.current = 0;\n }, 500);\n\n if (activePans.current == 0) setExpandedButton(null);\n };\n\n useMotionValueEvent(x, \"change\", updateExpandedButton);\n useMotionValueEvent(y, \"change\", updateExpandedButton);\n useMotionValueEvent(scale, \"change\", updateExpandedButton);\n\n const handlePan = useCallback(\n function handlePan(item: NavItem) {\n setExpandedButton(item.id);\n activePans.current++;\n\n // Predictive pre-render hint: mark the target section so its CanvasComponent can\n // render even before it comes fully into view.\n setNextTargetSection(item.id);\n\n if (item.isHome) {\n onReset();\n return;\n }\n\n const panCoords = getSectionPanCoordinates({\n windowDimensions: { width, height },\n coords: { x: item.x, y: item.y, width: item.width, height: item.height },\n targetZoom: defaultZoom,\n negative: true,\n });\n\n panToOffset(\n panCoords,\n () => {\n activePans.current--;\n },\n defaultZoom,\n );\n },\n [panToOffset, onReset, width, height, defaultZoom, setNextTargetSection],\n );\n\n // Register handlePan for external navigation via navigateToSection\n const handlePanRef = useRef(handlePan);\n useEffect(() => {\n handlePanRef.current = handlePan;\n }, [handlePan]);\n\n const itemsRef = useRef(items);\n useEffect(() => {\n itemsRef.current = items;\n }, [items]);\n\n useEffect(() => {\n if (!onRegisterNavigate) return;\n const handler = (sectionId: string) => {\n const item = itemsRef.current.find((i) => i.id === sectionId);\n if (item) handlePanRef.current(item);\n };\n onRegisterNavigate(handler);\n return () => onRegisterNavigate(null);\n }, [onRegisterNavigate]);\n\n // Clean up timers on unmount and pan to home on animation complete\n useEffect(() => {\n if (animationStage < 2) return;\n if (homeItem) {\n handlePan(homeItem);\n }\n return () => {\n if (panTimeout.current) clearTimeout(panTimeout.current);\n if (debounceCooldownTimeout.current)\n clearTimeout(debounceCooldownTimeout.current);\n };\n }, [handlePan, animationStage, homeItem]);\n\n // Compute container styles (positioning only)\n const containerStyle: React.CSSProperties = {\n position: \"fixed\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n display: \"flex\",\n justifyContent: \"center\",\n alignItems: \"center\",\n ...positionStyles[position],\n };\n\n // Compute inner container styles (visual styling)\n const innerStyle: React.CSSProperties = {\n ...(gap !== undefined && { gap: `${gap}px` }),\n ...(padding !== undefined && { padding: `${padding}px` }),\n ...(isVertical && { flexDirection: \"column\" }),\n ...style,\n };\n\n return (\n <div\n className={responsivePositionClasses[position]}\n style={containerStyle}\n >\n {/* padding to prevent edge bug */}\n <div className={isVertical ? \"py-4 md:py-8\" : \"px-4 md:px-8\"}>\n <motion.div\n className={cn(\n \"flex select-none items-center justify-center gap-1 rounded-[10px] border p-1 shadow-[0_6px_12px_rgba(0,0,0,0.08)]\",\n !style?.backgroundColor && \"bg-white\",\n !style?.borderColor && \"border-neutral-200\",\n isVertical && \"flex-col\",\n className,\n )}\n style={innerStyle}\n >\n <div className={cn(\"flex items-center gap-1\", isVertical && \"flex-col\")}>\n {items.map((item) => (\n <SingleButton\n key={item.id}\n label={item.label}\n icon={item.icon}\n onClick={() => handlePan(item)}\n isPushed={expandedButton === item.id}\n onDebouncedClick={handleDebouncedClick}\n displayMode={display}\n buttonConfig={buttonConfig}\n tooltipConfig={tooltipConfig}\n isVertical={isVertical}\n />\n ))}\n </div>\n </motion.div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export * from "./lib/canvas";
|
|
|
16
16
|
export * from "./lib/constants";
|
|
17
17
|
export * from "./lib/utils";
|
|
18
18
|
export * from "./utils/performance";
|
|
19
|
-
export type { SectionCoordinates, NavItem, CanvasSection, ToolbarConfig, ToolbarPosition, ToolbarDisplayMode, NavbarConfig, NavbarPosition, NavbarDisplayMode, NavbarButtonConfig, NavbarTooltipConfig, AnimationTimingConfig, } from "./types";
|
|
19
|
+
export type { SectionCoordinates, NavItem, CanvasSection, ToolbarConfig, ToolbarPosition, ToolbarDisplayMode, NavbarConfig, NavbarPosition, NavbarDisplayMode, NavbarButtonConfig, NavbarTooltipConfig, AnimationTimingConfig, ZoomConfig, } from "./types";
|
|
20
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,4BAA4B,EAC5B,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,kBAAkB,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAG5F,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AAGpC,YAAY,EACV,kBAAkB,EAClB,OAAO,EACP,aAAa,EACb,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGrE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,4BAA4B,EAC5B,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EACL,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,kBAAkB,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAG5F,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AAGpC,YAAY,EACV,kBAAkB,EAClB,OAAO,EACP,aAAa,EACb,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,UAAU,GACX,MAAM,SAAS,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Apps should extend these types with their specific enums and constants
|
|
4
4
|
*/
|
|
5
5
|
import type { Transition } from "framer-motion";
|
|
6
|
+
import { ScreenSizeEnum } from "../lib/constants";
|
|
6
7
|
export interface SectionCoordinates {
|
|
7
8
|
x: number;
|
|
8
9
|
y: number;
|
|
@@ -171,4 +172,14 @@ export interface NavbarConfig {
|
|
|
171
172
|
/** Padding inside the navbar in pixels. Default: 4 */
|
|
172
173
|
padding?: number;
|
|
173
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Configuration for zoom behavior per screen size.
|
|
177
|
+
* Partial overrides are merged with library defaults.
|
|
178
|
+
*/
|
|
179
|
+
export interface ZoomConfig {
|
|
180
|
+
/** Override minimum zoom levels per screen size. Merged with defaults from MIN_ZOOMS. */
|
|
181
|
+
minZooms?: Partial<Record<ScreenSizeEnum, number>>;
|
|
182
|
+
/** Override navigation zoom levels per screen size. Merged with defaults from RESPONSIVE_ZOOM_MAP. */
|
|
183
|
+
responsiveZoomMap?: Partial<Record<ScreenSizeEnum, number>>;
|
|
184
|
+
}
|
|
174
185
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -49,6 +49,7 @@ import type {
|
|
|
49
49
|
NavbarConfig,
|
|
50
50
|
SectionCoordinates,
|
|
51
51
|
ToolbarConfig,
|
|
52
|
+
ZoomConfig,
|
|
52
53
|
} from "../../types";
|
|
53
54
|
import { CanvasWrapper } from "./wrapper";
|
|
54
55
|
import { usePerformanceMode } from "../../hooks/usePerformanceMode";
|
|
@@ -100,6 +101,10 @@ interface Props {
|
|
|
100
101
|
// ============== Navbar Customization ==============
|
|
101
102
|
/** Navbar customization options */
|
|
102
103
|
navbarConfig?: NavbarConfig;
|
|
104
|
+
|
|
105
|
+
// ============== Zoom Customization ==============
|
|
106
|
+
/** Zoom level overrides per screen size */
|
|
107
|
+
zoomConfig?: ZoomConfig;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
const stopAllMotion = (
|
|
@@ -131,6 +136,7 @@ const Canvas: FC<Props> = ({
|
|
|
131
136
|
navbarConfig,
|
|
132
137
|
canvasHeight,
|
|
133
138
|
canvasWidth,
|
|
139
|
+
zoomConfig,
|
|
134
140
|
}) => {
|
|
135
141
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
136
142
|
|
|
@@ -141,7 +147,16 @@ const Canvas: FC<Props> = ({
|
|
|
141
147
|
const sceneWidth = canvasWidth ?? DEFAULT_CANVAS_WIDTH;
|
|
142
148
|
const sceneHeight = canvasHeight ?? DEFAULT_CANVAS_HEIGHT;
|
|
143
149
|
|
|
144
|
-
const
|
|
150
|
+
const resolvedMinZooms = useMemo(
|
|
151
|
+
() => zoomConfig?.minZooms ? { ...MIN_ZOOMS, ...zoomConfig.minZooms } : MIN_ZOOMS,
|
|
152
|
+
[zoomConfig?.minZooms]
|
|
153
|
+
);
|
|
154
|
+
const resolvedResponsiveZoomMap = useMemo(
|
|
155
|
+
() => zoomConfig?.responsiveZoomMap ? { ...RESPONSIVE_ZOOM_MAP, ...zoomConfig.responsiveZoomMap } : RESPONSIVE_ZOOM_MAP,
|
|
156
|
+
[zoomConfig?.responsiveZoomMap]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const MIN_ZOOM = resolvedMinZooms[getScreenSizeEnum(windowWidth)];
|
|
145
160
|
|
|
146
161
|
// tracks if user is panning the screen
|
|
147
162
|
const [isPanning, setIsPanning] = useState<boolean>(false);
|
|
@@ -671,7 +686,7 @@ const Canvas: FC<Props> = ({
|
|
|
671
686
|
if (item.isHome) {
|
|
672
687
|
onResetViewAndItems();
|
|
673
688
|
} else {
|
|
674
|
-
const zoom =
|
|
689
|
+
const zoom = resolvedResponsiveZoomMap[getScreenSizeEnum(windowWidth)];
|
|
675
690
|
const panCoords = getSectionPanCoordinates({
|
|
676
691
|
windowDimensions: { width: windowWidth, height: windowHeight },
|
|
677
692
|
coords: {
|
|
@@ -694,6 +709,7 @@ const Canvas: FC<Props> = ({
|
|
|
694
709
|
onResetViewAndItems,
|
|
695
710
|
handlePanToOffset,
|
|
696
711
|
setNextTargetSection,
|
|
712
|
+
resolvedResponsiveZoomMap,
|
|
697
713
|
]
|
|
698
714
|
);
|
|
699
715
|
|
|
@@ -737,6 +753,7 @@ const Canvas: FC<Props> = ({
|
|
|
737
753
|
items={navItems}
|
|
738
754
|
config={navbarConfig}
|
|
739
755
|
onRegisterNavigate={registerNavbarNavigate}
|
|
756
|
+
responsiveZoomMap={resolvedResponsiveZoomMap}
|
|
740
757
|
/>
|
|
741
758
|
)}
|
|
742
759
|
</>
|
|
@@ -141,6 +141,8 @@ export interface DraggableImageProps extends DraggableProps {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function drawImageToCanvas(img: HTMLImageElement, canvas: HTMLCanvasElement) {
|
|
144
|
+
canvas.width = img.naturalWidth;
|
|
145
|
+
canvas.height = img.naturalHeight;
|
|
144
146
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
145
147
|
if (!ctx) return;
|
|
146
148
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
@@ -160,8 +162,8 @@ function getAlphaAtCoords(
|
|
|
160
162
|
|
|
161
163
|
const rect = img.getBoundingClientRect();
|
|
162
164
|
|
|
163
|
-
const x = ((clientX - rect.left) / rect.width) *
|
|
164
|
-
const y = ((clientY - rect.top) / rect.height) *
|
|
165
|
+
const x = ((clientX - rect.left) / rect.width) * canvas.width;
|
|
166
|
+
const y = ((clientY - rect.top) / rect.height) * canvas.height;
|
|
165
167
|
|
|
166
168
|
const alpha = ctx.getImageData(x, y, 1, 1).data[3] ?? 0;
|
|
167
169
|
return alpha;
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
getSectionPanCoordinates,
|
|
11
11
|
} from "../../../lib/canvas";
|
|
12
12
|
import {
|
|
13
|
-
RESPONSIVE_ZOOM_MAP,
|
|
14
13
|
NAVBAR_DEBOUNCE_MS,
|
|
14
|
+
type ScreenSizeEnum,
|
|
15
15
|
} from "../../../lib/constants";
|
|
16
16
|
import { cn } from "../../../lib/utils";
|
|
17
17
|
|
|
@@ -28,6 +28,8 @@ interface NavbarProps {
|
|
|
28
28
|
config?: NavbarConfig;
|
|
29
29
|
/** Register a handler so external code can trigger navigation via navigateToSection */
|
|
30
30
|
onRegisterNavigate?: (handler: ((sectionId: string) => void) | null) => void;
|
|
31
|
+
/** Resolved responsive zoom map (defaults merged with user overrides) */
|
|
32
|
+
responsiveZoomMap: Record<ScreenSizeEnum, number>;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const positionStyles: Record<NavbarPosition, React.CSSProperties> = {
|
|
@@ -71,6 +73,7 @@ export default function Navbar({
|
|
|
71
73
|
items,
|
|
72
74
|
config = {},
|
|
73
75
|
onRegisterNavigate,
|
|
76
|
+
responsiveZoomMap,
|
|
74
77
|
}: NavbarProps) {
|
|
75
78
|
const { x, y, scale, animationStage, setNextTargetSection } =
|
|
76
79
|
useCanvasContext();
|
|
@@ -87,7 +90,7 @@ export default function Navbar({
|
|
|
87
90
|
const { height, width } = useWindowDimensions();
|
|
88
91
|
const { mode } = usePerformanceMode();
|
|
89
92
|
|
|
90
|
-
const defaultZoom =
|
|
93
|
+
const defaultZoom = responsiveZoomMap[getScreenSizeEnum(width)];
|
|
91
94
|
|
|
92
95
|
// Derive debounce duration from performance mode
|
|
93
96
|
const debounceMs = NAVBAR_DEBOUNCE_MS[mode] ?? 0;
|
package/src/index.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Transition } from "framer-motion";
|
|
7
|
+
import { ScreenSizeEnum } from "../lib/constants";
|
|
7
8
|
|
|
8
9
|
export interface SectionCoordinates {
|
|
9
10
|
x: number;
|
|
@@ -212,3 +213,14 @@ export interface NavbarConfig {
|
|
|
212
213
|
/** Padding inside the navbar in pixels. Default: 4 */
|
|
213
214
|
padding?: number;
|
|
214
215
|
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Configuration for zoom behavior per screen size.
|
|
219
|
+
* Partial overrides are merged with library defaults.
|
|
220
|
+
*/
|
|
221
|
+
export interface ZoomConfig {
|
|
222
|
+
/** Override minimum zoom levels per screen size. Merged with defaults from MIN_ZOOMS. */
|
|
223
|
+
minZooms?: Partial<Record<ScreenSizeEnum, number>>;
|
|
224
|
+
/** Override navigation zoom levels per screen size. Merged with defaults from RESPONSIVE_ZOOM_MAP. */
|
|
225
|
+
responsiveZoomMap?: Partial<Record<ScreenSizeEnum, number>>;
|
|
226
|
+
}
|