gpucanvas 0.4.3 → 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.
- package/dist/{chunk-2EDYGRFR.js → chunk-IJ3R6X75.js} +557 -589
- package/dist/chunk-IJ3R6X75.js.map +1 -0
- package/dist/{editor-CuH4tLHg.d.ts → editor-B5nVs54L.d.ts} +7 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +4 -4
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2EDYGRFR.js.map +0 -1
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
794
|
+
styleNode(element);
|
|
716
795
|
element.style.position = "absolute";
|
|
717
|
-
|
|
718
|
-
this.updateNode(element, rect);
|
|
796
|
+
updateNode(element, rect);
|
|
719
797
|
world.append(element);
|
|
720
798
|
},
|
|
721
|
-
updateNode
|
|
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 =
|
|
829
|
+
const view = base.view();
|
|
774
830
|
let visible = 0;
|
|
775
|
-
for (const
|
|
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
|
-
|
|
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.
|
|
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].
|
|
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
|
-
|
|
913
|
-
|
|
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:
|
|
924
|
-
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 >
|
|
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 >
|
|
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
|
|
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
|
-
|
|
1163
|
-
|
|
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) =>
|
|
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
|
-
/**
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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;
|
|
1360
|
+
}
|
|
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;
|
|
1227
1392
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
|
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
|
|
1334
|
-
|
|
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,6 +1546,7 @@ 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;
|
|
@@ -1399,14 +1559,6 @@ function rasterScaleFor(deviceScale) {
|
|
|
1399
1559
|
const scale = 2 ** Math.ceil(Math.log2(deviceScale) - 0.14);
|
|
1400
1560
|
return Math.min(MAX_DETAIL_SCALE, Math.max(MIN_RASTER_SCALE, scale));
|
|
1401
1561
|
}
|
|
1402
|
-
var intersection = (a, b) => {
|
|
1403
|
-
const x = Math.max(a.x, b.x);
|
|
1404
|
-
const y = Math.max(a.y, b.y);
|
|
1405
|
-
const right2 = Math.min(a.x + a.width, b.x + b.width);
|
|
1406
|
-
const bottom2 = Math.min(a.y + a.height, b.y + b.height);
|
|
1407
|
-
return right2 > x && bottom2 > y ? { x, y, width: right2 - x, height: bottom2 - y } : null;
|
|
1408
|
-
};
|
|
1409
|
-
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;
|
|
1410
1562
|
function cssColorToRgba(color) {
|
|
1411
1563
|
const context = document.createElement("canvas").getContext("2d");
|
|
1412
1564
|
context.fillStyle = color;
|
|
@@ -1415,37 +1567,58 @@ function cssColorToRgba(color) {
|
|
|
1415
1567
|
return [r / 255, g / 255, b / 255, a / 255];
|
|
1416
1568
|
}
|
|
1417
1569
|
var premultiplied = ([r, g, b, a]) => [r * a, g * a, b * a, a];
|
|
1418
|
-
var distanceToCenter = (rect, view) =>
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
);
|
|
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
|
+
};
|
|
1422
1575
|
async function createGpuCanvas(host, options) {
|
|
1423
1576
|
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
1424
1577
|
if (!adapter) throw new Error("gpucanvas: no WebGPU adapter");
|
|
1425
1578
|
const device = await adapter.requestDevice();
|
|
1426
|
-
const surface = document.createElement("div");
|
|
1427
|
-
surface.dataset.gpucanvasSurface = "";
|
|
1428
|
-
Object.assign(surface.style, {
|
|
1429
|
-
position: "relative",
|
|
1430
|
-
width: "100%",
|
|
1431
|
-
height: "100%",
|
|
1432
|
-
overflow: "hidden",
|
|
1433
|
-
touchAction: "none"
|
|
1434
|
-
});
|
|
1435
1579
|
const canvas = document.createElement("canvas");
|
|
1436
1580
|
canvas.setAttribute("content", "drawable");
|
|
1437
|
-
canvas
|
|
1581
|
+
markSurface(canvas);
|
|
1438
1582
|
Object.assign(canvas.style, { display: "block", width: "100%", height: "100%" });
|
|
1439
|
-
surface.append(canvas);
|
|
1440
|
-
host.append(surface);
|
|
1441
1583
|
const context = canvas.getContext("webgpu");
|
|
1442
1584
|
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
1443
|
-
|
|
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);
|
|
1444
1604
|
const atlasSize = Math.min(options.atlasSize ?? 4096, device.limits.maxTextureDimension2D);
|
|
1445
1605
|
const atlas = new Atlas(
|
|
1446
1606
|
device,
|
|
1447
1607
|
atlasSize,
|
|
1448
|
-
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
|
+
}
|
|
1449
1622
|
);
|
|
1450
1623
|
const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, atlasSize);
|
|
1451
1624
|
const maxRasterCalls = options.maxRastersPerFrame ?? 12;
|
|
@@ -1465,18 +1638,16 @@ async function createGpuCanvas(host, options) {
|
|
|
1465
1638
|
context,
|
|
1466
1639
|
format,
|
|
1467
1640
|
atlas,
|
|
1468
|
-
...colorsFor(options.background ?? "#f4f4f5")
|
|
1641
|
+
...colorsFor(options.background ?? "#f4f4f5"),
|
|
1642
|
+
() => schedule()
|
|
1469
1643
|
);
|
|
1470
1644
|
const selectionColor = premultiplied(
|
|
1471
1645
|
cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
|
|
1472
1646
|
);
|
|
1473
1647
|
const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
|
|
1474
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
1475
1648
|
const effects = /* @__PURE__ */ new Map();
|
|
1476
1649
|
const startedAt = performance.now();
|
|
1477
1650
|
const FAR_AWAY = { x: -1e7, y: -1e7 };
|
|
1478
|
-
renderer.onEffectReady = () => schedule();
|
|
1479
|
-
const runs = [];
|
|
1480
1651
|
let effectsMoving = false;
|
|
1481
1652
|
let selection = /* @__PURE__ */ new Set();
|
|
1482
1653
|
let hovered = null;
|
|
@@ -1492,22 +1663,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1492
1663
|
return rect;
|
|
1493
1664
|
};
|
|
1494
1665
|
const pages = /* @__PURE__ */ new Map();
|
|
1495
|
-
atlas.keep = function* () {
|
|
1496
|
-
const onScreen = (tile) => intersects(tile.rect, frameView);
|
|
1497
|
-
for (const node of visible) yield node.slot;
|
|
1498
|
-
for (const node of visible) {
|
|
1499
|
-
for (const tile of node.tiles) {
|
|
1500
|
-
if (tile.scale === detailScale && !tile.stale && onScreen(tile)) yield tile.slot;
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
for (const node of visible) {
|
|
1504
|
-
yield node.preview;
|
|
1505
|
-
for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
1666
|
const pageByElement = /* @__PURE__ */ new Map();
|
|
1509
1667
|
let openPage = null;
|
|
1510
|
-
let camera = options.camera ?? DEFAULT_CAMERA;
|
|
1511
1668
|
let lastCameraChange = -Infinity;
|
|
1512
1669
|
let frameNumber = 0;
|
|
1513
1670
|
let promotionsPerFrame = 2;
|
|
@@ -1545,6 +1702,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1545
1702
|
frame(false);
|
|
1546
1703
|
});
|
|
1547
1704
|
};
|
|
1705
|
+
const canvasDpr = () => canvas.width / Math.max(1, canvas.clientWidth);
|
|
1548
1706
|
const resizeObserver = new ResizeObserver(([entry]) => {
|
|
1549
1707
|
const size = entry.devicePixelContentBoxSize?.[0];
|
|
1550
1708
|
canvas.width = Math.max(
|
|
@@ -1560,7 +1718,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1560
1718
|
resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
|
|
1561
1719
|
const transitioning = /* @__PURE__ */ new Map();
|
|
1562
1720
|
const captures = [];
|
|
1563
|
-
const
|
|
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);
|
|
1564
1723
|
let detailScale = 1;
|
|
1565
1724
|
let frameView = { x: 0, y: 0, width: 0, height: 0 };
|
|
1566
1725
|
let contentTimer = 0;
|
|
@@ -1570,7 +1729,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1570
1729
|
};
|
|
1571
1730
|
let refreshTimer = 0;
|
|
1572
1731
|
let refreshAt = Infinity;
|
|
1573
|
-
const
|
|
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;
|
|
1574
1734
|
const refreshLater = (wait) => {
|
|
1575
1735
|
const at = performance.now() + wait;
|
|
1576
1736
|
if (at >= refreshAt) return;
|
|
@@ -1658,8 +1818,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1658
1818
|
const size = Math.max(1, Math.floor(TILE_PIXELS / detailScale));
|
|
1659
1819
|
const jobs = [];
|
|
1660
1820
|
const first = (edge, origin) => Math.floor((edge - origin) / size);
|
|
1661
|
-
for (let row = first(shown.y, node.rect.y); row <= first(
|
|
1662
|
-
for (let column = first(shown.x, node.rect.x); column <= first(
|
|
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++) {
|
|
1663
1823
|
if (current.some((tile) => tile.column === column && tile.row === row)) continue;
|
|
1664
1824
|
const rect = intersection(node.rect, {
|
|
1665
1825
|
x: node.rect.x + column * size,
|
|
@@ -1713,15 +1873,24 @@ async function createGpuCanvas(host, options) {
|
|
|
1713
1873
|
}
|
|
1714
1874
|
};
|
|
1715
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
|
+
});
|
|
1716
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
|
+
};
|
|
1717
1889
|
const drawn = (node, slot, scale, now) => {
|
|
1718
1890
|
node.slot = slot;
|
|
1719
1891
|
node.slotScale = scale;
|
|
1720
|
-
if (scale === THUMBNAIL_SCALE)
|
|
1721
|
-
|
|
1722
|
-
node.thumbStale = false;
|
|
1723
|
-
node.thumbAt = now;
|
|
1724
|
-
} else if (node.dirty) node.thumbStale = true;
|
|
1892
|
+
if (scale === THUMBNAIL_SCALE) setThumb(node, slot, now);
|
|
1893
|
+
else if (node.dirty) node.thumbStale = true;
|
|
1725
1894
|
if (node.dirty) {
|
|
1726
1895
|
node.preview = null;
|
|
1727
1896
|
node.refreshedAt = now;
|
|
@@ -1784,17 +1953,11 @@ async function createGpuCanvas(host, options) {
|
|
|
1784
1953
|
const thumbnail = node.targetScale === THUMBNAIL_SCALE;
|
|
1785
1954
|
const slot = slotFor(
|
|
1786
1955
|
thumbnail ? node.thumb : node.slot,
|
|
1787
|
-
|
|
1788
|
-
|
|
1956
|
+
texels(node.rect.width, node.targetScale),
|
|
1957
|
+
texels(node.rect.height, node.targetScale),
|
|
1789
1958
|
thumbnail
|
|
1790
1959
|
);
|
|
1791
|
-
|
|
1792
|
-
x: node.pageX,
|
|
1793
|
-
y: node.pageY,
|
|
1794
|
-
width: node.rect.width,
|
|
1795
|
-
height: node.rect.height
|
|
1796
|
-
};
|
|
1797
|
-
if (!slot || !drawSnapshot(page, region, slot)) {
|
|
1960
|
+
if (!slot || !drawSnapshot(page, nodeRegion(node), slot)) {
|
|
1798
1961
|
pending++;
|
|
1799
1962
|
continue;
|
|
1800
1963
|
}
|
|
@@ -1807,11 +1970,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1807
1970
|
const warm = /* @__PURE__ */ new Set();
|
|
1808
1971
|
const drawRegion = (node, rect, scale2, previous) => {
|
|
1809
1972
|
if (!warm.has(scale2) && overBudget()) return null;
|
|
1810
|
-
const slot = slotFor(
|
|
1811
|
-
previous,
|
|
1812
|
-
Math.max(1, Math.round(rect.width * scale2)),
|
|
1813
|
-
Math.max(1, Math.round(rect.height * scale2))
|
|
1814
|
-
);
|
|
1973
|
+
const slot = slotFor(previous, texels(rect.width, scale2), texels(rect.height, scale2));
|
|
1815
1974
|
const region = {
|
|
1816
1975
|
x: node.pageX + rect.x - node.rect.x,
|
|
1817
1976
|
y: node.pageY + rect.y - node.rect.y,
|
|
@@ -1862,21 +2021,13 @@ async function createGpuCanvas(host, options) {
|
|
|
1862
2021
|
if (atlas.isValid(node.thumb) && start - node.thumbAt < THUMBNAIL_REFRESH_MS) continue;
|
|
1863
2022
|
const slot = slotFor(
|
|
1864
2023
|
node.thumb,
|
|
1865
|
-
|
|
1866
|
-
|
|
2024
|
+
texels(node.rect.width, THUMBNAIL_SCALE),
|
|
2025
|
+
texels(node.rect.height, THUMBNAIL_SCALE),
|
|
1867
2026
|
true
|
|
1868
2027
|
);
|
|
1869
|
-
|
|
1870
|
-
x: node.pageX,
|
|
1871
|
-
y: node.pageY,
|
|
1872
|
-
width: node.rect.width,
|
|
1873
|
-
height: node.rect.height
|
|
1874
|
-
};
|
|
1875
|
-
if (!slot || !drawSnapshot(page, region, slot)) continue;
|
|
2028
|
+
if (!slot || !drawSnapshot(page, nodeRegion(node), slot)) continue;
|
|
1876
2029
|
calls++;
|
|
1877
|
-
node
|
|
1878
|
-
node.thumbStale = false;
|
|
1879
|
-
node.thumbAt = start;
|
|
2030
|
+
setThumb(node, slot, start);
|
|
1880
2031
|
}
|
|
1881
2032
|
const done = plan.nodes.every((node) => !node.dirty && atlas.isValid(node.slot)) && plan.previews.every((node) => node.previewTarget === null) && tilesLeft === 0;
|
|
1882
2033
|
if (done && !isPinned(page)) page.setDrawable(false, frameNumber);
|
|
@@ -1889,76 +2040,17 @@ async function createGpuCanvas(host, options) {
|
|
|
1889
2040
|
let instances2 = visible.length;
|
|
1890
2041
|
for (const node of visible) instances2 += node.tiles.length + (node.preview ? 1 : 0);
|
|
1891
2042
|
instances2 += selection.size + 1;
|
|
1892
|
-
renderer.
|
|
1893
|
-
const data = renderer.instances;
|
|
1894
|
-
let count = 0;
|
|
1895
|
-
runs.length = 0;
|
|
2043
|
+
renderer.begin(instances2);
|
|
1896
2044
|
effectsMoving = false;
|
|
1897
|
-
let runEffect = null;
|
|
1898
|
-
const startRun = (effect) => {
|
|
1899
|
-
if (runs.length && runEffect === effect) return;
|
|
1900
|
-
runEffect = effect;
|
|
1901
|
-
runs.push({ effect, start: count, count: 0 });
|
|
1902
|
-
};
|
|
1903
|
-
const push = (rect, slot) => {
|
|
1904
|
-
startRun(null);
|
|
1905
|
-
runs[runs.length - 1].count++;
|
|
1906
|
-
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1907
|
-
data[offset] = rect.x;
|
|
1908
|
-
data[offset + 1] = rect.y;
|
|
1909
|
-
data[offset + 2] = rect.width;
|
|
1910
|
-
data[offset + 3] = rect.height;
|
|
1911
|
-
if (slot) {
|
|
1912
|
-
atlas.touch(slot.layer);
|
|
1913
|
-
data[offset + 4] = slot.x;
|
|
1914
|
-
data[offset + 5] = slot.y;
|
|
1915
|
-
data[offset + 6] = slot.x + slot.width;
|
|
1916
|
-
data[offset + 7] = slot.y + slot.height;
|
|
1917
|
-
data[offset + 8] = slot.layer;
|
|
1918
|
-
data[offset + 9] = 0;
|
|
1919
|
-
} else {
|
|
1920
|
-
data[offset + 9] = 1;
|
|
1921
|
-
}
|
|
1922
|
-
data[offset + 10] = 0;
|
|
1923
|
-
};
|
|
1924
|
-
const pushEffect = (node, slot, worn) => {
|
|
1925
|
-
startRun(worn.effect);
|
|
1926
|
-
runs[runs.length - 1].count++;
|
|
1927
|
-
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1928
|
-
const { rect } = node;
|
|
1929
|
-
data[offset] = rect.x;
|
|
1930
|
-
data[offset + 1] = rect.y;
|
|
1931
|
-
data[offset + 2] = rect.width;
|
|
1932
|
-
data[offset + 3] = rect.height;
|
|
1933
|
-
atlas.touch(slot.layer);
|
|
1934
|
-
data[offset + 4] = slot.x;
|
|
1935
|
-
data[offset + 5] = slot.y;
|
|
1936
|
-
data[offset + 6] = slot.x + slot.width;
|
|
1937
|
-
data[offset + 7] = slot.y + slot.height;
|
|
1938
|
-
data[offset + 8] = slot.layer;
|
|
1939
|
-
data[offset + 9] = 0;
|
|
1940
|
-
data[offset + 10] = worn.pad;
|
|
1941
|
-
data.set(worn.params, offset + 12);
|
|
1942
|
-
};
|
|
1943
2045
|
const pushRing = (element, color, width) => {
|
|
1944
2046
|
const rect = ringRect(element);
|
|
1945
|
-
if (
|
|
1946
|
-
startRun(null);
|
|
1947
|
-
runs[runs.length - 1].count++;
|
|
1948
|
-
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1949
|
-
data[offset] = rect.x;
|
|
1950
|
-
data[offset + 1] = rect.y;
|
|
1951
|
-
data[offset + 2] = rect.width;
|
|
1952
|
-
data[offset + 3] = rect.height;
|
|
1953
|
-
data.set(color, offset + 4);
|
|
1954
|
-
data[offset + 9] = 2;
|
|
1955
|
-
data[offset + 10] = width;
|
|
2047
|
+
if (rect && intersects(rect, frameView)) renderer.ring(rect, color, width);
|
|
1956
2048
|
};
|
|
1957
2049
|
let onThumbnail = 0;
|
|
1958
2050
|
for (const node of visible) {
|
|
1959
|
-
const
|
|
1960
|
-
if (
|
|
1961
|
-
const hasSlot =
|
|
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;
|
|
1962
2054
|
const sharp = node.tiles.length > 0 || node.preview !== null && atlas.isValid(node.preview);
|
|
1963
2055
|
const shown = sharp ? 3 : hasSlot ? 2 : 1;
|
|
1964
2056
|
const before = node.shownFrame === stats.frames - 1 ? node.shown : 0;
|
|
@@ -1967,29 +2059,31 @@ async function createGpuCanvas(host, options) {
|
|
|
1967
2059
|
node.shown = shown;
|
|
1968
2060
|
node.shownFrame = stats.frames;
|
|
1969
2061
|
const worn = effects.size ? effects.get(node.element) : void 0;
|
|
1970
|
-
if (worn) {
|
|
2062
|
+
if (worn && !renderer.effectFailed(worn.effect)) {
|
|
1971
2063
|
if (worn.effect.animated) effectsMoving = true;
|
|
1972
|
-
if (
|
|
1973
|
-
|
|
2064
|
+
if (image && renderer.hasEffect(worn.effect)) {
|
|
2065
|
+
renderer.effectQuad(node.rect, image, worn.effect, worn.pad, worn.params);
|
|
1974
2066
|
continue;
|
|
1975
2067
|
}
|
|
1976
2068
|
effectsMoving = true;
|
|
1977
2069
|
}
|
|
1978
|
-
|
|
2070
|
+
renderer.quad(node.rect, image);
|
|
1979
2071
|
const current = (tile) => tile.scale === detailScale && !tile.stale;
|
|
1980
2072
|
for (const tile of node.tiles)
|
|
1981
|
-
if (!current(tile) && atlas.isValid(tile.slot))
|
|
1982
|
-
if (node.preview && atlas.isValid(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);
|
|
1983
2076
|
for (const tile of node.tiles)
|
|
1984
|
-
if (current(tile) && atlas.isValid(tile.slot))
|
|
2077
|
+
if (current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
|
|
1985
2078
|
}
|
|
1986
2079
|
stats.onThumbnail = onThumbnail;
|
|
1987
2080
|
for (const element of selection) pushRing(element, selectionColor, SELECTION_WIDTH);
|
|
1988
2081
|
if (hovered && !selection.has(hovered)) pushRing(hovered, hoverColor, HOVER_WIDTH);
|
|
1989
2082
|
const cssWidth = canvas.clientWidth;
|
|
1990
|
-
const dpr =
|
|
2083
|
+
const dpr = canvasDpr();
|
|
1991
2084
|
const now = performance.now();
|
|
1992
|
-
|
|
2085
|
+
const camera = base.camera();
|
|
2086
|
+
renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, {
|
|
1993
2087
|
time: (now - startedAt) / 1e3,
|
|
1994
2088
|
pointer: pointer ? screenToWorld(camera, pointer) : FAR_AWAY,
|
|
1995
2089
|
press,
|
|
@@ -2000,9 +2094,9 @@ async function createGpuCanvas(host, options) {
|
|
|
2000
2094
|
if (destroyed) return;
|
|
2001
2095
|
const start = performance.now();
|
|
2002
2096
|
atlas.nextFrame();
|
|
2003
|
-
const
|
|
2004
|
-
const
|
|
2005
|
-
const view =
|
|
2097
|
+
const dpr = canvasDpr();
|
|
2098
|
+
const camera = base.camera();
|
|
2099
|
+
const view = base.view();
|
|
2006
2100
|
const moving = start - lastCameraChange < SETTLE_MS;
|
|
2007
2101
|
planFrame(view, rasterScaleFor(camera.zoom * dpr), camera.zoom * dpr, moving);
|
|
2008
2102
|
if (!inPaint && plans.size > 0) {
|
|
@@ -2044,69 +2138,37 @@ async function createGpuCanvas(host, options) {
|
|
|
2044
2138
|
const drawCaptures = () => {
|
|
2045
2139
|
for (const capture of [...captures]) {
|
|
2046
2140
|
const { node, scale } = capture;
|
|
2047
|
-
|
|
2048
|
-
const height = Math.max(1, Math.round(node.rect.height * scale));
|
|
2049
|
-
const texture = device.createTexture({
|
|
2050
|
-
size: { width, height },
|
|
2051
|
-
format: "rgba8unorm",
|
|
2052
|
-
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
2053
|
-
});
|
|
2141
|
+
let image;
|
|
2054
2142
|
try {
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
sourceHeight: node.rect.height
|
|
2062
|
-
},
|
|
2063
|
-
{ 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)
|
|
2064
2149
|
);
|
|
2065
2150
|
} catch (error) {
|
|
2066
|
-
|
|
2067
|
-
if (++capture.tries > 20) {
|
|
2151
|
+
if (++capture.tries > MAX_CAPTURE_TRIES) {
|
|
2068
2152
|
captures.splice(captures.indexOf(capture), 1);
|
|
2069
2153
|
capture.reject(error instanceof Error ? error : new Error(String(error)));
|
|
2070
2154
|
}
|
|
2071
2155
|
continue;
|
|
2072
2156
|
}
|
|
2073
2157
|
captures.splice(captures.indexOf(capture), 1);
|
|
2074
|
-
|
|
2158
|
+
image.then(capture.resolve, capture.reject);
|
|
2075
2159
|
}
|
|
2076
2160
|
if (captures.length) {
|
|
2077
2161
|
awaitingPaint = true;
|
|
2078
2162
|
canvas.requestPaint();
|
|
2079
2163
|
} else schedule();
|
|
2080
2164
|
};
|
|
2081
|
-
const readTexture = async (texture, width, height) => {
|
|
2082
|
-
const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
|
|
2083
|
-
const buffer = device.createBuffer({
|
|
2084
|
-
size: bytesPerRow * height,
|
|
2085
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
2086
|
-
});
|
|
2087
|
-
const encoder = device.createCommandEncoder();
|
|
2088
|
-
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
|
|
2089
|
-
device.queue.submit([encoder.finish()]);
|
|
2090
|
-
try {
|
|
2091
|
-
await buffer.mapAsync(GPUMapMode.READ);
|
|
2092
|
-
const rows = new Uint8Array(buffer.getMappedRange());
|
|
2093
|
-
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
2094
|
-
for (let y = 0; y < height; y++) {
|
|
2095
|
-
pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
2096
|
-
}
|
|
2097
|
-
return new ImageData(pixels, width, height);
|
|
2098
|
-
} finally {
|
|
2099
|
-
buffer.destroy();
|
|
2100
|
-
texture.destroy();
|
|
2101
|
-
}
|
|
2102
|
-
};
|
|
2103
2165
|
canvas.onpaint = (event) => {
|
|
2104
2166
|
if (captures.length) drawCaptures();
|
|
2105
2167
|
let changed = false;
|
|
2106
2168
|
for (const element of event.changedElements) {
|
|
2107
2169
|
const page = pageByElement.get(element);
|
|
2108
2170
|
if (!page) continue;
|
|
2109
|
-
for (const node of
|
|
2171
|
+
for (const node of heldNodes()) {
|
|
2110
2172
|
if (node?.page === page) {
|
|
2111
2173
|
node.dirty = true;
|
|
2112
2174
|
changed = true;
|
|
@@ -2118,13 +2180,10 @@ async function createGpuCanvas(host, options) {
|
|
|
2118
2180
|
awaitingPaint = false;
|
|
2119
2181
|
frame(true);
|
|
2120
2182
|
};
|
|
2121
|
-
const viewNow = () => visibleWorldRect(camera, canvas.clientWidth, canvas.clientHeight);
|
|
2122
2183
|
const nodeOf = (target) => {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
}
|
|
2127
|
-
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;
|
|
2128
2187
|
};
|
|
2129
2188
|
const mutations = new MutationObserver((records) => {
|
|
2130
2189
|
ringRects.clear();
|
|
@@ -2137,7 +2196,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2137
2196
|
const node = nodeOf(record.target);
|
|
2138
2197
|
if (node && !node.dirty) {
|
|
2139
2198
|
node.dirty = true;
|
|
2140
|
-
if (!changed) changed = intersects(node.rect,
|
|
2199
|
+
if (!changed) changed = intersects(node.rect, base.view());
|
|
2141
2200
|
}
|
|
2142
2201
|
}
|
|
2143
2202
|
if (changed) schedule();
|
|
@@ -2155,8 +2214,13 @@ async function createGpuCanvas(host, options) {
|
|
|
2155
2214
|
page.element.style.transform = "";
|
|
2156
2215
|
schedule();
|
|
2157
2216
|
};
|
|
2217
|
+
const letGo = (node) => {
|
|
2218
|
+
node.dirty = true;
|
|
2219
|
+
unpin(node.page);
|
|
2220
|
+
};
|
|
2158
2221
|
const positionLivePage = () => {
|
|
2159
2222
|
if (!live || livePositioned) return;
|
|
2223
|
+
const camera = base.camera();
|
|
2160
2224
|
const { zoom } = camera;
|
|
2161
2225
|
const x = (live.rect.x - camera.x - live.pageX) * zoom;
|
|
2162
2226
|
const y = (live.rect.y - camera.y - live.pageY) * zoom;
|
|
@@ -2166,7 +2230,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2166
2230
|
canvasTransform: new DOMMatrix([zoom, 0, 0, zoom, x, y])
|
|
2167
2231
|
});
|
|
2168
2232
|
} else {
|
|
2169
|
-
const dpr =
|
|
2233
|
+
const dpr = canvasDpr();
|
|
2170
2234
|
const drawn2 = new DOMMatrix([zoom, 0, 0, zoom, x * dpr, y * dpr]);
|
|
2171
2235
|
live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
|
|
2172
2236
|
}
|
|
@@ -2185,16 +2249,12 @@ async function createGpuCanvas(host, options) {
|
|
|
2185
2249
|
clearTimeout(lingerTimer);
|
|
2186
2250
|
const earlier = lingering;
|
|
2187
2251
|
lingering = previous;
|
|
2188
|
-
if (earlier && earlier !== previous)
|
|
2189
|
-
earlier.dirty = true;
|
|
2190
|
-
unpin(earlier.page);
|
|
2191
|
-
}
|
|
2252
|
+
if (earlier && earlier !== previous) letGo(earlier);
|
|
2192
2253
|
lingerTimer = window.setTimeout(() => {
|
|
2193
2254
|
const last = lingering;
|
|
2194
2255
|
if (!last) return;
|
|
2195
2256
|
lingering = null;
|
|
2196
|
-
last
|
|
2197
|
-
unpin(last.page);
|
|
2257
|
+
letGo(last);
|
|
2198
2258
|
}, LINGER_MS);
|
|
2199
2259
|
unpin(previous.page);
|
|
2200
2260
|
}
|
|
@@ -2211,14 +2271,10 @@ async function createGpuCanvas(host, options) {
|
|
|
2211
2271
|
};
|
|
2212
2272
|
const pickLive = () => {
|
|
2213
2273
|
if (!pointer) return setLive(null);
|
|
2214
|
-
|
|
2215
|
-
let hit = null;
|
|
2216
|
-
for (const node of nodes.values()) if (containsPoint(node.rect, point)) hit = node;
|
|
2217
|
-
setLive(hit);
|
|
2274
|
+
setLive(base.nodeAt(screenToWorld(base.camera(), pointer)));
|
|
2218
2275
|
};
|
|
2219
2276
|
const onPointerMove = (event) => {
|
|
2220
|
-
|
|
2221
|
-
pointer = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
2277
|
+
pointer = pointIn(canvas, event);
|
|
2222
2278
|
if (event.buttons === 0) pickLive();
|
|
2223
2279
|
};
|
|
2224
2280
|
const onPointerLeave = () => {
|
|
@@ -2229,9 +2285,8 @@ async function createGpuCanvas(host, options) {
|
|
|
2229
2285
|
let pressedAt = -Infinity;
|
|
2230
2286
|
let tapped = false;
|
|
2231
2287
|
const onPointerDown = (event) => {
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
press = screenToWorld(camera, at);
|
|
2288
|
+
const at = pointIn(canvas, event);
|
|
2289
|
+
press = screenToWorld(base.camera(), at);
|
|
2235
2290
|
pressedAt = performance.now();
|
|
2236
2291
|
if (event.pointerType !== "mouse") {
|
|
2237
2292
|
pointer = at;
|
|
@@ -2243,13 +2298,8 @@ async function createGpuCanvas(host, options) {
|
|
|
2243
2298
|
const onClick = (event) => {
|
|
2244
2299
|
if (!tapped) return;
|
|
2245
2300
|
tapped = false;
|
|
2246
|
-
const
|
|
2247
|
-
const
|
|
2248
|
-
x: event.clientX - bounds.left,
|
|
2249
|
-
y: event.clientY - bounds.top
|
|
2250
|
-
});
|
|
2251
|
-
let top = null;
|
|
2252
|
-
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);
|
|
2253
2303
|
if (!top || top.element.contains(event.target)) return;
|
|
2254
2304
|
let target = elementInNode(top.element, top.rect, point);
|
|
2255
2305
|
while (target && !(target instanceof HTMLElement)) target = target.parentElement;
|
|
@@ -2271,10 +2321,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2271
2321
|
if (node === focused) return;
|
|
2272
2322
|
const previous = focused;
|
|
2273
2323
|
focused = node;
|
|
2274
|
-
if (previous)
|
|
2275
|
-
previous.dirty = true;
|
|
2276
|
-
unpin(previous.page);
|
|
2277
|
-
}
|
|
2324
|
+
if (previous) letGo(previous);
|
|
2278
2325
|
if (node) {
|
|
2279
2326
|
node.page.setDrawable(true, frameNumber);
|
|
2280
2327
|
node.dirty = true;
|
|
@@ -2285,10 +2332,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2285
2332
|
if (event.relatedTarget && canvas.contains(event.relatedTarget)) return;
|
|
2286
2333
|
const previous = focused;
|
|
2287
2334
|
focused = null;
|
|
2288
|
-
if (previous)
|
|
2289
|
-
previous.dirty = true;
|
|
2290
|
-
unpin(previous.page);
|
|
2291
|
-
}
|
|
2335
|
+
if (previous) letGo(previous);
|
|
2292
2336
|
};
|
|
2293
2337
|
const onResourceLoad = (event) => {
|
|
2294
2338
|
const node = nodeOf(event.target);
|
|
@@ -2325,8 +2369,8 @@ async function createGpuCanvas(host, options) {
|
|
|
2325
2369
|
const node = nodeOf(event.target);
|
|
2326
2370
|
const now = performance.now();
|
|
2327
2371
|
if (!node || now - (usedAt.get(node) ?? -Infinity) > USED_MS) return;
|
|
2328
|
-
if (!intersects(node.rect,
|
|
2329
|
-
if (
|
|
2372
|
+
if (!intersects(node.rect, base.view())) return;
|
|
2373
|
+
if (smallOnScreen(node)) return;
|
|
2330
2374
|
const ms = transitionMs(event.target) + 32;
|
|
2331
2375
|
const end = now + ms;
|
|
2332
2376
|
if ((transitioning.get(node) ?? 0) >= end) return;
|
|
@@ -2334,38 +2378,34 @@ async function createGpuCanvas(host, options) {
|
|
|
2334
2378
|
node.dirty = true;
|
|
2335
2379
|
schedule();
|
|
2336
2380
|
setTimeout(() => {
|
|
2337
|
-
if (transitioning.get(node) !== end) return;
|
|
2381
|
+
if (destroyed || transitioning.get(node) !== end) return;
|
|
2338
2382
|
transitioning.delete(node);
|
|
2339
|
-
node
|
|
2340
|
-
unpin(node.page);
|
|
2383
|
+
letGo(node);
|
|
2341
2384
|
schedule();
|
|
2342
2385
|
}, ms);
|
|
2343
2386
|
};
|
|
2344
2387
|
for (const type of USE_EVENTS) canvas.addEventListener(type, onUse, true);
|
|
2345
2388
|
canvas.addEventListener("transitionrun", onTransitionRun);
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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();
|
|
2357
2401
|
};
|
|
2358
|
-
const unbindInput = bindCameraInput(surface, () => camera, setCamera);
|
|
2359
2402
|
const placeNode = (element, rect) => {
|
|
2360
2403
|
let slot = openPage?.place(rect.width, rect.height) ?? null;
|
|
2361
2404
|
if (!slot) {
|
|
2362
|
-
const page =
|
|
2405
|
+
const page = newPage(
|
|
2363
2406
|
Math.max(pageSize, Math.ceil(rect.width)),
|
|
2364
2407
|
Math.max(pageSize, Math.ceil(rect.height))
|
|
2365
2408
|
);
|
|
2366
|
-
pages.set(page, /* @__PURE__ */ new Set());
|
|
2367
|
-
pageByElement.set(page.element, page);
|
|
2368
|
-
canvas.append(page.element);
|
|
2369
2409
|
if (page.width === pageSize && page.height === pageSize) openPage = page;
|
|
2370
2410
|
slot = page.place(rect.width, rect.height);
|
|
2371
2411
|
}
|
|
@@ -2382,21 +2422,13 @@ async function createGpuCanvas(host, options) {
|
|
|
2382
2422
|
const releaseNode = (node) => {
|
|
2383
2423
|
const members = pages.get(node.page);
|
|
2384
2424
|
members.delete(node);
|
|
2385
|
-
node.page.
|
|
2386
|
-
if (members.size === 0 && node.page !== openPage) {
|
|
2387
|
-
pages.delete(node.page);
|
|
2388
|
-
pageByElement.delete(node.page.element);
|
|
2389
|
-
node.page.element.remove();
|
|
2390
|
-
}
|
|
2425
|
+
if (members.size === 0 && node.page !== openPage) dropPage(node.page);
|
|
2391
2426
|
};
|
|
2392
2427
|
const lift = (node) => {
|
|
2393
2428
|
const previous = node.page;
|
|
2394
2429
|
const wasLive = live === node;
|
|
2395
2430
|
if (wasLive) setLive(null);
|
|
2396
|
-
const page =
|
|
2397
|
-
pages.set(page, /* @__PURE__ */ new Set());
|
|
2398
|
-
pageByElement.set(page.element, page);
|
|
2399
|
-
canvas.append(page.element);
|
|
2431
|
+
const page = newPage(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
|
|
2400
2432
|
page.place(node.rect.width, node.rect.height);
|
|
2401
2433
|
const target = page.element;
|
|
2402
2434
|
if (target.moveBefore) target.moveBefore(node.element, null);
|
|
@@ -2427,15 +2459,11 @@ async function createGpuCanvas(host, options) {
|
|
|
2427
2459
|
pages.get(slot.page).add(node);
|
|
2428
2460
|
};
|
|
2429
2461
|
return {
|
|
2462
|
+
...base.api,
|
|
2430
2463
|
mode: "gpu",
|
|
2431
|
-
surface,
|
|
2432
2464
|
addNode(element, rect) {
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
contain: "strict",
|
|
2436
|
-
overflow: "hidden",
|
|
2437
|
-
pointerEvents: "none"
|
|
2438
|
-
});
|
|
2465
|
+
styleNode(element);
|
|
2466
|
+
element.style.pointerEvents = "none";
|
|
2439
2467
|
const node = {
|
|
2440
2468
|
element,
|
|
2441
2469
|
rect,
|
|
@@ -2489,25 +2517,10 @@ async function createGpuCanvas(host, options) {
|
|
|
2489
2517
|
element.remove();
|
|
2490
2518
|
schedule();
|
|
2491
2519
|
},
|
|
2492
|
-
getNodeRect: (element) => {
|
|
2493
|
-
const node = nodes.get(element);
|
|
2494
|
-
return node ? { ...node.rect } : null;
|
|
2495
|
-
},
|
|
2496
|
-
getNodesIn(rect) {
|
|
2497
|
-
const hits = [];
|
|
2498
|
-
for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
|
|
2499
|
-
return hits;
|
|
2500
|
-
},
|
|
2501
|
-
getNodeOf: (element) => nodeOfElement(nodes, element),
|
|
2502
2520
|
getElementRect(element) {
|
|
2503
2521
|
const rect = ringRect(element);
|
|
2504
2522
|
return rect ? { ...rect } : null;
|
|
2505
2523
|
},
|
|
2506
|
-
getElementAt(point) {
|
|
2507
|
-
let top = null;
|
|
2508
|
-
for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
|
|
2509
|
-
return top ? elementInNode(top.element, top.rect, point) : null;
|
|
2510
|
-
},
|
|
2511
2524
|
setSelection(elements) {
|
|
2512
2525
|
selection = new Set(elements);
|
|
2513
2526
|
ringRects.clear();
|
|
@@ -2527,10 +2540,6 @@ async function createGpuCanvas(host, options) {
|
|
|
2527
2540
|
} else effects.delete(element);
|
|
2528
2541
|
schedule();
|
|
2529
2542
|
},
|
|
2530
|
-
screenToWorld: (point) => screenToWorld(camera, point),
|
|
2531
|
-
worldToScreen: (point) => worldToScreen(camera, point),
|
|
2532
|
-
getCamera: () => camera,
|
|
2533
|
-
setCamera,
|
|
2534
2543
|
invalidate: onFontsLoaded,
|
|
2535
2544
|
snapshot(element, options2 = {}) {
|
|
2536
2545
|
const node = nodes.get(element);
|
|
@@ -2549,10 +2558,6 @@ async function createGpuCanvas(host, options) {
|
|
|
2549
2558
|
[renderer.background, renderer.placeholder] = colorsFor(color);
|
|
2550
2559
|
schedule();
|
|
2551
2560
|
},
|
|
2552
|
-
fit(padding) {
|
|
2553
|
-
const bounds = boundsOf([...nodes.values()].map((node) => node.rect));
|
|
2554
|
-
if (bounds) setCamera(fitCamera(bounds, canvas.clientWidth, canvas.clientHeight, padding));
|
|
2555
|
-
},
|
|
2556
2561
|
getStats: () => ({
|
|
2557
2562
|
mode: "gpu",
|
|
2558
2563
|
nodes: nodes.size,
|
|
@@ -2578,6 +2583,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2578
2583
|
clearTimeout(settleTimer);
|
|
2579
2584
|
clearTimeout(lingerTimer);
|
|
2580
2585
|
clearTimeout(contentTimer);
|
|
2586
|
+
clearTimeout(refreshTimer);
|
|
2581
2587
|
mutations.disconnect();
|
|
2582
2588
|
document.fonts.removeEventListener("loadingdone", onFontsLoaded);
|
|
2583
2589
|
canvas.removeEventListener("load", onResourceLoad, true);
|
|
@@ -2589,10 +2595,9 @@ async function createGpuCanvas(host, options) {
|
|
|
2589
2595
|
canvas.removeEventListener("focusout", onFocusOut);
|
|
2590
2596
|
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
2591
2597
|
for (const type of USE_EVENTS) canvas.removeEventListener(type, onUse, true);
|
|
2592
|
-
unbindInput();
|
|
2593
2598
|
resizeObserver.disconnect();
|
|
2594
2599
|
canvas.onpaint = null;
|
|
2595
|
-
|
|
2600
|
+
base.destroy();
|
|
2596
2601
|
renderer.destroy();
|
|
2597
2602
|
atlas.destroy();
|
|
2598
2603
|
device.destroy();
|
|
@@ -2651,7 +2656,6 @@ var labelText = (element) => {
|
|
|
2651
2656
|
}
|
|
2652
2657
|
return text.replace(/\s+/g, " ").trim();
|
|
2653
2658
|
};
|
|
2654
|
-
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;
|
|
2655
2659
|
var round = (rect) => ({
|
|
2656
2660
|
x: Math.round(rect.x),
|
|
2657
2661
|
y: Math.round(rect.y),
|
|
@@ -2703,11 +2707,7 @@ function createAgentView(canvas) {
|
|
|
2703
2707
|
return {
|
|
2704
2708
|
/** What is in `rect` (default: the view), node by node, as plain JSON. */
|
|
2705
2709
|
describe(options = {}) {
|
|
2706
|
-
const area = options.rect ??
|
|
2707
|
-
canvas.getCamera(),
|
|
2708
|
-
canvas.surface.clientWidth,
|
|
2709
|
-
canvas.surface.clientHeight
|
|
2710
|
-
);
|
|
2710
|
+
const area = options.rect ?? viewOf(canvas);
|
|
2711
2711
|
return canvas.getNodesIn(area).flatMap((node) => read(node, options.depth ?? 6, options));
|
|
2712
2712
|
},
|
|
2713
2713
|
/** The element behind an id from `describe`, or null once it is gone. */
|
|
@@ -2752,25 +2752,7 @@ function createAgentView(canvas) {
|
|
|
2752
2752
|
// src/layers.ts
|
|
2753
2753
|
var sameLayer = (a, b) => !!a && !!b && a[0] === b[0];
|
|
2754
2754
|
var layerIn = (layer, list) => list.some((other) => sameLayer(layer, other));
|
|
2755
|
-
|
|
2756
|
-
let union = null;
|
|
2757
|
-
for (const element of layer) {
|
|
2758
|
-
const rect = canvas.getElementRect(element);
|
|
2759
|
-
if (rect) union = union ? unionRect(union, rect) : rect;
|
|
2760
|
-
}
|
|
2761
|
-
return union;
|
|
2762
|
-
}
|
|
2763
|
-
function unionRect(a, b) {
|
|
2764
|
-
const x = Math.min(a.x, b.x);
|
|
2765
|
-
const y = Math.min(a.y, b.y);
|
|
2766
|
-
return {
|
|
2767
|
-
x,
|
|
2768
|
-
y,
|
|
2769
|
-
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
2770
|
-
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
2771
|
-
};
|
|
2772
|
-
}
|
|
2773
|
-
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)));
|
|
2774
2756
|
function createLayers(canvas, pick) {
|
|
2775
2757
|
const chainOf = (element) => {
|
|
2776
2758
|
const node = canvas.getNodeOf(element);
|
|
@@ -2789,7 +2771,7 @@ function createLayers(canvas, pick) {
|
|
|
2789
2771
|
let previous = canvas.getNodeRect(node);
|
|
2790
2772
|
for (const layer of inner) {
|
|
2791
2773
|
const rect = layerRect(canvas, layer);
|
|
2792
|
-
if (
|
|
2774
|
+
if (sameRect(rect, previous)) continue;
|
|
2793
2775
|
chain.push(layer);
|
|
2794
2776
|
previous = rect;
|
|
2795
2777
|
}
|
|
@@ -2807,7 +2789,7 @@ function createLayers(canvas, pick) {
|
|
|
2807
2789
|
for (const child of element.children) {
|
|
2808
2790
|
const rect = canvas.getElementRect(child);
|
|
2809
2791
|
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
2810
|
-
if (
|
|
2792
|
+
if (sameRect(rect, outer)) visit(child, outer);
|
|
2811
2793
|
else children.push([child]);
|
|
2812
2794
|
}
|
|
2813
2795
|
};
|
|
@@ -2855,7 +2837,7 @@ var pill = (color) => ({
|
|
|
2855
2837
|
whiteSpace: "nowrap",
|
|
2856
2838
|
pointerEvents: "none"
|
|
2857
2839
|
});
|
|
2858
|
-
function createOverlay(surface, color
|
|
2840
|
+
function createOverlay(surface, color) {
|
|
2859
2841
|
const root = document.createElement("div");
|
|
2860
2842
|
Object.assign(root.style, {
|
|
2861
2843
|
position: "absolute",
|
|
@@ -2870,7 +2852,7 @@ function createOverlay(surface, color, hoverColor) {
|
|
|
2870
2852
|
root.append(element);
|
|
2871
2853
|
return element;
|
|
2872
2854
|
};
|
|
2873
|
-
const hoverName = make(pill(
|
|
2855
|
+
const hoverName = make(pill(color));
|
|
2874
2856
|
const name = make(pill(color));
|
|
2875
2857
|
const size = make(pill(color));
|
|
2876
2858
|
const bounds = make({ position: "absolute", border: `1px solid ${color}` });
|
|
@@ -2955,9 +2937,10 @@ function createOverlay(surface, color, hoverColor) {
|
|
|
2955
2937
|
}
|
|
2956
2938
|
};
|
|
2957
2939
|
}
|
|
2958
|
-
var
|
|
2959
|
-
|
|
2960
|
-
const
|
|
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 });
|
|
2961
2944
|
return {
|
|
2962
2945
|
x: Math.min(a.x, b.x),
|
|
2963
2946
|
y: Math.min(a.y, b.y),
|
|
@@ -3004,16 +2987,16 @@ function guidesFor(rect, targets, edges) {
|
|
|
3004
2987
|
const hits = targets.filter((target) => anchorsX(target).some((at) => close(at, x)));
|
|
3005
2988
|
if (!hits.length) continue;
|
|
3006
2989
|
const top = Math.min(rect.y, ...hits.map((t) => t.y));
|
|
3007
|
-
const
|
|
3008
|
-
guides.push({ x1: x, y1: top, x2: x, y2:
|
|
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 });
|
|
3009
2992
|
}
|
|
3010
2993
|
for (const f of edges.y) {
|
|
3011
2994
|
const y = rect.y + rect.height * f;
|
|
3012
2995
|
const hits = targets.filter((target) => anchorsY(target).some((at) => close(at, y)));
|
|
3013
2996
|
if (!hits.length) continue;
|
|
3014
2997
|
const left = Math.min(rect.x, ...hits.map((t) => t.x));
|
|
3015
|
-
const
|
|
3016
|
-
guides.push({ x1: left, y1: y, x2:
|
|
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 });
|
|
3017
3000
|
}
|
|
3018
3001
|
return guides;
|
|
3019
3002
|
}
|
|
@@ -3061,9 +3044,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
|
|
|
3061
3044
|
}
|
|
3062
3045
|
};
|
|
3063
3046
|
}
|
|
3047
|
+
var defaultLabel = (canvas) => (layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName;
|
|
3064
3048
|
var CLICK_SLOP = 4;
|
|
3065
3049
|
var SNAP_PX = 6;
|
|
3066
3050
|
var MAX_NAMED = 1;
|
|
3051
|
+
var EVERYWHERE = { x: -1e9, y: -1e9, width: 2e9, height: 2e9 };
|
|
3067
3052
|
var isEditable = (element) => !!element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable);
|
|
3068
3053
|
function bindEditor(canvas, options = {}) {
|
|
3069
3054
|
const {
|
|
@@ -3077,11 +3062,11 @@ function bindEditor(canvas, options = {}) {
|
|
|
3077
3062
|
onChange,
|
|
3078
3063
|
onTransform
|
|
3079
3064
|
} = options;
|
|
3080
|
-
const label = options.label ?? (
|
|
3065
|
+
const label = options.label ?? defaultLabel(canvas);
|
|
3081
3066
|
const { surface } = canvas;
|
|
3082
3067
|
const layers = createLayers(canvas, pick);
|
|
3083
3068
|
const transforms = !!onTransform && trigger === "click";
|
|
3084
|
-
const overlay = createOverlay(surface, color
|
|
3069
|
+
const overlay = createOverlay(surface, color);
|
|
3085
3070
|
let marqueeLayers = [];
|
|
3086
3071
|
const box = marquee ? createMarquee(canvas, color, (world) => {
|
|
3087
3072
|
marqueeLayers = layers.layersIn(scopes.at(-1) ?? null, world);
|
|
@@ -3108,10 +3093,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3108
3093
|
canvas.setHover(layer?.[0] ?? null);
|
|
3109
3094
|
draw();
|
|
3110
3095
|
};
|
|
3111
|
-
const at = (event) =>
|
|
3112
|
-
const bounds = surface.getBoundingClientRect();
|
|
3113
|
-
return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
3114
|
-
};
|
|
3096
|
+
const at = (event) => pointIn(surface, event);
|
|
3115
3097
|
const chainAt = (point) => layers.chainAt(canvas.screenToWorld(point));
|
|
3116
3098
|
const fieldAt = (point) => {
|
|
3117
3099
|
const element = canvas.getElementAt(canvas.screenToWorld(point));
|
|
@@ -3140,15 +3122,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3140
3122
|
const draw = () => {
|
|
3141
3123
|
if (!frame) frame = requestAnimationFrame(render);
|
|
3142
3124
|
};
|
|
3143
|
-
const toScreen = (rect) =>
|
|
3144
|
-
const { x, y } = canvas.worldToScreen(rect);
|
|
3145
|
-
const zoom = canvas.getCamera().zoom;
|
|
3146
|
-
return { x, y, width: rect.width * zoom, height: rect.height * zoom };
|
|
3147
|
-
};
|
|
3148
|
-
const unionOf = (rects) => rects.reduce(
|
|
3149
|
-
(union, rect) => !rect ? union : union ? unionRect(union, rect) : rect,
|
|
3150
|
-
null
|
|
3151
|
-
);
|
|
3125
|
+
const toScreen = (rect) => rectToScreen(canvas.getCamera(), rect);
|
|
3152
3126
|
const render = () => {
|
|
3153
3127
|
frame = 0;
|
|
3154
3128
|
const transforming = gesture?.kind === "move" || gesture?.kind === "resize" ? gesture : null;
|
|
@@ -3164,9 +3138,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3164
3138
|
} : null,
|
|
3165
3139
|
bounds: selection.length > 1 || !!transforming,
|
|
3166
3140
|
handles: transforms && selection.length > 0 && gesture?.kind !== "marquee",
|
|
3167
|
-
guides: transforming ? transforming.guides.map(
|
|
3168
|
-
(guide) => guideRect(guide, (x, y) => canvas.worldToScreen({ x, y }))
|
|
3169
|
-
) : []
|
|
3141
|
+
guides: transforming ? transforming.guides.map((guide) => guideRect(guide, canvas.getCamera())) : []
|
|
3170
3142
|
});
|
|
3171
3143
|
if (selection.length || hovered || gesture) draw();
|
|
3172
3144
|
};
|
|
@@ -3184,8 +3156,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3184
3156
|
if (!snap) return [];
|
|
3185
3157
|
const scope = scopes.at(-1);
|
|
3186
3158
|
if (!scope) {
|
|
3187
|
-
|
|
3188
|
-
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));
|
|
3189
3160
|
}
|
|
3190
3161
|
const rects = layers.childrenOf(scope).filter((layer) => !layerIn(layer, moving)).map((layer) => layerRect(canvas, layer));
|
|
3191
3162
|
return [layerRect(canvas, scope), ...rects].filter((rect) => !!rect);
|
|
@@ -3225,21 +3196,21 @@ function bindEditor(canvas, options = {}) {
|
|
|
3225
3196
|
} else {
|
|
3226
3197
|
let left = bounds.x;
|
|
3227
3198
|
let top = bounds.y;
|
|
3228
|
-
let
|
|
3229
|
-
let
|
|
3230
|
-
if (handle.includes("w")) left = Math.min(left + dx,
|
|
3231
|
-
if (handle.includes("e"))
|
|
3232
|
-
if (handle.includes("n")) top = Math.min(top + dy,
|
|
3233
|
-
if (handle.includes("s"))
|
|
3234
|
-
next = { x: left, y: top, width:
|
|
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 };
|
|
3235
3206
|
const corner = handle.length === 2;
|
|
3236
3207
|
if (keepRatio && corner) {
|
|
3237
3208
|
const scale = Math.max(next.width / bounds.width, next.height / bounds.height);
|
|
3238
3209
|
const width = bounds.width * scale;
|
|
3239
3210
|
const height = bounds.height * scale;
|
|
3240
3211
|
next = {
|
|
3241
|
-
x: handle.includes("w") ?
|
|
3242
|
-
y: handle.includes("n") ?
|
|
3212
|
+
x: handle.includes("w") ? right - width : left,
|
|
3213
|
+
y: handle.includes("n") ? bottom - height : top,
|
|
3243
3214
|
width,
|
|
3244
3215
|
height
|
|
3245
3216
|
};
|
|
@@ -3287,10 +3258,10 @@ function bindEditor(canvas, options = {}) {
|
|
|
3287
3258
|
swallowClick = true;
|
|
3288
3259
|
surface.setPointerCapture(event.pointerId);
|
|
3289
3260
|
};
|
|
3290
|
-
const handle = event.target
|
|
3261
|
+
const handle = handleAt(event.target);
|
|
3291
3262
|
if (handle && transforms) {
|
|
3292
3263
|
take();
|
|
3293
|
-
return beginTransform(point, handle
|
|
3264
|
+
return beginTransform(point, handle);
|
|
3294
3265
|
}
|
|
3295
3266
|
const target = targetIn(chainAt(point));
|
|
3296
3267
|
if (!target) {
|
|
@@ -3427,7 +3398,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3427
3398
|
} else if (event.key === "Tab") {
|
|
3428
3399
|
event.preventDefault();
|
|
3429
3400
|
const scope = scopes.at(-1);
|
|
3430
|
-
const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn(
|
|
3401
|
+
const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn(EVERYWHERE).map((node) => [node]);
|
|
3431
3402
|
const index = siblings.findIndex((layer) => sameLayer(layer, selection[0]));
|
|
3432
3403
|
const step = event.shiftKey ? -1 : 1;
|
|
3433
3404
|
const next = siblings[(index + step + siblings.length) % siblings.length];
|
|
@@ -3487,7 +3458,7 @@ var pillStyle = (color) => ({
|
|
|
3487
3458
|
whiteSpace: "nowrap",
|
|
3488
3459
|
pointerEvents: "none"
|
|
3489
3460
|
});
|
|
3490
|
-
function createMarks(canvas, { color =
|
|
3461
|
+
function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
|
|
3491
3462
|
const id = `gpucanvas-marks-${++instances}`;
|
|
3492
3463
|
const root = document.createElement("div");
|
|
3493
3464
|
Object.assign(root.style, {
|
|
@@ -3530,11 +3501,7 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
|
|
|
3530
3501
|
return ref;
|
|
3531
3502
|
};
|
|
3532
3503
|
const toScreen = (point) => canvas.worldToScreen(point);
|
|
3533
|
-
const screenRect = (rect) =>
|
|
3534
|
-
const a = toScreen(rect);
|
|
3535
|
-
const b = toScreen({ x: rect.x + rect.width, y: rect.y + rect.height });
|
|
3536
|
-
return { x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y };
|
|
3537
|
-
};
|
|
3504
|
+
const screenRect = (rect) => rectToScreen(canvas.getCamera(), rect);
|
|
3538
3505
|
const worldRect = (target) => target instanceof Element ? canvas.getElementRect(target) : { x: target.x, y: target.y, width: 0, height: 0 };
|
|
3539
3506
|
const place = (element, x, y) => {
|
|
3540
3507
|
const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
|
@@ -3880,6 +3847,7 @@ export {
|
|
|
3880
3847
|
EFFECTS,
|
|
3881
3848
|
supportsGpuCanvas,
|
|
3882
3849
|
createAgentView,
|
|
3850
|
+
defaultLabel,
|
|
3883
3851
|
bindEditor,
|
|
3884
3852
|
createMarks,
|
|
3885
3853
|
toBlob,
|
|
@@ -3890,4 +3858,4 @@ export {
|
|
|
3890
3858
|
swapNodes,
|
|
3891
3859
|
createGpuCanvas2 as createGpuCanvas
|
|
3892
3860
|
};
|
|
3893
|
-
//# sourceMappingURL=chunk-
|
|
3861
|
+
//# sourceMappingURL=chunk-IJ3R6X75.js.map
|