@ue-too/board-react-adapter 0.10.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/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # @ue-too/board-react-adapter
2
+
3
+ React adapter for the @ue-too/board infinite canvas library.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@ue-too/board-react-adapter.svg)](https://www.npmjs.com/package/@ue-too/board-react-adapter)
6
+ [![license](https://img.shields.io/npm/l/@ue-too/board-react-adapter.svg)](https://github.com/ue-too/ue-too/blob/main/LICENSE.txt)
7
+
8
+ ## Overview
9
+
10
+ `@ue-too/board-react-adapter` provides React components and hooks to integrate the `@ue-too/board` infinite canvas into React applications. It handles lifecycle management, state synchronization, and provides idiomatic React patterns for working with the board.
11
+
12
+ ### Key Features
13
+
14
+ - **React Components**: `<Board>` component with full lifecycle management
15
+ - **State Synchronization**: Camera state changes trigger React re-renders
16
+ - **Performance Optimized**: Uses `useSyncExternalStore` for efficient subscriptions
17
+ - **Type-Safe Hooks**: Full TypeScript support with type inference
18
+ - **Context-Based**: Share board instance across component tree
19
+ - **Animation Integration**: Hooks for animation loops integrated with board
20
+ - **Camera Controls**: Idiomatic React hooks for pan, zoom, and rotation
21
+
22
+ ## Installation
23
+
24
+ Using Bun:
25
+ ```bash
26
+ bun add @ue-too/board-react-adapter react react-dom
27
+ ```
28
+
29
+ Using npm:
30
+ ```bash
31
+ npm install @ue-too/board-react-adapter react react-dom
32
+ ```
33
+
34
+ **Peer Dependencies:**
35
+ - React >= 19.0.0
36
+ - React-DOM >= 19.0.0
37
+
38
+ ## Quick Start
39
+
40
+ Here's a simple example creating an infinite canvas with React:
41
+
42
+ ```tsx
43
+ import Board from '@ue-too/board-react-adapter';
44
+
45
+ function App() {
46
+ return (
47
+ <Board
48
+ width={800}
49
+ height={600}
50
+ animationCallback={(timestamp, ctx, camera) => {
51
+ // Clear canvas
52
+ ctx.clearRect(0, 0, 800, 600);
53
+
54
+ // Draw a blue square at world origin
55
+ ctx.fillStyle = 'blue';
56
+ ctx.fillRect(0, 0, 100, 100);
57
+ }}
58
+ />
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Core APIs
64
+
65
+ ### Board Component
66
+
67
+ Main component that renders the canvas and manages the board instance.
68
+
69
+ ```tsx
70
+ <Board
71
+ width={number}
72
+ height={number}
73
+ animationCallback={(timestamp, ctx, camera) => void}
74
+ // Optional props
75
+ className?={string}
76
+ style?={React.CSSProperties}
77
+ />
78
+ ```
79
+
80
+ **Props:**
81
+ - `width`: Canvas width in pixels
82
+ - `height`: Canvas height in pixels
83
+ - `animationCallback`: Function called on each frame with timestamp, context, and camera
84
+ - `className`: CSS class name for the canvas element
85
+ - `style`: Inline styles for the canvas element
86
+
87
+ **Children:**
88
+ The component supports children, which will have access to the board via context.
89
+
90
+ ```tsx
91
+ <Board width={800} height={600}>
92
+ <Controls />
93
+ <StatusDisplay />
94
+ </Board>
95
+ ```
96
+
97
+ ### State Hooks
98
+
99
+ #### `useBoardCameraState(key)`
100
+
101
+ Subscribe to a specific camera state property.
102
+
103
+ ```tsx
104
+ function useBoardCameraState<K extends keyof CameraState>(
105
+ key: K
106
+ ): CameraState[K];
107
+ ```
108
+
109
+ **Example:**
110
+ ```tsx
111
+ function CameraPosition() {
112
+ const position = useBoardCameraState('position');
113
+
114
+ return <div>Position: ({position.x.toFixed(0)}, {position.y.toFixed(0)})</div>;
115
+ }
116
+ ```
117
+
118
+ **Available Keys:**
119
+ - `position`: `{ x: number, y: number }` - Camera world position
120
+ - `rotation`: `number` - Camera rotation in radians
121
+ - `zoomLevel`: `number` - Current zoom level
122
+
123
+ #### `useAllBoardCameraState()`
124
+
125
+ Subscribe to all camera state at once.
126
+
127
+ ```tsx
128
+ function useAllBoardCameraState(): CameraState;
129
+ ```
130
+
131
+ **Example:**
132
+ ```tsx
133
+ function CameraInfo() {
134
+ const camera = useAllBoardCameraState();
135
+
136
+ return (
137
+ <div>
138
+ <p>Position: ({camera.position.x}, {camera.position.y})</p>
139
+ <p>Rotation: {camera.rotation}rad</p>
140
+ <p>Zoom: {camera.zoomLevel}x</p>
141
+ </div>
142
+ );
143
+ }
144
+ ```
145
+
146
+ #### `useBoard()`
147
+
148
+ Access the board instance from context.
149
+
150
+ ```tsx
151
+ function useBoard(): BoardType | null;
152
+ ```
153
+
154
+ #### `useBoard Camera()`
155
+
156
+ Access the camera instance from context.
157
+
158
+ ```tsx
159
+ function useBoardCamera(): Camera | null;
160
+ ```
161
+
162
+ ### Control Hooks
163
+
164
+ #### `useCameraInput()`
165
+
166
+ Get camera control functions.
167
+
168
+ ```tsx
169
+ function useCameraInput(): {
170
+ panToWorld: (position: Point) => void;
171
+ panByScreen: (offset: Point) => void;
172
+ zoomTo: (level: number) => void;
173
+ zoomIn: () => void;
174
+ zoomOut: () => void;
175
+ rotateTo: (angle: number) => void;
176
+ };
177
+ ```
178
+
179
+ **Example:**
180
+ ```tsx
181
+ function Controls() {
182
+ const { panToWorld, zoomTo, rotateTo } = useCameraInput();
183
+
184
+ return (
185
+ <div>
186
+ <button onClick={() => panToWorld({ x: 0, y: 0 })}>Center</button>
187
+ <button onClick={() => zoomTo(1.0)}>Reset Zoom</button>
188
+ <button onClick={() => rotateTo(0)}>Reset Rotation</button>
189
+ </div>
190
+ );
191
+ }
192
+ ```
193
+
194
+ #### `useCustomCameraMux(customMux)`
195
+
196
+ Set a custom camera multiplexer for advanced camera control.
197
+
198
+ ```tsx
199
+ function useCustomCameraMux(
200
+ customMux: CameraMux | undefined
201
+ ): void;
202
+ ```
203
+
204
+ #### `useBoardify(width, height, animationCallback)`
205
+
206
+ Create a standalone board instance without using the provider pattern.
207
+
208
+ ```tsx
209
+ function useBoardify(
210
+ width: number,
211
+ height: number,
212
+ animationCallback: AnimationCallback
213
+ ): {
214
+ canvas: HTMLCanvasElement | null;
215
+ board: BoardType | null;
216
+ };
217
+ ```
218
+
219
+ ### Animation Hooks
220
+
221
+ #### `useAnimationFrame(callback)`
222
+
223
+ Generic animation frame hook.
224
+
225
+ ```tsx
226
+ function useAnimationFrame(
227
+ callback: (timestamp: number) => void
228
+ ): void;
229
+ ```
230
+
231
+ **Example:**
232
+ ```tsx
233
+ function AnimatedComponent() {
234
+ const [rotation, setRotation] = useState(0);
235
+
236
+ useAnimationFrame((timestamp) => {
237
+ setRotation(timestamp * 0.001); // Rotate based on time
238
+ });
239
+
240
+ return <div style={{ transform: `rotate(${rotation}rad)` }}>Spinning</div>;
241
+ }
242
+ ```
243
+
244
+ #### `useAnimationFrameWithBoard(callback)`
245
+
246
+ Animation loop integrated with `board.step()`.
247
+
248
+ ```tsx
249
+ function useAnimationFrameWithBoard(
250
+ callback: (timestamp: number, ctx: CanvasRenderingContext2D, camera: Camera) => void
251
+ ): void;
252
+ ```
253
+
254
+ ## Common Use Cases
255
+
256
+ ### Basic Canvas with Pan and Zoom
257
+
258
+ ```tsx
259
+ import Board, { useCameraInput, useBoardCameraState } from '@ue-too/board-react-adapter';
260
+
261
+ function Controls() {
262
+ const position = useBoardCameraState('position');
263
+ const zoom = useBoardCameraState('zoomLevel');
264
+ const { panToWorld, zoomTo } = useCameraInput();
265
+
266
+ return (
267
+ <div style={{ position: 'absolute', top: 10, left: 10 }}>
268
+ <p>Position: ({position.x.toFixed(0)}, {position.y.toFixed(0)})</p>
269
+ <p>Zoom: {zoom.toFixed(2)}x</p>
270
+ <button onClick={() => panToWorld({ x: 0, y: 0 })}>Center</button>
271
+ <button onClick={() => zoomTo(1.0)}>Reset Zoom</button>
272
+ </div>
273
+ );
274
+ }
275
+
276
+ function App() {
277
+ return (
278
+ <Board
279
+ width={800}
280
+ height={600}
281
+ animationCallback={(timestamp, ctx) => {
282
+ ctx.fillStyle = 'lightblue';
283
+ ctx.fillRect(-200, -200, 400, 400);
284
+
285
+ ctx.fillStyle = 'red';
286
+ ctx.fillRect(-50, -50, 100, 100);
287
+ }}
288
+ >
289
+ <Controls />
290
+ </Board>
291
+ );
292
+ }
293
+ ```
294
+
295
+ ### Interactive Drawing
296
+
297
+ ```tsx
298
+ import Board, { useBoard } from '@ue-too/board-react-adapter';
299
+ import { useState } from 'react';
300
+
301
+ function Drawing() {
302
+ const board = useBoard();
303
+ const [points, setPoints] = useState<Point[]>([]);
304
+
305
+ const handleClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
306
+ if (!board) return;
307
+
308
+ const rect = e.currentTarget.getBoundingClientRect();
309
+ const screenX = e.clientX - rect.left;
310
+ const screenY = e.clientY - rect.top;
311
+
312
+ const worldPoint = board.camera.screenToWorld({ x: screenX, y: screenY });
313
+ setPoints([...points, worldPoint]);
314
+ };
315
+
316
+ return (
317
+ <Board
318
+ width={800}
319
+ height={600}
320
+ onClick={handleClick}
321
+ animationCallback={(timestamp, ctx) => {
322
+ // Draw all points
323
+ ctx.fillStyle = 'red';
324
+ points.forEach(point => {
325
+ ctx.beginPath();
326
+ ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
327
+ ctx.fill();
328
+ });
329
+ }}
330
+ />
331
+ );
332
+ }
333
+ ```
334
+
335
+ ### Animated Scene
336
+
337
+ ```tsx
338
+ import Board, { useAnimationFrameWithBoard } from '@ue-too/board-react-adapter';
339
+ import { useState } from 'react';
340
+
341
+ function AnimatedScene() {
342
+ const [time, setTime] = useState(0);
343
+
344
+ useAnimationFrameWithBoard((timestamp, ctx, camera) => {
345
+ setTime(timestamp * 0.001);
346
+
347
+ // Draw animated circle
348
+ const x = Math.cos(time) * 100;
349
+ const y = Math.sin(time) * 100;
350
+
351
+ ctx.fillStyle = 'green';
352
+ ctx.beginPath();
353
+ ctx.arc(x, y, 20, 0, Math.PI * 2);
354
+ ctx.fill();
355
+ });
356
+
357
+ return null; // No UI, just updates
358
+ }
359
+
360
+ function App() {
361
+ return (
362
+ <Board width={800} height={600}>
363
+ <AnimatedScene />
364
+ </Board>
365
+ );
366
+ }
367
+ ```
368
+
369
+ ### Camera Controls with Buttons
370
+
371
+ ```tsx
372
+ import Board, { useCameraInput, useBoardCameraState } from '@ue-too/board-react-adapter';
373
+
374
+ function CameraControls() {
375
+ const { panByScreen, zoomIn, zoomOut, rotateTo } = useCameraInput();
376
+ const rotation = useBoardCameraState('rotation');
377
+
378
+ return (
379
+ <div style={{ position: 'absolute', top: 10, right: 10 }}>
380
+ <button onClick={() => panByScreen({ x: 0, y: -50 })}>↑</button>
381
+ <br />
382
+ <button onClick={() => panByScreen({ x: -50, y: 0 })}>←</button>
383
+ <button onClick={() => panByScreen({ x: 50, y: 0 })}>→</button>
384
+ <br />
385
+ <button onClick={() => panByScreen({ x: 0, y: 50 })}>↓</button>
386
+ <br />
387
+ <button onClick={zoomIn}>Zoom In</button>
388
+ <button onClick={zoomOut}>Zoom Out</button>
389
+ <br />
390
+ <button onClick={() => rotateTo((rotation + Math.PI / 4) % (Math.PI * 2))}>
391
+ Rotate 45°
392
+ </button>
393
+ </div>
394
+ );
395
+ }
396
+
397
+ function App() {
398
+ return (
399
+ <Board width={800} height={600}>
400
+ <CameraControls />
401
+ </Board>
402
+ );
403
+ }
404
+ ```
405
+
406
+ ### With Custom Animation Loop
407
+
408
+ ```tsx
409
+ import { useBoardify } from '@ue-too/board-react-adapter';
410
+ import { useEffect, useRef } from 'react';
411
+
412
+ function CustomBoard() {
413
+ const containerRef = useRef<HTMLDivElement>(null);
414
+ const { canvas, board } = useBoardify(
415
+ 800,
416
+ 600,
417
+ (timestamp, ctx, camera) => {
418
+ ctx.fillStyle = 'purple';
419
+ ctx.fillRect(-100, -100, 200, 200);
420
+ }
421
+ );
422
+
423
+ useEffect(() => {
424
+ if (canvas && containerRef.current) {
425
+ containerRef.current.appendChild(canvas);
426
+ }
427
+ }, [canvas]);
428
+
429
+ return <div ref={containerRef} />;
430
+ }
431
+ ```
432
+
433
+ ## API Reference
434
+
435
+ For complete API documentation with detailed type information, see the [TypeDoc-generated documentation](../../docs/board-react-adapter).
436
+
437
+ ## TypeScript Support
438
+
439
+ This package is written in TypeScript with complete type definitions:
440
+
441
+ ```tsx
442
+ import Board, {
443
+ useBoardCameraState,
444
+ useCameraInput,
445
+ type CameraState,
446
+ type AnimationCallback
447
+ } from '@ue-too/board-react-adapter';
448
+
449
+ // State is fully typed
450
+ const position: Point = useBoardCameraState('position');
451
+ const zoom: number = useBoardCameraState('zoomLevel');
452
+
453
+ // Functions are type-safe
454
+ const { panToWorld }: { panToWorld: (position: Point) => void } = useCameraInput();
455
+
456
+ // Callbacks are typed
457
+ const callback: AnimationCallback = (timestamp, ctx, camera) => {
458
+ // All parameters are properly typed
459
+ };
460
+ ```
461
+
462
+ ## Design Philosophy
463
+
464
+ This adapter follows these principles:
465
+
466
+ - **React Idiomatic**: Uses hooks, context, and component patterns
467
+ - **Performance First**: Optimized state subscriptions with `useSyncExternalStore`
468
+ - **Type Safety**: Full TypeScript support throughout
469
+ - **Minimal API Surface**: Simple, focused hooks and components
470
+ - **Flexible**: Supports both provider and standalone patterns
471
+
472
+ ## Performance Considerations
473
+
474
+ - **State Subscriptions**: Use specific state hooks (`useBoardCameraState('position')`) instead of `useAllBoardCameraState()` to minimize re-renders
475
+ - **Animation Callbacks**: Keep animation callbacks pure and avoid heavy computations
476
+ - **Canvas Updates**: Board automatically handles canvas clearing and transformation
477
+
478
+ **Performance Tips:**
479
+ - Subscribe only to the state you need
480
+ - Use `useMemo` for expensive calculations in render
481
+ - Avoid creating new objects in animation callbacks
482
+ - Use the board's built-in coordinate transformation instead of manual calculations
483
+
484
+ ## Related Packages
485
+
486
+ - **[@ue-too/board](../board)**: The core infinite canvas library
487
+ - **[@ue-too/math](../math)**: Vector operations for point calculations
488
+ - **[@ue-too/animate](../animate)**: Animation system for canvas objects
489
+
490
+ ## License
491
+
492
+ MIT
493
+
494
+ ## Repository
495
+
496
+ [https://github.com/ue-too/ue-too](https://github.com/ue-too/ue-too)
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Props for the Board component.
3
+ *
4
+ * @category Components
5
+ */
6
+ export type BoardProps = {
7
+ /** Enable fullscreen mode (canvas resizes with window) */
8
+ fullScreen?: boolean;
9
+ /** Canvas width in pixels */
10
+ width?: number;
11
+ /** Canvas height in pixels */
12
+ height?: number;
13
+ /** Callback function for drawing on each animation frame */
14
+ animationCallback?: (timestamp: number, ctx: CanvasRenderingContext2D) => void;
15
+ /** Child components that can access the board via hooks */
16
+ children?: React.ReactNode;
17
+ };
18
+ /**
19
+ * Main Board component with provider wrapper for React applications.
20
+ *
21
+ * @remarks
22
+ * This component provides a complete infinite canvas solution for React. It combines:
23
+ * - A {@link BoardProvider} to share the board instance across components
24
+ * - A canvas element configured with the @ue-too/board package
25
+ * - An integrated animation loop for drawing
26
+ * - Support for child components that can access board state and controls
27
+ *
28
+ * ## Features
29
+ *
30
+ * - **Infinite Canvas**: Pan, zoom, and rotate with mouse/touch input
31
+ * - **Animation Loop**: Automatic rendering loop with customizable draw callback
32
+ * - **React Integration**: Use hooks to access camera state and controls
33
+ * - **Fullscreen Support**: Optional auto-resize with window
34
+ * - **Type-Safe**: Full TypeScript support
35
+ *
36
+ * ## Usage Pattern
37
+ *
38
+ * 1. Render the Board component
39
+ * 2. Provide an animation callback for drawing
40
+ * 3. Use child components with board hooks for UI controls
41
+ * 4. Access camera state reactively via hooks
42
+ *
43
+ * @param props - Component props
44
+ * @param props.width - Canvas width in pixels (default: auto)
45
+ * @param props.height - Canvas height in pixels (default: auto)
46
+ * @param props.fullScreen - Enable fullscreen mode (canvas resizes with window)
47
+ * @param props.animationCallback - Function called on each frame with timestamp and context
48
+ * @param props.children - Child components that can use board hooks
49
+ *
50
+ * @example
51
+ * Basic usage with drawing
52
+ * ```tsx
53
+ * function App() {
54
+ * return (
55
+ * <Board
56
+ * width={800}
57
+ * height={600}
58
+ * animationCallback={(timestamp, ctx) => {
59
+ * // Draw a rectangle at world position (0, 0)
60
+ * ctx.fillStyle = 'blue';
61
+ * ctx.fillRect(0, 0, 100, 100);
62
+ * }}
63
+ * />
64
+ * );
65
+ * }
66
+ * ```
67
+ *
68
+ * @example
69
+ * With child components and camera controls
70
+ * ```tsx
71
+ * function CameraControls() {
72
+ * const { panToWorld, zoomTo } = useCameraInput();
73
+ * const position = useBoardCameraState('position');
74
+ *
75
+ * return (
76
+ * <div style={{ position: 'absolute', top: 10, left: 10 }}>
77
+ * <p>Position: ({position.x.toFixed(0)}, {position.y.toFixed(0)})</p>
78
+ * <button onClick={() => panToWorld({ x: 0, y: 0 })}>Center</button>
79
+ * <button onClick={() => zoomTo(1.0)}>Reset Zoom</button>
80
+ * </div>
81
+ * );
82
+ * }
83
+ *
84
+ * function App() {
85
+ * return (
86
+ * <Board width={800} height={600} animationCallback={drawScene}>
87
+ * <CameraControls />
88
+ * </Board>
89
+ * );
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * Fullscreen mode
95
+ * ```tsx
96
+ * function App() {
97
+ * return (
98
+ * <Board
99
+ * fullScreen
100
+ * animationCallback={(timestamp, ctx) => {
101
+ * // Canvas automatically resizes to window size
102
+ * ctx.fillStyle = 'green';
103
+ * ctx.fillRect(-50, -50, 100, 100);
104
+ * }}
105
+ * />
106
+ * );
107
+ * }
108
+ * ```
109
+ *
110
+ * @category Components
111
+ * @see {@link useBoardCameraState} for accessing camera state
112
+ * @see {@link useCameraInput} for camera control functions
113
+ * @see {@link useBoard} for accessing the board instance
114
+ */
115
+ export default function BoardWrapperWithChildren({ width, height, fullScreen, animationCallback: animationCallbackProp, children }: BoardProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Components module for @ue-too/board-react-adapter.
3
+ *
4
+ * @remarks
5
+ * This module exports React components for rendering the infinite canvas.
6
+ *
7
+ * @module
8
+ */
9
+ export * from "./Board";
10
+ export { default as Board } from "./Board";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Hooks module for @ue-too/board-react-adapter.
3
+ *
4
+ * @remarks
5
+ * This module exports all React hooks for working with the board:
6
+ * - State hooks for subscribing to camera changes
7
+ * - Control hooks for manipulating the camera
8
+ * - Animation hooks for rendering loops
9
+ * - Context hooks for accessing the board instance
10
+ *
11
+ * @module
12
+ */
13
+ export * from "./useAnimationFrame";
14
+ export * from "./useBoardify";
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Hook to run a callback on every animation frame.
3
+ *
4
+ * @remarks
5
+ * This hook uses `requestAnimationFrame` to execute a callback repeatedly for smooth animations.
6
+ * The animation loop starts when the component mounts and stops when it unmounts, automatically
7
+ * cleaning up the animation frame request.
8
+ *
9
+ * **Performance Note**: The callback is called on every frame, so ensure your callback is
10
+ * optimized to avoid performance issues. The callback dependency should be stable to prevent
11
+ * restarting the animation loop unnecessarily.
12
+ *
13
+ * @param callback - Function to call on each animation frame, receives the current timestamp
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * function AnimatedComponent() {
18
+ * const [rotation, setRotation] = useState(0);
19
+ *
20
+ * useAnimationFrame((timestamp) => {
21
+ * // Rotate 45 degrees per second
22
+ * setRotation((prev) => prev + (Math.PI / 4) * (1 / 60));
23
+ * });
24
+ *
25
+ * return <div style={{ transform: `rotate(${rotation}rad)` }}>Spinning!</div>;
26
+ * }
27
+ * ```
28
+ *
29
+ * @category Hooks
30
+ * @see {@link useAnimationFrameWithBoard} for board-integrated animation loop
31
+ */
32
+ export declare function useAnimationFrame(callback: (timestamp: number) => void): void;
33
+ /**
34
+ * Hook to run an animation loop integrated with the Board's step function.
35
+ *
36
+ * @remarks
37
+ * This hook automatically calls `board.step(timestamp)` on every frame to update the board's
38
+ * camera transform, then invokes your callback with the timestamp and canvas context.
39
+ * This is the recommended way to implement drawing logic for board-based applications.
40
+ *
41
+ * The hook handles:
42
+ * - Calling `board.step()` to update camera transforms
43
+ * - Providing the canvas context for drawing
44
+ * - Warning if context is not available
45
+ * - Cleaning up the animation loop on unmount
46
+ *
47
+ * **Typical Usage Pattern**:
48
+ * 1. Board calls `step()` to update transforms
49
+ * 2. Your callback draws on the canvas
50
+ * 3. Browser paints the frame
51
+ * 4. Repeat next frame
52
+ *
53
+ * @param callback - Optional function to call after board.step(), receives timestamp and canvas context
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * function MyBoard() {
58
+ * useAnimationFrameWithBoard((timestamp, ctx) => {
59
+ * // Draw a rectangle at world position (0, 0)
60
+ * ctx.fillStyle = 'red';
61
+ * ctx.fillRect(0, 0, 100, 100);
62
+ *
63
+ * // Draw a circle that moves
64
+ * const x = Math.sin(timestamp / 1000) * 200;
65
+ * const y = Math.cos(timestamp / 1000) * 200;
66
+ * ctx.fillStyle = 'blue';
67
+ * ctx.beginPath();
68
+ * ctx.arc(x, y, 20, 0, Math.PI * 2);
69
+ * ctx.fill();
70
+ * });
71
+ *
72
+ * return <Board width={800} height={600} />;
73
+ * }
74
+ * ```
75
+ *
76
+ * @category Hooks
77
+ * @see {@link useAnimationFrame} for generic animation frame hook
78
+ */
79
+ export declare function useAnimationFrameWithBoard(callback?: (timestamp: number, ctx: CanvasRenderingContext2D) => void): void;
@@ -0,0 +1,289 @@
1
+ import { Board as Boardify } from "@ue-too/board";
2
+ import { CameraMux, CameraState } from "@ue-too/board/camera";
3
+ import { Point } from "@ue-too/math";
4
+ /**
5
+ * Hook to create and manage a Board instance.
6
+ *
7
+ * @remarks
8
+ * This hook creates a stable Board instance that persists across re-renders.
9
+ * The board is created once and stored in a ref, making it suitable for use
10
+ * in React components without recreating the board on every render.
11
+ *
12
+ * **Important**: This hook creates an independent board instance. If you need
13
+ * to share a board across multiple components, use {@link BoardProvider} and
14
+ * {@link useBoard} instead.
15
+ *
16
+ * @param fullScreen - Whether the board should be in fullscreen mode (resizes with window)
17
+ * @returns Object containing the board instance and a subscribe function
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * function MyComponent() {
22
+ * const { board, subscribe } = useBoardify(true);
23
+ *
24
+ * useEffect(() => {
25
+ * // Subscribe to camera pan events
26
+ * const unsubscribe = subscribe(() => {
27
+ * console.log('Camera panned');
28
+ * });
29
+ * return unsubscribe;
30
+ * }, [subscribe]);
31
+ *
32
+ * return <canvas ref={(ref) => ref && board.attach(ref)} />;
33
+ * }
34
+ * ```
35
+ *
36
+ * @category Hooks
37
+ */
38
+ export declare function useBoardify(fullScreen?: boolean): {
39
+ board: Boardify;
40
+ subscribe: (callback: () => void) => import("@ue-too/board").UnSubscribe;
41
+ };
42
+ /**
43
+ * Hook to subscribe to a specific camera state property with automatic re-rendering.
44
+ *
45
+ * @remarks
46
+ * This hook uses React's `useSyncExternalStore` to efficiently subscribe to camera state changes.
47
+ * It only triggers re-renders when the specified property actually changes, and uses caching
48
+ * to maintain referential equality for object values (like position).
49
+ *
50
+ * **Performance**: The hook is optimized to prevent unnecessary re-renders by:
51
+ * - Caching object values (position) to maintain referential equality
52
+ * - Using `useSyncExternalStore` for efficient subscription management
53
+ * - Only subscribing to the specific state property needed
54
+ *
55
+ * @typeParam K - Key of the camera state to subscribe to
56
+ * @param state - The camera state property to track ("position", "rotation", or "zoomLevel")
57
+ * @returns The current value of the specified camera state property
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * function CameraInfo() {
62
+ * const position = useBoardCameraState('position');
63
+ * const rotation = useBoardCameraState('rotation');
64
+ * const zoomLevel = useBoardCameraState('zoomLevel');
65
+ *
66
+ * return (
67
+ * <div>
68
+ * Position: {position.x}, {position.y}<br/>
69
+ * Rotation: {rotation}<br/>
70
+ * Zoom: {zoomLevel}
71
+ * </div>
72
+ * );
73
+ * }
74
+ * ```
75
+ *
76
+ * @category Hooks
77
+ * @see {@link useAllBoardCameraState} for subscribing to all camera state at once
78
+ */
79
+ export declare function useBoardCameraState<K extends keyof CameraState>(state: K): CameraState[K];
80
+ /**
81
+ * Hook to get camera control functions for programmatic camera manipulation.
82
+ *
83
+ * @remarks
84
+ * This hook provides a stable set of functions to control the camera programmatically.
85
+ * The functions are memoized and only recreate when the board instance changes.
86
+ *
87
+ * All camera operations go through the camera rig, which enforces boundaries,
88
+ * restrictions, and other constraints configured on the board.
89
+ *
90
+ * @returns Object containing camera control functions:
91
+ * - `panToWorld` - Pan camera to a world position
92
+ * - `panToViewPort` - Pan camera to a viewport position
93
+ * - `zoomTo` - Set camera zoom to specific level
94
+ * - `zoomBy` - Adjust camera zoom by delta
95
+ * - `rotateTo` - Set camera rotation to specific angle
96
+ * - `rotateBy` - Adjust camera rotation by delta
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * function CameraControls() {
101
+ * const { panToWorld, zoomTo, rotateTo } = useCameraInput();
102
+ *
103
+ * return (
104
+ * <div>
105
+ * <button onClick={() => panToWorld({ x: 0, y: 0 })}>
106
+ * Center Camera
107
+ * </button>
108
+ * <button onClick={() => zoomTo(1.0)}>
109
+ * Reset Zoom
110
+ * </button>
111
+ * <button onClick={() => rotateTo(0)}>
112
+ * Reset Rotation
113
+ * </button>
114
+ * </div>
115
+ * );
116
+ * }
117
+ * ```
118
+ *
119
+ * @category Hooks
120
+ */
121
+ export declare function useCameraInput(): {
122
+ panToWorld: (worldPosition: Point) => void;
123
+ panToViewPort: (viewPortPosition: Point) => void;
124
+ zoomTo: (zoomLevel: number) => void;
125
+ zoomBy: (zoomDelta: number) => void;
126
+ rotateTo: (rotation: number) => void;
127
+ rotateBy: (rotationDelta: number) => void;
128
+ };
129
+ /**
130
+ * Hook to subscribe to all camera state properties with automatic re-rendering.
131
+ *
132
+ * @remarks
133
+ * This hook provides a snapshot of all camera state (position, rotation, zoomLevel) and
134
+ * re-renders only when any of these values change. It's more efficient than using multiple
135
+ * {@link useBoardCameraState} calls when you need all state properties.
136
+ *
137
+ * **Performance**: The hook uses snapshot caching to maintain referential equality when
138
+ * values haven't changed, preventing unnecessary re-renders in child components.
139
+ *
140
+ * @returns Object containing:
141
+ * - `position` - Current camera position {x, y}
142
+ * - `rotation` - Current camera rotation in radians
143
+ * - `zoomLevel` - Current camera zoom level
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * function CameraStateDisplay() {
148
+ * const { position, rotation, zoomLevel } = useAllBoardCameraState();
149
+ *
150
+ * return (
151
+ * <div>
152
+ * <h3>Camera State</h3>
153
+ * <p>Position: ({position.x.toFixed(2)}, {position.y.toFixed(2)})</p>
154
+ * <p>Rotation: {rotation.toFixed(2)} rad</p>
155
+ * <p>Zoom: {zoomLevel.toFixed(2)}x</p>
156
+ * </div>
157
+ * );
158
+ * }
159
+ * ```
160
+ *
161
+ * @category Hooks
162
+ * @see {@link useBoardCameraState} for subscribing to individual state properties
163
+ */
164
+ export declare function useAllBoardCameraState(): {
165
+ position: {
166
+ x: number;
167
+ y: number;
168
+ };
169
+ rotation: number;
170
+ zoomLevel: number;
171
+ };
172
+ /**
173
+ * Hook to set a custom camera multiplexer on the board.
174
+ *
175
+ * @remarks
176
+ * This hook allows you to replace the board's default camera mux with a custom implementation.
177
+ * Useful when you need custom input coordination, animation control, or state-based input blocking.
178
+ *
179
+ * The camera mux is updated whenever the provided `cameraMux` instance changes.
180
+ *
181
+ * @param cameraMux - Custom camera mux implementation to use
182
+ *
183
+ * @example
184
+ * ```tsx
185
+ * function CustomMuxBoard() {
186
+ * const myCustomMux = useMemo(() => {
187
+ * return createCameraMuxWithAnimationAndLock(camera);
188
+ * }, []);
189
+ *
190
+ * useCustomCameraMux(myCustomMux);
191
+ *
192
+ * return <Board />;
193
+ * }
194
+ * ```
195
+ *
196
+ * @category Hooks
197
+ * @see {@link CameraMux} from @ue-too/board for camera mux interface
198
+ */
199
+ export declare function useCustomCameraMux(cameraMux: CameraMux): void;
200
+ /**
201
+ * Provider component for sharing a Board instance across the component tree.
202
+ *
203
+ * @remarks
204
+ * This component creates a single Board instance and makes it available to all child
205
+ * components via the {@link useBoard} hook. This is the recommended way to use the
206
+ * board in React applications when you need to access it from multiple components.
207
+ *
208
+ * The board instance is created once when the provider mounts and persists for the
209
+ * lifetime of the provider.
210
+ *
211
+ * @param props - Component props
212
+ * @param props.children - Child components that will have access to the board
213
+ *
214
+ * @example
215
+ * ```tsx
216
+ * function App() {
217
+ * return (
218
+ * <BoardProvider>
219
+ * <Board width={800} height={600} />
220
+ * <CameraControls />
221
+ * <CameraStateDisplay />
222
+ * </BoardProvider>
223
+ * );
224
+ * }
225
+ * ```
226
+ *
227
+ * @category Components
228
+ * @see {@link useBoard} for accessing the board instance
229
+ */
230
+ export declare function BoardProvider({ children }: {
231
+ children: React.ReactNode;
232
+ }): import("react/jsx-runtime").JSX.Element;
233
+ /**
234
+ * Hook to access the Board instance from context.
235
+ *
236
+ * @remarks
237
+ * This hook retrieves the Board instance provided by {@link BoardProvider}.
238
+ * It must be used within a component that is a descendant of BoardProvider,
239
+ * otherwise it will throw an error.
240
+ *
241
+ * @returns The Board instance from context
242
+ * @throws Error if used outside of BoardProvider
243
+ *
244
+ * @example
245
+ * ```tsx
246
+ * function MyComponent() {
247
+ * const board = useBoard();
248
+ *
249
+ * useEffect(() => {
250
+ * // Configure board
251
+ * board.camera.boundaries = { min: { x: -1000, y: -1000 }, max: { x: 1000, y: 1000 } };
252
+ * }, [board]);
253
+ *
254
+ * return <div>Board ready</div>;
255
+ * }
256
+ * ```
257
+ *
258
+ * @category Hooks
259
+ * @see {@link BoardProvider} for providing the board instance
260
+ */
261
+ export declare function useBoard(): Boardify;
262
+ /**
263
+ * Hook to access the camera instance from the Board context.
264
+ *
265
+ * @remarks
266
+ * This is a convenience hook that returns the camera from the board instance.
267
+ * Equivalent to calling `useBoard().camera` but more concise.
268
+ *
269
+ * @returns The camera instance from the board
270
+ * @throws Error if used outside of BoardProvider
271
+ *
272
+ * @example
273
+ * ```tsx
274
+ * function CameraConfig() {
275
+ * const camera = useBoardCamera();
276
+ *
277
+ * useEffect(() => {
278
+ * camera.setMinZoomLevel(0.5);
279
+ * camera.setMaxZoomLevel(4.0);
280
+ * }, [camera]);
281
+ *
282
+ * return null;
283
+ * }
284
+ * ```
285
+ *
286
+ * @category Hooks
287
+ * @see {@link useBoard} for accessing the full board instance
288
+ */
289
+ export declare function useBoardCamera(): import("@ue-too/board").ObservableBoardCamera;
package/index.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * React adapter for the @ue-too/board infinite canvas library.
4
+ *
5
+ * @remarks
6
+ * This package provides React components and hooks to integrate the @ue-too/board
7
+ * infinite canvas into React applications. It handles lifecycle management, state
8
+ * synchronization, and provides idiomatic React patterns for working with the board.
9
+ *
10
+ * ## Core Components
11
+ *
12
+ * - **{@link Board}**: Main component that renders the canvas and manages the board instance
13
+ * - **{@link BoardProvider}**: Context provider for sharing board across components
14
+ *
15
+ * ## State Hooks
16
+ *
17
+ * - **{@link useBoardCameraState}**: Subscribe to specific camera state (position, rotation, zoomLevel)
18
+ * - **{@link useAllBoardCameraState}**: Subscribe to all camera state at once
19
+ * - **{@link useBoard}**: Access the board instance from context
20
+ * - **{@link useBoardCamera}**: Access the camera instance from context
21
+ *
22
+ * ## Control Hooks
23
+ *
24
+ * - **{@link useCameraInput}**: Get camera control functions (pan, zoom, rotate)
25
+ * - **{@link useCustomCameraMux}**: Set a custom camera multiplexer
26
+ * - **{@link useBoardify}**: Create a standalone board instance (alternative to provider)
27
+ *
28
+ * ## Animation Hooks
29
+ *
30
+ * - **{@link useAnimationFrame}**: Generic animation frame hook
31
+ * - **{@link useAnimationFrameWithBoard}**: Animation loop integrated with board.step()
32
+ *
33
+ * ## Key Features
34
+ *
35
+ * - **Automatic State Sync**: Camera state changes trigger React re-renders
36
+ * - **Performance Optimized**: Uses `useSyncExternalStore` for efficient subscriptions
37
+ * - **Type-Safe**: Full TypeScript support with type inference
38
+ * - **Context-Based**: Share board instance across component tree
39
+ * - **Lifecycle Management**: Automatic cleanup and resource management
40
+ *
41
+ * @example
42
+ * Basic usage
43
+ * ```tsx
44
+ * import Board from '@ue-too/board-react-adapter';
45
+ *
46
+ * function App() {
47
+ * return (
48
+ * <Board
49
+ * width={800}
50
+ * height={600}
51
+ * animationCallback={(timestamp, ctx) => {
52
+ * ctx.fillStyle = 'blue';
53
+ * ctx.fillRect(0, 0, 100, 100);
54
+ * }}
55
+ * />
56
+ * );
57
+ * }
58
+ * ```
59
+ *
60
+ * @example
61
+ * With camera controls
62
+ * ```tsx
63
+ * import Board, {
64
+ * useBoardCameraState,
65
+ * useCameraInput
66
+ * } from '@ue-too/board-react-adapter';
67
+ *
68
+ * function Controls() {
69
+ * const position = useBoardCameraState('position');
70
+ * const { panToWorld, zoomTo } = useCameraInput();
71
+ *
72
+ * return (
73
+ * <div>
74
+ * <p>Position: {position.x}, {position.y}</p>
75
+ * <button onClick={() => panToWorld({ x: 0, y: 0 })}>Center</button>
76
+ * <button onClick={() => zoomTo(1.0)}>Reset Zoom</button>
77
+ * </div>
78
+ * );
79
+ * }
80
+ *
81
+ * function App() {
82
+ * return (
83
+ * <Board width={800} height={600}>
84
+ * <Controls />
85
+ * </Board>
86
+ * );
87
+ * }
88
+ * ```
89
+ *
90
+ * @see {@link Board} for the main component
91
+ */
92
+ export * from "./components/Board";
93
+ export * from "./hooks";
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import{useCallback as L,useEffect as T,useRef as j}from"react";import{Board as q}from"@ue-too/board";import{createContext as I,useContext as U,useEffect as O,useMemo as Y,useRef as $,useSyncExternalStore as A}from"react";import{jsxDEV as Z}from"react/jsx-dev-runtime";function C(G=!1){let H=$(new q);return O(()=>{H.current.fullScreen=G},[G]),{board:H.current,subscribe:(K)=>{if(H.current==null)return()=>{};return H.current.on("pan",(J,N)=>{K()})}}}function E(G){let H=X(),K=G==="position"?"pan":G==="zoomLevel"?"zoom":"rotate",J=$(null);return A((N)=>H.camera.on(K,N),()=>{if(G==="position"){let N=H.camera.position,Q=J.current;if(Q&&Q.x===N.x&&Q.y===N.y)return Q;let _={...N};return J.current=_,_}return H.camera[G]})}function F(){let G=X();return Y(()=>{let K=G.getCameraRig();return{panToWorld:(J)=>{K.panToWorld(J)},panToViewPort:(J)=>{K.panToViewPort(J)},zoomTo:(J)=>{K.zoomTo(J)},zoomBy:(J)=>{K.zoomBy(J)},rotateTo:(J)=>{K.rotateTo(J)},rotateBy:(J)=>{K.rotateBy(J)}}},[G])}function y(){let G=X(),H=$(null);return A((K)=>{return G.camera.on("all",K)},()=>{let K=G.camera.position,J=G.camera.rotation,N=G.camera.zoomLevel,Q=H.current;if(Q&&Q.position.x===K.x&&Q.position.y===K.y&&Q.rotation===J&&Q.zoomLevel===N)return Q;let _={position:{...K},rotation:J,zoomLevel:N};return H.current=_,_})}function k(G){let H=X();O(()=>{H.cameraMux=G},[G])}var W=I(null);function V({children:G}){let H=Y(()=>new q,[]);return Z(W.Provider,{value:H,children:G},void 0,!1,void 0,this)}function X(){let G=U(W);if(G==null)throw Error("Board Provider not found");return G}function B(){return X().camera}function z(G){let H=j(null);T(()=>{let K=(J)=>{G(J),H.current=requestAnimationFrame(K)};return H.current=requestAnimationFrame(K),()=>{if(H.current)cancelAnimationFrame(H.current)}},[G])}function D(G){let H=X(),K=L((J)=>{H.step(J);let N=H.context;if(N==null){console.warn("Canvas context not available");return}G?.(J,N)},[G,H]);z(K)}import{jsxDEV as d}from"react/jsx-dev-runtime";export{k as useCustomCameraMux,F as useCameraInput,C as useBoardify,E as useBoardCameraState,B as useBoardCamera,X as useBoard,D as useAnimationFrameWithBoard,z as useAnimationFrame,y as useAllBoardCameraState,V as BoardProvider};
2
+
3
+ //# debugId=E0E749EB72A2F9E564756E2164756E21
package/index.js.map ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/hooks/useAnimationFrame.ts", "../src/hooks/useBoardify.tsx"],
4
+ "sourcesContent": [
5
+ "import { useCallback, useEffect, useRef } from 'react';\nimport { useBoard } from './useBoardify';\n\n/**\n * Hook to run a callback on every animation frame.\n *\n * @remarks\n * This hook uses `requestAnimationFrame` to execute a callback repeatedly for smooth animations.\n * The animation loop starts when the component mounts and stops when it unmounts, automatically\n * cleaning up the animation frame request.\n *\n * **Performance Note**: The callback is called on every frame, so ensure your callback is\n * optimized to avoid performance issues. The callback dependency should be stable to prevent\n * restarting the animation loop unnecessarily.\n *\n * @param callback - Function to call on each animation frame, receives the current timestamp\n *\n * @example\n * ```tsx\n * function AnimatedComponent() {\n * const [rotation, setRotation] = useState(0);\n *\n * useAnimationFrame((timestamp) => {\n * // Rotate 45 degrees per second\n * setRotation((prev) => prev + (Math.PI / 4) * (1 / 60));\n * });\n *\n * return <div style={{ transform: `rotate(${rotation}rad)` }}>Spinning!</div>;\n * }\n * ```\n *\n * @category Hooks\n * @see {@link useAnimationFrameWithBoard} for board-integrated animation loop\n */\nexport function useAnimationFrame(callback: (timestamp: number) => void) {\n const animationFrameRef = useRef<number | null>(null);\n\n useEffect(() => {\n const step = (timestamp: number) => {\n callback(timestamp);\n animationFrameRef.current = requestAnimationFrame(step);\n };\n\n // Start the animation loop\n animationFrameRef.current = requestAnimationFrame(step);\n\n // Cleanup function\n return () => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n };\n }, [callback]);\n}\n\n/**\n * Hook to run an animation loop integrated with the Board's step function.\n *\n * @remarks\n * This hook automatically calls `board.step(timestamp)` on every frame to update the board's\n * camera transform, then invokes your callback with the timestamp and canvas context.\n * This is the recommended way to implement drawing logic for board-based applications.\n *\n * The hook handles:\n * - Calling `board.step()` to update camera transforms\n * - Providing the canvas context for drawing\n * - Warning if context is not available\n * - Cleaning up the animation loop on unmount\n *\n * **Typical Usage Pattern**:\n * 1. Board calls `step()` to update transforms\n * 2. Your callback draws on the canvas\n * 3. Browser paints the frame\n * 4. Repeat next frame\n *\n * @param callback - Optional function to call after board.step(), receives timestamp and canvas context\n *\n * @example\n * ```tsx\n * function MyBoard() {\n * useAnimationFrameWithBoard((timestamp, ctx) => {\n * // Draw a rectangle at world position (0, 0)\n * ctx.fillStyle = 'red';\n * ctx.fillRect(0, 0, 100, 100);\n *\n * // Draw a circle that moves\n * const x = Math.sin(timestamp / 1000) * 200;\n * const y = Math.cos(timestamp / 1000) * 200;\n * ctx.fillStyle = 'blue';\n * ctx.beginPath();\n * ctx.arc(x, y, 20, 0, Math.PI * 2);\n * ctx.fill();\n * });\n *\n * return <Board width={800} height={600} />;\n * }\n * ```\n *\n * @category Hooks\n * @see {@link useAnimationFrame} for generic animation frame hook\n */\nexport function useAnimationFrameWithBoard(callback?: (timestamp: number, ctx: CanvasRenderingContext2D) => void) {\n\n const board = useBoard();\n\n const animationCallback = useCallback((timestamp: number) => {\n board.step(timestamp);\n const ctx = board.context;\n if (ctx == undefined) {\n console.warn('Canvas context not available');\n return;\n }\n callback?.(timestamp, ctx);\n }, [callback, board]);\n\n useAnimationFrame(animationCallback);\n}\n",
6
+ "import {Board as Boardify} from \"@ue-too/board\";\nimport {createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore} from \"react\";\nimport { CameraMux, CameraState } from \"@ue-too/board/camera\";\nimport { Point } from \"@ue-too/math\";\n\n/**\n * Maps camera state keys to their corresponding event names.\n * @internal\n */\ntype StateToEventKey<K extends keyof CameraState> =\n K extends \"position\" ? \"pan\" : K extends \"zoomLevel\" ? \"zoom\" : \"rotate\";\n\n/**\n * Hook to create and manage a Board instance.\n *\n * @remarks\n * This hook creates a stable Board instance that persists across re-renders.\n * The board is created once and stored in a ref, making it suitable for use\n * in React components without recreating the board on every render.\n *\n * **Important**: This hook creates an independent board instance. If you need\n * to share a board across multiple components, use {@link BoardProvider} and\n * {@link useBoard} instead.\n *\n * @param fullScreen - Whether the board should be in fullscreen mode (resizes with window)\n * @returns Object containing the board instance and a subscribe function\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { board, subscribe } = useBoardify(true);\n *\n * useEffect(() => {\n * // Subscribe to camera pan events\n * const unsubscribe = subscribe(() => {\n * console.log('Camera panned');\n * });\n * return unsubscribe;\n * }, [subscribe]);\n *\n * return <canvas ref={(ref) => ref && board.attach(ref)} />;\n * }\n * ```\n *\n * @category Hooks\n */\nexport function useBoardify(fullScreen: boolean = false) {\n\n const boardRef = useRef<Boardify>(new Boardify());\n\n useEffect(() => {\n boardRef.current.fullScreen = fullScreen;\n }, [fullScreen]);\n\n return {\n board: boardRef.current,\n subscribe: (callback: () => void) => {\n if (boardRef.current == null) {\n return () => {};\n }\n return boardRef.current.on(\"pan\", (_event, _data) => {\n callback();\n });\n }\n }\n}\n\n/**\n * Hook to subscribe to a specific camera state property with automatic re-rendering.\n *\n * @remarks\n * This hook uses React's `useSyncExternalStore` to efficiently subscribe to camera state changes.\n * It only triggers re-renders when the specified property actually changes, and uses caching\n * to maintain referential equality for object values (like position).\n *\n * **Performance**: The hook is optimized to prevent unnecessary re-renders by:\n * - Caching object values (position) to maintain referential equality\n * - Using `useSyncExternalStore` for efficient subscription management\n * - Only subscribing to the specific state property needed\n *\n * @typeParam K - Key of the camera state to subscribe to\n * @param state - The camera state property to track (\"position\", \"rotation\", or \"zoomLevel\")\n * @returns The current value of the specified camera state property\n *\n * @example\n * ```tsx\n * function CameraInfo() {\n * const position = useBoardCameraState('position');\n * const rotation = useBoardCameraState('rotation');\n * const zoomLevel = useBoardCameraState('zoomLevel');\n *\n * return (\n * <div>\n * Position: {position.x}, {position.y}<br/>\n * Rotation: {rotation}<br/>\n * Zoom: {zoomLevel}\n * </div>\n * );\n * }\n * ```\n *\n * @category Hooks\n * @see {@link useAllBoardCameraState} for subscribing to all camera state at once\n */\nexport function useBoardCameraState<K extends keyof CameraState>(state: K): CameraState[K] {\n const board = useBoard();\n const stateKey = (state === \"position\" ? \"pan\" : state === \"zoomLevel\" ? \"zoom\" : \"rotate\") as StateToEventKey<K>;\n const cachedPositionRef = useRef<{ x: number; y: number } | null>(null);\n\n return useSyncExternalStore(\n (cb) => board.camera.on(stateKey, cb),\n () => {\n // For position (object), we need to cache to avoid creating new objects\n if (state === \"position\") {\n const currentPosition = board.camera.position;\n const cached = cachedPositionRef.current;\n \n if (cached && cached.x === currentPosition.x && cached.y === currentPosition.y) {\n // Return cached snapshot to maintain referential equality\n return cached as CameraState[K];\n }\n \n // Cache the new position object\n const newPosition = {...currentPosition};\n cachedPositionRef.current = newPosition;\n return newPosition as CameraState[K];\n }\n \n // For primitive values (rotation, zoomLevel), return directly\n // Object.is works correctly for primitives\n return board.camera[state] as CameraState[K];\n },\n );\n}\n\n/**\n * Hook to get camera control functions for programmatic camera manipulation.\n *\n * @remarks\n * This hook provides a stable set of functions to control the camera programmatically.\n * The functions are memoized and only recreate when the board instance changes.\n *\n * All camera operations go through the camera rig, which enforces boundaries,\n * restrictions, and other constraints configured on the board.\n *\n * @returns Object containing camera control functions:\n * - `panToWorld` - Pan camera to a world position\n * - `panToViewPort` - Pan camera to a viewport position\n * - `zoomTo` - Set camera zoom to specific level\n * - `zoomBy` - Adjust camera zoom by delta\n * - `rotateTo` - Set camera rotation to specific angle\n * - `rotateBy` - Adjust camera rotation by delta\n *\n * @example\n * ```tsx\n * function CameraControls() {\n * const { panToWorld, zoomTo, rotateTo } = useCameraInput();\n *\n * return (\n * <div>\n * <button onClick={() => panToWorld({ x: 0, y: 0 })}>\n * Center Camera\n * </button>\n * <button onClick={() => zoomTo(1.0)}>\n * Reset Zoom\n * </button>\n * <button onClick={() => rotateTo(0)}>\n * Reset Rotation\n * </button>\n * </div>\n * );\n * }\n * ```\n *\n * @category Hooks\n */\nexport function useCameraInput(){\n const board = useBoard();\n\n const test = useMemo(()=>{\n const cameraRig = board.getCameraRig();\n\n return {\n panToWorld: (worldPosition: Point) => {\n cameraRig.panToWorld(worldPosition);\n },\n panToViewPort: (viewPortPosition: Point) => {\n cameraRig.panToViewPort(viewPortPosition);\n },\n zoomTo: (zoomLevel: number) => {\n cameraRig.zoomTo(zoomLevel);\n },\n zoomBy: (zoomDelta: number) => {\n cameraRig.zoomBy(zoomDelta);\n },\n rotateTo: (rotation: number) => {\n cameraRig.rotateTo(rotation);\n },\n rotateBy: (rotationDelta: number) => {\n cameraRig.rotateBy(rotationDelta);\n }\n }\n\n }, [board]);\n\n return test;\n}\n\n/**\n * Hook to subscribe to all camera state properties with automatic re-rendering.\n *\n * @remarks\n * This hook provides a snapshot of all camera state (position, rotation, zoomLevel) and\n * re-renders only when any of these values change. It's more efficient than using multiple\n * {@link useBoardCameraState} calls when you need all state properties.\n *\n * **Performance**: The hook uses snapshot caching to maintain referential equality when\n * values haven't changed, preventing unnecessary re-renders in child components.\n *\n * @returns Object containing:\n * - `position` - Current camera position {x, y}\n * - `rotation` - Current camera rotation in radians\n * - `zoomLevel` - Current camera zoom level\n *\n * @example\n * ```tsx\n * function CameraStateDisplay() {\n * const { position, rotation, zoomLevel } = useAllBoardCameraState();\n *\n * return (\n * <div>\n * <h3>Camera State</h3>\n * <p>Position: ({position.x.toFixed(2)}, {position.y.toFixed(2)})</p>\n * <p>Rotation: {rotation.toFixed(2)} rad</p>\n * <p>Zoom: {zoomLevel.toFixed(2)}x</p>\n * </div>\n * );\n * }\n * ```\n *\n * @category Hooks\n * @see {@link useBoardCameraState} for subscribing to individual state properties\n */\nexport function useAllBoardCameraState() {\n const board = useBoard();\n const cachedSnapshotRef = useRef<{\n position: { x: number; y: number };\n rotation: number;\n zoomLevel: number;\n } | null>(null);\n\n return useSyncExternalStore(\n (cb) => { return board.camera.on(\"all\", cb) },\n () => {\n const currentPosition = board.camera.position;\n const currentRotation = board.camera.rotation;\n const currentZoomLevel = board.camera.zoomLevel;\n\n // Check if values actually changed\n const cached = cachedSnapshotRef.current;\n if (\n cached &&\n cached.position.x === currentPosition.x &&\n cached.position.y === currentPosition.y &&\n cached.rotation === currentRotation &&\n cached.zoomLevel === currentZoomLevel\n ) {\n // Return cached snapshot to maintain referential equality\n return cached;\n }\n\n // Create new snapshot only when values changed\n const newSnapshot = {\n position: {...currentPosition},\n rotation: currentRotation,\n zoomLevel: currentZoomLevel,\n };\n cachedSnapshotRef.current = newSnapshot;\n return newSnapshot;\n },\n )\n}\n\n/**\n * Hook to set a custom camera multiplexer on the board.\n *\n * @remarks\n * This hook allows you to replace the board's default camera mux with a custom implementation.\n * Useful when you need custom input coordination, animation control, or state-based input blocking.\n *\n * The camera mux is updated whenever the provided `cameraMux` instance changes.\n *\n * @param cameraMux - Custom camera mux implementation to use\n *\n * @example\n * ```tsx\n * function CustomMuxBoard() {\n * const myCustomMux = useMemo(() => {\n * return createCameraMuxWithAnimationAndLock(camera);\n * }, []);\n *\n * useCustomCameraMux(myCustomMux);\n *\n * return <Board />;\n * }\n * ```\n *\n * @category Hooks\n * @see {@link CameraMux} from @ue-too/board for camera mux interface\n */\nexport function useCustomCameraMux(cameraMux: CameraMux) {\n const board = useBoard();\n\n useEffect(()=>{\n board.cameraMux = cameraMux;\n }, [cameraMux]);\n}\n\n/**\n * React context for sharing a Board instance across components.\n * @internal\n */\nconst BoardContext = createContext<Boardify | null>(null);\n\n/**\n * Provider component for sharing a Board instance across the component tree.\n *\n * @remarks\n * This component creates a single Board instance and makes it available to all child\n * components via the {@link useBoard} hook. This is the recommended way to use the\n * board in React applications when you need to access it from multiple components.\n *\n * The board instance is created once when the provider mounts and persists for the\n * lifetime of the provider.\n *\n * @param props - Component props\n * @param props.children - Child components that will have access to the board\n *\n * @example\n * ```tsx\n * function App() {\n * return (\n * <BoardProvider>\n * <Board width={800} height={600} />\n * <CameraControls />\n * <CameraStateDisplay />\n * </BoardProvider>\n * );\n * }\n * ```\n *\n * @category Components\n * @see {@link useBoard} for accessing the board instance\n */\nexport function BoardProvider({children}: {children: React.ReactNode}) {\n const board = useMemo(() => new Boardify(), []);\n return <BoardContext.Provider value={board}>{children}</BoardContext.Provider>;\n}\n\n/**\n * Hook to access the Board instance from context.\n *\n * @remarks\n * This hook retrieves the Board instance provided by {@link BoardProvider}.\n * It must be used within a component that is a descendant of BoardProvider,\n * otherwise it will throw an error.\n *\n * @returns The Board instance from context\n * @throws Error if used outside of BoardProvider\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const board = useBoard();\n *\n * useEffect(() => {\n * // Configure board\n * board.camera.boundaries = { min: { x: -1000, y: -1000 }, max: { x: 1000, y: 1000 } };\n * }, [board]);\n *\n * return <div>Board ready</div>;\n * }\n * ```\n *\n * @category Hooks\n * @see {@link BoardProvider} for providing the board instance\n */\nexport function useBoard() {\n const board = useContext(BoardContext);\n if (board == null) {\n throw new Error('Board Provider not found');\n }\n return board;\n}\n\n/**\n * Hook to access the camera instance from the Board context.\n *\n * @remarks\n * This is a convenience hook that returns the camera from the board instance.\n * Equivalent to calling `useBoard().camera` but more concise.\n *\n * @returns The camera instance from the board\n * @throws Error if used outside of BoardProvider\n *\n * @example\n * ```tsx\n * function CameraConfig() {\n * const camera = useBoardCamera();\n *\n * useEffect(() => {\n * camera.setMinZoomLevel(0.5);\n * camera.setMaxZoomLevel(4.0);\n * }, [camera]);\n *\n * return null;\n * }\n * ```\n *\n * @category Hooks\n * @see {@link useBoard} for accessing the full board instance\n */\nexport function useBoardCamera() {\n const board = useBoard();\n return board.camera;\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "E0E749EB72A2F9E564756E2164756E21",
10
+ "names": []
11
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@ue-too/board-react-adapter",
3
+ "type": "module",
4
+ "version": "0.10.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ue-too/ue-too.git"
8
+ },
9
+ "homepage": "https://github.com/ue-too/ue-too",
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./index.d.ts",
16
+ "import": "./index.js",
17
+ "default": "./index.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "license": "MIT",
22
+ "main": "./index.js",
23
+ "types": "./index.d.ts",
24
+ "module": "./index.js",
25
+ "dependencies": {
26
+ "@ue-too/board": "^0.10.0",
27
+ "@ue-too/math": "^0.10.0"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^19.0.1",
31
+ "react-dom": "^19.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "react": "^19.0.1",
35
+ "react-dom": "^19.0.1"
36
+ }
37
+ }