@hunterchen/canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/components/canvas/canvas.d.ts +29 -0
  2. package/dist/components/canvas/canvas.d.ts.map +1 -0
  3. package/dist/components/canvas/canvas.js +419 -0
  4. package/dist/components/canvas/canvas.js.map +1 -0
  5. package/dist/components/canvas/component.d.ts +47 -0
  6. package/dist/components/canvas/component.d.ts.map +1 -0
  7. package/dist/components/canvas/component.js +177 -0
  8. package/dist/components/canvas/component.js.map +1 -0
  9. package/dist/components/canvas/cursor.d.ts +8 -0
  10. package/dist/components/canvas/cursor.d.ts.map +1 -0
  11. package/dist/components/canvas/cursor.js +32 -0
  12. package/dist/components/canvas/cursor.js.map +1 -0
  13. package/dist/components/canvas/draggable.d.ts +21 -0
  14. package/dist/components/canvas/draggable.d.ts.map +1 -0
  15. package/dist/components/canvas/draggable.js +163 -0
  16. package/dist/components/canvas/draggable.js.map +1 -0
  17. package/dist/components/canvas/navbar/index.d.ts +19 -0
  18. package/dist/components/canvas/navbar/index.d.ts.map +1 -0
  19. package/dist/components/canvas/navbar/index.js +106 -0
  20. package/dist/components/canvas/navbar/index.js.map +1 -0
  21. package/dist/components/canvas/navbar/single-button.d.ts +17 -0
  22. package/dist/components/canvas/navbar/single-button.d.ts.map +1 -0
  23. package/dist/components/canvas/navbar/single-button.js +97 -0
  24. package/dist/components/canvas/navbar/single-button.js.map +1 -0
  25. package/dist/components/canvas/offest.d.ts +6 -0
  26. package/dist/components/canvas/offest.d.ts.map +1 -0
  27. package/dist/components/canvas/offest.js +12 -0
  28. package/dist/components/canvas/offest.js.map +1 -0
  29. package/dist/components/canvas/reset.d.ts +5 -0
  30. package/dist/components/canvas/reset.d.ts.map +1 -0
  31. package/dist/components/canvas/reset.js +7 -0
  32. package/dist/components/canvas/reset.js.map +1 -0
  33. package/dist/components/canvas/toolbar.d.ts +7 -0
  34. package/dist/components/canvas/toolbar.d.ts.map +1 -0
  35. package/dist/components/canvas/toolbar.js +28 -0
  36. package/dist/components/canvas/toolbar.js.map +1 -0
  37. package/dist/components/canvas/wrapper.d.ts +26 -0
  38. package/dist/components/canvas/wrapper.d.ts.map +1 -0
  39. package/dist/components/canvas/wrapper.js +107 -0
  40. package/dist/components/canvas/wrapper.js.map +1 -0
  41. package/dist/components/ui/FolderIcon.d.ts +9 -0
  42. package/dist/components/ui/FolderIcon.d.ts.map +1 -0
  43. package/dist/components/ui/FolderIcon.js +25 -0
  44. package/dist/components/ui/FolderIcon.js.map +1 -0
  45. package/dist/components/ui/button.d.ts +14 -0
  46. package/dist/components/ui/button.d.ts.map +1 -0
  47. package/dist/components/ui/button.js +54 -0
  48. package/dist/components/ui/button.js.map +1 -0
  49. package/dist/components/ui/label.d.ts +6 -0
  50. package/dist/components/ui/label.d.ts.map +1 -0
  51. package/dist/components/ui/label.js +10 -0
  52. package/dist/components/ui/label.js.map +1 -0
  53. package/dist/components/ui/toast.d.ts +16 -0
  54. package/dist/components/ui/toast.d.ts.map +1 -0
  55. package/dist/components/ui/toast.js +41 -0
  56. package/dist/components/ui/toast.js.map +1 -0
  57. package/dist/components/ui/toaster.d.ts +2 -0
  58. package/dist/components/ui/toaster.d.ts.map +1 -0
  59. package/dist/components/ui/toaster.js +10 -0
  60. package/dist/components/ui/toaster.js.map +1 -0
  61. package/dist/contexts/CanvasContext.d.ts +26 -0
  62. package/dist/contexts/CanvasContext.d.ts.map +1 -0
  63. package/dist/contexts/CanvasContext.js +22 -0
  64. package/dist/contexts/CanvasContext.js.map +1 -0
  65. package/dist/contexts/PerformanceContext.d.ts +31 -0
  66. package/dist/contexts/PerformanceContext.d.ts.map +1 -0
  67. package/dist/contexts/PerformanceContext.js +56 -0
  68. package/dist/contexts/PerformanceContext.js.map +1 -0
  69. package/dist/hooks/use-mobile.d.ts +2 -0
  70. package/dist/hooks/use-mobile.d.ts.map +1 -0
  71. package/dist/hooks/use-mobile.js +16 -0
  72. package/dist/hooks/use-mobile.js.map +1 -0
  73. package/dist/hooks/use-toast.d.ts +45 -0
  74. package/dist/hooks/use-toast.d.ts.map +1 -0
  75. package/dist/hooks/use-toast.js +126 -0
  76. package/dist/hooks/use-toast.js.map +1 -0
  77. package/dist/hooks/usePerformanceMode.d.ts +6 -0
  78. package/dist/hooks/usePerformanceMode.d.ts.map +1 -0
  79. package/dist/hooks/usePerformanceMode.js +6 -0
  80. package/dist/hooks/usePerformanceMode.js.map +1 -0
  81. package/dist/hooks/useWindowDimensions.d.ts +7 -0
  82. package/dist/hooks/useWindowDimensions.d.ts.map +1 -0
  83. package/dist/hooks/useWindowDimensions.js +22 -0
  84. package/dist/hooks/useWindowDimensions.js.map +1 -0
  85. package/dist/index.d.ts +26 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +28 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/lib/canvas.d.ts +35 -0
  90. package/dist/lib/canvas.d.ts.map +1 -0
  91. package/dist/lib/canvas.js +82 -0
  92. package/dist/lib/canvas.js.map +1 -0
  93. package/dist/lib/constants.d.ts +78 -0
  94. package/dist/lib/constants.d.ts.map +1 -0
  95. package/dist/lib/constants.js +122 -0
  96. package/dist/lib/constants.js.map +1 -0
  97. package/dist/lib/copy.d.ts +2 -0
  98. package/dist/lib/copy.d.ts.map +1 -0
  99. package/dist/lib/copy.js +20 -0
  100. package/dist/lib/copy.js.map +1 -0
  101. package/dist/lib/utils.d.ts +4 -0
  102. package/dist/lib/utils.d.ts.map +1 -0
  103. package/dist/lib/utils.js +14 -0
  104. package/dist/lib/utils.js.map +1 -0
  105. package/dist/types/index.d.ts +19 -0
  106. package/dist/types/index.d.ts.map +1 -0
  107. package/dist/types/index.js +14 -0
  108. package/dist/types/index.js.map +1 -0
  109. package/dist/utils/performance.d.ts +9 -0
  110. package/dist/utils/performance.d.ts.map +1 -0
  111. package/dist/utils/performance.js +29 -0
  112. package/dist/utils/performance.js.map +1 -0
  113. package/package.json +55 -0
  114. package/src/components/canvas/canvas.tsx +728 -0
  115. package/src/components/canvas/component.tsx +230 -0
  116. package/src/components/canvas/cursor.tsx +161 -0
  117. package/src/components/canvas/draggable.tsx +298 -0
  118. package/src/components/canvas/navbar/index.tsx +213 -0
  119. package/src/components/canvas/navbar/single-button.tsx +199 -0
  120. package/src/components/canvas/offest.tsx +23 -0
  121. package/src/components/canvas/reset.tsx +21 -0
  122. package/src/components/canvas/toolbar.tsx +67 -0
  123. package/src/components/canvas/wrapper.tsx +219 -0
  124. package/src/components/ui/FolderIcon.tsx +116 -0
  125. package/src/components/ui/button.tsx +162 -0
  126. package/src/components/ui/label.tsx +24 -0
  127. package/src/components/ui/toast.tsx +136 -0
  128. package/src/components/ui/toaster.tsx +33 -0
  129. package/src/contexts/CanvasContext.tsx +54 -0
  130. package/src/contexts/PerformanceContext.tsx +81 -0
  131. package/src/hooks/use-mobile.ts +21 -0
  132. package/src/hooks/use-toast.ts +186 -0
  133. package/src/hooks/usePerformanceMode.ts +5 -0
  134. package/src/hooks/useWindowDimensions.ts +32 -0
  135. package/src/index.ts +36 -0
  136. package/src/lib/canvas.ts +132 -0
  137. package/src/lib/constants.ts +153 -0
  138. package/src/lib/copy.ts +18 -0
  139. package/src/lib/utils.ts +18 -0
  140. package/src/types/index.ts +20 -0
  141. package/src/utils/performance.ts +37 -0
