@jiujue/react-canvas-fiber 2.0.2 → 2.0.4
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/AI_GUIDE.md +142 -0
- package/dist/index.cjs +288 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +288 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/AI_GUIDE.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# AI Guide for react-canvas-fiber
|
|
2
|
+
|
|
3
|
+
This document provides a comprehensive guide for AI agents to understand, use, and extend the `react-canvas-fiber` project. It covers the architecture, core concepts, usage patterns, and development guidelines.
|
|
4
|
+
|
|
5
|
+
## 1. Project Overview
|
|
6
|
+
|
|
7
|
+
`react-canvas-fiber` is a custom React Renderer that renders React components to an HTML5 Canvas. It leverages `react-reconciler` for state management and updates, and integrates **Yoga Layout** (via `yoga-layout-prebuilt`) for a web-like Flexbox layout system.
|
|
8
|
+
|
|
9
|
+
### Key Characteristics
|
|
10
|
+
|
|
11
|
+
- **Renderer**: Custom Host Config implementation mapping React Virtual DOM to Canvas Scene Graph.
|
|
12
|
+
- **Layout**: Full Flexbox support powered by Yoga.
|
|
13
|
+
- **Rendering**: Canvas 2D API based rendering pipeline.
|
|
14
|
+
- **Events**: Synthetic event system simulating DOM events (bubbling, capturing) within the Canvas.
|
|
15
|
+
- **Runtime**: Supports high-performance updates using `requestAnimationFrame` loop (batched updates).
|
|
16
|
+
|
|
17
|
+
## 2. Directory Structure
|
|
18
|
+
|
|
19
|
+
- `src/components/`: Public React components (e.g., `<Canvas />`).
|
|
20
|
+
- `src/jsx/`: JSX type definitions.
|
|
21
|
+
- `src/layout/`: Layout calculation logic bridging internal nodes and Yoga.
|
|
22
|
+
- `src/render/`: Painting logic (draw calls).
|
|
23
|
+
- `src/runtime/`: Core runtime logic.
|
|
24
|
+
- `reconciler.ts`: `react-reconciler` HostConfig implementation.
|
|
25
|
+
- `nodes.ts`: Scene graph node definitions (`ViewNode`, `RectNode`, `TextNode`).
|
|
26
|
+
- `root.ts`: Root container managing the render loop and event dispatching.
|
|
27
|
+
- `src/types/`: TypeScript type definitions.
|
|
28
|
+
- `src/utils/`: Utility functions.
|
|
29
|
+
|
|
30
|
+
## 3. Usage & Supported Elements
|
|
31
|
+
|
|
32
|
+
### Entry Point
|
|
33
|
+
|
|
34
|
+
The entry point is the `<Canvas />` component.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { Canvas } from 'react-canvas-fiber'
|
|
38
|
+
|
|
39
|
+
function App() {
|
|
40
|
+
return (
|
|
41
|
+
<Canvas width={800} height={600} style={{ border: '1px solid black' }}>
|
|
42
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
43
|
+
<Text text="Hello World" style={{ fontSize: 20 }} color="black" />
|
|
44
|
+
</View>
|
|
45
|
+
</Canvas>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Intrinsic Elements
|
|
51
|
+
|
|
52
|
+
#### `<View />`
|
|
53
|
+
|
|
54
|
+
Container element for layout and grouping.
|
|
55
|
+
|
|
56
|
+
- **Props**: `ViewProps`
|
|
57
|
+
- **Key Attributes**:
|
|
58
|
+
- `style`: `YogaStyle` (flex, padding, margin, position, etc.)
|
|
59
|
+
- `background`: Background color string.
|
|
60
|
+
- `borderRadius`: Number.
|
|
61
|
+
- `scrollX`, `scrollY`: Booleans to enable scrolling.
|
|
62
|
+
- `onScroll`, `onScrollX`: Scroll callbacks.
|
|
63
|
+
- `pointerEvents`: `'auto' | 'none'`.
|
|
64
|
+
- Event handlers: `onClick`, `onPointerDown`, etc.
|
|
65
|
+
|
|
66
|
+
#### `<Text />`
|
|
67
|
+
|
|
68
|
+
Text rendering element.
|
|
69
|
+
|
|
70
|
+
- **Props**: `TextProps`
|
|
71
|
+
- **Key Attributes**:
|
|
72
|
+
- `text`: **Mandatory** string content (do not use children).
|
|
73
|
+
- `style`: `YogaStyle` (supports `fontSize`, `fontFamily`, `fontWeight`, `lineHeight`).
|
|
74
|
+
- `color`: Text color string.
|
|
75
|
+
- `maxWidth`: Max width for wrapping.
|
|
76
|
+
|
|
77
|
+
#### `<Rect />`
|
|
78
|
+
|
|
79
|
+
Basic rectangle shape.
|
|
80
|
+
|
|
81
|
+
- **Props**: `RectProps`
|
|
82
|
+
- **Key Attributes**:
|
|
83
|
+
- `style`: `YogaStyle`.
|
|
84
|
+
- `fill`: Fill color.
|
|
85
|
+
- `stroke`: Stroke color.
|
|
86
|
+
- `lineWidth`: Stroke width.
|
|
87
|
+
- `borderRadius`: Corner radius.
|
|
88
|
+
|
|
89
|
+
### Style Properties (`YogaStyle`)
|
|
90
|
+
|
|
91
|
+
Supports standard Flexbox properties:
|
|
92
|
+
|
|
93
|
+
- Layout: `width`, `height`, `flexDirection`, `justifyContent`, `alignItems`, `flexWrap`, `flexGrow`, `padding`, `margin`.
|
|
94
|
+
- Positioning: `position` ('relative' | 'absolute'), `top`, `left`, `right`, `bottom`.
|
|
95
|
+
|
|
96
|
+
## 4. Internal Architecture
|
|
97
|
+
|
|
98
|
+
### The Render Loop
|
|
99
|
+
|
|
100
|
+
1. **React Updates**: React commits changes via `reconciler.ts`.
|
|
101
|
+
2. **Invalidation**: `commitUpdate` triggers `container.invalidate()`.
|
|
102
|
+
3. **rAF Loop**: `requestAnimationFrame` calls `renderFrame` in `root.ts`.
|
|
103
|
+
4. **Layout Pass**:
|
|
104
|
+
- Syncs Scene Graph props to Yoga Nodes.
|
|
105
|
+
- Calculates layout via `yogaNode.calculateLayout()`.
|
|
106
|
+
- Writes computed layout (x, y, w, h) back to Scene Graph nodes.
|
|
107
|
+
5. **Draw Pass**:
|
|
108
|
+
- Clears canvas.
|
|
109
|
+
- Recursively calls `drawNode` (in `src/render/drawTree.ts`).
|
|
110
|
+
|
|
111
|
+
### Event System
|
|
112
|
+
|
|
113
|
+
- **Hit Testing**: `hitTest` function in `root.ts` traverses the Scene Graph (accounting for scroll offsets) to find the target node.
|
|
114
|
+
- **Dispatch**: Simulates `capture` and `bubble` phases.
|
|
115
|
+
- **Pointer Capture**: Supports implicit and explicit pointer capture for drag operations (e.g., scrollbars).
|
|
116
|
+
|
|
117
|
+
## 5. Development & Extension
|
|
118
|
+
|
|
119
|
+
### Adding a New Node Type
|
|
120
|
+
|
|
121
|
+
1. **Define Node**: Add a new type in `src/types/nodes.ts` (e.g., `ImageNode`).
|
|
122
|
+
2. **Define Props**: Add props interface in `src/types/jsx.ts`.
|
|
123
|
+
3. **Register Intrinsic**: Add to `JSX.IntrinsicElements` in `src/intrinsics.d.ts`.
|
|
124
|
+
4. **Implement Creation**: Update `createInstance` in `src/runtime/reconciler.ts` to instantiate the new node.
|
|
125
|
+
5. **Implement Drawing**: Update `drawNode` in `src/render/drawTree.ts` to handle the new node type.
|
|
126
|
+
|
|
127
|
+
### Debugging
|
|
128
|
+
|
|
129
|
+
- Nodes have a `debugId`.
|
|
130
|
+
- Layout issues usually stem from Yoga configuration or `syncYogaTree` logic in `src/layout/layoutTree.ts`.
|
|
131
|
+
- Rendering issues are handled in `src/render/drawTree.ts`.
|
|
132
|
+
|
|
133
|
+
### Build & Watch
|
|
134
|
+
|
|
135
|
+
- **Build**: `npm run build` (uses tsup)
|
|
136
|
+
- **Watch**: `npm run dev`
|
|
137
|
+
|
|
138
|
+
## 6. Common Pitfalls for AI
|
|
139
|
+
|
|
140
|
+
- **Text Children**: `<Text>Content</Text>` is **NOT** supported. Must use `<Text text="Content" />`.
|
|
141
|
+
- **Z-Index**: Currently determined by tree order (painting order). No explicit `zIndex` support in styles yet.
|
|
142
|
+
- **Scrolling**: Requires `scrollX` or `scrollY` prop on `<View>` AND fixed dimensions (or flex constraints) to work.
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ function createRootNode() {
|
|
|
21
21
|
children: [],
|
|
22
22
|
props: {},
|
|
23
23
|
layout: { x: 0, y: 0, width: 0, height: 0 },
|
|
24
|
+
contentBounds: void 0,
|
|
24
25
|
yogaNode: null,
|
|
25
26
|
scrollLeft: 0,
|
|
26
27
|
scrollTop: 0,
|
|
@@ -30,13 +31,14 @@ function createRootNode() {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
function createNode(type, props) {
|
|
33
|
-
|
|
34
|
+
const node = {
|
|
34
35
|
type,
|
|
35
36
|
debugId: nextDebugId++,
|
|
36
37
|
parent: null,
|
|
37
38
|
children: [],
|
|
38
39
|
props,
|
|
39
40
|
layout: { x: 0, y: 0, width: 0, height: 0 },
|
|
41
|
+
contentBounds: void 0,
|
|
40
42
|
yogaNode: null,
|
|
41
43
|
scrollLeft: 0,
|
|
42
44
|
scrollTop: 0,
|
|
@@ -44,6 +46,10 @@ function createNode(type, props) {
|
|
|
44
46
|
scrollContentHeight: 0,
|
|
45
47
|
scrollbarDrag: null
|
|
46
48
|
};
|
|
49
|
+
if (type === "Image") {
|
|
50
|
+
node.imageInstance = null;
|
|
51
|
+
}
|
|
52
|
+
return node;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
// src/render/drawTree.ts
|
|
@@ -85,6 +91,9 @@ function drawRoundedRect(ctx, x, y, w, h, r) {
|
|
|
85
91
|
ctx.arcTo(x, y, x + w, y, radius);
|
|
86
92
|
ctx.closePath();
|
|
87
93
|
}
|
|
94
|
+
function rectsIntersect(ax, ay, aw, ah, bx, by, bw, bh) {
|
|
95
|
+
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
96
|
+
}
|
|
88
97
|
function drawNode(state, node, offsetX, offsetY) {
|
|
89
98
|
const { ctx } = state;
|
|
90
99
|
const x = offsetX + node.layout.x;
|
|
@@ -120,7 +129,32 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
120
129
|
ctx.beginPath();
|
|
121
130
|
ctx.rect(x, y, w, h);
|
|
122
131
|
ctx.clip();
|
|
132
|
+
const cullPadding = 1;
|
|
133
|
+
const viewportX = x - cullPadding;
|
|
134
|
+
const viewportY = y - cullPadding;
|
|
135
|
+
const viewportW = w + cullPadding * 2;
|
|
136
|
+
const viewportH = h + cullPadding * 2;
|
|
123
137
|
for (const child of node.children) {
|
|
138
|
+
const bounds = child.contentBounds ?? {
|
|
139
|
+
x: 0,
|
|
140
|
+
y: 0,
|
|
141
|
+
width: child.layout.width,
|
|
142
|
+
height: child.layout.height
|
|
143
|
+
};
|
|
144
|
+
const bx = x - scrollLeft + child.layout.x + bounds.x;
|
|
145
|
+
const by = y - scrollTop + child.layout.y + bounds.y;
|
|
146
|
+
if (!rectsIntersect(
|
|
147
|
+
viewportX,
|
|
148
|
+
viewportY,
|
|
149
|
+
viewportW,
|
|
150
|
+
viewportH,
|
|
151
|
+
bx,
|
|
152
|
+
by,
|
|
153
|
+
bounds.width,
|
|
154
|
+
bounds.height
|
|
155
|
+
)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
124
158
|
drawNode(state, child, x - scrollLeft, y - scrollTop);
|
|
125
159
|
}
|
|
126
160
|
ctx.restore();
|
|
@@ -202,6 +236,43 @@ function drawNode(state, node, offsetX, offsetY) {
|
|
|
202
236
|
}
|
|
203
237
|
ctx.restore();
|
|
204
238
|
}
|
|
239
|
+
if (node.type === "Image") {
|
|
240
|
+
const { imageInstance } = node;
|
|
241
|
+
if (imageInstance && imageInstance.complete && imageInstance.naturalWidth > 0) {
|
|
242
|
+
const objectFit = node.props.objectFit || "contain";
|
|
243
|
+
const srcW = imageInstance.naturalWidth;
|
|
244
|
+
const srcH = imageInstance.naturalHeight;
|
|
245
|
+
let dstX = x;
|
|
246
|
+
let dstY = y;
|
|
247
|
+
let dstW = w;
|
|
248
|
+
let dstH = h;
|
|
249
|
+
let srcX = 0;
|
|
250
|
+
let srcY = 0;
|
|
251
|
+
let finalSrcW = srcW;
|
|
252
|
+
let finalSrcH = srcH;
|
|
253
|
+
if (objectFit === "fill") ; else if (objectFit === "contain") {
|
|
254
|
+
const ratio = Math.min(w / srcW, h / srcH);
|
|
255
|
+
dstW = srcW * ratio;
|
|
256
|
+
dstH = srcH * ratio;
|
|
257
|
+
dstX = x + (w - dstW) / 2;
|
|
258
|
+
dstY = y + (h - dstH) / 2;
|
|
259
|
+
} else if (objectFit === "cover") {
|
|
260
|
+
const ratio = Math.max(w / srcW, h / srcH);
|
|
261
|
+
const renderW = srcW * ratio;
|
|
262
|
+
const renderH = srcH * ratio;
|
|
263
|
+
srcX = (renderW - w) / 2 / ratio;
|
|
264
|
+
srcY = (renderH - h) / 2 / ratio;
|
|
265
|
+
finalSrcW = w / ratio;
|
|
266
|
+
finalSrcH = h / ratio;
|
|
267
|
+
}
|
|
268
|
+
ctx.save();
|
|
269
|
+
ctx.beginPath();
|
|
270
|
+
drawRoundedRect(ctx, x, y, w, h, 0);
|
|
271
|
+
ctx.clip();
|
|
272
|
+
ctx.drawImage(imageInstance, srcX, srcY, finalSrcW, finalSrcH, dstX, dstY, dstW, dstH);
|
|
273
|
+
ctx.restore();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
205
276
|
for (const child of node.children) {
|
|
206
277
|
drawNode(state, child, x, y);
|
|
207
278
|
}
|
|
@@ -457,6 +528,31 @@ async function layoutTree(root, width, height, measureText, defaults) {
|
|
|
457
528
|
}
|
|
458
529
|
};
|
|
459
530
|
walk(root);
|
|
531
|
+
const computeSubtreeContentBounds = (node) => {
|
|
532
|
+
let minX = 0;
|
|
533
|
+
let minY = 0;
|
|
534
|
+
let maxX = node.layout.width;
|
|
535
|
+
let maxY = node.layout.height;
|
|
536
|
+
for (const child of node.children) {
|
|
537
|
+
if (child.children.length) computeSubtreeContentBounds(child);
|
|
538
|
+
const childBounds = child.contentBounds ?? {
|
|
539
|
+
x: 0,
|
|
540
|
+
y: 0,
|
|
541
|
+
width: child.layout.width,
|
|
542
|
+
height: child.layout.height
|
|
543
|
+
};
|
|
544
|
+
const bx = child.layout.x + childBounds.x;
|
|
545
|
+
const by = child.layout.y + childBounds.y;
|
|
546
|
+
const br = bx + childBounds.width;
|
|
547
|
+
const bb = by + childBounds.height;
|
|
548
|
+
minX = Math.min(minX, bx);
|
|
549
|
+
minY = Math.min(minY, by);
|
|
550
|
+
maxX = Math.max(maxX, br);
|
|
551
|
+
maxY = Math.max(maxY, bb);
|
|
552
|
+
}
|
|
553
|
+
node.contentBounds = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
554
|
+
};
|
|
555
|
+
for (const child of root.children) computeSubtreeContentBounds(child);
|
|
460
556
|
}
|
|
461
557
|
function freeYogaSubtree(node) {
|
|
462
558
|
if (node.yogaNode) {
|
|
@@ -488,8 +584,22 @@ var hostConfig = {
|
|
|
488
584
|
shouldSetTextContent() {
|
|
489
585
|
return false;
|
|
490
586
|
},
|
|
491
|
-
createInstance(type, props) {
|
|
492
|
-
|
|
587
|
+
createInstance(type, props, rootContainer) {
|
|
588
|
+
const node = createNode(type, props);
|
|
589
|
+
if (type === "Image" && props.src) {
|
|
590
|
+
const imgNode = node;
|
|
591
|
+
const img = new Image();
|
|
592
|
+
img.crossOrigin = "anonymous";
|
|
593
|
+
img.src = props.src;
|
|
594
|
+
if (img.dataset) {
|
|
595
|
+
img.dataset.src = props.src;
|
|
596
|
+
}
|
|
597
|
+
imgNode.imageInstance = img;
|
|
598
|
+
if (!img.complete) {
|
|
599
|
+
img.onload = () => rootContainer.invalidate();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return node;
|
|
493
603
|
},
|
|
494
604
|
createTextInstance() {
|
|
495
605
|
throw new Error('Text instances are not supported. Use <Text text="..."/>.');
|
|
@@ -502,7 +612,7 @@ var hostConfig = {
|
|
|
502
612
|
parent.children.push(child);
|
|
503
613
|
},
|
|
504
614
|
appendChildToContainer(container, child) {
|
|
505
|
-
child.parent =
|
|
615
|
+
child.parent = container.root;
|
|
506
616
|
container.root.children.push(child);
|
|
507
617
|
container.invalidate();
|
|
508
618
|
},
|
|
@@ -513,7 +623,7 @@ var hostConfig = {
|
|
|
513
623
|
else parent.children.push(child);
|
|
514
624
|
},
|
|
515
625
|
insertInContainerBefore(container, child, beforeChild) {
|
|
516
|
-
child.parent =
|
|
626
|
+
child.parent = container.root;
|
|
517
627
|
const idx = container.root.children.indexOf(beforeChild);
|
|
518
628
|
if (idx >= 0) container.root.children.splice(idx, 0, child);
|
|
519
629
|
else container.root.children.push(child);
|
|
@@ -540,6 +650,39 @@ var hostConfig = {
|
|
|
540
650
|
},
|
|
541
651
|
commitUpdate(instance, updatePayload) {
|
|
542
652
|
instance.props = updatePayload;
|
|
653
|
+
if (instance.type === "Image") {
|
|
654
|
+
const imgNode = instance;
|
|
655
|
+
const newSrc = instance.props.src;
|
|
656
|
+
const currentSrc = imgNode.imageInstance?.dataset?.src;
|
|
657
|
+
if (newSrc !== currentSrc) {
|
|
658
|
+
if (!newSrc) {
|
|
659
|
+
imgNode.imageInstance = null;
|
|
660
|
+
} else {
|
|
661
|
+
const img = new Image();
|
|
662
|
+
img.crossOrigin = "anonymous";
|
|
663
|
+
img.src = newSrc;
|
|
664
|
+
if (img.dataset) {
|
|
665
|
+
img.dataset.src = newSrc;
|
|
666
|
+
}
|
|
667
|
+
imgNode.imageInstance = img;
|
|
668
|
+
const invalidate = () => {
|
|
669
|
+
let p = imgNode;
|
|
670
|
+
while (p) {
|
|
671
|
+
if (p.type === "Root") {
|
|
672
|
+
p.container?.invalidate();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
p = p.parent;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
if (!img.complete) {
|
|
679
|
+
img.onload = invalidate;
|
|
680
|
+
} else {
|
|
681
|
+
invalidate();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
543
686
|
},
|
|
544
687
|
commitTextUpdate() {
|
|
545
688
|
},
|
|
@@ -606,6 +749,7 @@ function createCanvasRoot(canvas, options) {
|
|
|
606
749
|
};
|
|
607
750
|
let hoverId = null;
|
|
608
751
|
let selectedId = null;
|
|
752
|
+
let lastHoveredNode = null;
|
|
609
753
|
const toCanvasPoint = (clientX, clientY) => {
|
|
610
754
|
const rect = canvas.getBoundingClientRect();
|
|
611
755
|
const scaleX = rect.width ? options.width / rect.width : 1;
|
|
@@ -634,7 +778,8 @@ function createCanvasRoot(canvas, options) {
|
|
|
634
778
|
let current = node;
|
|
635
779
|
while (current) {
|
|
636
780
|
path.push(current);
|
|
637
|
-
|
|
781
|
+
const nextParent = current.parent;
|
|
782
|
+
current = nextParent && nextParent.type !== "Root" ? nextParent : null;
|
|
638
783
|
}
|
|
639
784
|
let absLeft = 0;
|
|
640
785
|
let absTop = 0;
|
|
@@ -650,14 +795,15 @@ function createCanvasRoot(canvas, options) {
|
|
|
650
795
|
};
|
|
651
796
|
const getScrollClipRects = (node) => {
|
|
652
797
|
const rects = [];
|
|
653
|
-
let current = node.parent;
|
|
798
|
+
let current = node.parent && node.parent.type !== "Root" ? node.parent : null;
|
|
654
799
|
while (current) {
|
|
655
800
|
if (current.type === "View") {
|
|
656
801
|
const scrollX = !!current.props?.scrollX;
|
|
657
802
|
const scrollY = !!current.props?.scrollY;
|
|
658
803
|
if (scrollX || scrollY) rects.push(getAbsoluteRect(current));
|
|
659
804
|
}
|
|
660
|
-
|
|
805
|
+
const nextParent = current.parent;
|
|
806
|
+
current = nextParent && nextParent.type !== "Root" ? nextParent : null;
|
|
661
807
|
}
|
|
662
808
|
return rects.reverse();
|
|
663
809
|
};
|
|
@@ -687,7 +833,8 @@ function createCanvasRoot(canvas, options) {
|
|
|
687
833
|
return out;
|
|
688
834
|
};
|
|
689
835
|
let frameId = null;
|
|
690
|
-
let
|
|
836
|
+
let dirtyLayout = true;
|
|
837
|
+
let dirtyDraw = true;
|
|
691
838
|
const measureText = (text, font, maxWidth) => {
|
|
692
839
|
ctx.save();
|
|
693
840
|
ctx.font = font;
|
|
@@ -700,14 +847,77 @@ function createCanvasRoot(canvas, options) {
|
|
|
700
847
|
return { width, height: Math.max(1, height) };
|
|
701
848
|
};
|
|
702
849
|
const invalidate = () => {
|
|
703
|
-
|
|
850
|
+
dirtyLayout = true;
|
|
851
|
+
dirtyDraw = true;
|
|
704
852
|
if (frameId != null) return;
|
|
705
853
|
frameId = requestAnimationFrame(async () => {
|
|
706
854
|
frameId = null;
|
|
707
|
-
if (!
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
855
|
+
if (!dirtyLayout && !dirtyDraw) return;
|
|
856
|
+
const needsLayout = dirtyLayout;
|
|
857
|
+
const needsDraw = dirtyDraw || dirtyLayout;
|
|
858
|
+
dirtyLayout = false;
|
|
859
|
+
dirtyDraw = false;
|
|
860
|
+
if (needsLayout) {
|
|
861
|
+
await layoutTree(rootNode, options.width, options.height, measureText, options);
|
|
862
|
+
}
|
|
863
|
+
if (needsDraw) {
|
|
864
|
+
drawTree(rootNode, ctx, options.dpr, options.clearColor, options);
|
|
865
|
+
}
|
|
866
|
+
const overlayHover = typeof hoverId === "number" ? findNodeById(hoverId) : null;
|
|
867
|
+
const overlaySelected = typeof selectedId === "number" ? findNodeById(selectedId) : null;
|
|
868
|
+
if (overlayHover || overlaySelected) {
|
|
869
|
+
ctx.save();
|
|
870
|
+
ctx.setTransform(options.dpr, 0, 0, options.dpr, 0, 0);
|
|
871
|
+
if (overlayHover && (!overlaySelected || overlayHover.debugId !== overlaySelected.debugId)) {
|
|
872
|
+
const r = getAbsoluteRect(overlayHover);
|
|
873
|
+
ctx.save();
|
|
874
|
+
for (const clip of getScrollClipRects(overlayHover)) {
|
|
875
|
+
ctx.beginPath();
|
|
876
|
+
ctx.rect(clip.x, clip.y, clip.width, clip.height);
|
|
877
|
+
ctx.clip();
|
|
878
|
+
}
|
|
879
|
+
ctx.fillStyle = "rgba(59,130,246,0.12)";
|
|
880
|
+
ctx.strokeStyle = "rgba(59,130,246,0.9)";
|
|
881
|
+
ctx.lineWidth = 1;
|
|
882
|
+
ctx.fillRect(r.x, r.y, r.width, r.height);
|
|
883
|
+
ctx.strokeRect(r.x + 0.5, r.y + 0.5, Math.max(0, r.width - 1), Math.max(0, r.height - 1));
|
|
884
|
+
ctx.restore();
|
|
885
|
+
}
|
|
886
|
+
if (overlaySelected) {
|
|
887
|
+
const r = getAbsoluteRect(overlaySelected);
|
|
888
|
+
ctx.save();
|
|
889
|
+
for (const clip of getScrollClipRects(overlaySelected)) {
|
|
890
|
+
ctx.beginPath();
|
|
891
|
+
ctx.rect(clip.x, clip.y, clip.width, clip.height);
|
|
892
|
+
ctx.clip();
|
|
893
|
+
}
|
|
894
|
+
ctx.fillStyle = "rgba(16,185,129,0.12)";
|
|
895
|
+
ctx.strokeStyle = "rgba(16,185,129,0.95)";
|
|
896
|
+
ctx.lineWidth = 2;
|
|
897
|
+
ctx.fillRect(r.x, r.y, r.width, r.height);
|
|
898
|
+
ctx.strokeRect(r.x + 1, r.y + 1, Math.max(0, r.width - 2), Math.max(0, r.height - 2));
|
|
899
|
+
ctx.restore();
|
|
900
|
+
}
|
|
901
|
+
ctx.restore();
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
};
|
|
905
|
+
const invalidateDrawOnly = () => {
|
|
906
|
+
dirtyDraw = true;
|
|
907
|
+
if (frameId != null) return;
|
|
908
|
+
frameId = requestAnimationFrame(async () => {
|
|
909
|
+
frameId = null;
|
|
910
|
+
if (!dirtyLayout && !dirtyDraw) return;
|
|
911
|
+
const needsLayout = dirtyLayout;
|
|
912
|
+
const needsDraw = dirtyDraw || dirtyLayout;
|
|
913
|
+
dirtyLayout = false;
|
|
914
|
+
dirtyDraw = false;
|
|
915
|
+
if (needsLayout) {
|
|
916
|
+
await layoutTree(rootNode, options.width, options.height, measureText, options);
|
|
917
|
+
}
|
|
918
|
+
if (needsDraw) {
|
|
919
|
+
drawTree(rootNode, ctx, options.dpr, options.clearColor, options);
|
|
920
|
+
}
|
|
711
921
|
const overlayHover = typeof hoverId === "number" ? findNodeById(hoverId) : null;
|
|
712
922
|
const overlaySelected = typeof selectedId === "number" ? findNodeById(selectedId) : null;
|
|
713
923
|
if (overlayHover || overlaySelected) {
|
|
@@ -874,7 +1084,8 @@ function createCanvasRoot(canvas, options) {
|
|
|
874
1084
|
let current = target;
|
|
875
1085
|
while (current) {
|
|
876
1086
|
path.push(current);
|
|
877
|
-
|
|
1087
|
+
const nextParent = current.parent;
|
|
1088
|
+
current = nextParent && nextParent.type !== "Root" ? nextParent : null;
|
|
878
1089
|
}
|
|
879
1090
|
return path;
|
|
880
1091
|
};
|
|
@@ -976,7 +1187,7 @@ function createCanvasRoot(canvas, options) {
|
|
|
976
1187
|
capturedForScroll.scrollTop = clamped2;
|
|
977
1188
|
const onScroll = capturedForScroll.props?.onScroll;
|
|
978
1189
|
if (typeof onScroll === "function") onScroll(clamped2);
|
|
979
|
-
|
|
1190
|
+
invalidateDrawOnly();
|
|
980
1191
|
return { defaultPrevented: true };
|
|
981
1192
|
}
|
|
982
1193
|
const metrics = getScrollbarMetricsX(capturedForScroll, absLeft, absTop);
|
|
@@ -989,14 +1200,14 @@ function createCanvasRoot(canvas, options) {
|
|
|
989
1200
|
capturedForScroll.scrollLeft = clamped;
|
|
990
1201
|
const onScrollX = capturedForScroll.props?.onScrollX;
|
|
991
1202
|
if (typeof onScrollX === "function") onScrollX(clamped);
|
|
992
|
-
|
|
1203
|
+
invalidateDrawOnly();
|
|
993
1204
|
return { defaultPrevented: true };
|
|
994
1205
|
}
|
|
995
1206
|
if (eventType === "pointerup" || eventType === "pointercancel") {
|
|
996
1207
|
capturedForScroll.scrollbarDrag = null;
|
|
997
1208
|
pointerCapture.delete(pointerId);
|
|
998
1209
|
pointerDownTarget.delete(pointerId);
|
|
999
|
-
|
|
1210
|
+
invalidateDrawOnly();
|
|
1000
1211
|
return { defaultPrevented: true };
|
|
1001
1212
|
}
|
|
1002
1213
|
}
|
|
@@ -1021,6 +1232,58 @@ function createCanvasRoot(canvas, options) {
|
|
|
1021
1232
|
if (eventType === "pointermove") {
|
|
1022
1233
|
const captured = pointerCapture.get(pointerId);
|
|
1023
1234
|
const target2 = captured ?? hitTest(init.x, init.y);
|
|
1235
|
+
if (target2 !== lastHoveredNode) {
|
|
1236
|
+
const prevChain = [];
|
|
1237
|
+
let p = lastHoveredNode;
|
|
1238
|
+
while (p) {
|
|
1239
|
+
prevChain.push(p);
|
|
1240
|
+
const nextParent = p.parent;
|
|
1241
|
+
p = nextParent && nextParent.type !== "Root" ? nextParent : null;
|
|
1242
|
+
}
|
|
1243
|
+
const nextChain = [];
|
|
1244
|
+
let n = target2;
|
|
1245
|
+
while (n) {
|
|
1246
|
+
nextChain.push(n);
|
|
1247
|
+
const nextParent = n.parent;
|
|
1248
|
+
n = nextParent && nextParent.type !== "Root" ? nextParent : null;
|
|
1249
|
+
}
|
|
1250
|
+
for (const node of prevChain) {
|
|
1251
|
+
if (!nextChain.includes(node)) {
|
|
1252
|
+
const handler = node.props?.onPointerLeave;
|
|
1253
|
+
if (typeof handler === "function") {
|
|
1254
|
+
handler({
|
|
1255
|
+
type: "pointerleave",
|
|
1256
|
+
...init,
|
|
1257
|
+
target: node,
|
|
1258
|
+
currentTarget: node,
|
|
1259
|
+
defaultPrevented: false,
|
|
1260
|
+
stopPropagation: () => {
|
|
1261
|
+
},
|
|
1262
|
+
preventDefault: () => {
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const enteringNodes = nextChain.filter((node) => !prevChain.includes(node)).reverse();
|
|
1269
|
+
for (const node of enteringNodes) {
|
|
1270
|
+
const handler = node.props?.onPointerEnter;
|
|
1271
|
+
if (typeof handler === "function") {
|
|
1272
|
+
handler({
|
|
1273
|
+
type: "pointerenter",
|
|
1274
|
+
...init,
|
|
1275
|
+
target: node,
|
|
1276
|
+
currentTarget: node,
|
|
1277
|
+
defaultPrevented: false,
|
|
1278
|
+
stopPropagation: () => {
|
|
1279
|
+
},
|
|
1280
|
+
preventDefault: () => {
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
lastHoveredNode = target2;
|
|
1286
|
+
}
|
|
1024
1287
|
if (!target2) return { defaultPrevented: false };
|
|
1025
1288
|
return dispatchOnPath(eventType, buildPath(target2), init, target2);
|
|
1026
1289
|
}
|
|
@@ -1076,10 +1339,11 @@ function createCanvasRoot(canvas, options) {
|
|
|
1076
1339
|
}
|
|
1077
1340
|
if (remainingX === 0 && remainingY === 0) break;
|
|
1078
1341
|
}
|
|
1079
|
-
if (defaultPrevented)
|
|
1342
|
+
if (defaultPrevented) invalidateDrawOnly();
|
|
1080
1343
|
return { defaultPrevented };
|
|
1081
1344
|
};
|
|
1082
1345
|
const container = { root: rootNode, invalidate, notifyCommit };
|
|
1346
|
+
rootNode.container = container;
|
|
1083
1347
|
const reconcilerRoot = createReconcilerRoot(container);
|
|
1084
1348
|
invalidate();
|
|
1085
1349
|
const __devtools = {
|
|
@@ -1131,7 +1395,7 @@ function createCanvasRoot(canvas, options) {
|
|
|
1131
1395
|
setHighlight(next) {
|
|
1132
1396
|
if ("hoverId" in next) hoverId = next.hoverId ?? null;
|
|
1133
1397
|
if ("selectedId" in next) selectedId = next.selectedId ?? null;
|
|
1134
|
-
|
|
1398
|
+
invalidateDrawOnly();
|
|
1135
1399
|
},
|
|
1136
1400
|
subscribe(cb) {
|
|
1137
1401
|
commitSubscribers.add(cb);
|
|
@@ -1437,8 +1701,12 @@ function Rect(props) {
|
|
|
1437
1701
|
function Text(props) {
|
|
1438
1702
|
return react.createElement("Text", props);
|
|
1439
1703
|
}
|
|
1704
|
+
function Image2(props) {
|
|
1705
|
+
return react.createElement("Image", props);
|
|
1706
|
+
}
|
|
1440
1707
|
|
|
1441
1708
|
exports.Canvas = Canvas;
|
|
1709
|
+
exports.Image = Image2;
|
|
1442
1710
|
exports.Rect = Rect;
|
|
1443
1711
|
exports.Text = Text;
|
|
1444
1712
|
exports.View = View;
|