gpucanvas 0.4.2 → 0.4.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.
@@ -7,6 +7,11 @@ var worldToScreen = (camera, point) => ({
7
7
  x: (point.x - camera.x) * camera.zoom,
8
8
  y: (point.y - camera.y) * camera.zoom
9
9
  });
10
+ var rectToScreen = (camera, rect) => ({
11
+ ...worldToScreen(camera, rect),
12
+ width: rect.width * camera.zoom,
13
+ height: rect.height * camera.zoom
14
+ });
10
15
  var MIN_ZOOM = 0.02;
11
16
  var MAX_ZOOM = 16;
12
17
  var clampZoom = (zoom) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
@@ -24,8 +29,7 @@ function visibleWorldRect(camera, viewportWidth, viewportHeight) {
24
29
  height: viewportHeight / camera.zoom
25
30
  };
26
31
  }
27
- var containsPoint = (rect, point) => point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height;
28
- var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
32
+ var viewOf = (canvas) => visibleWorldRect(canvas.getCamera(), canvas.surface.clientWidth, canvas.surface.clientHeight);
29
33
  function fitCamera(bounds, viewportWidth, viewportHeight, padding = 40) {
30
34
  const zoom = clampZoom(
31
35
  Math.min(
@@ -39,14 +43,61 @@ function fitCamera(bounds, viewportWidth, viewportHeight, padding = 40) {
39
43
  zoom
40
44
  };
41
45
  }
46
+
47
+ // src/elements.ts
48
+ function rectInNode(node, nodeRect, element) {
49
+ if (element === node) return { ...nodeRect };
50
+ const outer = node.getBoundingClientRect();
51
+ const inner = element.getBoundingClientRect();
52
+ const scale = outer.width > 0 ? nodeRect.width / outer.width : 1;
53
+ return {
54
+ x: nodeRect.x + (inner.left - outer.left) * scale,
55
+ y: nodeRect.y + (inner.top - outer.top) * scale,
56
+ width: inner.width * scale,
57
+ height: inner.height * scale
58
+ };
59
+ }
60
+ function elementInNode(node, nodeRect, point) {
61
+ const outer = node.getBoundingClientRect();
62
+ const scale = nodeRect.width > 0 ? outer.width / nodeRect.width : 1;
63
+ const x = outer.left + (point.x - nodeRect.x) * scale;
64
+ const y = outer.top + (point.y - nodeRect.y) * scale;
65
+ let hit = node;
66
+ descend: for (; ; ) {
67
+ for (let child = hit.lastElementChild; child; child = child.previousElementSibling) {
68
+ const box = child.getBoundingClientRect();
69
+ if (box.width > 0 && box.height > 0 && x >= box.left && x < box.right && y >= box.top && y < box.bottom) {
70
+ hit = child;
71
+ continue descend;
72
+ }
73
+ }
74
+ return hit;
75
+ }
76
+ }
77
+ function nodeOfElement(nodes, element) {
78
+ for (let at = element; at; at = at.parentElement) {
79
+ if (nodes.has(at)) return at;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // src/input.ts
85
+ var markSurface = (element) => {
86
+ element.dataset.gpucanvasSurface = "";
87
+ };
88
+ var isSurface = (target) => target instanceof Element && target.hasAttribute("data-gpucanvas-surface");
89
+ var pointIn = (element, event) => {
90
+ const bounds = element.getBoundingClientRect();
91
+ return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
92
+ };
42
93
  function bindCameraInput(element, getCamera, onChange) {
43
94
  const onWheel = (event) => {
44
95
  event.preventDefault();
45
- const bounds = element.getBoundingClientRect();
46
96
  const camera = getCamera();
47
97
  if (event.ctrlKey || event.metaKey) {
48
98
  const factor = Math.exp(-event.deltaY * (event.deltaMode === 1 ? 0.05 : 0.01));
49
- onChange(zoomAt(camera, event.clientX - bounds.left, event.clientY - bounds.top, factor));
99
+ const at = pointIn(element, event);
100
+ onChange(zoomAt(camera, at.x, at.y, factor));
50
101
  } else {
51
102
  onChange({
52
103
  ...camera,
@@ -76,8 +127,7 @@ function bindCameraInput(element, getCamera, onChange) {
76
127
  }
77
128
  }
78
129
  if (event.button !== 0 && event.button !== 1 || event.shiftKey) return;
79
- if (event.target !== element && !event.target.hasAttribute?.("data-gpucanvas-surface"))
80
- return;
130
+ if (!isSurface(event.target)) return;
81
131
  drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
82
132
  element.setPointerCapture(event.pointerId);
83
133
  };
@@ -89,13 +139,8 @@ function bindCameraInput(element, getCamera, onChange) {
89
139
  touch.y = event.clientY;
90
140
  const after = pinch();
91
141
  if (!before.distance) return;
92
- const bounds = element.getBoundingClientRect();
93
- const zoomed = zoomAt(
94
- getCamera(),
95
- before.x - bounds.left,
96
- before.y - bounds.top,
97
- after.distance / before.distance
98
- );
142
+ const at = pointIn(element, { clientX: before.x, clientY: before.y });
143
+ const zoomed = zoomAt(getCamera(), at.x, at.y, after.distance / before.distance);
99
144
  onChange({
100
145
  ...zoomed,
101
146
  x: zoomed.x - (after.x - before.x) / zoomed.zoom,
@@ -135,6 +180,113 @@ function bindCameraInput(element, getCamera, onChange) {
135
180
  };
136
181
  }
137
182
 
183
+ // src/rect.ts
184
+ var center = (rect) => ({
185
+ x: rect.x + rect.width / 2,
186
+ y: rect.y + rect.height / 2
187
+ });
188
+ var containsPoint = (rect, point) => point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height;
189
+ var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
190
+ var intersection = (a, b) => {
191
+ const x = Math.max(a.x, b.x);
192
+ const y = Math.max(a.y, b.y);
193
+ const right = Math.min(a.x + a.width, b.x + b.width);
194
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
195
+ return right > x && bottom > y ? { x, y, width: right - x, height: bottom - y } : null;
196
+ };
197
+ var contains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
198
+ function unionOf(rects) {
199
+ let only = null;
200
+ let count = 0;
201
+ let minX = Infinity;
202
+ let minY = Infinity;
203
+ let maxX = -Infinity;
204
+ let maxY = -Infinity;
205
+ for (const rect of rects) {
206
+ if (!rect) continue;
207
+ only = rect;
208
+ count++;
209
+ minX = Math.min(minX, rect.x);
210
+ minY = Math.min(minY, rect.y);
211
+ maxX = Math.max(maxX, rect.x + rect.width);
212
+ maxY = Math.max(maxY, rect.y + rect.height);
213
+ }
214
+ if (count < 2) return only;
215
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
216
+ }
217
+ var sameRect = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
218
+
219
+ // src/types.ts
220
+ var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
221
+ var DEFAULT_SELECTION_COLOR = "#4f8cff";
222
+ var SELECTION_WIDTH = 2;
223
+ var HOVER_WIDTH = 1;
224
+
225
+ // src/base.ts
226
+ var styleNode = (element) => Object.assign(element.style, { boxSizing: "border-box", contain: "strict", overflow: "hidden" });
227
+ function createBase(host, options, onCamera) {
228
+ const surface = document.createElement("div");
229
+ markSurface(surface);
230
+ Object.assign(surface.style, {
231
+ position: "relative",
232
+ width: "100%",
233
+ height: "100%",
234
+ overflow: "hidden",
235
+ touchAction: "none"
236
+ });
237
+ host.append(surface);
238
+ const nodes = /* @__PURE__ */ new Map();
239
+ let camera = options.camera ?? DEFAULT_CAMERA;
240
+ const setCamera = (next) => {
241
+ camera = next;
242
+ onCamera(camera);
243
+ options.onCameraChange?.(camera);
244
+ };
245
+ const unbindInput = bindCameraInput(surface, () => camera, setCamera);
246
+ const nodeAt = (point) => {
247
+ let top = null;
248
+ for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
249
+ return top;
250
+ };
251
+ const api = {
252
+ surface,
253
+ getNodeRect: (element) => {
254
+ const node = nodes.get(element);
255
+ return node ? { ...node.rect } : null;
256
+ },
257
+ getNodesIn(rect) {
258
+ const hits = [];
259
+ for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
260
+ return hits;
261
+ },
262
+ getNodeOf: (element) => nodeOfElement(nodes, element),
263
+ getElementAt(point) {
264
+ const top = nodeAt(point);
265
+ return top ? elementInNode(top.element, top.rect, point) : null;
266
+ },
267
+ getCamera: () => camera,
268
+ setCamera,
269
+ screenToWorld: (point) => screenToWorld(camera, point),
270
+ worldToScreen: (point) => worldToScreen(camera, point),
271
+ fit(padding) {
272
+ const bounds = unionOf([...nodes.values()].map((node) => node.rect));
273
+ if (bounds) setCamera(fitCamera(bounds, surface.clientWidth, surface.clientHeight, padding));
274
+ }
275
+ };
276
+ return {
277
+ surface,
278
+ nodes,
279
+ camera: () => camera,
280
+ view: () => viewOf(api),
281
+ nodeAt,
282
+ api,
283
+ destroy() {
284
+ unbindInput();
285
+ surface.remove();
286
+ }
287
+ };
288
+ }
289
+
138
290
  // src/effects.ts
139
291
  var defineEffect = (definition) => Object.freeze({
140
292
  animated: false,
@@ -518,84 +670,20 @@ var EFFECTS = {
518
670
  grayscale
519
671
  };
520
672
 
521
- // src/elements.ts
522
- function rectInNode(node, nodeRect, element) {
523
- if (element === node) return { ...nodeRect };
524
- const outer = node.getBoundingClientRect();
525
- const inner = element.getBoundingClientRect();
526
- const scale = outer.width > 0 ? nodeRect.width / outer.width : 1;
527
- return {
528
- x: nodeRect.x + (inner.left - outer.left) * scale,
529
- y: nodeRect.y + (inner.top - outer.top) * scale,
530
- width: inner.width * scale,
531
- height: inner.height * scale
532
- };
533
- }
534
- function elementInNode(node, nodeRect, point) {
535
- const outer = node.getBoundingClientRect();
536
- const scale = nodeRect.width > 0 ? outer.width / nodeRect.width : 1;
537
- const x = outer.left + (point.x - nodeRect.x) * scale;
538
- const y = outer.top + (point.y - nodeRect.y) * scale;
539
- let hit = node;
540
- descend: for (; ; ) {
541
- for (let child = hit.lastElementChild; child; child = child.previousElementSibling) {
542
- const box = child.getBoundingClientRect();
543
- if (box.width > 0 && box.height > 0 && x >= box.left && x < box.right && y >= box.top && y < box.bottom) {
544
- hit = child;
545
- continue descend;
546
- }
547
- }
548
- return hit;
549
- }
550
- }
551
- function nodeOfElement(nodes, element) {
552
- for (let at = element; at; at = at.parentElement) {
553
- if (nodes.has(at)) return at;
554
- }
555
- return null;
556
- }
557
-
558
- // src/node-style.ts
559
- function applyNodeSize(element, rect) {
560
- element.style.width = `${rect.width}px`;
561
- element.style.height = `${rect.height}px`;
562
- }
563
- function applyNodeBase(element) {
564
- element.style.boxSizing = "border-box";
565
- element.style.contain = "strict";
566
- element.style.overflow = "hidden";
567
- }
568
- function boundsOf(rects) {
569
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
570
- for (const rect of rects) {
571
- minX = Math.min(minX, rect.x);
572
- minY = Math.min(minY, rect.y);
573
- maxX = Math.max(maxX, rect.x + rect.width);
574
- maxY = Math.max(maxY, rect.y + rect.height);
575
- }
576
- return minX === Infinity ? null : { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
577
- }
578
-
579
- // src/types.ts
580
- var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
581
- var DEFAULT_SELECTION_COLOR = "#4f8cff";
582
- var SELECTION_WIDTH = 2;
583
- var HOVER_WIDTH = 1;
584
-
585
673
  // src/dom-backend.ts
586
674
  function createDomCanvas(host, options) {
587
- const surface = document.createElement("div");
588
- surface.dataset.gpucanvasSurface = "";
589
- Object.assign(surface.style, {
590
- position: "relative",
591
- width: "100%",
592
- height: "100%",
593
- overflow: "hidden",
594
- touchAction: "none",
595
- background: options.background ?? "transparent"
596
- });
675
+ let frameMs = 0;
676
+ const apply = (camera) => {
677
+ const start = performance.now();
678
+ world.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
679
+ world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
680
+ frameMs = performance.now() - start;
681
+ };
682
+ const base = createBase(host, options, apply);
683
+ const { surface, nodes } = base;
684
+ surface.style.background = options.background ?? "transparent";
597
685
  const world = document.createElement("div");
598
- world.dataset.gpucanvasSurface = "";
686
+ markSurface(world);
599
687
  Object.assign(world.style, {
600
688
  position: "absolute",
601
689
  left: "0",
@@ -607,8 +695,6 @@ function createDomCanvas(host, options) {
607
695
  Object.assign(overlay.style, { position: "absolute", left: "0", top: "0", zIndex: "1" });
608
696
  world.append(overlay);
609
697
  surface.append(world);
610
- host.append(surface);
611
- const nodes = /* @__PURE__ */ new Map();
612
698
  const ring = (color, width) => `calc(${width}px / var(--gpucanvas-zoom)) solid ${color}`;
613
699
  const selectionColor = options.selectionColor ?? DEFAULT_SELECTION_COLOR;
614
700
  const selectionRing = ring(selectionColor, SELECTION_WIDTH);
@@ -619,7 +705,7 @@ function createDomCanvas(host, options) {
619
705
  const boxes = /* @__PURE__ */ new Map();
620
706
  const elementRect = (element) => {
621
707
  const node = element.isConnected ? nodeOfElement(nodes, element) : null;
622
- return node ? rectInNode(node, nodes.get(node), element) : null;
708
+ return node ? rectInNode(node, nodes.get(node).rect, element) : null;
623
709
  };
624
710
  let boxFrame = 0;
625
711
  const placeBoxes = () => {
@@ -693,55 +779,33 @@ function createDomCanvas(host, options) {
693
779
  element.animate(keyframes, { duration: period * 1e3, iterations: Infinity })
694
780
  );
695
781
  };
696
- let camera = options.camera ?? DEFAULT_CAMERA;
697
- let frameMs = 0;
698
- const apply = () => {
699
- const start = performance.now();
700
- world.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
701
- world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
702
- frameMs = performance.now() - start;
782
+ const updateNode = (element, rect) => {
783
+ nodes.set(element, { element, rect });
784
+ element.style.left = `${rect.x}px`;
785
+ element.style.top = `${rect.y}px`;
786
+ element.style.width = `${rect.width}px`;
787
+ element.style.height = `${rect.height}px`;
703
788
  };
704
- const setCamera = (next) => {
705
- camera = next;
706
- apply();
707
- options.onCameraChange?.(camera);
708
- };
709
- const unbindInput = bindCameraInput(surface, () => camera, setCamera);
710
- apply();
789
+ apply(base.camera());
711
790
  return {
791
+ ...base.api,
712
792
  mode: "dom",
713
- surface,
714
793
  addNode(element, rect) {
715
- applyNodeBase(element);
794
+ styleNode(element);
716
795
  element.style.position = "absolute";
717
- nodes.set(element, rect);
718
- this.updateNode(element, rect);
796
+ updateNode(element, rect);
719
797
  world.append(element);
720
798
  },
721
- updateNode(element, rect) {
722
- nodes.set(element, rect);
723
- element.style.left = `${rect.x}px`;
724
- element.style.top = `${rect.y}px`;
725
- applyNodeSize(element, rect);
726
- },
799
+ updateNode,
727
800
  removeNode(element) {
728
801
  clearEffect(element);
729
802
  nodes.delete(element);
803
+ selection.delete(element);
730
804
  outlinedNodes.delete(element);
731
805
  element.style.outline = "";
732
806
  element.remove();
733
807
  },
734
- getNodeRect: (element) => nodes.get(element) ?? null,
735
- getNodesIn(rect) {
736
- return [...nodes].filter(([, node]) => intersects(node, rect)).map(([element]) => element);
737
- },
738
- getNodeOf: (element) => nodeOfElement(nodes, element),
739
808
  getElementRect: elementRect,
740
- getElementAt(point) {
741
- let top = null;
742
- for (const [element, rect] of nodes) if (containsPoint(rect, point)) top = element;
743
- return top ? elementInNode(top, nodes.get(top), point) : null;
744
- },
745
809
  setSelection(elements) {
746
810
  selection = new Set(elements);
747
811
  drawOutlines();
@@ -754,10 +818,6 @@ function createDomCanvas(host, options) {
754
818
  setEffect(element, effect, params) {
755
819
  if (nodes.has(element)) applyEffect(element, effect, params);
756
820
  },
757
- getCamera: () => camera,
758
- setCamera,
759
- screenToWorld: (point) => screenToWorld(camera, point),
760
- worldToScreen: (point) => worldToScreen(camera, point),
761
821
  // The browser repaints DOM nodes itself.
762
822
  invalidate() {
763
823
  },
@@ -765,14 +825,10 @@ function createDomCanvas(host, options) {
765
825
  setBackground(color) {
766
826
  surface.style.background = color;
767
827
  },
768
- fit(padding) {
769
- const bounds = boundsOf(nodes.values());
770
- if (bounds) setCamera(fitCamera(bounds, surface.clientWidth, surface.clientHeight, padding));
771
- },
772
828
  getStats() {
773
- const view = visibleWorldRect(camera, surface.clientWidth, surface.clientHeight);
829
+ const view = base.view();
774
830
  let visible = 0;
775
- for (const rect of nodes.values()) if (intersects(rect, view)) visible++;
831
+ for (const node of nodes.values()) if (intersects(node.rect, view)) visible++;
776
832
  return {
777
833
  mode: "dom",
778
834
  nodes: nodes.size,
@@ -786,17 +842,52 @@ function createDomCanvas(host, options) {
786
842
  },
787
843
  destroy() {
788
844
  cancelAnimationFrame(boxFrame);
789
- unbindInput();
790
- surface.remove();
845
+ base.destroy();
791
846
  }
792
847
  };
793
848
  }
794
849
 
850
+ // src/shelves.ts
851
+ var Shelves = class {
852
+ constructor(width, height, tolerance) {
853
+ this.width = width;
854
+ this.height = height;
855
+ this.tolerance = tolerance;
856
+ }
857
+ width;
858
+ height;
859
+ tolerance;
860
+ shelves = [];
861
+ /** Height taken by rows so far. */
862
+ filled = 0;
863
+ /** Top left corner of a free `w` x `h` box, or null when it does not fit. */
864
+ place(w, h) {
865
+ if (w > this.width || h > this.height) return null;
866
+ let shelf = this.shelves.find(
867
+ (candidate) => candidate.height >= h && candidate.height <= h * this.tolerance && candidate.x + w <= this.width
868
+ );
869
+ if (!shelf) {
870
+ if (this.filled + h > this.height) return null;
871
+ shelf = { x: 0, y: this.filled, height: h };
872
+ this.shelves.push(shelf);
873
+ this.filled += h;
874
+ }
875
+ const at = { x: shelf.x, y: shelf.y };
876
+ shelf.x += w;
877
+ return at;
878
+ }
879
+ clear() {
880
+ this.shelves = [];
881
+ this.filled = 0;
882
+ }
883
+ };
884
+
795
885
  // src/atlas.ts
796
886
  var PADDING = 1;
797
887
  var Atlas = class {
798
- constructor(device, size, layerCount) {
888
+ constructor(device, size, layerCount, keep) {
799
889
  this.device = device;
890
+ this.keep = keep;
800
891
  this.size = size;
801
892
  this.texture = device.createTexture({
802
893
  label: "gpucanvas atlas",
@@ -806,8 +897,7 @@ var Atlas = class {
806
897
  });
807
898
  this.view = this.texture.createView({ dimension: "2d-array" });
808
899
  this.layers = Array.from({ length: layerCount }, () => ({
809
- shelves: [],
810
- nextY: 0,
900
+ shelves: new Shelves(size, size, 1.5),
811
901
  generation: 0,
812
902
  lastUsed: -1,
813
903
  lastAllocated: -1
@@ -815,25 +905,21 @@ var Atlas = class {
815
905
  this.spare = layerCount - 1;
816
906
  }
817
907
  device;
908
+ keep;
818
909
  texture;
819
910
  view;
820
911
  size;
821
- /** Slots to rescue when their layer is recycled, most important first. */
822
- keep = () => [];
823
912
  layers;
824
913
  spare;
825
914
  frame = 0;
826
915
  /** The layer `allocatePinned` fills; it is never recycled. */
827
916
  pinned = 0;
828
- get layerCount() {
829
- return this.layers.length;
830
- }
831
917
  get usedLayers() {
832
- return this.layers.filter((layer, index) => index !== this.spare && layer.nextY > 0).length;
918
+ return this.layers.filter((layer, index) => index !== this.spare && layer.shelves.filled > 0).length;
833
919
  }
834
920
  /** How much of the pinned layer is taken, 0 to 1. */
835
921
  get pinnedFill() {
836
- return this.layers[this.pinned].nextY / this.size;
922
+ return this.layers[this.pinned].shelves.filled / this.size;
837
923
  }
838
924
  nextFrame() {
839
925
  this.frame++;
@@ -909,32 +995,23 @@ var Atlas = class {
909
995
  }
910
996
  allocateIn(index, w, h) {
911
997
  const layer = this.layers[index];
912
- let shelf = layer.shelves.find(
913
- (candidate) => candidate.height >= h && candidate.height <= h * 1.5 && candidate.x + w <= this.size
914
- );
915
- if (!shelf) {
916
- if (layer.nextY + h > this.size) return null;
917
- shelf = { y: layer.nextY, height: h, x: 0 };
918
- layer.shelves.push(shelf);
919
- layer.nextY += h;
920
- }
998
+ const at = layer.shelves.place(w, h);
999
+ if (!at) return null;
921
1000
  const slot = {
922
1001
  layer: index,
923
- x: shelf.x,
924
- y: shelf.y,
1002
+ x: at.x,
1003
+ y: at.y,
925
1004
  width: w - PADDING,
926
1005
  height: h - PADDING,
927
1006
  generation: layer.generation
928
1007
  };
929
- shelf.x += w;
930
1008
  layer.lastUsed = this.frame;
931
1009
  layer.lastAllocated = this.frame;
932
1010
  return slot;
933
1011
  }
934
1012
  evict(index) {
935
1013
  const layer = this.layers[index];
936
- layer.shelves = [];
937
- layer.nextY = 0;
1014
+ layer.shelves.clear();
938
1015
  layer.generation++;
939
1016
  }
940
1017
  destroy() {
@@ -942,7 +1019,55 @@ var Atlas = class {
942
1019
  }
943
1020
  };
944
1021
 
1022
+ // src/capture.ts
1023
+ function captureRegion(device, source, region, width, height) {
1024
+ const texture = device.createTexture({
1025
+ size: { width, height },
1026
+ format: "rgba8unorm",
1027
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
1028
+ });
1029
+ try {
1030
+ device.queue.drawElementImageToTexture(
1031
+ {
1032
+ source,
1033
+ sourceX: region.x,
1034
+ sourceY: region.y,
1035
+ sourceWidth: region.width,
1036
+ sourceHeight: region.height
1037
+ },
1038
+ { texture, size: { width, height }, premultipliedAlpha: false }
1039
+ );
1040
+ } catch (error) {
1041
+ texture.destroy();
1042
+ throw error;
1043
+ }
1044
+ return readTexture(device, texture, width, height);
1045
+ }
1046
+ async function readTexture(device, texture, width, height) {
1047
+ const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
1048
+ const buffer = device.createBuffer({
1049
+ size: bytesPerRow * height,
1050
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
1051
+ });
1052
+ const encoder = device.createCommandEncoder();
1053
+ encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
1054
+ device.queue.submit([encoder.finish()]);
1055
+ try {
1056
+ await buffer.mapAsync(GPUMapMode.READ);
1057
+ const rows = new Uint8Array(buffer.getMappedRange());
1058
+ const pixels = new Uint8ClampedArray(width * height * 4);
1059
+ for (let y = 0; y < height; y++) {
1060
+ pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
1061
+ }
1062
+ return new ImageData(pixels, width, height);
1063
+ } finally {
1064
+ buffer.destroy();
1065
+ texture.destroy();
1066
+ }
1067
+ }
1068
+
945
1069
  // src/composite-shader.ts
1070
+ var KIND = { snapshot: 0, placeholder: 1, ring: 2 };
946
1071
  var prelude = (
947
1072
  /* wgsl */
948
1073
  `
@@ -985,7 +1110,7 @@ struct Varyings {
985
1110
  fn vs(@builtin(vertex_index) vertex: u32, instance: Instance) -> Varyings {
986
1111
  let corner = vec2f(f32(vertex & 1u), f32(vertex >> 1u));
987
1112
  // A ring's quad reaches past its element by the ring's width, an effect's by its padding.
988
- let ring = instance.info.y > 1.5;
1113
+ let ring = instance.info.y > ${KIND.ring - 0.5};
989
1114
  let grow = select(instance.info.z, instance.info.z / view.zoom, ring);
990
1115
  let start = instance.rect.xy - grow;
991
1116
  let end = instance.rect.xy + instance.rect.zw + grow;
@@ -1026,12 +1151,12 @@ fn fs(in: Varyings) -> @location(0) vec4f {
1026
1151
  // Derivatives first: WGSL only allows them in uniform control flow.
1027
1152
  let dx = dpdx(in.texel);
1028
1153
  let dy = dpdy(in.texel);
1029
- if (in.info.y > 1.5) {
1154
+ if (in.info.y > ${KIND.ring - 0.5}) {
1030
1155
  // The ring covers what lies outside the element; the element itself shows through.
1031
1156
  let outside = max(in.bounds.xy - in.texel, in.texel - in.bounds.zw);
1032
1157
  return select(vec4f(0.0), in.color, max(outside.x, outside.y) > 0.0);
1033
1158
  }
1034
- if (in.info.y > 0.5) {
1159
+ if (in.info.y > ${KIND.placeholder - 0.5}) {
1035
1160
  return view.placeholder;
1036
1161
  }
1037
1162
  let layer = i32(in.info.x);
@@ -1108,12 +1233,14 @@ fn fs(in: Varyings) -> @location(0) vec4f {
1108
1233
  // src/gpu-renderer.ts
1109
1234
  var FLOATS_PER_INSTANCE = 16;
1110
1235
  var CompositeRenderer = class {
1111
- constructor(device, context, format, atlas, background, placeholder) {
1236
+ constructor(device, context, format, atlas, background, placeholder, onEffectReady) {
1112
1237
  this.device = device;
1113
1238
  this.context = context;
1239
+ this.format = format;
1114
1240
  this.atlas = atlas;
1115
1241
  this.background = background;
1116
1242
  this.placeholder = placeholder;
1243
+ this.onEffectReady = onEffectReady;
1117
1244
  this.bindGroupLayout = device.createBindGroupLayout({
1118
1245
  entries: [
1119
1246
  { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
@@ -1127,9 +1254,8 @@ var CompositeRenderer = class {
1127
1254
  });
1128
1255
  this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
1129
1256
  this.pipeline = device.createRenderPipeline(
1130
- this.pipelineDescriptor("gpucanvas composite", compositeShader, format)
1257
+ this.pipelineDescriptor("gpucanvas composite", compositeShader)
1131
1258
  );
1132
- this.format = format;
1133
1259
  this.uniforms = device.createBuffer({
1134
1260
  size: this.uniformData.byteLength,
1135
1261
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
@@ -1146,10 +1272,14 @@ var CompositeRenderer = class {
1146
1272
  }
1147
1273
  device;
1148
1274
  context;
1275
+ format;
1149
1276
  atlas;
1150
1277
  background;
1151
1278
  placeholder;
1279
+ onEffectReady;
1152
1280
  instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
1281
+ count = 0;
1282
+ runs = [];
1153
1283
  instanceBuffer;
1154
1284
  uniforms;
1155
1285
  uniformData = new Float32Array(20);
@@ -1159,11 +1289,8 @@ var CompositeRenderer = class {
1159
1289
  sampler;
1160
1290
  /** Effect pipelines, null while one compiles. Compiling never blocks a frame. */
1161
1291
  effects = /* @__PURE__ */ new Map();
1162
- /** Called when an effect pipeline is ready, so the caller can draw it. */
1163
- onEffectReady = () => {
1164
- };
1165
- format;
1166
- pipelineDescriptor(label, code, format) {
1292
+ failed = /* @__PURE__ */ new Set();
1293
+ pipelineDescriptor(label, code) {
1167
1294
  const module = this.device.createShaderModule({ label, code });
1168
1295
  return {
1169
1296
  label,
@@ -1189,7 +1316,7 @@ var CompositeRenderer = class {
1189
1316
  entryPoint: "fs",
1190
1317
  targets: [
1191
1318
  {
1192
- format,
1319
+ format: this.format,
1193
1320
  // Snapshots are requested premultiplied.
1194
1321
  blend: {
1195
1322
  color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
@@ -1206,27 +1333,82 @@ var CompositeRenderer = class {
1206
1333
  if (this.effects.has(effect)) return this.effects.get(effect) !== null;
1207
1334
  this.effects.set(effect, null);
1208
1335
  this.device.createRenderPipelineAsync(
1209
- this.pipelineDescriptor(
1210
- `gpucanvas effect ${effect.name}`,
1211
- effectShader(effect.wgsl),
1212
- this.format
1213
- )
1336
+ this.pipelineDescriptor(`gpucanvas effect ${effect.name}`, effectShader(effect.wgsl))
1214
1337
  ).then((pipeline) => {
1215
1338
  this.effects.set(effect, pipeline);
1216
1339
  this.onEffectReady();
1217
- }).catch((error) => console.error(`[gpucanvas] effect "${effect.name}" failed`, error));
1340
+ }).catch((error) => {
1341
+ this.failed.add(effect);
1342
+ console.error(`[gpucanvas] effect "${effect.name}" failed`, error);
1343
+ });
1218
1344
  return false;
1219
1345
  }
1220
- /** Grows the CPU and GPU instance storage to hold at least `count` instances. */
1221
- reserve(count) {
1222
- const floats = count * FLOATS_PER_INSTANCE;
1223
- if (this.instances.length >= floats) return;
1224
- this.instances = new Float32Array(Math.max(floats, this.instances.length * 2));
1225
- this.instanceBuffer.destroy();
1226
- this.instanceBuffer = this.createInstanceBuffer();
1346
+ /** Whether `effect`'s pipeline failed to compile; it never draws then. */
1347
+ effectFailed(effect) {
1348
+ return this.failed.has(effect);
1349
+ }
1350
+ /** Starts a frame with room for at least `capacity` quads. */
1351
+ begin(capacity) {
1352
+ const floats = capacity * FLOATS_PER_INSTANCE;
1353
+ if (this.instances.length < floats) {
1354
+ this.instances = new Float32Array(Math.max(floats, this.instances.length * 2));
1355
+ this.instanceBuffer.destroy();
1356
+ this.instanceBuffer = this.createInstanceBuffer();
1357
+ }
1358
+ this.count = 0;
1359
+ this.runs.length = 0;
1227
1360
  }
1228
- draw(camera, cssWidth, cssHeight, dpr, count, runs, effects) {
1229
- const { device } = this;
1361
+ /** Room for the next quad, in the run of `effect`; returns its first float. */
1362
+ next(effect) {
1363
+ const last = this.runs.at(-1);
1364
+ if (last && last.effect === effect) last.count++;
1365
+ else this.runs.push({ effect, start: this.count, count: 1 });
1366
+ return this.count++ * FLOATS_PER_INSTANCE;
1367
+ }
1368
+ writeRect(offset, rect) {
1369
+ const data = this.instances;
1370
+ data[offset] = rect.x;
1371
+ data[offset + 1] = rect.y;
1372
+ data[offset + 2] = rect.width;
1373
+ data[offset + 3] = rect.height;
1374
+ }
1375
+ writeSlot(offset, slot) {
1376
+ const data = this.instances;
1377
+ this.atlas.touch(slot.layer);
1378
+ data[offset + 4] = slot.x;
1379
+ data[offset + 5] = slot.y;
1380
+ data[offset + 6] = slot.x + slot.width;
1381
+ data[offset + 7] = slot.y + slot.height;
1382
+ data[offset + 8] = slot.layer;
1383
+ data[offset + 9] = KIND.snapshot;
1384
+ }
1385
+ /** `rect` showing the snapshot in `slot`, or the placeholder color while there is none. */
1386
+ quad(rect, slot) {
1387
+ const offset = this.next(null);
1388
+ this.writeRect(offset, rect);
1389
+ if (slot) this.writeSlot(offset, slot);
1390
+ else this.instances[offset + 9] = KIND.placeholder;
1391
+ this.instances[offset + 10] = 0;
1392
+ }
1393
+ /** `rect` drawn through `effect` from the snapshot in `slot`, reaching `pad` past its edges. */
1394
+ effectQuad(rect, slot, effect, pad, params) {
1395
+ const offset = this.next(effect);
1396
+ this.writeRect(offset, rect);
1397
+ this.writeSlot(offset, slot);
1398
+ this.instances[offset + 10] = pad;
1399
+ this.instances.set(params, offset + 12);
1400
+ }
1401
+ /** An outline `width` CSS pixels wide just outside `rect`, in a premultiplied color. */
1402
+ ring(rect, color, width) {
1403
+ const offset = this.next(null);
1404
+ this.writeRect(offset, rect);
1405
+ this.instances.set(color, offset + 4);
1406
+ this.instances[offset + 9] = KIND.ring;
1407
+ this.instances[offset + 10] = width;
1408
+ }
1409
+ /** Draws the quads listed since `begin`. */
1410
+ draw(camera, cssWidth, cssHeight, dpr, effects) {
1411
+ const { device, count, runs } = this;
1230
1412
  this.uniformData.set([
1231
1413
  cssWidth,
1232
1414
  cssHeight,
@@ -1269,7 +1451,7 @@ var CompositeRenderer = class {
1269
1451
  pass.setBindGroup(0, this.bindGroup);
1270
1452
  pass.setVertexBuffer(0, this.instanceBuffer);
1271
1453
  let current = null;
1272
- for (const run of runs.length ? runs : [{ effect: null, start: 0, count }]) {
1454
+ for (const run of runs) {
1273
1455
  const pipeline = run.effect && this.effects.get(run.effect) || this.pipeline;
1274
1456
  if (pipeline !== current) pass.setPipeline(current = pipeline);
1275
1457
  pass.draw(4, run.count, 0, run.start);
@@ -1297,21 +1479,17 @@ var Page = class {
1297
1479
  element;
1298
1480
  width;
1299
1481
  height;
1300
- /** Nodes currently placed on this page. */
1301
- count = 0;
1302
1482
  /** Has the `drawable` attribute and is rendered; parked pages use `content-visibility: hidden`. */
1303
1483
  drawable = false;
1304
1484
  /** Frame in which the page became drawable. Its snapshot exists from that frame's paint on. */
1305
1485
  promotedFrame = -1;
1306
- /** Kept drawable for the live (hit-testable) or focused node. */
1307
- pinned = false;
1308
1486
  /** Registered for hit testing with `updateElementGeometry`. */
1309
1487
  registered = false;
1310
- shelves = [];
1311
- nextY = 0;
1488
+ shelves;
1312
1489
  constructor(width, height) {
1313
1490
  this.width = width;
1314
1491
  this.height = height;
1492
+ this.shelves = new Shelves(width + GAP, height + GAP, 1.25);
1315
1493
  this.element = document.createElement("div");
1316
1494
  this.element.dataset.gpucanvasPage = "";
1317
1495
  Object.assign(this.element.style, {
@@ -1330,25 +1508,8 @@ var Page = class {
1330
1508
  }
1331
1509
  /** Reserves a spot for a `width` x `height` node, or returns null when the page is full. */
1332
1510
  place(width, height) {
1333
- const w = Math.ceil(width) + GAP;
1334
- const h = Math.ceil(height) + GAP;
1335
- if (w > this.width + GAP || h > this.height + GAP) return null;
1336
- let shelf = this.shelves.find(
1337
- (candidate) => candidate.height >= h && candidate.height <= h * 1.25 && candidate.x + w <= this.width + GAP
1338
- );
1339
- if (!shelf) {
1340
- if (this.nextY + h > this.height + GAP) return null;
1341
- shelf = { y: this.nextY, height: h, x: 0 };
1342
- this.shelves.push(shelf);
1343
- this.nextY += h;
1344
- }
1345
- const slot = { page: this, x: shelf.x, y: shelf.y };
1346
- shelf.x += w;
1347
- this.count++;
1348
- return slot;
1349
- }
1350
- release() {
1351
- this.count--;
1511
+ const at = this.shelves.place(Math.ceil(width) + GAP, Math.ceil(height) + GAP);
1512
+ return at && { page: this, ...at };
1352
1513
  }
1353
1514
  setDrawable(drawable, frame) {
1354
1515
  if (this.drawable === drawable) return;
@@ -1374,8 +1535,6 @@ var TILE_PIXELS = 512;
1374
1535
  var CONTENT_SETTLE_MS = 150;
1375
1536
  var SMALL_NODE_PX = 200;
1376
1537
  var SMALL_REFRESH_MS = 250;
1377
- var right = (rect) => rect.x + rect.width;
1378
- var bottom = (rect) => rect.y + rect.height;
1379
1538
  var SETTLE_MS = 120;
1380
1539
  var USE_EVENTS = ["click", "input", "change", "keydown"];
1381
1540
  var USED_MS = 1e3;
@@ -1387,10 +1546,12 @@ var THUMBNAIL_REFRESH_MS = 1e3;
1387
1546
  var MAX_PROMOTIONS_PER_FRAME = 8;
1388
1547
  var SLOW_FRAME_MS = 22;
1389
1548
  var FAST_FRAME_MS = 12;
1549
+ var MAX_CAPTURE_TRIES = 20;
1390
1550
  var MISSING = 0;
1391
1551
  var DIRTY = 1;
1392
1552
  var BLURRY = 2;
1393
1553
  var OVERSAMPLED = 3;
1554
+ var CANVAS_TRANSFORM_HIT_TESTS = typeof HTMLCanvasElement !== "undefined" && "getElementImageDefaultSize" in HTMLCanvasElement.prototype;
1394
1555
  function supportsGpuCanvas() {
1395
1556
  return typeof navigator !== "undefined" && "gpu" in navigator && typeof HTMLCanvasElement !== "undefined" && "requestPaint" in HTMLCanvasElement.prototype && "getElementTransform" in HTMLCanvasElement.prototype && typeof GPUQueue !== "undefined" && "drawElementImageToTexture" in GPUQueue.prototype;
1396
1557
  }
@@ -1398,14 +1559,6 @@ function rasterScaleFor(deviceScale) {
1398
1559
  const scale = 2 ** Math.ceil(Math.log2(deviceScale) - 0.14);
1399
1560
  return Math.min(MAX_DETAIL_SCALE, Math.max(MIN_RASTER_SCALE, scale));
1400
1561
  }
1401
- var intersection = (a, b) => {
1402
- const x = Math.max(a.x, b.x);
1403
- const y = Math.max(a.y, b.y);
1404
- const right2 = Math.min(a.x + a.width, b.x + b.width);
1405
- const bottom2 = Math.min(a.y + a.height, b.y + b.height);
1406
- return right2 > x && bottom2 > y ? { x, y, width: right2 - x, height: bottom2 - y } : null;
1407
- };
1408
- var contains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
1409
1562
  function cssColorToRgba(color) {
1410
1563
  const context = document.createElement("canvas").getContext("2d");
1411
1564
  context.fillStyle = color;
@@ -1414,37 +1567,58 @@ function cssColorToRgba(color) {
1414
1567
  return [r / 255, g / 255, b / 255, a / 255];
1415
1568
  }
1416
1569
  var premultiplied = ([r, g, b, a]) => [r * a, g * a, b * a, a];
1417
- var distanceToCenter = (rect, view) => Math.hypot(
1418
- rect.x + rect.width / 2 - (view.x + view.width / 2),
1419
- rect.y + rect.height / 2 - (view.y + view.height / 2)
1420
- );
1570
+ var distanceToCenter = (rect, view) => {
1571
+ const a = center(rect);
1572
+ const b = center(view);
1573
+ return Math.hypot(a.x - b.x, a.y - b.y);
1574
+ };
1421
1575
  async function createGpuCanvas(host, options) {
1422
1576
  const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
1423
1577
  if (!adapter) throw new Error("gpucanvas: no WebGPU adapter");
1424
1578
  const device = await adapter.requestDevice();
1425
- const surface = document.createElement("div");
1426
- surface.dataset.gpucanvasSurface = "";
1427
- Object.assign(surface.style, {
1428
- position: "relative",
1429
- width: "100%",
1430
- height: "100%",
1431
- overflow: "hidden",
1432
- touchAction: "none"
1433
- });
1434
1579
  const canvas = document.createElement("canvas");
1435
1580
  canvas.setAttribute("content", "drawable");
1436
- canvas.dataset.gpucanvasSurface = "";
1581
+ markSurface(canvas);
1437
1582
  Object.assign(canvas.style, { display: "block", width: "100%", height: "100%" });
1438
- surface.append(canvas);
1439
- host.append(surface);
1440
1583
  const context = canvas.getContext("webgpu");
1441
1584
  const format = navigator.gpu.getPreferredCanvasFormat();
1442
- context.configure({ device, format, alphaMode: "opaque" });
1585
+ try {
1586
+ if (!context) throw new Error("gpucanvas: no WebGPU canvas context");
1587
+ context.configure({ device, format, alphaMode: "opaque" });
1588
+ } catch (error) {
1589
+ device.destroy();
1590
+ throw error;
1591
+ }
1592
+ const base = createBase(host, options, () => {
1593
+ lastCameraChange = performance.now();
1594
+ setLive(null);
1595
+ clearTimeout(settleTimer);
1596
+ settleTimer = window.setTimeout(() => {
1597
+ pickLive();
1598
+ schedule();
1599
+ }, SETTLE_MS);
1600
+ schedule();
1601
+ });
1602
+ const { surface, nodes } = base;
1603
+ surface.append(canvas);
1443
1604
  const atlasSize = Math.min(options.atlasSize ?? 4096, device.limits.maxTextureDimension2D);
1444
1605
  const atlas = new Atlas(
1445
1606
  device,
1446
1607
  atlasSize,
1447
- Math.min(options.atlasLayers ?? 5, device.limits.maxTextureArrayLayers)
1608
+ Math.min(options.atlasLayers ?? 5, device.limits.maxTextureArrayLayers),
1609
+ function* () {
1610
+ const onScreen = (tile) => intersects(tile.rect, frameView);
1611
+ for (const node of visible) yield node.slot;
1612
+ for (const node of visible) {
1613
+ for (const tile of node.tiles) {
1614
+ if (tile.scale === detailScale && !tile.stale && onScreen(tile)) yield tile.slot;
1615
+ }
1616
+ }
1617
+ for (const node of visible) {
1618
+ yield node.preview;
1619
+ for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
1620
+ }
1621
+ }
1448
1622
  );
1449
1623
  const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, atlasSize);
1450
1624
  const maxRasterCalls = options.maxRastersPerFrame ?? 12;
@@ -1464,18 +1638,16 @@ async function createGpuCanvas(host, options) {
1464
1638
  context,
1465
1639
  format,
1466
1640
  atlas,
1467
- ...colorsFor(options.background ?? "#f4f4f5")
1641
+ ...colorsFor(options.background ?? "#f4f4f5"),
1642
+ () => schedule()
1468
1643
  );
1469
1644
  const selectionColor = premultiplied(
1470
1645
  cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
1471
1646
  );
1472
1647
  const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
1473
- const nodes = /* @__PURE__ */ new Map();
1474
1648
  const effects = /* @__PURE__ */ new Map();
1475
1649
  const startedAt = performance.now();
1476
1650
  const FAR_AWAY = { x: -1e7, y: -1e7 };
1477
- renderer.onEffectReady = () => schedule();
1478
- const runs = [];
1479
1651
  let effectsMoving = false;
1480
1652
  let selection = /* @__PURE__ */ new Set();
1481
1653
  let hovered = null;
@@ -1491,22 +1663,8 @@ async function createGpuCanvas(host, options) {
1491
1663
  return rect;
1492
1664
  };
1493
1665
  const pages = /* @__PURE__ */ new Map();
1494
- atlas.keep = function* () {
1495
- const onScreen = (tile) => intersects(tile.rect, frameView);
1496
- for (const node of visible) yield node.slot;
1497
- for (const node of visible) {
1498
- for (const tile of node.tiles) {
1499
- if (tile.scale === detailScale && !tile.stale && onScreen(tile)) yield tile.slot;
1500
- }
1501
- }
1502
- for (const node of visible) {
1503
- yield node.preview;
1504
- for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
1505
- }
1506
- };
1507
1666
  const pageByElement = /* @__PURE__ */ new Map();
1508
1667
  let openPage = null;
1509
- let camera = options.camera ?? DEFAULT_CAMERA;
1510
1668
  let lastCameraChange = -Infinity;
1511
1669
  let frameNumber = 0;
1512
1670
  let promotionsPerFrame = 2;
@@ -1544,6 +1702,7 @@ async function createGpuCanvas(host, options) {
1544
1702
  frame(false);
1545
1703
  });
1546
1704
  };
1705
+ const canvasDpr = () => canvas.width / Math.max(1, canvas.clientWidth);
1547
1706
  const resizeObserver = new ResizeObserver(([entry]) => {
1548
1707
  const size = entry.devicePixelContentBoxSize?.[0];
1549
1708
  canvas.width = Math.max(
@@ -1559,7 +1718,8 @@ async function createGpuCanvas(host, options) {
1559
1718
  resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
1560
1719
  const transitioning = /* @__PURE__ */ new Map();
1561
1720
  const captures = [];
1562
- const isPinned = (page) => captures.some((capture) => capture.node.page === page) || live?.page === page || lingering?.page === page || focused?.page === page || [...transitioning.keys()].some((node) => node.page === page);
1721
+ const heldNodes = () => [live, lingering, focused, ...transitioning.keys()];
1722
+ const isPinned = (page) => captures.some((capture) => capture.node.page === page) || heldNodes().some((node) => node?.page === page);
1563
1723
  let detailScale = 1;
1564
1724
  let frameView = { x: 0, y: 0, width: 0, height: 0 };
1565
1725
  let contentTimer = 0;
@@ -1569,7 +1729,8 @@ async function createGpuCanvas(host, options) {
1569
1729
  };
1570
1730
  let refreshTimer = 0;
1571
1731
  let refreshAt = Infinity;
1572
- const refreshWait = (node, now) => Math.max(node.rect.width, node.rect.height) * camera.zoom < SMALL_NODE_PX ? node.refreshedAt + SMALL_REFRESH_MS - now : 0;
1732
+ const smallOnScreen = (node) => Math.max(node.rect.width, node.rect.height) * base.camera().zoom < SMALL_NODE_PX;
1733
+ const refreshWait = (node, now) => smallOnScreen(node) ? node.refreshedAt + SMALL_REFRESH_MS - now : 0;
1573
1734
  const refreshLater = (wait) => {
1574
1735
  const at = performance.now() + wait;
1575
1736
  if (at >= refreshAt) return;
@@ -1657,8 +1818,8 @@ async function createGpuCanvas(host, options) {
1657
1818
  const size = Math.max(1, Math.floor(TILE_PIXELS / detailScale));
1658
1819
  const jobs = [];
1659
1820
  const first = (edge, origin) => Math.floor((edge - origin) / size);
1660
- for (let row = first(shown.y, node.rect.y); row <= first(bottom(shown) - 1e-6, node.rect.y); row++) {
1661
- for (let column = first(shown.x, node.rect.x); column <= first(right(shown) - 1e-6, node.rect.x); column++) {
1821
+ for (let row = first(shown.y, node.rect.y); row <= first(shown.y + shown.height - 1e-6, node.rect.y); row++) {
1822
+ for (let column = first(shown.x, node.rect.x); column <= first(shown.x + shown.width - 1e-6, node.rect.x); column++) {
1662
1823
  if (current.some((tile) => tile.column === column && tile.row === row)) continue;
1663
1824
  const rect = intersection(node.rect, {
1664
1825
  x: node.rect.x + column * size,
@@ -1712,15 +1873,24 @@ async function createGpuCanvas(host, options) {
1712
1873
  }
1713
1874
  };
1714
1875
  const slotFor = (previous, width, height, thumbnail = false) => atlas.isValid(previous) && atlas.isPinned(previous) === thumbnail && previous.width === width && previous.height === height && Number.isInteger(previous.x) && Number.isInteger(previous.y) ? previous : (thumbnail ? atlas.allocatePinned(width, height) : null) ?? atlas.allocate(width, height);
1876
+ const texels = (length, scale) => Math.max(1, Math.round(length * scale));
1877
+ const nodeRegion = (node) => ({
1878
+ x: node.pageX,
1879
+ y: node.pageY,
1880
+ width: node.rect.width,
1881
+ height: node.rect.height
1882
+ });
1715
1883
  const freshThumb = (node) => atlas.isValid(node.thumb) && !node.thumbStale;
1884
+ const setThumb = (node, slot, now) => {
1885
+ node.thumb = slot;
1886
+ node.thumbStale = false;
1887
+ node.thumbAt = now;
1888
+ };
1716
1889
  const drawn = (node, slot, scale, now) => {
1717
1890
  node.slot = slot;
1718
1891
  node.slotScale = scale;
1719
- if (scale === THUMBNAIL_SCALE) {
1720
- node.thumb = slot;
1721
- node.thumbStale = false;
1722
- node.thumbAt = now;
1723
- } else if (node.dirty) node.thumbStale = true;
1892
+ if (scale === THUMBNAIL_SCALE) setThumb(node, slot, now);
1893
+ else if (node.dirty) node.thumbStale = true;
1724
1894
  if (node.dirty) {
1725
1895
  node.preview = null;
1726
1896
  node.refreshedAt = now;
@@ -1783,17 +1953,11 @@ async function createGpuCanvas(host, options) {
1783
1953
  const thumbnail = node.targetScale === THUMBNAIL_SCALE;
1784
1954
  const slot = slotFor(
1785
1955
  thumbnail ? node.thumb : node.slot,
1786
- Math.max(1, Math.round(node.rect.width * node.targetScale)),
1787
- Math.max(1, Math.round(node.rect.height * node.targetScale)),
1956
+ texels(node.rect.width, node.targetScale),
1957
+ texels(node.rect.height, node.targetScale),
1788
1958
  thumbnail
1789
1959
  );
1790
- const region = {
1791
- x: node.pageX,
1792
- y: node.pageY,
1793
- width: node.rect.width,
1794
- height: node.rect.height
1795
- };
1796
- if (!slot || !drawSnapshot(page, region, slot)) {
1960
+ if (!slot || !drawSnapshot(page, nodeRegion(node), slot)) {
1797
1961
  pending++;
1798
1962
  continue;
1799
1963
  }
@@ -1806,11 +1970,7 @@ async function createGpuCanvas(host, options) {
1806
1970
  const warm = /* @__PURE__ */ new Set();
1807
1971
  const drawRegion = (node, rect, scale2, previous) => {
1808
1972
  if (!warm.has(scale2) && overBudget()) return null;
1809
- const slot = slotFor(
1810
- previous,
1811
- Math.max(1, Math.round(rect.width * scale2)),
1812
- Math.max(1, Math.round(rect.height * scale2))
1813
- );
1973
+ const slot = slotFor(previous, texels(rect.width, scale2), texels(rect.height, scale2));
1814
1974
  const region = {
1815
1975
  x: node.pageX + rect.x - node.rect.x,
1816
1976
  y: node.pageY + rect.y - node.rect.y,
@@ -1861,21 +2021,13 @@ async function createGpuCanvas(host, options) {
1861
2021
  if (atlas.isValid(node.thumb) && start - node.thumbAt < THUMBNAIL_REFRESH_MS) continue;
1862
2022
  const slot = slotFor(
1863
2023
  node.thumb,
1864
- Math.max(1, Math.round(node.rect.width * THUMBNAIL_SCALE)),
1865
- Math.max(1, Math.round(node.rect.height * THUMBNAIL_SCALE)),
2024
+ texels(node.rect.width, THUMBNAIL_SCALE),
2025
+ texels(node.rect.height, THUMBNAIL_SCALE),
1866
2026
  true
1867
2027
  );
1868
- const region = {
1869
- x: node.pageX,
1870
- y: node.pageY,
1871
- width: node.rect.width,
1872
- height: node.rect.height
1873
- };
1874
- if (!slot || !drawSnapshot(page, region, slot)) continue;
2028
+ if (!slot || !drawSnapshot(page, nodeRegion(node), slot)) continue;
1875
2029
  calls++;
1876
- node.thumb = slot;
1877
- node.thumbStale = false;
1878
- node.thumbAt = start;
2030
+ setThumb(node, slot, start);
1879
2031
  }
1880
2032
  const done = plan.nodes.every((node) => !node.dirty && atlas.isValid(node.slot)) && plan.previews.every((node) => node.previewTarget === null) && tilesLeft === 0;
1881
2033
  if (done && !isPinned(page)) page.setDrawable(false, frameNumber);
@@ -1888,76 +2040,17 @@ async function createGpuCanvas(host, options) {
1888
2040
  let instances2 = visible.length;
1889
2041
  for (const node of visible) instances2 += node.tiles.length + (node.preview ? 1 : 0);
1890
2042
  instances2 += selection.size + 1;
1891
- renderer.reserve(instances2);
1892
- const data = renderer.instances;
1893
- let count = 0;
1894
- runs.length = 0;
2043
+ renderer.begin(instances2);
1895
2044
  effectsMoving = false;
1896
- let runEffect = null;
1897
- const startRun = (effect) => {
1898
- if (runs.length && runEffect === effect) return;
1899
- runEffect = effect;
1900
- runs.push({ effect, start: count, count: 0 });
1901
- };
1902
- const push = (rect, slot) => {
1903
- startRun(null);
1904
- runs[runs.length - 1].count++;
1905
- const offset = count++ * FLOATS_PER_INSTANCE;
1906
- data[offset] = rect.x;
1907
- data[offset + 1] = rect.y;
1908
- data[offset + 2] = rect.width;
1909
- data[offset + 3] = rect.height;
1910
- if (slot) {
1911
- atlas.touch(slot.layer);
1912
- data[offset + 4] = slot.x;
1913
- data[offset + 5] = slot.y;
1914
- data[offset + 6] = slot.x + slot.width;
1915
- data[offset + 7] = slot.y + slot.height;
1916
- data[offset + 8] = slot.layer;
1917
- data[offset + 9] = 0;
1918
- } else {
1919
- data[offset + 9] = 1;
1920
- }
1921
- data[offset + 10] = 0;
1922
- };
1923
- const pushEffect = (node, slot, worn) => {
1924
- startRun(worn.effect);
1925
- runs[runs.length - 1].count++;
1926
- const offset = count++ * FLOATS_PER_INSTANCE;
1927
- const { rect } = node;
1928
- data[offset] = rect.x;
1929
- data[offset + 1] = rect.y;
1930
- data[offset + 2] = rect.width;
1931
- data[offset + 3] = rect.height;
1932
- atlas.touch(slot.layer);
1933
- data[offset + 4] = slot.x;
1934
- data[offset + 5] = slot.y;
1935
- data[offset + 6] = slot.x + slot.width;
1936
- data[offset + 7] = slot.y + slot.height;
1937
- data[offset + 8] = slot.layer;
1938
- data[offset + 9] = 0;
1939
- data[offset + 10] = worn.pad;
1940
- data.set(worn.params, offset + 12);
1941
- };
1942
2045
  const pushRing = (element, color, width) => {
1943
2046
  const rect = ringRect(element);
1944
- if (!rect || !intersects(rect, frameView)) return;
1945
- startRun(null);
1946
- runs[runs.length - 1].count++;
1947
- const offset = count++ * FLOATS_PER_INSTANCE;
1948
- data[offset] = rect.x;
1949
- data[offset + 1] = rect.y;
1950
- data[offset + 2] = rect.width;
1951
- data[offset + 3] = rect.height;
1952
- data.set(color, offset + 4);
1953
- data[offset + 9] = 2;
1954
- data[offset + 10] = width;
2047
+ if (rect && intersects(rect, frameView)) renderer.ring(rect, color, width);
1955
2048
  };
1956
2049
  let onThumbnail = 0;
1957
2050
  for (const node of visible) {
1958
- const base = atlas.isValid(node.slot) ? node.slot : atlas.isValid(node.thumb) ? node.thumb : null;
1959
- if (base && base !== node.slot) onThumbnail++;
1960
- const hasSlot = base !== null;
2051
+ const image = atlas.isValid(node.slot) ? node.slot : atlas.isValid(node.thumb) ? node.thumb : null;
2052
+ if (image && image !== node.slot) onThumbnail++;
2053
+ const hasSlot = image !== null;
1961
2054
  const sharp = node.tiles.length > 0 || node.preview !== null && atlas.isValid(node.preview);
1962
2055
  const shown = sharp ? 3 : hasSlot ? 2 : 1;
1963
2056
  const before = node.shownFrame === stats.frames - 1 ? node.shown : 0;
@@ -1966,29 +2059,31 @@ async function createGpuCanvas(host, options) {
1966
2059
  node.shown = shown;
1967
2060
  node.shownFrame = stats.frames;
1968
2061
  const worn = effects.size ? effects.get(node.element) : void 0;
1969
- if (worn) {
2062
+ if (worn && !renderer.effectFailed(worn.effect)) {
1970
2063
  if (worn.effect.animated) effectsMoving = true;
1971
- if (base && renderer.hasEffect(worn.effect)) {
1972
- pushEffect(node, base, worn);
2064
+ if (image && renderer.hasEffect(worn.effect)) {
2065
+ renderer.effectQuad(node.rect, image, worn.effect, worn.pad, worn.params);
1973
2066
  continue;
1974
2067
  }
1975
2068
  effectsMoving = true;
1976
2069
  }
1977
- push(node.rect, base);
2070
+ renderer.quad(node.rect, image);
1978
2071
  const current = (tile) => tile.scale === detailScale && !tile.stale;
1979
2072
  for (const tile of node.tiles)
1980
- if (!current(tile) && atlas.isValid(tile.slot)) push(tile.rect, tile.slot);
1981
- if (node.preview && atlas.isValid(node.preview)) push(node.previewRect, node.preview);
2073
+ if (!current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
2074
+ if (node.preview && atlas.isValid(node.preview))
2075
+ renderer.quad(node.previewRect, node.preview);
1982
2076
  for (const tile of node.tiles)
1983
- if (current(tile) && atlas.isValid(tile.slot)) push(tile.rect, tile.slot);
2077
+ if (current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
1984
2078
  }
1985
2079
  stats.onThumbnail = onThumbnail;
1986
2080
  for (const element of selection) pushRing(element, selectionColor, SELECTION_WIDTH);
1987
2081
  if (hovered && !selection.has(hovered)) pushRing(hovered, hoverColor, HOVER_WIDTH);
1988
2082
  const cssWidth = canvas.clientWidth;
1989
- const dpr = canvas.width / Math.max(1, cssWidth);
2083
+ const dpr = canvasDpr();
1990
2084
  const now = performance.now();
1991
- renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, count, runs, {
2085
+ const camera = base.camera();
2086
+ renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, {
1992
2087
  time: (now - startedAt) / 1e3,
1993
2088
  pointer: pointer ? screenToWorld(camera, pointer) : FAR_AWAY,
1994
2089
  press,
@@ -1999,9 +2094,9 @@ async function createGpuCanvas(host, options) {
1999
2094
  if (destroyed) return;
2000
2095
  const start = performance.now();
2001
2096
  atlas.nextFrame();
2002
- const cssWidth = canvas.clientWidth;
2003
- const dpr = canvas.width / Math.max(1, cssWidth);
2004
- const view = visibleWorldRect(camera, cssWidth, canvas.clientHeight);
2097
+ const dpr = canvasDpr();
2098
+ const camera = base.camera();
2099
+ const view = base.view();
2005
2100
  const moving = start - lastCameraChange < SETTLE_MS;
2006
2101
  planFrame(view, rasterScaleFor(camera.zoom * dpr), camera.zoom * dpr, moving);
2007
2102
  if (!inPaint && plans.size > 0) {
@@ -2043,69 +2138,37 @@ async function createGpuCanvas(host, options) {
2043
2138
  const drawCaptures = () => {
2044
2139
  for (const capture of [...captures]) {
2045
2140
  const { node, scale } = capture;
2046
- const width = Math.max(1, Math.round(node.rect.width * scale));
2047
- const height = Math.max(1, Math.round(node.rect.height * scale));
2048
- const texture = device.createTexture({
2049
- size: { width, height },
2050
- format: "rgba8unorm",
2051
- usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
2052
- });
2141
+ let image;
2053
2142
  try {
2054
- device.queue.drawElementImageToTexture(
2055
- {
2056
- source: node.page.element,
2057
- sourceX: node.pageX,
2058
- sourceY: node.pageY,
2059
- sourceWidth: node.rect.width,
2060
- sourceHeight: node.rect.height
2061
- },
2062
- { texture, size: { width, height }, premultipliedAlpha: false }
2143
+ image = captureRegion(
2144
+ device,
2145
+ node.page.element,
2146
+ nodeRegion(node),
2147
+ texels(node.rect.width, scale),
2148
+ texels(node.rect.height, scale)
2063
2149
  );
2064
2150
  } catch (error) {
2065
- texture.destroy();
2066
- if (++capture.tries > 20) {
2151
+ if (++capture.tries > MAX_CAPTURE_TRIES) {
2067
2152
  captures.splice(captures.indexOf(capture), 1);
2068
2153
  capture.reject(error instanceof Error ? error : new Error(String(error)));
2069
2154
  }
2070
2155
  continue;
2071
2156
  }
2072
2157
  captures.splice(captures.indexOf(capture), 1);
2073
- readTexture(texture, width, height).then(capture.resolve, capture.reject);
2158
+ image.then(capture.resolve, capture.reject);
2074
2159
  }
2075
2160
  if (captures.length) {
2076
2161
  awaitingPaint = true;
2077
2162
  canvas.requestPaint();
2078
2163
  } else schedule();
2079
2164
  };
2080
- const readTexture = async (texture, width, height) => {
2081
- const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
2082
- const buffer = device.createBuffer({
2083
- size: bytesPerRow * height,
2084
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
2085
- });
2086
- const encoder = device.createCommandEncoder();
2087
- encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
2088
- device.queue.submit([encoder.finish()]);
2089
- try {
2090
- await buffer.mapAsync(GPUMapMode.READ);
2091
- const rows = new Uint8Array(buffer.getMappedRange());
2092
- const pixels = new Uint8ClampedArray(width * height * 4);
2093
- for (let y = 0; y < height; y++) {
2094
- pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
2095
- }
2096
- return new ImageData(pixels, width, height);
2097
- } finally {
2098
- buffer.destroy();
2099
- texture.destroy();
2100
- }
2101
- };
2102
2165
  canvas.onpaint = (event) => {
2103
2166
  if (captures.length) drawCaptures();
2104
2167
  let changed = false;
2105
2168
  for (const element of event.changedElements) {
2106
2169
  const page = pageByElement.get(element);
2107
2170
  if (!page) continue;
2108
- for (const node of [live, lingering, focused, ...transitioning.keys()]) {
2171
+ for (const node of heldNodes()) {
2109
2172
  if (node?.page === page) {
2110
2173
  node.dirty = true;
2111
2174
  changed = true;
@@ -2117,13 +2180,10 @@ async function createGpuCanvas(host, options) {
2117
2180
  awaitingPaint = false;
2118
2181
  frame(true);
2119
2182
  };
2120
- const viewNow = () => visibleWorldRect(camera, canvas.clientWidth, canvas.clientHeight);
2121
2183
  const nodeOf = (target) => {
2122
- let element = target instanceof HTMLElement ? target : target.parentElement;
2123
- while (element?.parentElement && !pageByElement.has(element.parentElement)) {
2124
- element = element.parentElement;
2125
- }
2126
- return element ? nodes.get(element) : void 0;
2184
+ const element = target instanceof Element ? target : target.parentElement;
2185
+ const node = element && nodeOfElement(nodes, element);
2186
+ return node ? nodes.get(node) : void 0;
2127
2187
  };
2128
2188
  const mutations = new MutationObserver((records) => {
2129
2189
  ringRects.clear();
@@ -2136,7 +2196,7 @@ async function createGpuCanvas(host, options) {
2136
2196
  const node = nodeOf(record.target);
2137
2197
  if (node && !node.dirty) {
2138
2198
  node.dirty = true;
2139
- if (!changed) changed = intersects(node.rect, viewNow());
2199
+ if (!changed) changed = intersects(node.rect, base.view());
2140
2200
  }
2141
2201
  }
2142
2202
  if (changed) schedule();
@@ -2154,20 +2214,26 @@ async function createGpuCanvas(host, options) {
2154
2214
  page.element.style.transform = "";
2155
2215
  schedule();
2156
2216
  };
2217
+ const letGo = (node) => {
2218
+ node.dirty = true;
2219
+ unpin(node.page);
2220
+ };
2157
2221
  const positionLivePage = () => {
2158
2222
  if (!live || livePositioned) return;
2159
- const dpr = canvas.width / Math.max(1, canvas.clientWidth);
2223
+ const camera = base.camera();
2160
2224
  const { zoom } = camera;
2161
- const drawn2 = new DOMMatrix([
2162
- zoom,
2163
- 0,
2164
- 0,
2165
- zoom,
2166
- ((live.rect.x - camera.x) * zoom - live.pageX * zoom) * dpr,
2167
- ((live.rect.y - camera.y) * zoom - live.pageY * zoom) * dpr
2168
- ]);
2225
+ const x = (live.rect.x - camera.x - live.pageX) * zoom;
2226
+ const y = (live.rect.y - camera.y - live.pageY) * zoom;
2169
2227
  try {
2170
- live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
2228
+ if (CANVAS_TRANSFORM_HIT_TESTS) {
2229
+ canvas.updateElementGeometry(live.page.element, {
2230
+ canvasTransform: new DOMMatrix([zoom, 0, 0, zoom, x, y])
2231
+ });
2232
+ } else {
2233
+ const dpr = canvasDpr();
2234
+ const drawn2 = new DOMMatrix([zoom, 0, 0, zoom, x * dpr, y * dpr]);
2235
+ live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
2236
+ }
2171
2237
  live.element.style.pointerEvents = "auto";
2172
2238
  livePositioned = true;
2173
2239
  } catch {
@@ -2183,16 +2249,12 @@ async function createGpuCanvas(host, options) {
2183
2249
  clearTimeout(lingerTimer);
2184
2250
  const earlier = lingering;
2185
2251
  lingering = previous;
2186
- if (earlier && earlier !== previous) {
2187
- earlier.dirty = true;
2188
- unpin(earlier.page);
2189
- }
2252
+ if (earlier && earlier !== previous) letGo(earlier);
2190
2253
  lingerTimer = window.setTimeout(() => {
2191
2254
  const last = lingering;
2192
2255
  if (!last) return;
2193
2256
  lingering = null;
2194
- last.dirty = true;
2195
- unpin(last.page);
2257
+ letGo(last);
2196
2258
  }, LINGER_MS);
2197
2259
  unpin(previous.page);
2198
2260
  }
@@ -2209,14 +2271,10 @@ async function createGpuCanvas(host, options) {
2209
2271
  };
2210
2272
  const pickLive = () => {
2211
2273
  if (!pointer) return setLive(null);
2212
- const point = screenToWorld(camera, pointer);
2213
- let hit = null;
2214
- for (const node of nodes.values()) if (containsPoint(node.rect, point)) hit = node;
2215
- setLive(hit);
2274
+ setLive(base.nodeAt(screenToWorld(base.camera(), pointer)));
2216
2275
  };
2217
2276
  const onPointerMove = (event) => {
2218
- const bounds = canvas.getBoundingClientRect();
2219
- pointer = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
2277
+ pointer = pointIn(canvas, event);
2220
2278
  if (event.buttons === 0) pickLive();
2221
2279
  };
2222
2280
  const onPointerLeave = () => {
@@ -2227,9 +2285,8 @@ async function createGpuCanvas(host, options) {
2227
2285
  let pressedAt = -Infinity;
2228
2286
  let tapped = false;
2229
2287
  const onPointerDown = (event) => {
2230
- const bounds = canvas.getBoundingClientRect();
2231
- const at = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
2232
- press = screenToWorld(camera, at);
2288
+ const at = pointIn(canvas, event);
2289
+ press = screenToWorld(base.camera(), at);
2233
2290
  pressedAt = performance.now();
2234
2291
  if (event.pointerType !== "mouse") {
2235
2292
  pointer = at;
@@ -2241,13 +2298,8 @@ async function createGpuCanvas(host, options) {
2241
2298
  const onClick = (event) => {
2242
2299
  if (!tapped) return;
2243
2300
  tapped = false;
2244
- const bounds = canvas.getBoundingClientRect();
2245
- const point = screenToWorld(camera, {
2246
- x: event.clientX - bounds.left,
2247
- y: event.clientY - bounds.top
2248
- });
2249
- let top = null;
2250
- for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
2301
+ const point = screenToWorld(base.camera(), pointIn(canvas, event));
2302
+ const top = base.nodeAt(point);
2251
2303
  if (!top || top.element.contains(event.target)) return;
2252
2304
  let target = elementInNode(top.element, top.rect, point);
2253
2305
  while (target && !(target instanceof HTMLElement)) target = target.parentElement;
@@ -2269,10 +2321,7 @@ async function createGpuCanvas(host, options) {
2269
2321
  if (node === focused) return;
2270
2322
  const previous = focused;
2271
2323
  focused = node;
2272
- if (previous) {
2273
- previous.dirty = true;
2274
- unpin(previous.page);
2275
- }
2324
+ if (previous) letGo(previous);
2276
2325
  if (node) {
2277
2326
  node.page.setDrawable(true, frameNumber);
2278
2327
  node.dirty = true;
@@ -2283,10 +2332,7 @@ async function createGpuCanvas(host, options) {
2283
2332
  if (event.relatedTarget && canvas.contains(event.relatedTarget)) return;
2284
2333
  const previous = focused;
2285
2334
  focused = null;
2286
- if (previous) {
2287
- previous.dirty = true;
2288
- unpin(previous.page);
2289
- }
2335
+ if (previous) letGo(previous);
2290
2336
  };
2291
2337
  const onResourceLoad = (event) => {
2292
2338
  const node = nodeOf(event.target);
@@ -2323,8 +2369,8 @@ async function createGpuCanvas(host, options) {
2323
2369
  const node = nodeOf(event.target);
2324
2370
  const now = performance.now();
2325
2371
  if (!node || now - (usedAt.get(node) ?? -Infinity) > USED_MS) return;
2326
- if (!intersects(node.rect, viewNow())) return;
2327
- if (Math.max(node.rect.width, node.rect.height) * camera.zoom < SMALL_NODE_PX) return;
2372
+ if (!intersects(node.rect, base.view())) return;
2373
+ if (smallOnScreen(node)) return;
2328
2374
  const ms = transitionMs(event.target) + 32;
2329
2375
  const end = now + ms;
2330
2376
  if ((transitioning.get(node) ?? 0) >= end) return;
@@ -2332,38 +2378,34 @@ async function createGpuCanvas(host, options) {
2332
2378
  node.dirty = true;
2333
2379
  schedule();
2334
2380
  setTimeout(() => {
2335
- if (transitioning.get(node) !== end) return;
2381
+ if (destroyed || transitioning.get(node) !== end) return;
2336
2382
  transitioning.delete(node);
2337
- node.dirty = true;
2338
- unpin(node.page);
2383
+ letGo(node);
2339
2384
  schedule();
2340
2385
  }, ms);
2341
2386
  };
2342
2387
  for (const type of USE_EVENTS) canvas.addEventListener(type, onUse, true);
2343
2388
  canvas.addEventListener("transitionrun", onTransitionRun);
2344
- const setCamera = (next) => {
2345
- camera = next;
2346
- lastCameraChange = performance.now();
2347
- setLive(null);
2348
- clearTimeout(settleTimer);
2349
- settleTimer = window.setTimeout(() => {
2350
- pickLive();
2351
- schedule();
2352
- }, SETTLE_MS);
2353
- schedule();
2354
- options.onCameraChange?.(camera);
2389
+ const newPage = (width, height) => {
2390
+ const page = new Page(width, height);
2391
+ pages.set(page, /* @__PURE__ */ new Set());
2392
+ pageByElement.set(page.element, page);
2393
+ canvas.append(page.element);
2394
+ return page;
2395
+ };
2396
+ const dropPage = (page) => {
2397
+ pages.delete(page);
2398
+ pageByElement.delete(page.element);
2399
+ pageSlots.delete(page);
2400
+ page.element.remove();
2355
2401
  };
2356
- const unbindInput = bindCameraInput(surface, () => camera, setCamera);
2357
2402
  const placeNode = (element, rect) => {
2358
2403
  let slot = openPage?.place(rect.width, rect.height) ?? null;
2359
2404
  if (!slot) {
2360
- const page = new Page(
2405
+ const page = newPage(
2361
2406
  Math.max(pageSize, Math.ceil(rect.width)),
2362
2407
  Math.max(pageSize, Math.ceil(rect.height))
2363
2408
  );
2364
- pages.set(page, /* @__PURE__ */ new Set());
2365
- pageByElement.set(page.element, page);
2366
- canvas.append(page.element);
2367
2409
  if (page.width === pageSize && page.height === pageSize) openPage = page;
2368
2410
  slot = page.place(rect.width, rect.height);
2369
2411
  }
@@ -2380,21 +2422,13 @@ async function createGpuCanvas(host, options) {
2380
2422
  const releaseNode = (node) => {
2381
2423
  const members = pages.get(node.page);
2382
2424
  members.delete(node);
2383
- node.page.release();
2384
- if (members.size === 0 && node.page !== openPage) {
2385
- pages.delete(node.page);
2386
- pageByElement.delete(node.page.element);
2387
- node.page.element.remove();
2388
- }
2425
+ if (members.size === 0 && node.page !== openPage) dropPage(node.page);
2389
2426
  };
2390
2427
  const lift = (node) => {
2391
2428
  const previous = node.page;
2392
2429
  const wasLive = live === node;
2393
2430
  if (wasLive) setLive(null);
2394
- const page = new Page(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
2395
- pages.set(page, /* @__PURE__ */ new Set());
2396
- pageByElement.set(page.element, page);
2397
- canvas.append(page.element);
2431
+ const page = newPage(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
2398
2432
  page.place(node.rect.width, node.rect.height);
2399
2433
  const target = page.element;
2400
2434
  if (target.moveBefore) target.moveBefore(node.element, null);
@@ -2425,15 +2459,11 @@ async function createGpuCanvas(host, options) {
2425
2459
  pages.get(slot.page).add(node);
2426
2460
  };
2427
2461
  return {
2462
+ ...base.api,
2428
2463
  mode: "gpu",
2429
- surface,
2430
2464
  addNode(element, rect) {
2431
- Object.assign(element.style, {
2432
- boxSizing: "border-box",
2433
- contain: "strict",
2434
- overflow: "hidden",
2435
- pointerEvents: "none"
2436
- });
2465
+ styleNode(element);
2466
+ element.style.pointerEvents = "none";
2437
2467
  const node = {
2438
2468
  element,
2439
2469
  rect,
@@ -2487,25 +2517,10 @@ async function createGpuCanvas(host, options) {
2487
2517
  element.remove();
2488
2518
  schedule();
2489
2519
  },
2490
- getNodeRect: (element) => {
2491
- const node = nodes.get(element);
2492
- return node ? { ...node.rect } : null;
2493
- },
2494
- getNodesIn(rect) {
2495
- const hits = [];
2496
- for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
2497
- return hits;
2498
- },
2499
- getNodeOf: (element) => nodeOfElement(nodes, element),
2500
2520
  getElementRect(element) {
2501
2521
  const rect = ringRect(element);
2502
2522
  return rect ? { ...rect } : null;
2503
2523
  },
2504
- getElementAt(point) {
2505
- let top = null;
2506
- for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
2507
- return top ? elementInNode(top.element, top.rect, point) : null;
2508
- },
2509
2524
  setSelection(elements) {
2510
2525
  selection = new Set(elements);
2511
2526
  ringRects.clear();
@@ -2525,10 +2540,6 @@ async function createGpuCanvas(host, options) {
2525
2540
  } else effects.delete(element);
2526
2541
  schedule();
2527
2542
  },
2528
- screenToWorld: (point) => screenToWorld(camera, point),
2529
- worldToScreen: (point) => worldToScreen(camera, point),
2530
- getCamera: () => camera,
2531
- setCamera,
2532
2543
  invalidate: onFontsLoaded,
2533
2544
  snapshot(element, options2 = {}) {
2534
2545
  const node = nodes.get(element);
@@ -2547,10 +2558,6 @@ async function createGpuCanvas(host, options) {
2547
2558
  [renderer.background, renderer.placeholder] = colorsFor(color);
2548
2559
  schedule();
2549
2560
  },
2550
- fit(padding) {
2551
- const bounds = boundsOf([...nodes.values()].map((node) => node.rect));
2552
- if (bounds) setCamera(fitCamera(bounds, canvas.clientWidth, canvas.clientHeight, padding));
2553
- },
2554
2561
  getStats: () => ({
2555
2562
  mode: "gpu",
2556
2563
  nodes: nodes.size,
@@ -2576,6 +2583,7 @@ async function createGpuCanvas(host, options) {
2576
2583
  clearTimeout(settleTimer);
2577
2584
  clearTimeout(lingerTimer);
2578
2585
  clearTimeout(contentTimer);
2586
+ clearTimeout(refreshTimer);
2579
2587
  mutations.disconnect();
2580
2588
  document.fonts.removeEventListener("loadingdone", onFontsLoaded);
2581
2589
  canvas.removeEventListener("load", onResourceLoad, true);
@@ -2587,10 +2595,9 @@ async function createGpuCanvas(host, options) {
2587
2595
  canvas.removeEventListener("focusout", onFocusOut);
2588
2596
  canvas.removeEventListener("transitionrun", onTransitionRun);
2589
2597
  for (const type of USE_EVENTS) canvas.removeEventListener(type, onUse, true);
2590
- unbindInput();
2591
2598
  resizeObserver.disconnect();
2592
2599
  canvas.onpaint = null;
2593
- surface.remove();
2600
+ base.destroy();
2594
2601
  renderer.destroy();
2595
2602
  atlas.destroy();
2596
2603
  device.destroy();
@@ -2649,7 +2656,6 @@ var labelText = (element) => {
2649
2656
  }
2650
2657
  return text.replace(/\s+/g, " ").trim();
2651
2658
  };
2652
- var sameRect = (a, b) => Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
2653
2659
  var round = (rect) => ({
2654
2660
  x: Math.round(rect.x),
2655
2661
  y: Math.round(rect.y),
@@ -2701,11 +2707,7 @@ function createAgentView(canvas) {
2701
2707
  return {
2702
2708
  /** What is in `rect` (default: the view), node by node, as plain JSON. */
2703
2709
  describe(options = {}) {
2704
- const area = options.rect ?? visibleWorldRect(
2705
- canvas.getCamera(),
2706
- canvas.surface.clientWidth,
2707
- canvas.surface.clientHeight
2708
- );
2710
+ const area = options.rect ?? viewOf(canvas);
2709
2711
  return canvas.getNodesIn(area).flatMap((node) => read(node, options.depth ?? 6, options));
2710
2712
  },
2711
2713
  /** The element behind an id from `describe`, or null once it is gone. */
@@ -2750,25 +2752,7 @@ function createAgentView(canvas) {
2750
2752
  // src/layers.ts
2751
2753
  var sameLayer = (a, b) => !!a && !!b && a[0] === b[0];
2752
2754
  var layerIn = (layer, list) => list.some((other) => sameLayer(layer, other));
2753
- function layerRect(canvas, layer) {
2754
- let union = null;
2755
- for (const element of layer) {
2756
- const rect = canvas.getElementRect(element);
2757
- if (rect) union = union ? unionRect(union, rect) : rect;
2758
- }
2759
- return union;
2760
- }
2761
- function unionRect(a, b) {
2762
- const x = Math.min(a.x, b.x);
2763
- const y = Math.min(a.y, b.y);
2764
- return {
2765
- x,
2766
- y,
2767
- width: Math.max(a.x + a.width, b.x + b.width) - x,
2768
- height: Math.max(a.y + a.height, b.y + b.height) - y
2769
- };
2770
- }
2771
- var sameRect2 = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
2755
+ var layerRect = (canvas, layer) => unionOf(layer.map((element) => canvas.getElementRect(element)));
2772
2756
  function createLayers(canvas, pick) {
2773
2757
  const chainOf = (element) => {
2774
2758
  const node = canvas.getNodeOf(element);
@@ -2787,7 +2771,7 @@ function createLayers(canvas, pick) {
2787
2771
  let previous = canvas.getNodeRect(node);
2788
2772
  for (const layer of inner) {
2789
2773
  const rect = layerRect(canvas, layer);
2790
- if (sameRect2(rect, previous)) continue;
2774
+ if (sameRect(rect, previous)) continue;
2791
2775
  chain.push(layer);
2792
2776
  previous = rect;
2793
2777
  }
@@ -2805,7 +2789,7 @@ function createLayers(canvas, pick) {
2805
2789
  for (const child of element.children) {
2806
2790
  const rect = canvas.getElementRect(child);
2807
2791
  if (!rect || rect.width <= 0 || rect.height <= 0) continue;
2808
- if (sameRect2(rect, outer)) visit(child, outer);
2792
+ if (sameRect(rect, outer)) visit(child, outer);
2809
2793
  else children.push([child]);
2810
2794
  }
2811
2795
  };
@@ -2853,7 +2837,7 @@ var pill = (color) => ({
2853
2837
  whiteSpace: "nowrap",
2854
2838
  pointerEvents: "none"
2855
2839
  });
2856
- function createOverlay(surface, color, hoverColor) {
2840
+ function createOverlay(surface, color) {
2857
2841
  const root = document.createElement("div");
2858
2842
  Object.assign(root.style, {
2859
2843
  position: "absolute",
@@ -2868,7 +2852,7 @@ function createOverlay(surface, color, hoverColor) {
2868
2852
  root.append(element);
2869
2853
  return element;
2870
2854
  };
2871
- const hoverName = make(pill(hoverColor));
2855
+ const hoverName = make(pill(color));
2872
2856
  const name = make(pill(color));
2873
2857
  const size = make(pill(color));
2874
2858
  const bounds = make({ position: "absolute", border: `1px solid ${color}` });
@@ -2953,9 +2937,10 @@ function createOverlay(surface, color, hoverColor) {
2953
2937
  }
2954
2938
  };
2955
2939
  }
2956
- var guideRect = (guide, toScreen) => {
2957
- const a = toScreen(guide.x1, guide.y1);
2958
- const b = toScreen(guide.x2, guide.y2);
2940
+ var handleAt = (target) => target instanceof Element ? target.closest("[data-gpucanvas-handle]")?.dataset.gpucanvasHandle ?? null : null;
2941
+ var guideRect = (guide, camera) => {
2942
+ const a = worldToScreen(camera, { x: guide.x1, y: guide.y1 });
2943
+ const b = worldToScreen(camera, { x: guide.x2, y: guide.y2 });
2959
2944
  return {
2960
2945
  x: Math.min(a.x, b.x),
2961
2946
  y: Math.min(a.y, b.y),
@@ -3002,16 +2987,16 @@ function guidesFor(rect, targets, edges) {
3002
2987
  const hits = targets.filter((target) => anchorsX(target).some((at) => close(at, x)));
3003
2988
  if (!hits.length) continue;
3004
2989
  const top = Math.min(rect.y, ...hits.map((t) => t.y));
3005
- const bottom2 = Math.max(rect.y + rect.height, ...hits.map((t) => t.y + t.height));
3006
- guides.push({ x1: x, y1: top, x2: x, y2: bottom2 });
2990
+ const bottom = Math.max(rect.y + rect.height, ...hits.map((t) => t.y + t.height));
2991
+ guides.push({ x1: x, y1: top, x2: x, y2: bottom });
3007
2992
  }
3008
2993
  for (const f of edges.y) {
3009
2994
  const y = rect.y + rect.height * f;
3010
2995
  const hits = targets.filter((target) => anchorsY(target).some((at) => close(at, y)));
3011
2996
  if (!hits.length) continue;
3012
2997
  const left = Math.min(rect.x, ...hits.map((t) => t.x));
3013
- const right2 = Math.max(rect.x + rect.width, ...hits.map((t) => t.x + t.width));
3014
- guides.push({ x1: left, y1: y, x2: right2, y2: y });
2998
+ const right = Math.max(rect.x + rect.width, ...hits.map((t) => t.x + t.width));
2999
+ guides.push({ x1: left, y1: y, x2: right, y2: y });
3015
3000
  }
3016
3001
  return guides;
3017
3002
  }
@@ -3059,9 +3044,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
3059
3044
  }
3060
3045
  };
3061
3046
  }
3047
+ var defaultLabel = (canvas) => (layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName;
3062
3048
  var CLICK_SLOP = 4;
3063
3049
  var SNAP_PX = 6;
3064
3050
  var MAX_NAMED = 1;
3051
+ var EVERYWHERE = { x: -1e9, y: -1e9, width: 2e9, height: 2e9 };
3065
3052
  var isEditable = (element) => !!element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable);
3066
3053
  function bindEditor(canvas, options = {}) {
3067
3054
  const {
@@ -3075,11 +3062,11 @@ function bindEditor(canvas, options = {}) {
3075
3062
  onChange,
3076
3063
  onTransform
3077
3064
  } = options;
3078
- const label = options.label ?? ((layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName);
3065
+ const label = options.label ?? defaultLabel(canvas);
3079
3066
  const { surface } = canvas;
3080
3067
  const layers = createLayers(canvas, pick);
3081
3068
  const transforms = !!onTransform && trigger === "click";
3082
- const overlay = createOverlay(surface, color, color);
3069
+ const overlay = createOverlay(surface, color);
3083
3070
  let marqueeLayers = [];
3084
3071
  const box = marquee ? createMarquee(canvas, color, (world) => {
3085
3072
  marqueeLayers = layers.layersIn(scopes.at(-1) ?? null, world);
@@ -3106,10 +3093,7 @@ function bindEditor(canvas, options = {}) {
3106
3093
  canvas.setHover(layer?.[0] ?? null);
3107
3094
  draw();
3108
3095
  };
3109
- const at = (event) => {
3110
- const bounds = surface.getBoundingClientRect();
3111
- return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
3112
- };
3096
+ const at = (event) => pointIn(surface, event);
3113
3097
  const chainAt = (point) => layers.chainAt(canvas.screenToWorld(point));
3114
3098
  const fieldAt = (point) => {
3115
3099
  const element = canvas.getElementAt(canvas.screenToWorld(point));
@@ -3138,15 +3122,7 @@ function bindEditor(canvas, options = {}) {
3138
3122
  const draw = () => {
3139
3123
  if (!frame) frame = requestAnimationFrame(render);
3140
3124
  };
3141
- const toScreen = (rect) => {
3142
- const { x, y } = canvas.worldToScreen(rect);
3143
- const zoom = canvas.getCamera().zoom;
3144
- return { x, y, width: rect.width * zoom, height: rect.height * zoom };
3145
- };
3146
- const unionOf = (rects) => rects.reduce(
3147
- (union, rect) => !rect ? union : union ? unionRect(union, rect) : rect,
3148
- null
3149
- );
3125
+ const toScreen = (rect) => rectToScreen(canvas.getCamera(), rect);
3150
3126
  const render = () => {
3151
3127
  frame = 0;
3152
3128
  const transforming = gesture?.kind === "move" || gesture?.kind === "resize" ? gesture : null;
@@ -3162,9 +3138,7 @@ function bindEditor(canvas, options = {}) {
3162
3138
  } : null,
3163
3139
  bounds: selection.length > 1 || !!transforming,
3164
3140
  handles: transforms && selection.length > 0 && gesture?.kind !== "marquee",
3165
- guides: transforming ? transforming.guides.map(
3166
- (guide) => guideRect(guide, (x, y) => canvas.worldToScreen({ x, y }))
3167
- ) : []
3141
+ guides: transforming ? transforming.guides.map((guide) => guideRect(guide, canvas.getCamera())) : []
3168
3142
  });
3169
3143
  if (selection.length || hovered || gesture) draw();
3170
3144
  };
@@ -3182,8 +3156,7 @@ function bindEditor(canvas, options = {}) {
3182
3156
  if (!snap) return [];
3183
3157
  const scope = scopes.at(-1);
3184
3158
  if (!scope) {
3185
- const view = visibleWorldRect(canvas.getCamera(), surface.clientWidth, surface.clientHeight);
3186
- return canvas.getNodesIn(view).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
3159
+ return canvas.getNodesIn(viewOf(canvas)).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
3187
3160
  }
3188
3161
  const rects = layers.childrenOf(scope).filter((layer) => !layerIn(layer, moving)).map((layer) => layerRect(canvas, layer));
3189
3162
  return [layerRect(canvas, scope), ...rects].filter((rect) => !!rect);
@@ -3223,21 +3196,21 @@ function bindEditor(canvas, options = {}) {
3223
3196
  } else {
3224
3197
  let left = bounds.x;
3225
3198
  let top = bounds.y;
3226
- let right2 = bounds.x + bounds.width;
3227
- let bottom2 = bounds.y + bounds.height;
3228
- if (handle.includes("w")) left = Math.min(left + dx, right2 - 1);
3229
- if (handle.includes("e")) right2 = Math.max(right2 + dx, left + 1);
3230
- if (handle.includes("n")) top = Math.min(top + dy, bottom2 - 1);
3231
- if (handle.includes("s")) bottom2 = Math.max(bottom2 + dy, top + 1);
3232
- next = { x: left, y: top, width: right2 - left, height: bottom2 - top };
3199
+ let right = bounds.x + bounds.width;
3200
+ let bottom = bounds.y + bounds.height;
3201
+ if (handle.includes("w")) left = Math.min(left + dx, right - 1);
3202
+ if (handle.includes("e")) right = Math.max(right + dx, left + 1);
3203
+ if (handle.includes("n")) top = Math.min(top + dy, bottom - 1);
3204
+ if (handle.includes("s")) bottom = Math.max(bottom + dy, top + 1);
3205
+ next = { x: left, y: top, width: right - left, height: bottom - top };
3233
3206
  const corner = handle.length === 2;
3234
3207
  if (keepRatio && corner) {
3235
3208
  const scale = Math.max(next.width / bounds.width, next.height / bounds.height);
3236
3209
  const width = bounds.width * scale;
3237
3210
  const height = bounds.height * scale;
3238
3211
  next = {
3239
- x: handle.includes("w") ? right2 - width : left,
3240
- y: handle.includes("n") ? bottom2 - height : top,
3212
+ x: handle.includes("w") ? right - width : left,
3213
+ y: handle.includes("n") ? bottom - height : top,
3241
3214
  width,
3242
3215
  height
3243
3216
  };
@@ -3285,10 +3258,10 @@ function bindEditor(canvas, options = {}) {
3285
3258
  swallowClick = true;
3286
3259
  surface.setPointerCapture(event.pointerId);
3287
3260
  };
3288
- const handle = event.target.closest?.("[data-gpucanvas-handle]");
3261
+ const handle = handleAt(event.target);
3289
3262
  if (handle && transforms) {
3290
3263
  take();
3291
- return beginTransform(point, handle.dataset.gpucanvasHandle);
3264
+ return beginTransform(point, handle);
3292
3265
  }
3293
3266
  const target = targetIn(chainAt(point));
3294
3267
  if (!target) {
@@ -3425,7 +3398,7 @@ function bindEditor(canvas, options = {}) {
3425
3398
  } else if (event.key === "Tab") {
3426
3399
  event.preventDefault();
3427
3400
  const scope = scopes.at(-1);
3428
- const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn({ x: -1e9, y: -1e9, width: 2e9, height: 2e9 }).map((node) => [node]);
3401
+ const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn(EVERYWHERE).map((node) => [node]);
3429
3402
  const index = siblings.findIndex((layer) => sameLayer(layer, selection[0]));
3430
3403
  const step = event.shiftKey ? -1 : 1;
3431
3404
  const next = siblings[(index + step + siblings.length) % siblings.length];
@@ -3485,7 +3458,7 @@ var pillStyle = (color) => ({
3485
3458
  whiteSpace: "nowrap",
3486
3459
  pointerEvents: "none"
3487
3460
  });
3488
- function createMarks(canvas, { color = "#4f8cff" } = {}) {
3461
+ function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
3489
3462
  const id = `gpucanvas-marks-${++instances}`;
3490
3463
  const root = document.createElement("div");
3491
3464
  Object.assign(root.style, {
@@ -3528,11 +3501,7 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
3528
3501
  return ref;
3529
3502
  };
3530
3503
  const toScreen = (point) => canvas.worldToScreen(point);
3531
- const screenRect = (rect) => {
3532
- const a = toScreen(rect);
3533
- const b = toScreen({ x: rect.x + rect.width, y: rect.y + rect.height });
3534
- return { x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y };
3535
- };
3504
+ const screenRect = (rect) => rectToScreen(canvas.getCamera(), rect);
3536
3505
  const worldRect = (target) => target instanceof Element ? canvas.getElementRect(target) : { x: target.x, y: target.y, width: 0, height: 0 };
3537
3506
  const place = (element, x, y) => {
3538
3507
  const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
@@ -3878,6 +3847,7 @@ export {
3878
3847
  EFFECTS,
3879
3848
  supportsGpuCanvas,
3880
3849
  createAgentView,
3850
+ defaultLabel,
3881
3851
  bindEditor,
3882
3852
  createMarks,
3883
3853
  toBlob,
@@ -3888,4 +3858,4 @@ export {
3888
3858
  swapNodes,
3889
3859
  createGpuCanvas2 as createGpuCanvas
3890
3860
  };
3891
- //# sourceMappingURL=chunk-TC6XBFOY.js.map
3861
+ //# sourceMappingURL=chunk-IJ3R6X75.js.map