@jamesyong42/infinite-canvas 0.0.1 → 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.
- package/README.md +264 -107
- package/dist/advanced.cjs +84 -1486
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +16 -22
- package/dist/advanced.d.ts +16 -22
- package/dist/advanced.js +36 -105
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-LQMLG3P5.js → chunk-CH3TR4LF.js} +37 -18
- package/dist/chunk-CH3TR4LF.js.map +1 -0
- package/dist/chunk-JT3KDQYX.js +3088 -0
- package/dist/chunk-JT3KDQYX.js.map +1 -0
- package/dist/chunk-OFQ75B4X.cjs +3171 -0
- package/dist/chunk-OFQ75B4X.cjs.map +1 -0
- package/dist/chunk-TX3ZABAK.cjs +482 -0
- package/dist/chunk-TX3ZABAK.cjs.map +1 -0
- package/dist/context-BI_YakHi.d.ts +691 -0
- package/dist/context-C6VM7KNh.d.cts +691 -0
- package/dist/ecs.cjs +26 -481
- package/dist/ecs.cjs.map +1 -1
- package/dist/ecs.d.cts +12 -28
- package/dist/ecs.d.ts +12 -28
- package/dist/ecs.js +2 -16
- package/dist/ecs.js.map +1 -1
- package/dist/index.cjs +768 -3485
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -99
- package/dist/index.d.ts +69 -99
- package/dist/index.js +530 -1557
- package/dist/index.js.map +1 -1
- package/dist/{profiler-DKuXy4MW.d.cts → profiler-DnHBllDV.d.cts} +33 -5
- package/dist/{profiler-DKuXy4MW.d.ts → profiler-DnHBllDV.d.ts} +33 -5
- package/package.json +27 -6
- package/dist/chunk-LQMLG3P5.js.map +0 -1
- package/dist/chunk-YVWTBG7H.js +0 -1447
- package/dist/chunk-YVWTBG7H.js.map +0 -1
- package/dist/context-BEyR4AhJ.d.ts +0 -441
- package/dist/context-tR7KjG_v.d.cts +0 -441
package/README.md
CHANGED
|
@@ -4,33 +4,21 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@jamesyong42/infinite-canvas)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Build Figma-style infinite canvases in React -- drag, resize, snap, zoom, nested containers, and WebGL widgets from a single composable component.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**[Live Demo](https://jamesyong-42.github.io/infinite-canvas/)** | **[npm](https://www.npmjs.com/package/@jamesyong42/infinite-canvas)**
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **Dark mode** — Full dark mode support across canvas, widgets, and UI panels
|
|
23
|
-
- **Configurable** — Grid, selection, snap, zoom, and breakpoint parameters all exposed
|
|
24
|
-
|
|
25
|
-
## Package
|
|
26
|
-
|
|
27
|
-
Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exposes three entry points:
|
|
28
|
-
|
|
29
|
-
| Import | Purpose |
|
|
30
|
-
|--------|---------|
|
|
31
|
-
| `@jamesyong42/infinite-canvas` | Main API — `<InfiniteCanvas>`, `createLayoutEngine`, hooks, built-in components |
|
|
32
|
-
| `@jamesyong42/infinite-canvas/ecs` | ECS primitives for advanced users (`defineComponent`, `defineSystem`, `World`) |
|
|
33
|
-
| `@jamesyong42/infinite-canvas/advanced` | WebGL renderers, serialization, profiler, spatial index |
|
|
13
|
+
- **Figma-style interactions** -- Snap alignment (edge + center), equal spacing detection, multi-select with group bounding box
|
|
14
|
+
- **Mobile-first gestures** -- Pinch-to-zoom, single-finger pan, tap-to-select, double-tap to enter containers
|
|
15
|
+
- **Responsive widgets** -- Breakpoint system adapts widget rendering based on screen-space size (micro / compact / normal / expanded / detailed)
|
|
16
|
+
- **Dual rendering** -- DOM and WebGL (React Three Fiber) widgets on the same canvas
|
|
17
|
+
- **Undo / redo** -- Command buffer with grouped operations (an entire drag is one undo step)
|
|
18
|
+
- **Hierarchical navigation** -- Enter and exit nested containers with camera state preservation
|
|
19
|
+
- **ECS architecture** -- Extensible via custom components, tags, and systems with topologically-sorted scheduling
|
|
20
|
+
- **Performance** -- SDF shaders for grid and selection chrome, RBush spatial indexing, viewport culling, per-system profiling
|
|
21
|
+
- **Dark mode** -- Full dark mode support across canvas, widgets, and UI chrome
|
|
34
22
|
|
|
35
23
|
## Quick Start
|
|
36
24
|
|
|
@@ -41,73 +29,99 @@ npm install three @react-three/fiber
|
|
|
41
29
|
```
|
|
42
30
|
|
|
43
31
|
```tsx
|
|
32
|
+
import { useMemo } from 'react';
|
|
44
33
|
import { createLayoutEngine, InfiniteCanvas, useWidgetData } from '@jamesyong42/infinite-canvas';
|
|
45
34
|
|
|
46
|
-
// 1. Define your widget component
|
|
47
35
|
function MyCard({ entityId }) {
|
|
48
36
|
const data = useWidgetData(entityId);
|
|
49
|
-
return <div
|
|
37
|
+
return <div style={{ padding: 16, background: 'white', borderRadius: 8 }}>{data.title}</div>;
|
|
50
38
|
}
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
40
|
+
const widgets = [
|
|
41
|
+
{ type: 'card', component: MyCard, defaultSize: { width: 250, height: 180 } },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export default function App() {
|
|
45
|
+
const engine = useMemo(() => {
|
|
46
|
+
const e = createLayoutEngine({ zoom: { min: 0.05, max: 8 } });
|
|
47
|
+
e.addWidget({
|
|
48
|
+
type: 'card',
|
|
49
|
+
position: { x: 100, y: 100 },
|
|
50
|
+
size: { width: 250, height: 180 },
|
|
51
|
+
data: { title: 'Hello World' },
|
|
52
|
+
});
|
|
53
|
+
return e;
|
|
54
|
+
}, []);
|
|
62
55
|
|
|
63
|
-
// 4. Render — widgets prop wires up the widget types
|
|
64
|
-
function App() {
|
|
65
56
|
return (
|
|
66
57
|
<InfiniteCanvas
|
|
67
58
|
engine={engine}
|
|
68
|
-
widgets={
|
|
69
|
-
|
|
70
|
-
]}
|
|
71
|
-
className="h-screen w-screen"
|
|
59
|
+
widgets={widgets}
|
|
60
|
+
style={{ width: '100vw', height: '100vh' }}
|
|
72
61
|
/>
|
|
73
62
|
);
|
|
74
63
|
}
|
|
75
64
|
```
|
|
76
65
|
|
|
77
|
-
##
|
|
78
|
-
|
|
79
|
-
Register widgets with `surface: 'webgl'` to render 3D content via React Three Fiber:
|
|
80
|
-
|
|
81
|
-
```tsx
|
|
82
|
-
import { useFrame } from '@react-three/fiber';
|
|
83
|
-
import { useRef } from 'react';
|
|
66
|
+
## Package
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
const meshRef = useRef();
|
|
87
|
-
useFrame((_, delta) => { meshRef.current.rotation.y += delta; });
|
|
88
|
-
return (
|
|
89
|
-
<mesh ref={meshRef}>
|
|
90
|
-
<boxGeometry args={[width * 0.5, height * 0.5, 50]} />
|
|
91
|
-
<meshBasicMaterial color="hotpink" wireframe />
|
|
92
|
-
</mesh>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
68
|
+
Everything ships in a single package: **`@jamesyong42/infinite-canvas`**. It exposes three entry points:
|
|
95
69
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
/>
|
|
102
|
-
```
|
|
70
|
+
| Import | Purpose |
|
|
71
|
+
|--------|---------|
|
|
72
|
+
| `@jamesyong42/infinite-canvas` | Main API -- `<InfiniteCanvas>`, `createLayoutEngine`, hooks, built-in components |
|
|
73
|
+
| `@jamesyong42/infinite-canvas/ecs` | ECS primitives for advanced users (`defineComponent`, `defineSystem`, `World`) |
|
|
74
|
+
| `@jamesyong42/infinite-canvas/advanced` | WebGL renderers, serialization, profiler, spatial index |
|
|
103
75
|
|
|
104
|
-
|
|
76
|
+
## Why This Library?
|
|
77
|
+
|
|
78
|
+
| | Infinite Canvas | React Flow | Konva | Excalidraw |
|
|
79
|
+
|---|---|---|---|---|
|
|
80
|
+
| **Use case** | Freeform spatial positioning of arbitrary React components | Node-edge graphs and flowcharts | Imperative canvas-mode rendering | Whiteboard application |
|
|
81
|
+
| **Rendering** | React DOM + WebGL widgets | React DOM | HTML5 Canvas | HTML5 Canvas |
|
|
82
|
+
| **Extension model** | ECS components, tags, and systems | Plugins and custom nodes | Shapes and layers | Not designed as a library |
|
|
83
|
+
|
|
84
|
+
**What makes this library unique:** ECS extension system, mixed DOM + WebGL widgets on the same canvas, responsive breakpoint system that adapts widgets to their screen-space size, and SDF-rendered chrome (grid, selection, snap guides) in a single draw call.
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### Hooks
|
|
89
|
+
|
|
90
|
+
| Hook | Description |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| `useWidgetData<T>(entityId)` | Custom data attached to a widget |
|
|
93
|
+
| `useBreakpoint(entityId)` | Responsive breakpoint (`'micro'` / `'compact'` / `'normal'` / `'expanded'` / `'detailed'`) |
|
|
94
|
+
| `useIsSelected(entityId)` | Whether the entity is currently selected |
|
|
95
|
+
| `useUpdateWidget(entityId)` | Returns a function to patch widget data |
|
|
96
|
+
| `useChildren(entityId)` | Child entity IDs of a container |
|
|
97
|
+
| `useComponent<T>(entityId, type)` | Read any ECS component reactively |
|
|
98
|
+
| `useTag(entityId, type)` | Check if an entity has a tag |
|
|
99
|
+
| `useQuery(...types)` | Entity IDs matching component/tag types |
|
|
100
|
+
| `useTaggedEntities(type)` | All entity IDs with a specific tag |
|
|
101
|
+
| `useResource<T>(type)` | Read an ECS resource reactively |
|
|
102
|
+
| `useLayoutEngine()` | Access the `LayoutEngine` instance from context |
|
|
103
|
+
|
|
104
|
+
### InfiniteCanvas Props
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Description |
|
|
107
|
+
|------|------|-------------|
|
|
108
|
+
| `engine` | `LayoutEngine` | Engine instance (required) |
|
|
109
|
+
| `widgets` | `WidgetDef[]` | Widget type definitions |
|
|
110
|
+
| `grid` | `Partial<GridConfig> \| false` | Grid configuration, or `false` to disable |
|
|
111
|
+
| `selection` | `Partial<SelectionConfig>` | Selection style overrides |
|
|
112
|
+
| `onSelectionChange` | `(ids: EntityId[]) => void` | Called when selected entities change |
|
|
113
|
+
| `onCameraChange` | `(camera) => void` | Called on pan/zoom |
|
|
114
|
+
| `onNavigationChange` | `(depth, containerId) => void` | Called when entering/exiting containers |
|
|
115
|
+
| `style` | `CSSProperties` | Inline styles for the root element |
|
|
116
|
+
| `className` | `string` | CSS class for the root element |
|
|
117
|
+
| `ref` | `Ref<InfiniteCanvasHandle>` | Imperative handle for `panTo`, `zoomToFit`, `undo`, `redo` |
|
|
105
118
|
|
|
106
119
|
## Widget Development
|
|
107
120
|
|
|
108
|
-
Widgets are React components that receive `entityId` and use hooks to read/write ECS data:
|
|
121
|
+
Widgets are React components that receive an `entityId` prop and use hooks to read/write ECS data:
|
|
109
122
|
|
|
110
123
|
```tsx
|
|
124
|
+
import type { WidgetProps } from '@jamesyong42/infinite-canvas';
|
|
111
125
|
import {
|
|
112
126
|
Transform2D,
|
|
113
127
|
useBreakpoint,
|
|
@@ -117,7 +131,7 @@ import {
|
|
|
117
131
|
useWidgetData,
|
|
118
132
|
} from '@jamesyong42/infinite-canvas';
|
|
119
133
|
|
|
120
|
-
function MyWidget({ entityId }) {
|
|
134
|
+
function MyWidget({ entityId }: WidgetProps) {
|
|
121
135
|
const data = useWidgetData(entityId); // custom widget data
|
|
122
136
|
const breakpoint = useBreakpoint(entityId); // 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed'
|
|
123
137
|
const isSelected = useIsSelected(entityId); // selection state
|
|
@@ -130,6 +144,37 @@ function MyWidget({ entityId }) {
|
|
|
130
144
|
}
|
|
131
145
|
```
|
|
132
146
|
|
|
147
|
+
The `WidgetProps` interface provides `entityId`, `width`, `height`, and `zoom`.
|
|
148
|
+
|
|
149
|
+
## WebGL Widgets (R3F)
|
|
150
|
+
|
|
151
|
+
Register widgets with `surface: 'webgl'` to render 3D content via React Three Fiber:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { useFrame } from '@react-three/fiber';
|
|
155
|
+
import { useRef } from 'react';
|
|
156
|
+
|
|
157
|
+
function My3DWidget({ entityId, width, height }) {
|
|
158
|
+
const meshRef = useRef();
|
|
159
|
+
useFrame((_, delta) => { meshRef.current.rotation.y += delta; });
|
|
160
|
+
return (
|
|
161
|
+
<mesh ref={meshRef}>
|
|
162
|
+
<boxGeometry args={[width * 0.5, height * 0.5, 50]} />
|
|
163
|
+
<meshBasicMaterial color="hotpink" wireframe />
|
|
164
|
+
</mesh>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
<InfiniteCanvas
|
|
169
|
+
engine={engine}
|
|
170
|
+
widgets={[
|
|
171
|
+
{ type: 'my-3d', surface: 'webgl', component: My3DWidget, defaultSize: { width: 250, height: 250 } },
|
|
172
|
+
]}
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
WebGL widgets get a transparent R3F canvas layered between the grid and DOM layers. The R3F camera is synced with the engine camera every frame. Widget components receive `entityId`, `width`, and `height` props and work in centered local coordinates.
|
|
177
|
+
|
|
133
178
|
## Configuration
|
|
134
179
|
|
|
135
180
|
### Grid
|
|
@@ -177,38 +222,149 @@ function MyWidget({ entityId }) {
|
|
|
177
222
|
const engine = createLayoutEngine({
|
|
178
223
|
zoom: { min: 0.05, max: 8 },
|
|
179
224
|
breakpoints: { micro: 40, compact: 120, normal: 500, expanded: 1200 },
|
|
225
|
+
snap: { enabled: true, threshold: 5 },
|
|
180
226
|
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Serialization
|
|
181
230
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
Save and restore canvas state with the serialization API:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { serializeWorld, deserializeWorld } from '@jamesyong42/infinite-canvas/advanced';
|
|
235
|
+
import {
|
|
236
|
+
Transform2D, Widget, WidgetData, WidgetBreakpoint, ZIndex,
|
|
237
|
+
Parent, Children, Container, Hitbox, InteractionRole, HandleSet, CursorHint,
|
|
238
|
+
Selectable, Draggable, Resizable, Locked, Selected, Active, Visible,
|
|
239
|
+
} from '@jamesyong42/infinite-canvas';
|
|
240
|
+
|
|
241
|
+
const componentTypes = [
|
|
242
|
+
Transform2D, Widget, WidgetData, WidgetBreakpoint, ZIndex,
|
|
243
|
+
Parent, Children, Container, Hitbox, InteractionRole, HandleSet, CursorHint,
|
|
244
|
+
];
|
|
245
|
+
const tagTypes = [Selectable, Draggable, Resizable, Locked, Selected, Active, Visible];
|
|
246
|
+
|
|
247
|
+
// Save
|
|
248
|
+
const camera = engine.getCamera();
|
|
249
|
+
const doc = serializeWorld(engine.world, componentTypes, tagTypes, camera, []);
|
|
250
|
+
localStorage.setItem('canvas', JSON.stringify(doc));
|
|
251
|
+
|
|
252
|
+
// Load
|
|
253
|
+
const saved = JSON.parse(localStorage.getItem('canvas'));
|
|
254
|
+
deserializeWorld(engine.world, saved, componentTypes, tagTypes);
|
|
255
|
+
engine.markDirty();
|
|
185
256
|
```
|
|
186
257
|
|
|
258
|
+
## Programmatic Control
|
|
259
|
+
|
|
260
|
+
### Camera
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
engine.panTo(500, 300); // pan to world coordinates
|
|
264
|
+
engine.zoomTo(1.5); // set zoom level
|
|
265
|
+
engine.zoomToFit(); // fit all entities in viewport
|
|
266
|
+
engine.zoomToFit([id1, id2]); // fit specific entities
|
|
267
|
+
engine.markDirty(); // schedule a re-render
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Undo / Redo
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
engine.undo();
|
|
274
|
+
engine.redo();
|
|
275
|
+
engine.markDirty();
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Imperative Handle
|
|
279
|
+
|
|
280
|
+
Use a ref on `<InfiniteCanvas>` for imperative control from outside:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
const canvasRef = useRef<InfiniteCanvasHandle>(null);
|
|
284
|
+
|
|
285
|
+
<InfiniteCanvas ref={canvasRef} engine={engine} widgets={widgets} />
|
|
286
|
+
|
|
287
|
+
// Later:
|
|
288
|
+
canvasRef.current?.zoomToFit();
|
|
289
|
+
canvasRef.current?.undo();
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Keyboard Shortcuts
|
|
293
|
+
|
|
294
|
+
Wire up shortcuts in your app (this pattern is from the playground):
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
299
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
300
|
+
if (mod && !e.shiftKey && e.key === 'z') { e.preventDefault(); engine.undo(); engine.markDirty(); }
|
|
301
|
+
if (mod && e.shiftKey && e.key === 'z') { e.preventDefault(); engine.redo(); engine.markDirty(); }
|
|
302
|
+
if (e.key === 'Escape' && engine.getNavigationDepth() > 0) {
|
|
303
|
+
engine.exitContainer(); engine.markDirty();
|
|
304
|
+
}
|
|
305
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
306
|
+
const selected = engine.getSelectedEntities();
|
|
307
|
+
for (const id of selected) engine.destroyEntity(id);
|
|
308
|
+
if (selected.length > 0) engine.markDirty();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
window.addEventListener('keydown', onKeyDown);
|
|
312
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
313
|
+
}, [engine]);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Custom ECS Extensions
|
|
317
|
+
|
|
318
|
+
Define custom components and systems to extend the canvas:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
import { defineComponent, defineSystem } from '@jamesyong42/infinite-canvas/ecs';
|
|
322
|
+
import { Visible } from '@jamesyong42/infinite-canvas';
|
|
323
|
+
|
|
324
|
+
const Health = defineComponent('Health', { hp: 100, maxHp: 100 });
|
|
325
|
+
|
|
326
|
+
const healthSystem = defineSystem({
|
|
327
|
+
name: 'health',
|
|
328
|
+
after: 'breakpoint',
|
|
329
|
+
execute: (world) => {
|
|
330
|
+
for (const id of world.queryChanged(Health)) {
|
|
331
|
+
const h = world.getComponent(id, Health);
|
|
332
|
+
if (h && h.hp <= 0) world.removeTag(id, Visible);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Register with the engine
|
|
338
|
+
engine.registerSystem(healthSystem);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Systems are topologically sorted based on `after` and `before` constraints, so you can insert custom logic at any point in the pipeline.
|
|
342
|
+
|
|
187
343
|
## Architecture
|
|
188
344
|
|
|
189
345
|
```
|
|
190
346
|
@jamesyong42/infinite-canvas
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
347
|
+
+-- Main API (InfiniteCanvas, createLayoutEngine, hooks, components)
|
|
348
|
+
+-- /ecs (ECS primitives: defineComponent, defineSystem, World)
|
|
349
|
+
+-- /advanced (WebGL renderers, serialization, profiler, spatial index)
|
|
194
350
|
```
|
|
195
351
|
|
|
196
352
|
### Rendering Stack
|
|
197
353
|
|
|
198
354
|
```
|
|
199
355
|
z:0 WebGL canvas (Three.js)
|
|
200
|
-
|
|
201
|
-
|
|
356
|
+
+-- GridRenderer -- multi-level dot grid (SDF shader)
|
|
357
|
+
+-- SelectionRenderer -- outlines, handles, hover, snap guides (SDF shader)
|
|
202
358
|
|
|
203
359
|
z:1 R3F canvas (React Three Fiber, lazy)
|
|
204
|
-
|
|
360
|
+
+-- WebGL widgets -- 3D content with synced orthographic camera
|
|
205
361
|
|
|
206
362
|
z:2 DOM layer
|
|
207
|
-
|
|
208
|
-
|
|
363
|
+
+-- WidgetSlots -- DOM widget content + pointer events
|
|
364
|
+
+-- SelectionOverlays -- invisible pointer event layer for WebGL widgets
|
|
209
365
|
|
|
210
366
|
z:3 UI chrome
|
|
211
|
-
|
|
367
|
+
+-- Panels, buttons, toggles
|
|
212
368
|
```
|
|
213
369
|
|
|
214
370
|
### ECS Components
|
|
@@ -223,6 +379,10 @@ z:3 UI chrome
|
|
|
223
379
|
| `ZIndex` | Rendering order |
|
|
224
380
|
| `Parent` / `Children` | Hierarchy |
|
|
225
381
|
| `Container` | Marks entity as enterable |
|
|
382
|
+
| `Hitbox` | Hit-test geometry |
|
|
383
|
+
| `InteractionRole` | Interaction behavior (drag, select, resize, etc.) |
|
|
384
|
+
| `HandleSet` | Child handle entity references |
|
|
385
|
+
| `CursorHint` | Cursor style on hover/active |
|
|
226
386
|
|
|
227
387
|
### ECS Tags
|
|
228
388
|
|
|
@@ -230,22 +390,13 @@ z:3 UI chrome
|
|
|
230
390
|
|
|
231
391
|
### Systems (execution order)
|
|
232
392
|
|
|
233
|
-
1. `transformPropagate`
|
|
234
|
-
2. `
|
|
235
|
-
3. `
|
|
236
|
-
4. `
|
|
237
|
-
5. `
|
|
238
|
-
6. `
|
|
239
|
-
|
|
240
|
-
## Keyboard Shortcuts (Playground)
|
|
241
|
-
|
|
242
|
-
| Shortcut | Action |
|
|
243
|
-
|----------|--------|
|
|
244
|
-
| `Cmd/Ctrl+Z` | Undo |
|
|
245
|
-
| `Cmd/Ctrl+Shift+Z` | Redo |
|
|
246
|
-
| `Escape` | Exit container |
|
|
247
|
-
| `Delete` / `Backspace` | Delete selected |
|
|
248
|
-
| Double-click | Enter container |
|
|
393
|
+
1. `transformPropagate` -- Propagate transforms down hierarchy, compute WorldBounds
|
|
394
|
+
2. `handleSync` -- Synchronize resize handle entities with parent widgets
|
|
395
|
+
3. `hitboxWorldBounds` -- Compute world-space hitbox bounds
|
|
396
|
+
4. `navigationFilter` -- Filter entities to active navigation layer
|
|
397
|
+
5. `cull` -- Mark viewport-visible entities
|
|
398
|
+
6. `breakpoint` -- Compute responsive breakpoints
|
|
399
|
+
7. `sort` -- Z-index ordering
|
|
249
400
|
|
|
250
401
|
## Performance Profiling
|
|
251
402
|
|
|
@@ -264,6 +415,20 @@ console.log(stats.budgetUsed); // % of 16.67ms budget used
|
|
|
264
415
|
|
|
265
416
|
All timing data integrates with Chrome DevTools Performance tab via the User Timing API (`performance.mark`/`performance.measure`).
|
|
266
417
|
|
|
418
|
+
## SSR / Next.js
|
|
419
|
+
|
|
420
|
+
This library requires browser APIs (WebGL, ResizeObserver, requestAnimationFrame). For Next.js, use dynamic import with SSR disabled or ensure the component only mounts client-side:
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
import dynamic from 'next/dynamic';
|
|
424
|
+
|
|
425
|
+
const Canvas = dynamic(() => import('./MyCanvas'), { ssr: false });
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Browser Support
|
|
429
|
+
|
|
430
|
+
Chrome 90+, Firefox 88+, Safari 14+, Edge 90+. Requires WebGL 2.
|
|
431
|
+
|
|
267
432
|
## Development
|
|
268
433
|
|
|
269
434
|
```bash
|
|
@@ -287,17 +452,9 @@ pnpm exec tsc --noEmit -p packages/infinite-canvas/tsconfig.json
|
|
|
287
452
|
|
|
288
453
|
## Tech Stack
|
|
289
454
|
|
|
290
|
-
- **
|
|
291
|
-
- **
|
|
292
|
-
- **
|
|
293
|
-
- **React Three Fiber** — WebGL widget rendering (optional peer dependency)
|
|
294
|
-
- **RBush** — Spatial indexing for hit testing and viewport culling
|
|
295
|
-
- **tsup** — Library bundling (ESM + CJS + DTS)
|
|
296
|
-
- **Vite** — Playground bundling
|
|
297
|
-
- **Tailwind CSS v4** — Playground styling
|
|
298
|
-
- **Biome** — Linting and formatting
|
|
299
|
-
- **Vitest** — Testing
|
|
300
|
-
- **pnpm** — Workspace management
|
|
455
|
+
- **React 18 / 19** -- Compatible with both versions
|
|
456
|
+
- **Three.js** -- WebGL rendering (grid, selection chrome, WebGL widgets via R3F)
|
|
457
|
+
- **RBush** -- Spatial indexing for hit testing and viewport culling
|
|
301
458
|
|
|
302
459
|
## Contributing
|
|
303
460
|
|