@@ -0,0 +1,54 @@
1
+ import React, { createContext, useContext, type ReactNode } from "react";
2
+ import { type MotionValue } from "framer-motion";
3
+ import { CanvasSection } from "../types";
4
+
5
+ export interface Point {
6
+ x: number;
7
+ y: number;
8
+ }
9
+
10
+ export interface CanvasContextState {
11
+ x: MotionValue<number>;
12
+ y: MotionValue<number>;
13
+ scale: MotionValue<number>;
14
+ isResetting: boolean;
15
+ maxZIndex: number;
16
+ setMaxZIndex: (zIndex: number) => void;
17
+ animationStage: number;
18
+ nextTargetSection: CanvasSection | null; // predictive pre-render target
19
+ setNextTargetSection: (section: CanvasSection | null) => void;
20
+ }
21
+
22
+ const defaultState = {
23
+ x: undefined as unknown as MotionValue<number>,
24
+ y: undefined as unknown as MotionValue<number>,
25
+ scale: 1 as unknown as MotionValue<number>,
26
+ isResetting: false,
27
+ maxZIndex: 1,
28
+ setMaxZIndex: () => {
29
+ console.log("setMaxZIndex not set");
30
+ },
31
+ animationStage: 0,
32
+ nextTargetSection: null,
33
+ setNextTargetSection: () => {
34
+ console.log("setNextTargetSection not set");
35
+ },
36
+ } as const;
37
+
38
+ export const CanvasContext = createContext<CanvasContextState>(defaultState);
39
+
40
+ export const useCanvasContext = () => useContext(CanvasContext);
41
+
42
+ interface CanvasProviderProps extends CanvasContextState {
43
+ children: ReactNode;
44
+ }
45
+
46
+ export const CanvasProvider: React.FC<CanvasProviderProps> = React.memo(
47
+ ({ children, ...value }) => (
48
+ <CanvasContext.Provider value={value as CanvasContextState}>
49
+ {children}
50
+ </CanvasContext.Provider>
51
+ ),
52
+ );
53
+
54
+ CanvasProvider.displayName = "CanvasProvider";
@@ -0,0 +1,81 @@
1
+ import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react";
2
+ import useWindowDimensions from "../hooks/useWindowDimensions";
3
+ import { isIOS, isMobile, prefersReducedMotion } from "../utils/performance";
4
+
5
+ export type PerformanceMode = "high" | "medium" | "low";
6
+
7
+ export interface PerformanceConfig {
8
+ mode: PerformanceMode;
9
+ isIOS: boolean;
10
+ isMobile: boolean;
11
+ prefersReducedMotion: boolean;
12
+ enableComplexShadows: boolean;
13
+ }
14
+
15
+ const defaultConfig: PerformanceConfig = {
16
+ mode: "high",
17
+ isIOS: false,
18
+ isMobile: false,
19
+ prefersReducedMotion: false,
20
+ enableComplexShadows: true,
21
+ };
22
+
23
+ const PerformanceContext = createContext<PerformanceConfig>(defaultConfig);
24
+
25
+ export const usePerformance = () => useContext(PerformanceContext);
26
+
27
+ // Backward compatibility alias
28
+ export const usePerformanceMode = usePerformance;
29
+
30
+ interface PerformanceProviderProps {
31
+ children: ReactNode;
32
+ }
33
+
34
+ /**
35
+ * Performance Provider - Centralized performance mode detection
36
+ *
37
+ * Detects device capabilities and user preferences once at the top level,
38
+ * avoiding redundant device detection across multiple components.
39
+ *
40
+ * Usage:
41
+ * <PerformanceProvider>
42
+ * <App />
43
+ * </PerformanceProvider>
44
+ *
45
+ * Then in components:
46
+ * const { mode, isIOS, enableComplexShadows } = usePerformance();
47
+ */
48
+ export const PerformanceProvider: React.FC<PerformanceProviderProps> = ({ children }) => {
49
+ const [config, setConfig] = useState<PerformanceConfig>(defaultConfig);
50
+ const { width } = useWindowDimensions();
51
+
52
+ useEffect(() => {
53
+ const isIOSDevice = isIOS();
54
+ const isMobileDevice = isMobile();
55
+ const reducedMotion = prefersReducedMotion();
56
+
57
+ let mode: PerformanceMode = "high";
58
+
59
+ // Determine performance mode based on device and screen size
60
+ if (isIOSDevice || reducedMotion || width < 768) {
61
+ mode = "low";
62
+ } else if (isMobileDevice || width < 1024) {
63
+ mode = "medium";
64
+ }
65
+
66
+ setConfig({
67
+ mode,
68
+ isIOS: isIOSDevice,
69
+ isMobile: isMobileDevice,
70
+ prefersReducedMotion: reducedMotion,
71
+ // Use simpler shadows on iOS and low-end devices for better performance
72
+ enableComplexShadows: mode !== "low",
73
+ });
74
+ }, [width]);
75
+
76
+ return (
77
+ <PerformanceContext.Provider value={config}>
78
+ {children}
79
+ </PerformanceContext.Provider>
80
+ );
81
+ };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+
3
+ const MOBILE_BREAKPOINT = 768;
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
7
+ undefined,
8
+ );
9
+
10
+ React.useEffect(() => {
11
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
+ const onChange = () => {
13
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
+ };
15
+ mql.addEventListener("change", onChange);
16
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17
+ return () => mql.removeEventListener("change", onChange);
18
+ }, []);
19
+
20
+ return !!isMobile;
21
+ }
@@ -0,0 +1,186 @@
1
+ import * as React from "react";
2
+
3
+ import type { ToastActionElement, ToastProps } from "../components/ui/toast";
4
+
5
+ const TOAST_LIMIT = 1;
6
+ const TOAST_REMOVE_DELAY = 5000;
7
+
8
+ type ToasterToast = ToastProps & {
9
+ id: string;
10
+ title?: React.ReactNode;
11
+ description?: React.ReactNode;
12
+ action?: ToastActionElement;
13
+ };
14
+
15
+ const actionTypes = {
16
+ ADD_TOAST: "ADD_TOAST",
17
+ UPDATE_TOAST: "UPDATE_TOAST",
18
+ DISMISS_TOAST: "DISMISS_TOAST",
19
+ REMOVE_TOAST: "REMOVE_TOAST",
20
+ } as const;
21
+
22
+ let count = 0;
23
+
24
+ function genId() {
25
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
26
+ return count.toString();
27
+ }
28
+
29
+ type ActionType = typeof actionTypes;
30
+
31
+ type Action =
32
+ | {
33
+ type: ActionType["ADD_TOAST"];
34
+ toast: ToasterToast;
35
+ }
36
+ | {
37
+ type: ActionType["UPDATE_TOAST"];
38
+ toast: Partial<ToasterToast>;
39
+ }
40
+ | {
41
+ type: ActionType["DISMISS_TOAST"];
42
+ toastId?: ToasterToast["id"];
43
+ }
44
+ | {
45
+ type: ActionType["REMOVE_TOAST"];
46
+ toastId?: ToasterToast["id"];
47
+ };
48
+
49
+ interface State {
50
+ toasts: ToasterToast[];
51
+ }
52
+
53
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
54
+
55
+ const addToRemoveQueue = (toastId: string) => {
56
+ if (toastTimeouts.has(toastId)) {
57
+ return;
58
+ }
59
+
60
+ const timeout = setTimeout(() => {
61
+ toastTimeouts.delete(toastId);
62
+ dispatch({
63
+ type: "REMOVE_TOAST",
64
+ toastId: toastId,
65
+ });
66
+ }, TOAST_REMOVE_DELAY);
67
+
68
+ toastTimeouts.set(toastId, timeout);
69
+ };
70
+
71
+ export const reducer = (state: State, action: Action): State => {
72
+ switch (action.type) {
73
+ case "ADD_TOAST":
74
+ return {
75
+ ...state,
76
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
77
+ };
78
+
79
+ case "UPDATE_TOAST":
80
+ return {
81
+ ...state,
82
+ toasts: state.toasts.map((t) =>
83
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
84
+ ),
85
+ };
86
+
87
+ case "DISMISS_TOAST": {
88
+ const { toastId } = action;
89
+
90
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
91
+ // but I'll keep it here for simplicity
92
+ if (toastId) {
93
+ addToRemoveQueue(toastId);
94
+ } else {
95
+ state.toasts.forEach((toast) => {
96
+ addToRemoveQueue(toast.id);
97
+ });
98
+ }
99
+
100
+ return {
101
+ ...state,
102
+ toasts: state.toasts.map((t) =>
103
+ t.id === toastId || toastId === undefined
104
+ ? {
105
+ ...t,
106
+ open: false,
107
+ }
108
+ : t,
109
+ ),
110
+ };
111
+ }
112
+ case "REMOVE_TOAST":
113
+ if (action.toastId === undefined) {
114
+ return {
115
+ ...state,
116
+ toasts: [],
117
+ };
118
+ }
119
+ return {
120
+ ...state,
121
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
122
+ };
123
+ }
124
+ };
125
+
126
+ const listeners: Array<(state: State) => void> = [];
127
+
128
+ let memoryState: State = { toasts: [] };
129
+
130
+ function dispatch(action: Action) {
131
+ memoryState = reducer(memoryState, action);
132
+ listeners.forEach((listener) => {
133
+ listener(memoryState);
134
+ });
135
+ }
136
+
137
+ type Toast = Omit<ToasterToast, "id">;
138
+
139
+ export function toast({ ...props }: Toast) {
140
+ const id = genId();
141
+
142
+ const update = (props: ToasterToast) =>
143
+ dispatch({
144
+ type: "UPDATE_TOAST",
145
+ toast: { ...props, id },
146
+ });
147
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
148
+
149
+ dispatch({
150
+ type: "ADD_TOAST",
151
+ toast: {
152
+ ...props,
153
+ id,
154
+ open: true,
155
+ onOpenChange: (open) => {
156
+ if (!open) dismiss();
157
+ },
158
+ },
159
+ });
160
+
161
+ return {
162
+ id: id,
163
+ dismiss,
164
+ update,
165
+ };
166
+ }
167
+
168
+ export function useToast() {
169
+ const [state, setState] = React.useState<State>(memoryState);
170
+
171
+ React.useEffect(() => {
172
+ listeners.push(setState);
173
+ return () => {
174
+ const index = listeners.indexOf(setState);
175
+ if (index > -1) {
176
+ listeners.splice(index, 1);
177
+ }
178
+ };
179
+ }, [state]);
180
+
181
+ return {
182
+ ...state,
183
+ toast,
184
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
185
+ };
186
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @deprecated Import from ~/contexts/PerformanceContext instead
3
+ * This file is kept for backward compatibility
4
+ */
5
+ export { usePerformance, usePerformanceMode, type PerformanceMode, type PerformanceConfig } from "../contexts/PerformanceContext";
@@ -0,0 +1,32 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ type WindowDimentions = {
4
+ width: number;
5
+ height: number;
6
+ };
7
+
8
+ const useWindowDimensions = (): WindowDimentions => {
9
+ const [windowDimensions, setWindowDimensions] = useState<WindowDimentions>({
10
+ width: typeof window !== "undefined" ? window.innerWidth : 1200,
11
+ height: typeof window !== "undefined" ? window.innerHeight : 800,
12
+ });
13
+
14
+ useEffect(() => {
15
+ function handleResize(): void {
16
+ setWindowDimensions({
17
+ width: window.innerWidth,
18
+ height: window.innerHeight,
19
+ });
20
+ }
21
+
22
+ // Set initial dimensions on mount
23
+ handleResize();
24
+
25
+ window.addEventListener("resize", handleResize);
26
+ return (): void => window.removeEventListener("resize", handleResize);
27
+ }, []); // Empty array ensures that effect is only run on mount
28
+
29
+ return windowDimensions;
30
+ };
31
+
32
+ export default useWindowDimensions;
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Components
2
+ export { default as Canvas, gradientBgImage } from './components/canvas/canvas';
3
+ export { CanvasComponent } from './components/canvas/component';
4
+ export { Draggable, DraggableImage } from './components/canvas/draggable';
5
+ export { CanvasWrapper, growTransition } from './components/canvas/wrapper';
6
+ export { default as CanvasCursor } from './components/canvas/cursor';
7
+ export { default as CanvasToolbar } from './components/canvas/toolbar';
8
+ export { default as CanvasNavbar } from './components/canvas/navbar';
9
+
10
+ // UI Components
11
+ export { Button } from './components/ui/button';
12
+ export { Label } from './components/ui/label';
13
+ export { default as FolderIcon } from './components/ui/FolderIcon';
14
+ export { Toaster } from './components/ui/toaster';
15
+ export { Toast, ToastAction, ToastClose, ToastTitle, ToastDescription, ToastViewport } from './components/ui/toast';
16
+
17
+ // Contexts
18
+ export { CanvasContext, CanvasProvider, useCanvasContext } from './contexts/CanvasContext';
19
+ export type { CanvasContextState } from './contexts/CanvasContext';
20
+ export { PerformanceProvider, usePerformanceMode, usePerformance } from './contexts/PerformanceContext';
21
+ export type { PerformanceMode, PerformanceConfig } from './contexts/PerformanceContext';
22
+
23
+ // Hooks
24
+ export { default as useWindowDimensions } from './hooks/useWindowDimensions';
25
+ export { usePerformanceMode as usePerformanceModeLegacy } from './hooks/usePerformanceMode';
26
+ export { useToast, toast } from './hooks/use-toast';
27
+
28
+ // Utilities
29
+ export * from './lib/canvas';
30
+ export * from './lib/constants';
31
+ export * from './utils/performance';
32
+ export { copyText } from './lib/copy';
33
+
34
+ // Types
35
+ export type { SectionCoordinates } from './types';
36
+ export { CanvasSection } from './types';
@@ -0,0 +1,132 @@
1
+ import { animate, type MotionValue, type Point } from "framer-motion";
2
+ import { useMemo } from "react";
3
+ import {
4
+ CANVAS_WIDTH,
5
+ CANVAS_HEIGHT,
6
+ MAX_DIM_RATIO,
7
+ INTRO_ASPECT_RATIO,
8
+ PAN_SPRING,
9
+ ScreenSizeEnum,
10
+ } from "./constants";
11
+
12
+ export const canvasWidth = CANVAS_WIDTH;
13
+ export const canvasHeight = CANVAS_HEIGHT;
14
+
15
+ // Re-export ScreenSizeEnum for backward compatibility
16
+ export { ScreenSizeEnum } from "./constants";
17
+
18
+ export const useMemoPoint = (x: number, y: number): Point => {
19
+ return useMemo(() => ({ x, y }), [x, y]);
20
+ };
21
+
22
+ export interface MinimalPointerInput {
23
+ clientX: number;
24
+ clientY: number;
25
+ }
26
+
27
+ export const getDistance = (
28
+ p1: MinimalPointerInput,
29
+ p2: MinimalPointerInput,
30
+ ) => {
31
+ const dx = p1.clientX - p2.clientX;
32
+ const dy = p1.clientY - p2.clientY;
33
+ return Math.sqrt(dx ** 2 + dy ** 2);
34
+ };
35
+
36
+ export const getMidpoint = (
37
+ p1: MinimalPointerInput,
38
+ p2: MinimalPointerInput,
39
+ ): Point => {
40
+ return {
41
+ x: (p1.clientX + p2.clientX) / 2,
42
+ y: (p1.clientY + p2.clientY) / 2,
43
+ };
44
+ };
45
+
46
+ export const getScreenSizeEnum = (width: number): ScreenSizeEnum => {
47
+ // iphone 12 pro is 390px, iphone 14 pro max is 430px, SE 3rd gen is 375px
48
+ if (width < 400) return ScreenSizeEnum.SMALL_MOBILE;
49
+ if (width < 768) return ScreenSizeEnum.MOBILE;
50
+ if (width < 1440) return ScreenSizeEnum.TABLET;
51
+ if (width < 1920) return ScreenSizeEnum.SMALL_DESKTOP;
52
+ if (width < 2560) return ScreenSizeEnum.MEDIUM_DESKTOP;
53
+ if (width <= 3440) return ScreenSizeEnum.LARGE_DESKTOP;
54
+ return ScreenSizeEnum.HUGE_DESKTOP;
55
+ };
56
+
57
+ export function getSectionPanCoordinates({
58
+ windowDimensions,
59
+ coords,
60
+ targetZoom,
61
+ negative,
62
+ }: {
63
+ windowDimensions: { width: number; height: number };
64
+ coords: { x: number; y: number; width: number; height: number };
65
+ targetZoom: number;
66
+ negative?: boolean;
67
+ }) {
68
+ const { width, height } = windowDimensions;
69
+ // Calculate the center of the section
70
+ const sectionCenterX = coords.x + coords.width / 2;
71
+ const sectionCenterY = coords.y + coords.height / 2;
72
+
73
+ // Calculate the required pan offset to center the section in the viewport
74
+ const targetX = width / 2 - sectionCenterX * targetZoom;
75
+ const targetY = height / 2 - sectionCenterY * targetZoom;
76
+
77
+ if (negative) {
78
+ return {
79
+ x: -targetX,
80
+ y: -targetY,
81
+ };
82
+ }
83
+
84
+ return {
85
+ x: targetX,
86
+ y: targetY,
87
+ };
88
+ }
89
+
90
+ export async function panToOffsetScene(
91
+ offset: Point,
92
+ x: MotionValue<number>,
93
+ y: MotionValue<number>,
94
+ scale: MotionValue<number>,
95
+ newZoom?: number,
96
+ ): Promise<void> {
97
+ const animX = animate(x, offset.x, PAN_SPRING);
98
+ const animY = animate(y, offset.y, PAN_SPRING);
99
+ const animScale = animate(scale, newZoom ?? 1, PAN_SPRING);
100
+ await Promise.all([animScale, animX, animY]);
101
+ }
102
+
103
+ export const calcInitialBoxWidth = (
104
+ windowWidth: number,
105
+ windowHeight: number,
106
+ ) => {
107
+ // math CanvasWrapper's bounding box size and compute scale s.t. canvas fits entirely within
108
+ const maxWidth = windowWidth * MAX_DIM_RATIO.width;
109
+ const maxHeight = windowHeight * MAX_DIM_RATIO.height;
110
+
111
+ let boxWidth, boxHeight;
112
+
113
+ if (maxWidth / INTRO_ASPECT_RATIO <= maxHeight) {
114
+ boxWidth = maxWidth;
115
+ boxHeight = boxWidth / INTRO_ASPECT_RATIO;
116
+ } else {
117
+ boxHeight = maxHeight;
118
+ boxWidth = boxHeight * INTRO_ASPECT_RATIO;
119
+ }
120
+
121
+ // scale so the canvas fits inside the computed 3:2 box
122
+ return Math.min(boxWidth / canvasWidth, boxHeight / canvasHeight);
123
+ };
124
+
125
+ // Re-export commonly used constants for backward compatibility
126
+ export { MAX_DIM_RATIO } from "./constants";
127
+ export {
128
+ INTERACTIVE_SELECTOR,
129
+ ZOOM_BOUND,
130
+ MAX_ZOOM,
131
+ MIN_ZOOMS,
132
+ } from "./constants";
@@ -0,0 +1,153 @@
1
+ import { type Easing } from "framer-motion";
2
+
3
+ /**
4
+ * Canvas Library Constants
5
+ * All configurable constants consolidated in one place
6
+ */
7
+
8
+ // ============================================================================
9
+ // SCREEN SIZE BREAKPOINTS
10
+ // ============================================================================
11
+
12
+ export enum ScreenSizeEnum {
13
+ SMALL_MOBILE = "small-mobile",
14
+ MOBILE = "mobile",
15
+ TABLET = "tablet",
16
+ SMALL_DESKTOP = "small-desktop",
17
+ MEDIUM_DESKTOP = "medium-desktop",
18
+ LARGE_DESKTOP = "large-desktop",
19
+ HUGE_DESKTOP = "huge-desktop",
20
+ }
21
+
22
+ // ============================================================================
23
+ // CANVAS DIMENSIONS
24
+ // ============================================================================
25
+
26
+ /** Default canvas width in pixels */
27
+ export const CANVAS_WIDTH = 6000;
28
+
29
+ /** Default canvas height in pixels */
30
+ export const CANVAS_HEIGHT = 4000;
31
+
32
+ // ============================================================================
33
+ // INTRO ANIMATION
34
+ // ============================================================================
35
+
36
+ /** Maximum dimensions ratio for the intro box relative to viewport */
37
+ export const MAX_DIM_RATIO = {
38
+ width: 0.8,
39
+ height: 0.5,
40
+ } as const;
41
+
42
+ /** Intro box aspect ratio (width:height) */
43
+ export const INTRO_ASPECT_RATIO = 3 / 2;
44
+
45
+ /** Grow animation transition config */
46
+ export const GROW_TRANSITION = {
47
+ duration: 0.96,
48
+ delay: 3.14,
49
+ ease: [0.35, 0.1, 0.8, 1] as Easing,
50
+ } as const;
51
+
52
+ /** Blur mask animation transition config */
53
+ export const BLUR_TRANSITION = {
54
+ duration: 0.85,
55
+ delay: 1.25,
56
+ ease: "easeIn" as Easing,
57
+ } as const;
58
+
59
+ /** Stage 2 pan-to-home transition config */
60
+ export const STAGE2_TRANSITION = {
61
+ duration: 0.96,
62
+ ease: [0.37, 0.1, 0.6, 1],
63
+ } as const;
64
+
65
+ // ============================================================================
66
+ // ZOOM & PAN
67
+ // ============================================================================
68
+
69
+ /** Maximum zoom level */
70
+ export const MAX_ZOOM = 3;
71
+
72
+ /** Minimum zoom bound multiplier to prevent zooming out past canvas edges */
73
+ export const ZOOM_BOUND = 1.05;
74
+
75
+ /** Minimum zoom levels per screen size */
76
+ export const MIN_ZOOMS: Record<ScreenSizeEnum, number> = {
77
+ [ScreenSizeEnum.SMALL_MOBILE]: 0.3,
78
+ [ScreenSizeEnum.MOBILE]: 0.35,
79
+ [ScreenSizeEnum.TABLET]: 0.25,
80
+ [ScreenSizeEnum.SMALL_DESKTOP]: 0.15,
81
+ [ScreenSizeEnum.MEDIUM_DESKTOP]: 0.1,
82
+ [ScreenSizeEnum.LARGE_DESKTOP]: 0.1,
83
+ [ScreenSizeEnum.HUGE_DESKTOP]: 0.1,
84
+ } as const;
85
+
86
+ /** Pan animation spring config */
87
+ export const PAN_SPRING = {
88
+ visualDuration: 0.34,
89
+ type: "spring",
90
+ stiffness: 200,
91
+ damping: 25,
92
+ } as const;
93
+
94
+ /** Wheel zoom sensitivity for mouse wheel */
95
+ export const MOUSE_WHEEL_ZOOM_SENSITIVITY = 0.0015;
96
+
97
+ /** Wheel zoom sensitivity for trackpad */
98
+ export const TRACKPAD_ZOOM_SENSITIVITY = 0.015;
99
+
100
+ // ============================================================================
101
+ // INTERACTIONS
102
+ // ============================================================================
103
+
104
+ /** CSS selector for interactive elements that should not trigger pan */
105
+ export const INTERACTIVE_SELECTOR =
106
+ "button,[role='button'],input,textarea,[contenteditable='true']," +
107
+ "[data-toolbar-button],[data-navbar-button]";
108
+
109
+ // ============================================================================
110
+ // VIEWPORT CULLING
111
+ // ============================================================================
112
+
113
+ /** Buffer zone in pixels for hysteresis visibility detection */
114
+ export const VIEWPORT_HYSTERESIS_BUFFER = 120;
115
+
116
+ /** Threshold for showing image fallback on smaller screens */
117
+ export const IMAGE_FALLBACK_WIDTH_THRESHOLD = 2000;
118
+
119
+ // ============================================================================
120
+ // NAVBAR
121
+ // ============================================================================
122
+
123
+ /** Responsive zoom levels for navbar navigation per screen size */
124
+ export const RESPONSIVE_ZOOM_MAP: Record<ScreenSizeEnum, number> = {
125
+ [ScreenSizeEnum.SMALL_MOBILE]: 0.5,
126
+ [ScreenSizeEnum.MOBILE]: 0.6,
127
+ [ScreenSizeEnum.TABLET]: 0.8,
128
+ [ScreenSizeEnum.SMALL_DESKTOP]: 0.9,
129
+ [ScreenSizeEnum.MEDIUM_DESKTOP]: 1,
130
+ [ScreenSizeEnum.LARGE_DESKTOP]: 1.25,
131
+ [ScreenSizeEnum.HUGE_DESKTOP]: 1.5,
132
+ } as const;
133
+
134
+ // ============================================================================
135
+ // PERFORMANCE
136
+ // ============================================================================
137
+
138
+ /** Debounce duration in ms for navbar clicks based on performance mode */
139
+ export const NAVBAR_DEBOUNCE_MS = {
140
+ high: 0,
141
+ medium: 100,
142
+ low: 400,
143
+ } as const;
144
+
145
+ // ============================================================================
146
+ // TOOLBAR
147
+ // ============================================================================
148
+
149
+ /** Epsilon for position comparison to hide toolbar when at home */
150
+ export const TOOLBAR_OPACITY_POS_EPS = 1; // px
151
+
152
+ /** Epsilon for scale comparison to hide toolbar when at 1x */
153
+ export const TOOLBAR_OPACITY_SCALE_EPS = 0.01; // scale delta