@jamesyong42/infinite-canvas 0.0.1
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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/advanced.cjs +1578 -0
- package/dist/advanced.cjs.map +1 -0
- package/dist/advanced.d.cts +93 -0
- package/dist/advanced.d.ts +93 -0
- package/dist/advanced.js +194 -0
- package/dist/advanced.js.map +1 -0
- package/dist/chunk-LQMLG3P5.js +456 -0
- package/dist/chunk-LQMLG3P5.js.map +1 -0
- package/dist/chunk-YVWTBG7H.js +1447 -0
- package/dist/chunk-YVWTBG7H.js.map +1 -0
- package/dist/context-BEyR4AhJ.d.ts +441 -0
- package/dist/context-tR7KjG_v.d.cts +441 -0
- package/dist/ecs.cjs +487 -0
- package/dist/ecs.cjs.map +1 -0
- package/dist/ecs.d.cts +65 -0
- package/dist/ecs.d.ts +65 -0
- package/dist/ecs.js +17 -0
- package/dist/ecs.js.map +1 -0
- package/dist/index.cjs +3550 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +154 -0
- package/dist/index.d.ts +154 -0
- package/dist/index.js +1652 -0
- package/dist/index.js.map +1 -0
- package/dist/profiler-DKuXy4MW.d.cts +137 -0
- package/dist/profiler-DKuXy4MW.d.ts +137 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 James Yong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Infinite Canvas
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jamesyong-42/infinite-canvas/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@jamesyong42/infinite-canvas)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
**[Live Demo](https://jamesyong-42.github.io/infinite-canvas/)** | **[npm](https://www.npmjs.com/org/jamesyong42)**
|
|
8
|
+
|
|
9
|
+
A high-performance infinite canvas library for React, built on an Entity Component System (ECS) architecture with WebGL-accelerated rendering.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **ECS-powered** — Decoupled entity/component architecture with spatial indexing (RBush), change detection, and topologically-sorted system scheduling
|
|
14
|
+
- **Dual rendering surfaces** — DOM widgets and WebGL (R3F) widgets on the same canvas
|
|
15
|
+
- **GPU-rendered chrome** — Dot grid, selection outlines, resize handles, hover highlights, and snap guides all rendered via SDF shaders in a single draw call
|
|
16
|
+
- **Figma-style interactions** — Snap alignment (edge/center), equal spacing detection, multi-select group bounding box
|
|
17
|
+
- **Mobile-first gestures** — iOS Freeform-style touch: single-finger pan, pinch-to-zoom, tap-to-select, double-tap to enter containers
|
|
18
|
+
- **Responsive widgets** — Breakpoint system adapts widget rendering based on screen-space size (micro/compact/normal/expanded/detailed)
|
|
19
|
+
- **Undo/redo** — Command buffer with grouped operations (entire drag = one undo step)
|
|
20
|
+
- **Hierarchical navigation** — Enter/exit nested containers with camera state preservation
|
|
21
|
+
- **Performance profiling** — Built-in profiler with User Timing API integration, per-system timing, percentile stats
|
|
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 |
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @jamesyong42/infinite-canvas react react-dom
|
|
39
|
+
# For WebGL widgets (optional):
|
|
40
|
+
npm install three @react-three/fiber
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { createLayoutEngine, InfiniteCanvas, useWidgetData } from '@jamesyong42/infinite-canvas';
|
|
45
|
+
|
|
46
|
+
// 1. Define your widget component
|
|
47
|
+
function MyCard({ entityId }) {
|
|
48
|
+
const data = useWidgetData(entityId);
|
|
49
|
+
return <div className="p-4 bg-white rounded shadow">{data.title}</div>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Create the layout engine
|
|
53
|
+
const engine = createLayoutEngine({ zoom: { min: 0.05, max: 8 } });
|
|
54
|
+
|
|
55
|
+
// 3. Add widgets
|
|
56
|
+
engine.addWidget({
|
|
57
|
+
type: 'card',
|
|
58
|
+
position: { x: 100, y: 100 },
|
|
59
|
+
size: { width: 250, height: 180 },
|
|
60
|
+
data: { title: 'Hello World' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 4. Render — widgets prop wires up the widget types
|
|
64
|
+
function App() {
|
|
65
|
+
return (
|
|
66
|
+
<InfiniteCanvas
|
|
67
|
+
engine={engine}
|
|
68
|
+
widgets={[
|
|
69
|
+
{ type: 'card', component: MyCard, defaultSize: { width: 250, height: 180 } },
|
|
70
|
+
]}
|
|
71
|
+
className="h-screen w-screen"
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## WebGL Widgets (R3F)
|
|
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';
|
|
84
|
+
|
|
85
|
+
function My3DWidget({ entityId, width, height }) {
|
|
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
|
+
}
|
|
95
|
+
|
|
96
|
+
<InfiniteCanvas
|
|
97
|
+
engine={engine}
|
|
98
|
+
widgets={[
|
|
99
|
+
{ type: 'my-3d', surface: 'webgl', component: My3DWidget, defaultSize: { width: 250, height: 250 } },
|
|
100
|
+
]}
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
## Widget Development
|
|
107
|
+
|
|
108
|
+
Widgets are React components that receive `entityId` and use hooks to read/write ECS data:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import {
|
|
112
|
+
Transform2D,
|
|
113
|
+
useBreakpoint,
|
|
114
|
+
useComponent,
|
|
115
|
+
useIsSelected,
|
|
116
|
+
useUpdateWidget,
|
|
117
|
+
useWidgetData,
|
|
118
|
+
} from '@jamesyong42/infinite-canvas';
|
|
119
|
+
|
|
120
|
+
function MyWidget({ entityId }) {
|
|
121
|
+
const data = useWidgetData(entityId); // custom widget data
|
|
122
|
+
const breakpoint = useBreakpoint(entityId); // 'micro' | 'compact' | 'normal' | 'expanded' | 'detailed'
|
|
123
|
+
const isSelected = useIsSelected(entityId); // selection state
|
|
124
|
+
const transform = useComponent(entityId, Transform2D); // position/size
|
|
125
|
+
const updateWidget = useUpdateWidget(entityId); // update widget data
|
|
126
|
+
|
|
127
|
+
if (breakpoint === 'micro') return <div>...</div>; // minimal view
|
|
128
|
+
if (breakpoint === 'compact') return <div>...</div>; // condensed view
|
|
129
|
+
return <div>...</div>; // full view
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
### Grid
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<InfiniteCanvas
|
|
139
|
+
engine={engine}
|
|
140
|
+
grid={{
|
|
141
|
+
spacings: [8, 64, 512], // world-px grid levels [fine, medium, coarse]
|
|
142
|
+
dotColor: [0, 0, 0], // RGB 0-1
|
|
143
|
+
dotAlpha: 0.18, // base opacity
|
|
144
|
+
fadeIn: [4, 12], // CSS-px fade in range
|
|
145
|
+
fadeOut: [250, 500], // CSS-px fade out range
|
|
146
|
+
dotRadius: [0.5, 1.4], // CSS-px dot size range
|
|
147
|
+
levelWeight: [1.0, 0.4], // opacity weight per grid level
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
// Disable grid
|
|
152
|
+
<InfiniteCanvas engine={engine} grid={false} />
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Selection
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<InfiniteCanvas
|
|
159
|
+
engine={engine}
|
|
160
|
+
selection={{
|
|
161
|
+
outlineColor: [0.051, 0.6, 1.0], // Figma blue
|
|
162
|
+
outlineWidth: 1.5,
|
|
163
|
+
handleSize: 8,
|
|
164
|
+
handleFill: [1, 1, 1],
|
|
165
|
+
handleBorder: [0.051, 0.6, 1.0],
|
|
166
|
+
handleBorderWidth: 1.5,
|
|
167
|
+
hoverColor: [0.051, 0.6, 1.0],
|
|
168
|
+
hoverWidth: 1.0,
|
|
169
|
+
groupDash: 4,
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Engine
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const engine = createLayoutEngine({
|
|
178
|
+
zoom: { min: 0.05, max: 8 },
|
|
179
|
+
breakpoints: { micro: 40, compact: 120, normal: 500, expanded: 1200 },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Snap guides
|
|
183
|
+
engine.setSnapEnabled(true);
|
|
184
|
+
engine.setSnapThreshold(5); // world pixels
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Architecture
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
@jamesyong42/infinite-canvas
|
|
191
|
+
├── Main API (InfiniteCanvas, createLayoutEngine, hooks, components)
|
|
192
|
+
├── /ecs (ECS primitives: defineComponent, defineSystem, World)
|
|
193
|
+
└── /advanced (WebGL renderers, serialization, profiler, spatial index)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Rendering Stack
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
z:0 WebGL canvas (Three.js)
|
|
200
|
+
├── GridRenderer — multi-level dot grid (SDF shader)
|
|
201
|
+
└── SelectionRenderer — outlines, handles, hover, snap guides (SDF shader)
|
|
202
|
+
|
|
203
|
+
z:1 R3F canvas (React Three Fiber, lazy)
|
|
204
|
+
└── WebGL widgets — 3D content with synced orthographic camera
|
|
205
|
+
|
|
206
|
+
z:2 DOM layer
|
|
207
|
+
├── WidgetSlots — DOM widget content + pointer events
|
|
208
|
+
└── SelectionOverlays — invisible pointer event layer for WebGL widgets
|
|
209
|
+
|
|
210
|
+
z:3 UI chrome
|
|
211
|
+
└── Panels, buttons, toggles
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### ECS Components
|
|
215
|
+
|
|
216
|
+
| Component | Description |
|
|
217
|
+
|-----------|-------------|
|
|
218
|
+
| `Transform2D` | Position, size, rotation |
|
|
219
|
+
| `WorldBounds` | Computed world-space bounds (propagated from parent) |
|
|
220
|
+
| `Widget` | Surface (`'dom'`/`'webgl'`) and type identifier |
|
|
221
|
+
| `WidgetData` | Arbitrary widget-specific data |
|
|
222
|
+
| `WidgetBreakpoint` | Computed responsive breakpoint |
|
|
223
|
+
| `ZIndex` | Rendering order |
|
|
224
|
+
| `Parent` / `Children` | Hierarchy |
|
|
225
|
+
| `Container` | Marks entity as enterable |
|
|
226
|
+
|
|
227
|
+
### ECS Tags
|
|
228
|
+
|
|
229
|
+
`Selectable` `Draggable` `Resizable` `Locked` `Selected` `Active` `Visible`
|
|
230
|
+
|
|
231
|
+
### Systems (execution order)
|
|
232
|
+
|
|
233
|
+
1. `transformPropagate` — Propagate transforms down hierarchy, compute WorldBounds
|
|
234
|
+
2. `spatialIndex` — Update RBush spatial index
|
|
235
|
+
3. `navigationFilter` — Filter entities to active navigation layer
|
|
236
|
+
4. `cull` — Mark viewport-visible entities
|
|
237
|
+
5. `breakpoint` — Compute responsive breakpoints
|
|
238
|
+
6. `sort` — Z-index ordering
|
|
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 |
|
|
249
|
+
|
|
250
|
+
## Performance Profiling
|
|
251
|
+
|
|
252
|
+
Enable the built-in profiler via the Inspector panel or programmatically:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
engine.profiler.setEnabled(true);
|
|
256
|
+
|
|
257
|
+
// After some frames:
|
|
258
|
+
const stats = engine.profiler.getStats();
|
|
259
|
+
console.log(stats.fps); // frames per second
|
|
260
|
+
console.log(stats.frameTime.p95); // 95th percentile frame time (ms)
|
|
261
|
+
console.log(stats.systemAvg); // per-system average timing
|
|
262
|
+
console.log(stats.budgetUsed); // % of 16.67ms budget used
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
All timing data integrates with Chrome DevTools Performance tab via the User Timing API (`performance.mark`/`performance.measure`).
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Clone and install
|
|
271
|
+
git clone https://github.com/jamesyong-42/infinite-canvas.git
|
|
272
|
+
cd infinite-canvas
|
|
273
|
+
pnpm install
|
|
274
|
+
|
|
275
|
+
# Build the library
|
|
276
|
+
pnpm build
|
|
277
|
+
|
|
278
|
+
# Run the playground demo
|
|
279
|
+
pnpm dev
|
|
280
|
+
|
|
281
|
+
# Run tests
|
|
282
|
+
pnpm test
|
|
283
|
+
|
|
284
|
+
# Typecheck
|
|
285
|
+
pnpm exec tsc --noEmit -p packages/infinite-canvas/tsconfig.json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Tech Stack
|
|
289
|
+
|
|
290
|
+
- **TypeScript** — Strict mode, fully typed public API
|
|
291
|
+
- **React 18 / 19** — Compatible with both versions
|
|
292
|
+
- **Three.js** — WebGL rendering (grid, selection chrome)
|
|
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
|
|
301
|
+
|
|
302
|
+
## Contributing
|
|
303
|
+
|
|
304
|
+
Contributions are welcome! See the [live demo](https://jamesyong-42.github.io/infinite-canvas/) for an overview of the features, then check out the playground at `apps/playground/` to experiment with changes locally.
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
[MIT](./LICENSE)
|