@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 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
- return {
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
- return createNode(type, props);
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 = null;
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 = null;
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
- current = current.parent;
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
- current = current.parent;
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 dirty = true;
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
- dirty = true;
850
+ dirtyLayout = true;
851
+ dirtyDraw = true;
704
852
  if (frameId != null) return;
705
853
  frameId = requestAnimationFrame(async () => {
706
854
  frameId = null;
707
- if (!dirty) return;
708
- dirty = false;
709
- await layoutTree(rootNode, options.width, options.height, measureText, options);
710
- drawTree(rootNode, ctx, options.dpr, options.clearColor, options);
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
- current = current.parent;
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
- invalidate();
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
- invalidate();
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
- invalidate();
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) invalidate();
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
- invalidate();
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;