@jiujue/react-canvas-fiber 2.0.3 → 2.0.5

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/dist/index.d.cts CHANGED
@@ -22,7 +22,7 @@ type CanvasProps = {
22
22
  children?: ReactNode;
23
23
  };
24
24
 
25
- type CanvasPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel' | 'click';
25
+ type CanvasPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel' | 'click' | 'pointerenter' | 'pointerleave';
26
26
  type CanvasPointerEvent = {
27
27
  type: CanvasPointerEventType;
28
28
  x: number;
@@ -108,6 +108,8 @@ type ViewProps = {
108
108
  onPointerCancel?: CanvasPointerEventHandler;
109
109
  onClickCapture?: CanvasPointerEventHandler;
110
110
  onClick?: CanvasPointerEventHandler;
111
+ onPointerEnter?: CanvasPointerEventHandler;
112
+ onPointerLeave?: CanvasPointerEventHandler;
111
113
  };
112
114
  type RectProps = {
113
115
  children?: ReactNode;
@@ -127,6 +129,8 @@ type RectProps = {
127
129
  onPointerCancel?: CanvasPointerEventHandler;
128
130
  onClickCapture?: CanvasPointerEventHandler;
129
131
  onClick?: CanvasPointerEventHandler;
132
+ onPointerEnter?: CanvasPointerEventHandler;
133
+ onPointerLeave?: CanvasPointerEventHandler;
130
134
  };
131
135
  type TextProps = {
132
136
  children?: never;
@@ -145,6 +149,27 @@ type TextProps = {
145
149
  onPointerCancel?: CanvasPointerEventHandler;
146
150
  onClickCapture?: CanvasPointerEventHandler;
147
151
  onClick?: CanvasPointerEventHandler;
152
+ onPointerEnter?: CanvasPointerEventHandler;
153
+ onPointerLeave?: CanvasPointerEventHandler;
154
+ };
155
+ type ImageProps = {
156
+ children?: never;
157
+ style?: YogaStyle;
158
+ src: string;
159
+ objectFit?: 'cover' | 'contain' | 'fill';
160
+ pointerEvents?: PointerEventsMode;
161
+ onPointerDownCapture?: CanvasPointerEventHandler;
162
+ onPointerDown?: CanvasPointerEventHandler;
163
+ onPointerMoveCapture?: CanvasPointerEventHandler;
164
+ onPointerMove?: CanvasPointerEventHandler;
165
+ onPointerUpCapture?: CanvasPointerEventHandler;
166
+ onPointerUp?: CanvasPointerEventHandler;
167
+ onPointerCancelCapture?: CanvasPointerEventHandler;
168
+ onPointerCancel?: CanvasPointerEventHandler;
169
+ onClickCapture?: CanvasPointerEventHandler;
170
+ onClick?: CanvasPointerEventHandler;
171
+ onPointerEnter?: CanvasPointerEventHandler;
172
+ onPointerLeave?: CanvasPointerEventHandler;
148
173
  };
149
174
 
150
175
  /**
@@ -162,9 +187,11 @@ declare function Canvas(props: CanvasProps): react_jsx_runtime.JSX.Element;
162
187
  * - <View/> -> CanvasNode(type='View')
163
188
  * - <Rect/> -> CanvasNode(type='Rect')
164
189
  * - <Text/> -> CanvasNode(type='Text')
190
+ * - <Image/> -> CanvasNode(type='Image')
165
191
  */
166
192
  declare function View(props: ViewProps): react.ReactElement<ViewProps, string | react.JSXElementConstructor<any>>;
167
193
  declare function Rect(props: RectProps): react.ReactElement<RectProps, string | react.JSXElementConstructor<any>>;
168
194
  declare function Text(props: TextProps): react.ReactElement<TextProps, string | react.JSXElementConstructor<any>>;
195
+ declare function Image(props: ImageProps): react.ReactElement<ImageProps, string | react.JSXElementConstructor<any>>;
169
196
 
170
- export { Canvas, Rect, Text, View };
197
+ export { Canvas, Image, Rect, Text, View };
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ type CanvasProps = {
22
22
  children?: ReactNode;
23
23
  };
24
24
 
25
- type CanvasPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel' | 'click';
25
+ type CanvasPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel' | 'click' | 'pointerenter' | 'pointerleave';
26
26
  type CanvasPointerEvent = {
27
27
  type: CanvasPointerEventType;
28
28
  x: number;
@@ -108,6 +108,8 @@ type ViewProps = {
108
108
  onPointerCancel?: CanvasPointerEventHandler;
109
109
  onClickCapture?: CanvasPointerEventHandler;
110
110
  onClick?: CanvasPointerEventHandler;
111
+ onPointerEnter?: CanvasPointerEventHandler;
112
+ onPointerLeave?: CanvasPointerEventHandler;
111
113
  };
112
114
  type RectProps = {
113
115
  children?: ReactNode;
@@ -127,6 +129,8 @@ type RectProps = {
127
129
  onPointerCancel?: CanvasPointerEventHandler;
128
130
  onClickCapture?: CanvasPointerEventHandler;
129
131
  onClick?: CanvasPointerEventHandler;
132
+ onPointerEnter?: CanvasPointerEventHandler;
133
+ onPointerLeave?: CanvasPointerEventHandler;
130
134
  };
131
135
  type TextProps = {
132
136
  children?: never;
@@ -145,6 +149,27 @@ type TextProps = {
145
149
  onPointerCancel?: CanvasPointerEventHandler;
146
150
  onClickCapture?: CanvasPointerEventHandler;
147
151
  onClick?: CanvasPointerEventHandler;
152
+ onPointerEnter?: CanvasPointerEventHandler;
153
+ onPointerLeave?: CanvasPointerEventHandler;
154
+ };
155
+ type ImageProps = {
156
+ children?: never;
157
+ style?: YogaStyle;
158
+ src: string;
159
+ objectFit?: 'cover' | 'contain' | 'fill';
160
+ pointerEvents?: PointerEventsMode;
161
+ onPointerDownCapture?: CanvasPointerEventHandler;
162
+ onPointerDown?: CanvasPointerEventHandler;
163
+ onPointerMoveCapture?: CanvasPointerEventHandler;
164
+ onPointerMove?: CanvasPointerEventHandler;
165
+ onPointerUpCapture?: CanvasPointerEventHandler;
166
+ onPointerUp?: CanvasPointerEventHandler;
167
+ onPointerCancelCapture?: CanvasPointerEventHandler;
168
+ onPointerCancel?: CanvasPointerEventHandler;
169
+ onClickCapture?: CanvasPointerEventHandler;
170
+ onClick?: CanvasPointerEventHandler;
171
+ onPointerEnter?: CanvasPointerEventHandler;
172
+ onPointerLeave?: CanvasPointerEventHandler;
148
173
  };
149
174
 
150
175
  /**
@@ -162,9 +187,11 @@ declare function Canvas(props: CanvasProps): react_jsx_runtime.JSX.Element;
162
187
  * - <View/> -> CanvasNode(type='View')
163
188
  * - <Rect/> -> CanvasNode(type='Rect')
164
189
  * - <Text/> -> CanvasNode(type='Text')
190
+ * - <Image/> -> CanvasNode(type='Image')
165
191
  */
166
192
  declare function View(props: ViewProps): react.ReactElement<ViewProps, string | react.JSXElementConstructor<any>>;
167
193
  declare function Rect(props: RectProps): react.ReactElement<RectProps, string | react.JSXElementConstructor<any>>;
168
194
  declare function Text(props: TextProps): react.ReactElement<TextProps, string | react.JSXElementConstructor<any>>;
195
+ declare function Image(props: ImageProps): react.ReactElement<ImageProps, string | react.JSXElementConstructor<any>>;
169
196
 
170
- export { Canvas, Rect, Text, View };
197
+ export { Canvas, Image, Rect, Text, View };
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ function createRootNode() {
15
15
  children: [],
16
16
  props: {},
17
17
  layout: { x: 0, y: 0, width: 0, height: 0 },
18
+ contentBounds: void 0,
18
19
  yogaNode: null,
19
20
  scrollLeft: 0,
20
21
  scrollTop: 0,
@@ -24,13 +25,14 @@ function createRootNode() {
24
25
  };
25
26
  }
26
27
  function createNode(type, props) {
27
- return {
28
+ const node = {
28
29
  type,
29
30
  debugId: nextDebugId++,
30
31
  parent: null,
31
32
  children: [],
32
33
  props,
33
34
  layout: { x: 0, y: 0, width: 0, height: 0 },
35
+ contentBounds: void 0,
34
36
  yogaNode: null,
35
37
  scrollLeft: 0,
36
38
  scrollTop: 0,
@@ -38,6 +40,10 @@ function createNode(type, props) {
38
40
  scrollContentHeight: 0,
39
41
  scrollbarDrag: null
40
42
  };
43
+ if (type === "Image") {
44
+ node.imageInstance = null;
45
+ }
46
+ return node;
41
47
  }
42
48
 
43
49
  // src/render/drawTree.ts
@@ -79,6 +85,9 @@ function drawRoundedRect(ctx, x, y, w, h, r) {
79
85
  ctx.arcTo(x, y, x + w, y, radius);
80
86
  ctx.closePath();
81
87
  }
88
+ function rectsIntersect(ax, ay, aw, ah, bx, by, bw, bh) {
89
+ return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
90
+ }
82
91
  function drawNode(state, node, offsetX, offsetY) {
83
92
  const { ctx } = state;
84
93
  const x = offsetX + node.layout.x;
@@ -114,7 +123,32 @@ function drawNode(state, node, offsetX, offsetY) {
114
123
  ctx.beginPath();
115
124
  ctx.rect(x, y, w, h);
116
125
  ctx.clip();
126
+ const cullPadding = 1;
127
+ const viewportX = x - cullPadding;
128
+ const viewportY = y - cullPadding;
129
+ const viewportW = w + cullPadding * 2;
130
+ const viewportH = h + cullPadding * 2;
117
131
  for (const child of node.children) {
132
+ const bounds = child.contentBounds ?? {
133
+ x: 0,
134
+ y: 0,
135
+ width: child.layout.width,
136
+ height: child.layout.height
137
+ };
138
+ const bx = x - scrollLeft + child.layout.x + bounds.x;
139
+ const by = y - scrollTop + child.layout.y + bounds.y;
140
+ if (!rectsIntersect(
141
+ viewportX,
142
+ viewportY,
143
+ viewportW,
144
+ viewportH,
145
+ bx,
146
+ by,
147
+ bounds.width,
148
+ bounds.height
149
+ )) {
150
+ continue;
151
+ }
118
152
  drawNode(state, child, x - scrollLeft, y - scrollTop);
119
153
  }
120
154
  ctx.restore();
@@ -196,6 +230,43 @@ function drawNode(state, node, offsetX, offsetY) {
196
230
  }
197
231
  ctx.restore();
198
232
  }
233
+ if (node.type === "Image") {
234
+ const { imageInstance } = node;
235
+ if (imageInstance && imageInstance.complete && imageInstance.naturalWidth > 0) {
236
+ const objectFit = node.props.objectFit || "contain";
237
+ const srcW = imageInstance.naturalWidth;
238
+ const srcH = imageInstance.naturalHeight;
239
+ let dstX = x;
240
+ let dstY = y;
241
+ let dstW = w;
242
+ let dstH = h;
243
+ let srcX = 0;
244
+ let srcY = 0;
245
+ let finalSrcW = srcW;
246
+ let finalSrcH = srcH;
247
+ if (objectFit === "fill") ; else if (objectFit === "contain") {
248
+ const ratio = Math.min(w / srcW, h / srcH);
249
+ dstW = srcW * ratio;
250
+ dstH = srcH * ratio;
251
+ dstX = x + (w - dstW) / 2;
252
+ dstY = y + (h - dstH) / 2;
253
+ } else if (objectFit === "cover") {
254
+ const ratio = Math.max(w / srcW, h / srcH);
255
+ const renderW = srcW * ratio;
256
+ const renderH = srcH * ratio;
257
+ srcX = (renderW - w) / 2 / ratio;
258
+ srcY = (renderH - h) / 2 / ratio;
259
+ finalSrcW = w / ratio;
260
+ finalSrcH = h / ratio;
261
+ }
262
+ ctx.save();
263
+ ctx.beginPath();
264
+ drawRoundedRect(ctx, x, y, w, h, 0);
265
+ ctx.clip();
266
+ ctx.drawImage(imageInstance, srcX, srcY, finalSrcW, finalSrcH, dstX, dstY, dstW, dstH);
267
+ ctx.restore();
268
+ }
269
+ }
199
270
  for (const child of node.children) {
200
271
  drawNode(state, child, x, y);
201
272
  }
@@ -451,6 +522,31 @@ async function layoutTree(root, width, height, measureText, defaults) {
451
522
  }
452
523
  };
453
524
  walk(root);
525
+ const computeSubtreeContentBounds = (node) => {
526
+ let minX = 0;
527
+ let minY = 0;
528
+ let maxX = node.layout.width;
529
+ let maxY = node.layout.height;
530
+ for (const child of node.children) {
531
+ if (child.children.length) computeSubtreeContentBounds(child);
532
+ const childBounds = child.contentBounds ?? {
533
+ x: 0,
534
+ y: 0,
535
+ width: child.layout.width,
536
+ height: child.layout.height
537
+ };
538
+ const bx = child.layout.x + childBounds.x;
539
+ const by = child.layout.y + childBounds.y;
540
+ const br = bx + childBounds.width;
541
+ const bb = by + childBounds.height;
542
+ minX = Math.min(minX, bx);
543
+ minY = Math.min(minY, by);
544
+ maxX = Math.max(maxX, br);
545
+ maxY = Math.max(maxY, bb);
546
+ }
547
+ node.contentBounds = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
548
+ };
549
+ for (const child of root.children) computeSubtreeContentBounds(child);
454
550
  }
455
551
  function freeYogaSubtree(node) {
456
552
  if (node.yogaNode) {
@@ -482,8 +578,22 @@ var hostConfig = {
482
578
  shouldSetTextContent() {
483
579
  return false;
484
580
  },
485
- createInstance(type, props) {
486
- return createNode(type, props);
581
+ createInstance(type, props, rootContainer) {
582
+ const node = createNode(type, props);
583
+ if (type === "Image" && props.src) {
584
+ const imgNode = node;
585
+ const img = new Image();
586
+ img.crossOrigin = "anonymous";
587
+ img.src = props.src;
588
+ if (img.dataset) {
589
+ img.dataset.src = props.src;
590
+ }
591
+ imgNode.imageInstance = img;
592
+ if (!img.complete) {
593
+ img.onload = () => rootContainer.invalidate();
594
+ }
595
+ }
596
+ return node;
487
597
  },
488
598
  createTextInstance() {
489
599
  throw new Error('Text instances are not supported. Use <Text text="..."/>.');
@@ -496,7 +606,7 @@ var hostConfig = {
496
606
  parent.children.push(child);
497
607
  },
498
608
  appendChildToContainer(container, child) {
499
- child.parent = null;
609
+ child.parent = container.root;
500
610
  container.root.children.push(child);
501
611
  container.invalidate();
502
612
  },
@@ -507,7 +617,7 @@ var hostConfig = {
507
617
  else parent.children.push(child);
508
618
  },
509
619
  insertInContainerBefore(container, child, beforeChild) {
510
- child.parent = null;
620
+ child.parent = container.root;
511
621
  const idx = container.root.children.indexOf(beforeChild);
512
622
  if (idx >= 0) container.root.children.splice(idx, 0, child);
513
623
  else container.root.children.push(child);
@@ -534,6 +644,39 @@ var hostConfig = {
534
644
  },
535
645
  commitUpdate(instance, updatePayload) {
536
646
  instance.props = updatePayload;
647
+ if (instance.type === "Image") {
648
+ const imgNode = instance;
649
+ const newSrc = instance.props.src;
650
+ const currentSrc = imgNode.imageInstance?.dataset?.src;
651
+ if (newSrc !== currentSrc) {
652
+ if (!newSrc) {
653
+ imgNode.imageInstance = null;
654
+ } else {
655
+ const img = new Image();
656
+ img.crossOrigin = "anonymous";
657
+ img.src = newSrc;
658
+ if (img.dataset) {
659
+ img.dataset.src = newSrc;
660
+ }
661
+ imgNode.imageInstance = img;
662
+ const invalidate = () => {
663
+ let p = imgNode;
664
+ while (p) {
665
+ if (p.type === "Root") {
666
+ p.container?.invalidate();
667
+ return;
668
+ }
669
+ p = p.parent;
670
+ }
671
+ };
672
+ if (!img.complete) {
673
+ img.onload = invalidate;
674
+ } else {
675
+ invalidate();
676
+ }
677
+ }
678
+ }
679
+ }
537
680
  },
538
681
  commitTextUpdate() {
539
682
  },
@@ -600,6 +743,7 @@ function createCanvasRoot(canvas, options) {
600
743
  };
601
744
  let hoverId = null;
602
745
  let selectedId = null;
746
+ let lastHoveredNode = null;
603
747
  const toCanvasPoint = (clientX, clientY) => {
604
748
  const rect = canvas.getBoundingClientRect();
605
749
  const scaleX = rect.width ? options.width / rect.width : 1;
@@ -628,7 +772,8 @@ function createCanvasRoot(canvas, options) {
628
772
  let current = node;
629
773
  while (current) {
630
774
  path.push(current);
631
- current = current.parent;
775
+ const nextParent = current.parent;
776
+ current = nextParent && nextParent.type !== "Root" ? nextParent : null;
632
777
  }
633
778
  let absLeft = 0;
634
779
  let absTop = 0;
@@ -644,14 +789,15 @@ function createCanvasRoot(canvas, options) {
644
789
  };
645
790
  const getScrollClipRects = (node) => {
646
791
  const rects = [];
647
- let current = node.parent;
792
+ let current = node.parent && node.parent.type !== "Root" ? node.parent : null;
648
793
  while (current) {
649
794
  if (current.type === "View") {
650
795
  const scrollX = !!current.props?.scrollX;
651
796
  const scrollY = !!current.props?.scrollY;
652
797
  if (scrollX || scrollY) rects.push(getAbsoluteRect(current));
653
798
  }
654
- current = current.parent;
799
+ const nextParent = current.parent;
800
+ current = nextParent && nextParent.type !== "Root" ? nextParent : null;
655
801
  }
656
802
  return rects.reverse();
657
803
  };
@@ -681,7 +827,8 @@ function createCanvasRoot(canvas, options) {
681
827
  return out;
682
828
  };
683
829
  let frameId = null;
684
- let dirty = true;
830
+ let dirtyLayout = true;
831
+ let dirtyDraw = true;
685
832
  const measureText = (text, font, maxWidth) => {
686
833
  ctx.save();
687
834
  ctx.font = font;
@@ -694,14 +841,77 @@ function createCanvasRoot(canvas, options) {
694
841
  return { width, height: Math.max(1, height) };
695
842
  };
696
843
  const invalidate = () => {
697
- dirty = true;
844
+ dirtyLayout = true;
845
+ dirtyDraw = true;
698
846
  if (frameId != null) return;
699
847
  frameId = requestAnimationFrame(async () => {
700
848
  frameId = null;
701
- if (!dirty) return;
702
- dirty = false;
703
- await layoutTree(rootNode, options.width, options.height, measureText, options);
704
- drawTree(rootNode, ctx, options.dpr, options.clearColor, options);
849
+ if (!dirtyLayout && !dirtyDraw) return;
850
+ const needsLayout = dirtyLayout;
851
+ const needsDraw = dirtyDraw || dirtyLayout;
852
+ dirtyLayout = false;
853
+ dirtyDraw = false;
854
+ if (needsLayout) {
855
+ await layoutTree(rootNode, options.width, options.height, measureText, options);
856
+ }
857
+ if (needsDraw) {
858
+ drawTree(rootNode, ctx, options.dpr, options.clearColor, options);
859
+ }
860
+ const overlayHover = typeof hoverId === "number" ? findNodeById(hoverId) : null;
861
+ const overlaySelected = typeof selectedId === "number" ? findNodeById(selectedId) : null;
862
+ if (overlayHover || overlaySelected) {
863
+ ctx.save();
864
+ ctx.setTransform(options.dpr, 0, 0, options.dpr, 0, 0);
865
+ if (overlayHover && (!overlaySelected || overlayHover.debugId !== overlaySelected.debugId)) {
866
+ const r = getAbsoluteRect(overlayHover);
867
+ ctx.save();
868
+ for (const clip of getScrollClipRects(overlayHover)) {
869
+ ctx.beginPath();
870
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
871
+ ctx.clip();
872
+ }
873
+ ctx.fillStyle = "rgba(59,130,246,0.12)";
874
+ ctx.strokeStyle = "rgba(59,130,246,0.9)";
875
+ ctx.lineWidth = 1;
876
+ ctx.fillRect(r.x, r.y, r.width, r.height);
877
+ ctx.strokeRect(r.x + 0.5, r.y + 0.5, Math.max(0, r.width - 1), Math.max(0, r.height - 1));
878
+ ctx.restore();
879
+ }
880
+ if (overlaySelected) {
881
+ const r = getAbsoluteRect(overlaySelected);
882
+ ctx.save();
883
+ for (const clip of getScrollClipRects(overlaySelected)) {
884
+ ctx.beginPath();
885
+ ctx.rect(clip.x, clip.y, clip.width, clip.height);
886
+ ctx.clip();
887
+ }
888
+ ctx.fillStyle = "rgba(16,185,129,0.12)";
889
+ ctx.strokeStyle = "rgba(16,185,129,0.95)";
890
+ ctx.lineWidth = 2;
891
+ ctx.fillRect(r.x, r.y, r.width, r.height);
892
+ ctx.strokeRect(r.x + 1, r.y + 1, Math.max(0, r.width - 2), Math.max(0, r.height - 2));
893
+ ctx.restore();
894
+ }
895
+ ctx.restore();
896
+ }
897
+ });
898
+ };
899
+ const invalidateDrawOnly = () => {
900
+ dirtyDraw = true;
901
+ if (frameId != null) return;
902
+ frameId = requestAnimationFrame(async () => {
903
+ frameId = null;
904
+ if (!dirtyLayout && !dirtyDraw) return;
905
+ const needsLayout = dirtyLayout;
906
+ const needsDraw = dirtyDraw || dirtyLayout;
907
+ dirtyLayout = false;
908
+ dirtyDraw = false;
909
+ if (needsLayout) {
910
+ await layoutTree(rootNode, options.width, options.height, measureText, options);
911
+ }
912
+ if (needsDraw) {
913
+ drawTree(rootNode, ctx, options.dpr, options.clearColor, options);
914
+ }
705
915
  const overlayHover = typeof hoverId === "number" ? findNodeById(hoverId) : null;
706
916
  const overlaySelected = typeof selectedId === "number" ? findNodeById(selectedId) : null;
707
917
  if (overlayHover || overlaySelected) {
@@ -868,7 +1078,8 @@ function createCanvasRoot(canvas, options) {
868
1078
  let current = target;
869
1079
  while (current) {
870
1080
  path.push(current);
871
- current = current.parent;
1081
+ const nextParent = current.parent;
1082
+ current = nextParent && nextParent.type !== "Root" ? nextParent : null;
872
1083
  }
873
1084
  return path;
874
1085
  };
@@ -970,7 +1181,7 @@ function createCanvasRoot(canvas, options) {
970
1181
  capturedForScroll.scrollTop = clamped2;
971
1182
  const onScroll = capturedForScroll.props?.onScroll;
972
1183
  if (typeof onScroll === "function") onScroll(clamped2);
973
- invalidate();
1184
+ invalidateDrawOnly();
974
1185
  return { defaultPrevented: true };
975
1186
  }
976
1187
  const metrics = getScrollbarMetricsX(capturedForScroll, absLeft, absTop);
@@ -983,14 +1194,14 @@ function createCanvasRoot(canvas, options) {
983
1194
  capturedForScroll.scrollLeft = clamped;
984
1195
  const onScrollX = capturedForScroll.props?.onScrollX;
985
1196
  if (typeof onScrollX === "function") onScrollX(clamped);
986
- invalidate();
1197
+ invalidateDrawOnly();
987
1198
  return { defaultPrevented: true };
988
1199
  }
989
1200
  if (eventType === "pointerup" || eventType === "pointercancel") {
990
1201
  capturedForScroll.scrollbarDrag = null;
991
1202
  pointerCapture.delete(pointerId);
992
1203
  pointerDownTarget.delete(pointerId);
993
- invalidate();
1204
+ invalidateDrawOnly();
994
1205
  return { defaultPrevented: true };
995
1206
  }
996
1207
  }
@@ -1015,6 +1226,58 @@ function createCanvasRoot(canvas, options) {
1015
1226
  if (eventType === "pointermove") {
1016
1227
  const captured = pointerCapture.get(pointerId);
1017
1228
  const target2 = captured ?? hitTest(init.x, init.y);
1229
+ if (target2 !== lastHoveredNode) {
1230
+ const prevChain = [];
1231
+ let p = lastHoveredNode;
1232
+ while (p) {
1233
+ prevChain.push(p);
1234
+ const nextParent = p.parent;
1235
+ p = nextParent && nextParent.type !== "Root" ? nextParent : null;
1236
+ }
1237
+ const nextChain = [];
1238
+ let n = target2;
1239
+ while (n) {
1240
+ nextChain.push(n);
1241
+ const nextParent = n.parent;
1242
+ n = nextParent && nextParent.type !== "Root" ? nextParent : null;
1243
+ }
1244
+ for (const node of prevChain) {
1245
+ if (!nextChain.includes(node)) {
1246
+ const handler = node.props?.onPointerLeave;
1247
+ if (typeof handler === "function") {
1248
+ handler({
1249
+ type: "pointerleave",
1250
+ ...init,
1251
+ target: node,
1252
+ currentTarget: node,
1253
+ defaultPrevented: false,
1254
+ stopPropagation: () => {
1255
+ },
1256
+ preventDefault: () => {
1257
+ }
1258
+ });
1259
+ }
1260
+ }
1261
+ }
1262
+ const enteringNodes = nextChain.filter((node) => !prevChain.includes(node)).reverse();
1263
+ for (const node of enteringNodes) {
1264
+ const handler = node.props?.onPointerEnter;
1265
+ if (typeof handler === "function") {
1266
+ handler({
1267
+ type: "pointerenter",
1268
+ ...init,
1269
+ target: node,
1270
+ currentTarget: node,
1271
+ defaultPrevented: false,
1272
+ stopPropagation: () => {
1273
+ },
1274
+ preventDefault: () => {
1275
+ }
1276
+ });
1277
+ }
1278
+ }
1279
+ lastHoveredNode = target2;
1280
+ }
1018
1281
  if (!target2) return { defaultPrevented: false };
1019
1282
  return dispatchOnPath(eventType, buildPath(target2), init, target2);
1020
1283
  }
@@ -1070,10 +1333,11 @@ function createCanvasRoot(canvas, options) {
1070
1333
  }
1071
1334
  if (remainingX === 0 && remainingY === 0) break;
1072
1335
  }
1073
- if (defaultPrevented) invalidate();
1336
+ if (defaultPrevented) invalidateDrawOnly();
1074
1337
  return { defaultPrevented };
1075
1338
  };
1076
1339
  const container = { root: rootNode, invalidate, notifyCommit };
1340
+ rootNode.container = container;
1077
1341
  const reconcilerRoot = createReconcilerRoot(container);
1078
1342
  invalidate();
1079
1343
  const __devtools = {
@@ -1125,7 +1389,7 @@ function createCanvasRoot(canvas, options) {
1125
1389
  setHighlight(next) {
1126
1390
  if ("hoverId" in next) hoverId = next.hoverId ?? null;
1127
1391
  if ("selectedId" in next) selectedId = next.selectedId ?? null;
1128
- invalidate();
1392
+ invalidateDrawOnly();
1129
1393
  },
1130
1394
  subscribe(cb) {
1131
1395
  commitSubscribers.add(cb);
@@ -1431,7 +1695,10 @@ function Rect(props) {
1431
1695
  function Text(props) {
1432
1696
  return createElement("Text", props);
1433
1697
  }
1698
+ function Image2(props) {
1699
+ return createElement("Image", props);
1700
+ }
1434
1701
 
1435
- export { Canvas, Rect, Text, View };
1702
+ export { Canvas, Image2 as Image, Rect, Text, View };
1436
1703
  //# sourceMappingURL=index.js.map
1437
1704
  //# sourceMappingURL=index.js.map