gpucanvas 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2EDYGRFR.js → chunk-IIRGNX76.js} +1750 -1514
- package/dist/chunk-IIRGNX76.js.map +1 -0
- package/dist/{editor-CuH4tLHg.d.ts → editor-BuoBLRhF.d.ts} +11 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +24 -10
- 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,130 @@ 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 SURFACE_Z = { marks: "1", marquee: "1", overlay: "2" };
|
|
223
|
+
var SELECTION_WIDTH = 2;
|
|
224
|
+
var HOVER_WIDTH = 1;
|
|
225
|
+
|
|
226
|
+
// src/base.ts
|
|
227
|
+
var styleNode = (element) => Object.assign(element.style, { boxSizing: "border-box", contain: "strict", overflow: "hidden" });
|
|
228
|
+
function createBase(host, options, onCamera, hooks) {
|
|
229
|
+
const surface = document.createElement("div");
|
|
230
|
+
markSurface(surface);
|
|
231
|
+
Object.assign(surface.style, {
|
|
232
|
+
position: "relative",
|
|
233
|
+
width: "100%",
|
|
234
|
+
height: "100%",
|
|
235
|
+
overflow: "hidden",
|
|
236
|
+
touchAction: "none"
|
|
237
|
+
});
|
|
238
|
+
host.append(surface);
|
|
239
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
240
|
+
let camera = options.camera ?? DEFAULT_CAMERA;
|
|
241
|
+
const setCamera = (next) => {
|
|
242
|
+
camera = next;
|
|
243
|
+
onCamera(camera);
|
|
244
|
+
options.onCameraChange?.(camera);
|
|
245
|
+
};
|
|
246
|
+
const unbindInput = bindCameraInput(surface, () => camera, setCamera);
|
|
247
|
+
const nodeAt = (point) => {
|
|
248
|
+
let top = null;
|
|
249
|
+
for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
|
|
250
|
+
return top;
|
|
251
|
+
};
|
|
252
|
+
const api = {
|
|
253
|
+
surface,
|
|
254
|
+
addNode(element, rect) {
|
|
255
|
+
if (nodes.has(element)) return api.updateNode(element, rect);
|
|
256
|
+
styleNode(element);
|
|
257
|
+
nodes.set(element, hooks.add(element, rect));
|
|
258
|
+
},
|
|
259
|
+
updateNode(element, rect) {
|
|
260
|
+
const node = nodes.get(element);
|
|
261
|
+
if (node) hooks.update(node, rect);
|
|
262
|
+
},
|
|
263
|
+
removeNode(element) {
|
|
264
|
+
const node = nodes.get(element);
|
|
265
|
+
if (!node) return;
|
|
266
|
+
hooks.remove(node);
|
|
267
|
+
nodes.delete(element);
|
|
268
|
+
element.remove();
|
|
269
|
+
},
|
|
270
|
+
getNodeRect: (element) => {
|
|
271
|
+
const node = nodes.get(element);
|
|
272
|
+
return node ? { ...node.rect } : null;
|
|
273
|
+
},
|
|
274
|
+
getNodesIn(rect) {
|
|
275
|
+
const hits = [];
|
|
276
|
+
for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
|
|
277
|
+
return hits;
|
|
278
|
+
},
|
|
279
|
+
getNodeOf: (element) => nodeOfElement(nodes, element),
|
|
280
|
+
getElementAt(point) {
|
|
281
|
+
const top = nodeAt(point);
|
|
282
|
+
return top ? elementInNode(top.element, top.rect, point) : null;
|
|
283
|
+
},
|
|
284
|
+
getCamera: () => camera,
|
|
285
|
+
setCamera,
|
|
286
|
+
screenToWorld: (point) => screenToWorld(camera, point),
|
|
287
|
+
worldToScreen: (point) => worldToScreen(camera, point),
|
|
288
|
+
fit(padding) {
|
|
289
|
+
const bounds = unionOf([...nodes.values()].map((node) => node.rect));
|
|
290
|
+
if (bounds) setCamera(fitCamera(bounds, surface.clientWidth, surface.clientHeight, padding));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
surface,
|
|
295
|
+
nodes,
|
|
296
|
+
camera: () => camera,
|
|
297
|
+
view: () => viewOf(api),
|
|
298
|
+
nodeAt,
|
|
299
|
+
api,
|
|
300
|
+
destroy() {
|
|
301
|
+
unbindInput();
|
|
302
|
+
surface.remove();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
138
307
|
// src/effects.ts
|
|
139
308
|
var defineEffect = (definition) => Object.freeze({
|
|
140
309
|
animated: false,
|
|
@@ -518,84 +687,43 @@ var EFFECTS = {
|
|
|
518
687
|
grayscale
|
|
519
688
|
};
|
|
520
689
|
|
|
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
690
|
// src/dom-backend.ts
|
|
586
691
|
function createDomCanvas(host, options) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
692
|
+
let frameMs = 0;
|
|
693
|
+
const apply = (camera) => {
|
|
694
|
+
const start = performance.now();
|
|
695
|
+
world.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
|
|
696
|
+
world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
|
|
697
|
+
frameMs = performance.now() - start;
|
|
698
|
+
};
|
|
699
|
+
const place = (element, rect) => {
|
|
700
|
+
element.style.left = `${rect.x}px`;
|
|
701
|
+
element.style.top = `${rect.y}px`;
|
|
702
|
+
element.style.width = `${rect.width}px`;
|
|
703
|
+
element.style.height = `${rect.height}px`;
|
|
704
|
+
};
|
|
705
|
+
const base = createBase(host, options, apply, {
|
|
706
|
+
add(element, rect) {
|
|
707
|
+
element.style.position = "absolute";
|
|
708
|
+
place(element, rect);
|
|
709
|
+
world.append(element);
|
|
710
|
+
return { element, rect };
|
|
711
|
+
},
|
|
712
|
+
update(node, rect) {
|
|
713
|
+
node.rect = rect;
|
|
714
|
+
place(node.element, rect);
|
|
715
|
+
},
|
|
716
|
+
remove({ element }) {
|
|
717
|
+
clearEffect(element);
|
|
718
|
+
selection.delete(element);
|
|
719
|
+
outlinedNodes.delete(element);
|
|
720
|
+
element.style.outline = "";
|
|
721
|
+
}
|
|
596
722
|
});
|
|
723
|
+
const { surface, nodes } = base;
|
|
724
|
+
surface.style.background = options.background ?? "transparent";
|
|
597
725
|
const world = document.createElement("div");
|
|
598
|
-
world
|
|
726
|
+
markSurface(world);
|
|
599
727
|
Object.assign(world.style, {
|
|
600
728
|
position: "absolute",
|
|
601
729
|
left: "0",
|
|
@@ -607,8 +735,6 @@ function createDomCanvas(host, options) {
|
|
|
607
735
|
Object.assign(overlay.style, { position: "absolute", left: "0", top: "0", zIndex: "1" });
|
|
608
736
|
world.append(overlay);
|
|
609
737
|
surface.append(world);
|
|
610
|
-
host.append(surface);
|
|
611
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
612
738
|
const ring = (color, width) => `calc(${width}px / var(--gpucanvas-zoom)) solid ${color}`;
|
|
613
739
|
const selectionColor = options.selectionColor ?? DEFAULT_SELECTION_COLOR;
|
|
614
740
|
const selectionRing = ring(selectionColor, SELECTION_WIDTH);
|
|
@@ -619,7 +745,7 @@ function createDomCanvas(host, options) {
|
|
|
619
745
|
const boxes = /* @__PURE__ */ new Map();
|
|
620
746
|
const elementRect = (element) => {
|
|
621
747
|
const node = element.isConnected ? nodeOfElement(nodes, element) : null;
|
|
622
|
-
return node ? rectInNode(node, nodes.get(node), element) : null;
|
|
748
|
+
return node ? rectInNode(node, nodes.get(node).rect, element) : null;
|
|
623
749
|
};
|
|
624
750
|
let boxFrame = 0;
|
|
625
751
|
const placeBoxes = () => {
|
|
@@ -693,55 +819,11 @@ function createDomCanvas(host, options) {
|
|
|
693
819
|
element.animate(keyframes, { duration: period * 1e3, iterations: Infinity })
|
|
694
820
|
);
|
|
695
821
|
};
|
|
696
|
-
|
|
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;
|
|
703
|
-
};
|
|
704
|
-
const setCamera = (next) => {
|
|
705
|
-
camera = next;
|
|
706
|
-
apply();
|
|
707
|
-
options.onCameraChange?.(camera);
|
|
708
|
-
};
|
|
709
|
-
const unbindInput = bindCameraInput(surface, () => camera, setCamera);
|
|
710
|
-
apply();
|
|
822
|
+
apply(base.camera());
|
|
711
823
|
return {
|
|
824
|
+
...base.api,
|
|
712
825
|
mode: "dom",
|
|
713
|
-
surface,
|
|
714
|
-
addNode(element, rect) {
|
|
715
|
-
applyNodeBase(element);
|
|
716
|
-
element.style.position = "absolute";
|
|
717
|
-
nodes.set(element, rect);
|
|
718
|
-
this.updateNode(element, rect);
|
|
719
|
-
world.append(element);
|
|
720
|
-
},
|
|
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
|
-
},
|
|
727
|
-
removeNode(element) {
|
|
728
|
-
clearEffect(element);
|
|
729
|
-
nodes.delete(element);
|
|
730
|
-
outlinedNodes.delete(element);
|
|
731
|
-
element.style.outline = "";
|
|
732
|
-
element.remove();
|
|
733
|
-
},
|
|
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
826
|
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
827
|
setSelection(elements) {
|
|
746
828
|
selection = new Set(elements);
|
|
747
829
|
drawOutlines();
|
|
@@ -754,10 +836,6 @@ function createDomCanvas(host, options) {
|
|
|
754
836
|
setEffect(element, effect, params) {
|
|
755
837
|
if (nodes.has(element)) applyEffect(element, effect, params);
|
|
756
838
|
},
|
|
757
|
-
getCamera: () => camera,
|
|
758
|
-
setCamera,
|
|
759
|
-
screenToWorld: (point) => screenToWorld(camera, point),
|
|
760
|
-
worldToScreen: (point) => worldToScreen(camera, point),
|
|
761
839
|
// The browser repaints DOM nodes itself.
|
|
762
840
|
invalidate() {
|
|
763
841
|
},
|
|
@@ -765,14 +843,10 @@ function createDomCanvas(host, options) {
|
|
|
765
843
|
setBackground(color) {
|
|
766
844
|
surface.style.background = color;
|
|
767
845
|
},
|
|
768
|
-
fit(padding) {
|
|
769
|
-
const bounds = boundsOf(nodes.values());
|
|
770
|
-
if (bounds) setCamera(fitCamera(bounds, surface.clientWidth, surface.clientHeight, padding));
|
|
771
|
-
},
|
|
772
846
|
getStats() {
|
|
773
|
-
const view =
|
|
847
|
+
const view = base.view();
|
|
774
848
|
let visible = 0;
|
|
775
|
-
for (const
|
|
849
|
+
for (const node of nodes.values()) if (intersects(node.rect, view)) visible++;
|
|
776
850
|
return {
|
|
777
851
|
mode: "dom",
|
|
778
852
|
nodes: nodes.size,
|
|
@@ -786,28 +860,62 @@ function createDomCanvas(host, options) {
|
|
|
786
860
|
},
|
|
787
861
|
destroy() {
|
|
788
862
|
cancelAnimationFrame(boxFrame);
|
|
789
|
-
|
|
790
|
-
surface.remove();
|
|
863
|
+
base.destroy();
|
|
791
864
|
}
|
|
792
865
|
};
|
|
793
866
|
}
|
|
794
867
|
|
|
795
|
-
// src/
|
|
796
|
-
var
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
this.
|
|
800
|
-
this.
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
868
|
+
// src/shelves.ts
|
|
869
|
+
var Shelves = class {
|
|
870
|
+
constructor(width, height, tolerance) {
|
|
871
|
+
this.width = width;
|
|
872
|
+
this.height = height;
|
|
873
|
+
this.tolerance = tolerance;
|
|
874
|
+
}
|
|
875
|
+
width;
|
|
876
|
+
height;
|
|
877
|
+
tolerance;
|
|
878
|
+
shelves = [];
|
|
879
|
+
/** Height taken by rows so far. */
|
|
880
|
+
filled = 0;
|
|
881
|
+
/** Top left corner of a free `w` x `h` box, or null when it does not fit. */
|
|
882
|
+
place(w, h) {
|
|
883
|
+
if (w > this.width || h > this.height) return null;
|
|
884
|
+
let shelf = this.shelves.find(
|
|
885
|
+
(candidate) => candidate.height >= h && candidate.height <= h * this.tolerance && candidate.x + w <= this.width
|
|
886
|
+
);
|
|
887
|
+
if (!shelf) {
|
|
888
|
+
if (this.filled + h > this.height) return null;
|
|
889
|
+
shelf = { x: 0, y: this.filled, height: h };
|
|
890
|
+
this.shelves.push(shelf);
|
|
891
|
+
this.filled += h;
|
|
892
|
+
}
|
|
893
|
+
const at = { x: shelf.x, y: shelf.y };
|
|
894
|
+
shelf.x += w;
|
|
895
|
+
return at;
|
|
896
|
+
}
|
|
897
|
+
clear() {
|
|
898
|
+
this.shelves = [];
|
|
899
|
+
this.filled = 0;
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// src/atlas.ts
|
|
904
|
+
var PADDING = 1;
|
|
905
|
+
var Atlas = class {
|
|
906
|
+
constructor(device, size, layerCount, keep) {
|
|
907
|
+
this.device = device;
|
|
908
|
+
this.keep = keep;
|
|
909
|
+
this.size = size;
|
|
910
|
+
this.texture = device.createTexture({
|
|
911
|
+
label: "gpucanvas atlas",
|
|
912
|
+
size: [size, size, layerCount],
|
|
913
|
+
format: "rgba8unorm",
|
|
914
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
806
915
|
});
|
|
807
916
|
this.view = this.texture.createView({ dimension: "2d-array" });
|
|
808
917
|
this.layers = Array.from({ length: layerCount }, () => ({
|
|
809
|
-
shelves:
|
|
810
|
-
nextY: 0,
|
|
918
|
+
shelves: new Shelves(size, size, 1.5),
|
|
811
919
|
generation: 0,
|
|
812
920
|
lastUsed: -1,
|
|
813
921
|
lastAllocated: -1
|
|
@@ -815,25 +923,21 @@ var Atlas = class {
|
|
|
815
923
|
this.spare = layerCount - 1;
|
|
816
924
|
}
|
|
817
925
|
device;
|
|
926
|
+
keep;
|
|
818
927
|
texture;
|
|
819
928
|
view;
|
|
820
929
|
size;
|
|
821
|
-
/** Slots to rescue when their layer is recycled, most important first. */
|
|
822
|
-
keep = () => [];
|
|
823
930
|
layers;
|
|
824
931
|
spare;
|
|
825
932
|
frame = 0;
|
|
826
933
|
/** The layer `allocatePinned` fills; it is never recycled. */
|
|
827
934
|
pinned = 0;
|
|
828
|
-
get layerCount() {
|
|
829
|
-
return this.layers.length;
|
|
830
|
-
}
|
|
831
935
|
get usedLayers() {
|
|
832
|
-
return this.layers.filter((layer, index) => index !== this.spare && layer.
|
|
936
|
+
return this.layers.filter((layer, index) => index !== this.spare && layer.shelves.filled > 0).length;
|
|
833
937
|
}
|
|
834
938
|
/** How much of the pinned layer is taken, 0 to 1. */
|
|
835
939
|
get pinnedFill() {
|
|
836
|
-
return this.layers[this.pinned].
|
|
940
|
+
return this.layers[this.pinned].shelves.filled / this.size;
|
|
837
941
|
}
|
|
838
942
|
nextFrame() {
|
|
839
943
|
this.frame++;
|
|
@@ -909,32 +1013,23 @@ var Atlas = class {
|
|
|
909
1013
|
}
|
|
910
1014
|
allocateIn(index, w, h) {
|
|
911
1015
|
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
|
-
}
|
|
1016
|
+
const at = layer.shelves.place(w, h);
|
|
1017
|
+
if (!at) return null;
|
|
921
1018
|
const slot = {
|
|
922
1019
|
layer: index,
|
|
923
|
-
x:
|
|
924
|
-
y:
|
|
1020
|
+
x: at.x,
|
|
1021
|
+
y: at.y,
|
|
925
1022
|
width: w - PADDING,
|
|
926
1023
|
height: h - PADDING,
|
|
927
1024
|
generation: layer.generation
|
|
928
1025
|
};
|
|
929
|
-
shelf.x += w;
|
|
930
1026
|
layer.lastUsed = this.frame;
|
|
931
1027
|
layer.lastAllocated = this.frame;
|
|
932
1028
|
return slot;
|
|
933
1029
|
}
|
|
934
1030
|
evict(index) {
|
|
935
1031
|
const layer = this.layers[index];
|
|
936
|
-
layer.shelves
|
|
937
|
-
layer.nextY = 0;
|
|
1032
|
+
layer.shelves.clear();
|
|
938
1033
|
layer.generation++;
|
|
939
1034
|
}
|
|
940
1035
|
destroy() {
|
|
@@ -942,353 +1037,354 @@ var Atlas = class {
|
|
|
942
1037
|
}
|
|
943
1038
|
};
|
|
944
1039
|
|
|
945
|
-
// src/
|
|
946
|
-
var
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1040
|
+
// src/gpu-node.ts
|
|
1041
|
+
var THUMBNAIL_SCALE = 1 / 16;
|
|
1042
|
+
var SMALL_NODE_PX = 200;
|
|
1043
|
+
var SMALL_REFRESH_MS = 250;
|
|
1044
|
+
var smallOnScreen = (node, zoom) => Math.max(node.rect.width, node.rect.height) * zoom < SMALL_NODE_PX;
|
|
1045
|
+
var nodeOf = (nodes, target) => {
|
|
1046
|
+
const element = target instanceof Element ? target : target.parentElement;
|
|
1047
|
+
const node = element && nodeOfElement(nodes, element);
|
|
1048
|
+
return node ? nodes.get(node) : void 0;
|
|
1049
|
+
};
|
|
1050
|
+
var texels = (length, scale) => Math.max(1, Math.round(length * scale));
|
|
1051
|
+
var nodeRegion = (node) => ({
|
|
1052
|
+
x: node.pageX,
|
|
1053
|
+
y: node.pageY,
|
|
1054
|
+
width: node.rect.width,
|
|
1055
|
+
height: node.rect.height
|
|
1056
|
+
});
|
|
1057
|
+
var createGpuNode = (element, rect, at) => ({
|
|
1058
|
+
element,
|
|
1059
|
+
rect,
|
|
1060
|
+
page: at.page,
|
|
1061
|
+
pageX: at.x,
|
|
1062
|
+
pageY: at.y,
|
|
1063
|
+
slot: null,
|
|
1064
|
+
slotScale: 0,
|
|
1065
|
+
thumb: null,
|
|
1066
|
+
thumbStale: false,
|
|
1067
|
+
thumbAt: -Infinity,
|
|
1068
|
+
shown: 0,
|
|
1069
|
+
shownFrame: -1,
|
|
1070
|
+
dirty: true,
|
|
1071
|
+
tiles: [],
|
|
1072
|
+
preview: null,
|
|
1073
|
+
previewRect: null,
|
|
1074
|
+
previewScale: 0,
|
|
1075
|
+
previewTarget: null,
|
|
1076
|
+
previewTargetScale: 0,
|
|
1077
|
+
changedAt: -Infinity,
|
|
1078
|
+
refreshedAt: -Infinity,
|
|
1079
|
+
wholeStale: false,
|
|
1080
|
+
solo: false,
|
|
1081
|
+
targetScale: 0,
|
|
1082
|
+
priority: 0
|
|
1083
|
+
});
|
|
1084
|
+
function placeNode(node, at) {
|
|
1085
|
+
node.page = at.page;
|
|
1086
|
+
node.pageX = at.x;
|
|
1087
|
+
node.pageY = at.y;
|
|
1088
|
+
node.slot = null;
|
|
1089
|
+
node.thumb = null;
|
|
1090
|
+
node.tiles = [];
|
|
1091
|
+
node.preview = null;
|
|
1092
|
+
node.solo = false;
|
|
1093
|
+
node.dirty = true;
|
|
959
1094
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
@location(2) info: vec4f, // layer, kind (0 snapshot, 1 placeholder, 2 ring), ring width or effect pad
|
|
969
|
-
@location(3) params: vec4f, // an effect's parameters
|
|
1095
|
+
function translateNode(node, rect) {
|
|
1096
|
+
const dx = rect.x - node.rect.x;
|
|
1097
|
+
const dy = rect.y - node.rect.y;
|
|
1098
|
+
const shift = (r) => ({ ...r, x: r.x + dx, y: r.y + dy });
|
|
1099
|
+
for (const tile of node.tiles) tile.rect = shift(tile.rect);
|
|
1100
|
+
if (node.previewRect) node.previewRect = shift(node.previewRect);
|
|
1101
|
+
if (node.previewTarget) node.previewTarget = shift(node.previewTarget);
|
|
1102
|
+
node.rect = rect;
|
|
970
1103
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
@location(1) @interpolate(flat) bounds: vec4f,
|
|
976
|
-
@location(2) @interpolate(flat) info: vec2f,
|
|
977
|
-
@location(3) @interpolate(flat) color: vec4f,
|
|
978
|
-
@location(4) uv: vec2f, // 0 to 1 across the node, past that in an effect's padding
|
|
979
|
-
@location(5) @interpolate(flat) size: vec2f,
|
|
980
|
-
@location(6) @interpolate(flat) params: vec4f,
|
|
981
|
-
@location(7) @interpolate(flat) corner: vec2f, // the node's top left in world units
|
|
1104
|
+
function setThumb(node, slot, now) {
|
|
1105
|
+
node.thumb = slot;
|
|
1106
|
+
node.thumbStale = false;
|
|
1107
|
+
node.thumbAt = now;
|
|
982
1108
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
let end = instance.rect.xy + instance.rect.zw + grow;
|
|
992
|
-
// Snap edges to device pixels so a snapshot drawn at its raster scale stays crisp.
|
|
993
|
-
let scale = view.zoom * view.dpr;
|
|
994
|
-
let topLeft = round((start - view.origin) * scale) / view.dpr;
|
|
995
|
-
let bottomRight = round((end - view.origin) * scale) / view.dpr;
|
|
996
|
-
let point = mix(topLeft, bottomRight, corner);
|
|
997
|
-
var out: Varyings;
|
|
998
|
-
out.position = vec4f(point.x / view.size.x * 2.0 - 1.0, 1.0 - point.y / view.size.y * 2.0, 0.0, 1.0);
|
|
999
|
-
if (ring) {
|
|
1000
|
-
// World units from the element's top left, and the element's own edges.
|
|
1001
|
-
out.texel = mix(vec2f(-grow), instance.rect.zw + grow, corner);
|
|
1002
|
-
out.bounds = vec4f(0.0, 0.0, instance.rect.zw);
|
|
1003
|
-
out.color = instance.uv;
|
|
1004
|
-
} else {
|
|
1005
|
-
let pad = grow * (instance.uv.zw - instance.uv.xy) / instance.rect.zw;
|
|
1006
|
-
out.texel = mix(instance.uv.xy - pad, instance.uv.zw + pad, corner);
|
|
1007
|
-
out.color = instance.uv;
|
|
1008
|
-
// Half a texel inside the node's rectangle: the outermost sample center that cannot blend
|
|
1009
|
-
// with whatever sits next to it in the atlas.
|
|
1010
|
-
out.bounds = vec4f(instance.uv.xy + 0.5, instance.uv.zw - 0.5);
|
|
1109
|
+
function recordWhole(node, slot, scale, now) {
|
|
1110
|
+
node.slot = slot;
|
|
1111
|
+
node.slotScale = scale;
|
|
1112
|
+
if (scale === THUMBNAIL_SCALE) setThumb(node, slot, now);
|
|
1113
|
+
else if (node.dirty) node.thumbStale = true;
|
|
1114
|
+
if (node.dirty) {
|
|
1115
|
+
node.preview = null;
|
|
1116
|
+
node.refreshedAt = now;
|
|
1011
1117
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
out.size = instance.rect.zw;
|
|
1015
|
-
out.corner = instance.rect.xy;
|
|
1016
|
-
out.params = instance.params;
|
|
1017
|
-
return out;
|
|
1118
|
+
node.dirty = false;
|
|
1119
|
+
node.wholeStale = false;
|
|
1018
1120
|
}
|
|
1019
|
-
|
|
1020
|
-
);
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1121
|
+
function recordPartial(node, now) {
|
|
1122
|
+
if (!node.dirty) return;
|
|
1123
|
+
node.dirty = false;
|
|
1124
|
+
node.wholeStale = true;
|
|
1125
|
+
node.thumbStale = true;
|
|
1126
|
+
node.refreshedAt = now;
|
|
1127
|
+
}
|
|
1128
|
+
var isCurrentTile = (tile, detailScale) => tile.scale === detailScale && !tile.stale;
|
|
1129
|
+
var hasFreshThumb = (atlas, node) => atlas.isValid(node.thumb) && !node.thumbStale;
|
|
1130
|
+
|
|
1131
|
+
// src/gpu-live.ts
|
|
1132
|
+
var USE_EVENTS = ["click", "input", "change", "keydown"];
|
|
1133
|
+
var USED_MS = 1e3;
|
|
1134
|
+
var LINGER_MS = 300;
|
|
1135
|
+
var CANVAS_TRANSFORM_HIT_TESTS = typeof HTMLCanvasElement !== "undefined" && "getElementImageDefaultSize" in HTMLCanvasElement.prototype;
|
|
1136
|
+
var FAR_AWAY = { x: -1e7, y: -1e7 };
|
|
1137
|
+
var Live = class {
|
|
1138
|
+
constructor(o) {
|
|
1139
|
+
this.o = o;
|
|
1140
|
+
const { canvas, surface } = o;
|
|
1141
|
+
surface.addEventListener("pointermove", this.onPointerMove);
|
|
1142
|
+
surface.addEventListener("pointerleave", this.onPointerLeave);
|
|
1143
|
+
surface.addEventListener("pointerdown", this.onPointerDown, true);
|
|
1144
|
+
surface.addEventListener("click", this.onClick);
|
|
1145
|
+
canvas.addEventListener("focusin", this.onFocusIn);
|
|
1146
|
+
canvas.addEventListener("focusout", this.onFocusOut);
|
|
1147
|
+
for (const type of USE_EVENTS) canvas.addEventListener(type, this.onUse, true);
|
|
1148
|
+
canvas.addEventListener("transitionrun", this.onTransitionRun);
|
|
1033
1149
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1150
|
+
o;
|
|
1151
|
+
/** The node under the pointer. */
|
|
1152
|
+
current = null;
|
|
1153
|
+
/** The pointer over the surface, in surface pixels. */
|
|
1154
|
+
pointer = null;
|
|
1155
|
+
/** Where the last press landed, in world units, and when: effects such as shockwaves use it. */
|
|
1156
|
+
press = FAR_AWAY;
|
|
1157
|
+
pressedAt = -Infinity;
|
|
1158
|
+
positioned = false;
|
|
1159
|
+
lingering = null;
|
|
1160
|
+
lingerTimer = 0;
|
|
1161
|
+
focused = null;
|
|
1162
|
+
/** Readable nodes in view with CSS transitions running, and when the last one ends. */
|
|
1163
|
+
transitioning = /* @__PURE__ */ new Map();
|
|
1164
|
+
usedAt = /* @__PURE__ */ new WeakMap();
|
|
1165
|
+
/**
|
|
1166
|
+
* Touch and pen press without hovering first, so the node under them is not hit-testable yet and
|
|
1167
|
+
* the tap lands on the canvas or its page. The press makes the node live, and a click that did
|
|
1168
|
+
* not reach inside it is handed to the element under the finger.
|
|
1169
|
+
*/
|
|
1170
|
+
tapped = false;
|
|
1171
|
+
destroyed = false;
|
|
1172
|
+
/** Whether a held node keeps `page` drawable. */
|
|
1173
|
+
holds(page) {
|
|
1174
|
+
return this.held().some((node) => node?.page === page);
|
|
1036
1175
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1176
|
+
/** Nodes kept drawable and redrawn on every paint: under the pointer, focused, transitioning. */
|
|
1177
|
+
held() {
|
|
1178
|
+
return [this.current, this.lingering, this.focused, ...this.transitioning.keys()];
|
|
1179
|
+
}
|
|
1180
|
+
/** Moves the hit-test target to `node`: its page is registered and positioned under it. */
|
|
1181
|
+
set(node) {
|
|
1182
|
+
if (node === this.current) return;
|
|
1183
|
+
const { canvas, frameNumber, schedule } = this.o;
|
|
1184
|
+
const previous = this.current;
|
|
1185
|
+
this.current = node;
|
|
1186
|
+
if (node && node === this.lingering) {
|
|
1187
|
+
clearTimeout(this.lingerTimer);
|
|
1188
|
+
this.lingering = null;
|
|
1189
|
+
}
|
|
1190
|
+
if (previous) {
|
|
1191
|
+
previous.element.style.pointerEvents = "none";
|
|
1192
|
+
clearTimeout(this.lingerTimer);
|
|
1193
|
+
const earlier = this.lingering;
|
|
1194
|
+
this.lingering = previous;
|
|
1195
|
+
if (earlier && earlier !== previous) this.letGo(earlier);
|
|
1196
|
+
this.lingerTimer = window.setTimeout(() => {
|
|
1197
|
+
const last = this.lingering;
|
|
1198
|
+
if (!last) return;
|
|
1199
|
+
this.lingering = null;
|
|
1200
|
+
this.letGo(last);
|
|
1201
|
+
}, LINGER_MS);
|
|
1202
|
+
this.unpin(previous.page);
|
|
1203
|
+
}
|
|
1204
|
+
if (!node) return;
|
|
1205
|
+
const { page } = node;
|
|
1206
|
+
page.setDrawable(true, frameNumber());
|
|
1207
|
+
if (!page.registered) {
|
|
1208
|
+
canvas.updateElementGeometry(page.element, {});
|
|
1209
|
+
page.registered = true;
|
|
1047
1210
|
}
|
|
1211
|
+
this.positioned = false;
|
|
1212
|
+
this.position();
|
|
1213
|
+
schedule();
|
|
1048
1214
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
);
|
|
1053
|
-
var effectShader = (wgsl) => (
|
|
1054
|
-
/* wgsl */
|
|
1055
|
-
`${prelude}
|
|
1056
|
-
struct EffectInput {
|
|
1057
|
-
uv: vec2f,
|
|
1058
|
-
size: vec2f,
|
|
1059
|
-
time: f32,
|
|
1060
|
-
zoom: f32,
|
|
1061
|
-
params: vec4f,
|
|
1062
|
-
pointer: vec2f,
|
|
1063
|
-
press: vec2f,
|
|
1064
|
-
sincePress: f32,
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
var<private> sourceRect: vec4f;
|
|
1068
|
-
var<private> sourceLayer: i32;
|
|
1069
|
-
|
|
1070
|
-
fn source(uv: vec2f) -> vec4f {
|
|
1071
|
-
if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) {
|
|
1072
|
-
return vec4f(0.0);
|
|
1215
|
+
/** Makes the topmost node under the last pointer position live. */
|
|
1216
|
+
pick() {
|
|
1217
|
+
if (!this.pointer) return this.set(null);
|
|
1218
|
+
this.set(this.o.nodeAt(screenToWorld(this.o.camera(), this.pointer)));
|
|
1073
1219
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
`
|
|
1106
|
-
);
|
|
1107
|
-
|
|
1108
|
-
// src/gpu-renderer.ts
|
|
1109
|
-
var FLOATS_PER_INSTANCE = 16;
|
|
1110
|
-
var CompositeRenderer = class {
|
|
1111
|
-
constructor(device, context, format, atlas, background, placeholder) {
|
|
1112
|
-
this.device = device;
|
|
1113
|
-
this.context = context;
|
|
1114
|
-
this.atlas = atlas;
|
|
1115
|
-
this.background = background;
|
|
1116
|
-
this.placeholder = placeholder;
|
|
1117
|
-
this.bindGroupLayout = device.createBindGroupLayout({
|
|
1118
|
-
entries: [
|
|
1119
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
|
|
1120
|
-
{
|
|
1121
|
-
binding: 1,
|
|
1122
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
1123
|
-
texture: { viewDimension: "2d-array" }
|
|
1124
|
-
},
|
|
1125
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
|
|
1126
|
-
]
|
|
1127
|
-
});
|
|
1128
|
-
this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
|
|
1129
|
-
this.pipeline = device.createRenderPipeline(
|
|
1130
|
-
this.pipelineDescriptor("gpucanvas composite", compositeShader, format)
|
|
1131
|
-
);
|
|
1132
|
-
this.format = format;
|
|
1133
|
-
this.uniforms = device.createBuffer({
|
|
1134
|
-
size: this.uniformData.byteLength,
|
|
1135
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
1136
|
-
});
|
|
1137
|
-
this.bindGroup = device.createBindGroup({
|
|
1138
|
-
layout: this.bindGroupLayout,
|
|
1139
|
-
entries: [
|
|
1140
|
-
{ binding: 0, resource: { buffer: this.uniforms } },
|
|
1141
|
-
{ binding: 1, resource: atlas.view },
|
|
1142
|
-
{ binding: 2, resource: this.sampler }
|
|
1143
|
-
]
|
|
1144
|
-
});
|
|
1145
|
-
this.instanceBuffer = this.createInstanceBuffer();
|
|
1220
|
+
/**
|
|
1221
|
+
* Puts the live page's hit area under the live node. Chrome 157 hit tests at the canvas
|
|
1222
|
+
* transform given to `updateElementGeometry`, in CSS pixels. Chrome 156 ignores it and hit
|
|
1223
|
+
* tests at the CSS box, so the page gets the CSS transform `getElementTransform` derives from
|
|
1224
|
+
* the draw matrix, which maps the default-size snapshot (CSS px x dpr) to canvas pixels. A page
|
|
1225
|
+
* promoted this frame has no paint record there yet, so it is positioned on the next frame.
|
|
1226
|
+
*/
|
|
1227
|
+
position() {
|
|
1228
|
+
const live = this.current;
|
|
1229
|
+
if (!live || this.positioned) return;
|
|
1230
|
+
const { canvas } = this.o;
|
|
1231
|
+
const camera = this.o.camera();
|
|
1232
|
+
const { zoom } = camera;
|
|
1233
|
+
const x = (live.rect.x - camera.x - live.pageX) * zoom;
|
|
1234
|
+
const y = (live.rect.y - camera.y - live.pageY) * zoom;
|
|
1235
|
+
try {
|
|
1236
|
+
if (CANVAS_TRANSFORM_HIT_TESTS) {
|
|
1237
|
+
canvas.updateElementGeometry(live.page.element, {
|
|
1238
|
+
canvasTransform: new DOMMatrix([zoom, 0, 0, zoom, x, y])
|
|
1239
|
+
});
|
|
1240
|
+
} else {
|
|
1241
|
+
const dpr = this.o.canvasDpr();
|
|
1242
|
+
const drawn = new DOMMatrix([zoom, 0, 0, zoom, x * dpr, y * dpr]);
|
|
1243
|
+
live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn).toString();
|
|
1244
|
+
}
|
|
1245
|
+
live.element.style.pointerEvents = "auto";
|
|
1246
|
+
this.positioned = true;
|
|
1247
|
+
} catch {
|
|
1248
|
+
this.o.schedule();
|
|
1249
|
+
}
|
|
1146
1250
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
|
|
1153
|
-
instanceBuffer;
|
|
1154
|
-
uniforms;
|
|
1155
|
-
uniformData = new Float32Array(20);
|
|
1156
|
-
pipeline;
|
|
1157
|
-
bindGroup;
|
|
1158
|
-
bindGroupLayout;
|
|
1159
|
-
sampler;
|
|
1160
|
-
/** Effect pipelines, null while one compiles. Compiling never blocks a frame. */
|
|
1161
|
-
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) {
|
|
1167
|
-
const module = this.device.createShaderModule({ label, code });
|
|
1168
|
-
return {
|
|
1169
|
-
label,
|
|
1170
|
-
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bindGroupLayout] }),
|
|
1171
|
-
vertex: {
|
|
1172
|
-
module,
|
|
1173
|
-
entryPoint: "vs",
|
|
1174
|
-
buffers: [
|
|
1175
|
-
{
|
|
1176
|
-
arrayStride: FLOATS_PER_INSTANCE * 4,
|
|
1177
|
-
stepMode: "instance",
|
|
1178
|
-
attributes: [
|
|
1179
|
-
{ shaderLocation: 0, offset: 0, format: "float32x4" },
|
|
1180
|
-
{ shaderLocation: 1, offset: 16, format: "float32x4" },
|
|
1181
|
-
{ shaderLocation: 2, offset: 32, format: "float32x4" },
|
|
1182
|
-
{ shaderLocation: 3, offset: 48, format: "float32x4" }
|
|
1183
|
-
]
|
|
1184
|
-
}
|
|
1185
|
-
]
|
|
1186
|
-
},
|
|
1187
|
-
fragment: {
|
|
1188
|
-
module,
|
|
1189
|
-
entryPoint: "fs",
|
|
1190
|
-
targets: [
|
|
1191
|
-
{
|
|
1192
|
-
format,
|
|
1193
|
-
// Snapshots are requested premultiplied.
|
|
1194
|
-
blend: {
|
|
1195
|
-
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
|
|
1196
|
-
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
]
|
|
1200
|
-
},
|
|
1201
|
-
primitive: { topology: "triangle-strip" }
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
/** Whether `effect` can draw this frame. The first ask starts compiling it. */
|
|
1205
|
-
hasEffect(effect) {
|
|
1206
|
-
if (this.effects.has(effect)) return this.effects.get(effect) !== null;
|
|
1207
|
-
this.effects.set(effect, null);
|
|
1208
|
-
this.device.createRenderPipelineAsync(
|
|
1209
|
-
this.pipelineDescriptor(
|
|
1210
|
-
`gpucanvas effect ${effect.name}`,
|
|
1211
|
-
effectShader(effect.wgsl),
|
|
1212
|
-
this.format
|
|
1213
|
-
)
|
|
1214
|
-
).then((pipeline) => {
|
|
1215
|
-
this.effects.set(effect, pipeline);
|
|
1216
|
-
this.onEffectReady();
|
|
1217
|
-
}).catch((error) => console.error(`[gpucanvas] effect "${effect.name}" failed`, error));
|
|
1218
|
-
return false;
|
|
1251
|
+
/** Puts the hit area back under `node` after it moved, when it is the live node. */
|
|
1252
|
+
moved(node) {
|
|
1253
|
+
if (node !== this.current) return;
|
|
1254
|
+
this.positioned = false;
|
|
1255
|
+
this.position();
|
|
1219
1256
|
}
|
|
1220
|
-
/**
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (this.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
this.
|
|
1257
|
+
/** Releases a page from hit testing once nothing pins it; it parks after its next raster. */
|
|
1258
|
+
unpin(page) {
|
|
1259
|
+
if (this.o.isPinned(page)) return;
|
|
1260
|
+
if (page.registered) this.o.canvas.clearElementGeometry(page.element);
|
|
1261
|
+
page.registered = false;
|
|
1262
|
+
page.element.style.transform = "";
|
|
1263
|
+
this.o.schedule();
|
|
1227
1264
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
this.
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
camera.y,
|
|
1235
|
-
camera.zoom,
|
|
1236
|
-
dpr,
|
|
1237
|
-
this.atlas.size,
|
|
1238
|
-
effects.time,
|
|
1239
|
-
...this.placeholder,
|
|
1240
|
-
effects.pointer.x,
|
|
1241
|
-
effects.pointer.y,
|
|
1242
|
-
effects.press.x,
|
|
1243
|
-
effects.press.y,
|
|
1244
|
-
effects.sincePress
|
|
1245
|
-
]);
|
|
1246
|
-
device.queue.writeBuffer(this.uniforms, 0, this.uniformData);
|
|
1247
|
-
if (count > 0) {
|
|
1248
|
-
device.queue.writeBuffer(
|
|
1249
|
-
this.instanceBuffer,
|
|
1250
|
-
0,
|
|
1251
|
-
this.instances,
|
|
1252
|
-
0,
|
|
1253
|
-
count * FLOATS_PER_INSTANCE
|
|
1254
|
-
);
|
|
1255
|
-
}
|
|
1256
|
-
const [r, g, b] = this.background;
|
|
1257
|
-
const encoder = device.createCommandEncoder();
|
|
1258
|
-
const pass = encoder.beginRenderPass({
|
|
1259
|
-
colorAttachments: [
|
|
1260
|
-
{
|
|
1261
|
-
view: this.context.getCurrentTexture().createView(),
|
|
1262
|
-
clearValue: { r, g, b, a: 1 },
|
|
1263
|
-
loadOp: "clear",
|
|
1264
|
-
storeOp: "store"
|
|
1265
|
-
}
|
|
1266
|
-
]
|
|
1267
|
-
});
|
|
1268
|
-
if (count > 0) {
|
|
1269
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
1270
|
-
pass.setVertexBuffer(0, this.instanceBuffer);
|
|
1271
|
-
let current = null;
|
|
1272
|
-
for (const run of runs.length ? runs : [{ effect: null, start: 0, count }]) {
|
|
1273
|
-
const pipeline = run.effect && this.effects.get(run.effect) || this.pipeline;
|
|
1274
|
-
if (pipeline !== current) pass.setPipeline(current = pipeline);
|
|
1275
|
-
pass.draw(4, run.count, 0, run.start);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
pass.end();
|
|
1279
|
-
device.queue.submit([encoder.finish()]);
|
|
1265
|
+
/** Stops holding a removed node. */
|
|
1266
|
+
forget(node) {
|
|
1267
|
+
if (this.current === node) this.set(null);
|
|
1268
|
+
if (this.lingering === node) this.lingering = null;
|
|
1269
|
+
if (this.focused === node) this.focused = null;
|
|
1270
|
+
this.transitioning.delete(node);
|
|
1280
1271
|
}
|
|
1281
1272
|
destroy() {
|
|
1282
|
-
this.
|
|
1283
|
-
this.
|
|
1273
|
+
const { canvas, surface } = this.o;
|
|
1274
|
+
this.destroyed = true;
|
|
1275
|
+
clearTimeout(this.lingerTimer);
|
|
1276
|
+
surface.removeEventListener("pointermove", this.onPointerMove);
|
|
1277
|
+
surface.removeEventListener("pointerleave", this.onPointerLeave);
|
|
1278
|
+
surface.removeEventListener("pointerdown", this.onPointerDown, true);
|
|
1279
|
+
surface.removeEventListener("click", this.onClick);
|
|
1280
|
+
canvas.removeEventListener("focusin", this.onFocusIn);
|
|
1281
|
+
canvas.removeEventListener("focusout", this.onFocusOut);
|
|
1282
|
+
canvas.removeEventListener("transitionrun", this.onTransitionRun);
|
|
1283
|
+
for (const type of USE_EVENTS) canvas.removeEventListener(type, this.onUse, true);
|
|
1284
1284
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
|
|
1290
|
-
});
|
|
1285
|
+
/** A node no longer held: redrawn once as it rests, its page released. */
|
|
1286
|
+
letGo(node) {
|
|
1287
|
+
node.dirty = true;
|
|
1288
|
+
this.unpin(node.page);
|
|
1291
1289
|
}
|
|
1290
|
+
nodeOf(target) {
|
|
1291
|
+
return nodeOf(this.o.nodes, target);
|
|
1292
|
+
}
|
|
1293
|
+
onPointerMove = (event) => {
|
|
1294
|
+
this.pointer = pointIn(this.o.canvas, event);
|
|
1295
|
+
if (event.buttons === 0) this.pick();
|
|
1296
|
+
};
|
|
1297
|
+
onPointerLeave = () => {
|
|
1298
|
+
this.pointer = null;
|
|
1299
|
+
this.set(null);
|
|
1300
|
+
};
|
|
1301
|
+
onPointerDown = (event) => {
|
|
1302
|
+
const at = pointIn(this.o.canvas, event);
|
|
1303
|
+
this.press = screenToWorld(this.o.camera(), at);
|
|
1304
|
+
this.pressedAt = performance.now();
|
|
1305
|
+
if (event.pointerType !== "mouse") {
|
|
1306
|
+
this.pointer = at;
|
|
1307
|
+
this.pick();
|
|
1308
|
+
this.tapped = true;
|
|
1309
|
+
}
|
|
1310
|
+
this.o.onPress();
|
|
1311
|
+
};
|
|
1312
|
+
onClick = (event) => {
|
|
1313
|
+
if (!this.tapped) return;
|
|
1314
|
+
this.tapped = false;
|
|
1315
|
+
const point = screenToWorld(this.o.camera(), pointIn(this.o.canvas, event));
|
|
1316
|
+
const top = this.o.nodeAt(point);
|
|
1317
|
+
if (!top || top.element.contains(event.target)) return;
|
|
1318
|
+
let target = elementInNode(top.element, top.rect, point);
|
|
1319
|
+
while (target && !(target instanceof HTMLElement)) target = target.parentElement;
|
|
1320
|
+
if (!(target instanceof HTMLElement)) return;
|
|
1321
|
+
target.focus({ preventScroll: true });
|
|
1322
|
+
target.dispatchEvent(
|
|
1323
|
+
new MouseEvent("click", {
|
|
1324
|
+
bubbles: true,
|
|
1325
|
+
cancelable: true,
|
|
1326
|
+
composed: true,
|
|
1327
|
+
view: window,
|
|
1328
|
+
clientX: event.clientX,
|
|
1329
|
+
clientY: event.clientY
|
|
1330
|
+
})
|
|
1331
|
+
);
|
|
1332
|
+
};
|
|
1333
|
+
onFocusIn = (event) => {
|
|
1334
|
+
const node = this.nodeOf(event.target) ?? null;
|
|
1335
|
+
if (node === this.focused) return;
|
|
1336
|
+
const previous = this.focused;
|
|
1337
|
+
this.focused = node;
|
|
1338
|
+
if (previous) this.letGo(previous);
|
|
1339
|
+
if (node) {
|
|
1340
|
+
node.page.setDrawable(true, this.o.frameNumber());
|
|
1341
|
+
node.dirty = true;
|
|
1342
|
+
this.o.schedule();
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
onFocusOut = (event) => {
|
|
1346
|
+
if (event.relatedTarget && this.o.canvas.contains(event.relatedTarget)) return;
|
|
1347
|
+
const previous = this.focused;
|
|
1348
|
+
this.focused = null;
|
|
1349
|
+
if (previous) this.letGo(previous);
|
|
1350
|
+
};
|
|
1351
|
+
// A transition paints without touching the DOM, so a click (from a person or from script)
|
|
1352
|
+
// would leave a half-finished frame. A readable node in view that was just used keeps its page
|
|
1353
|
+
// pinned and redrawn on every paint until its transitions are over. Content that animates on
|
|
1354
|
+
// its own, like a looping indicator, keeps the cheaper refresh on mutations: pinning it would
|
|
1355
|
+
// repaint its whole page every frame. A deadline ends the pin, transitions in parked pages
|
|
1356
|
+
// never fire transitionend.
|
|
1357
|
+
onUse = (event) => {
|
|
1358
|
+
const node = this.nodeOf(event.target);
|
|
1359
|
+
if (node) this.usedAt.set(node, performance.now());
|
|
1360
|
+
};
|
|
1361
|
+
onTransitionRun = (event) => {
|
|
1362
|
+
const node = this.nodeOf(event.target);
|
|
1363
|
+
const now = performance.now();
|
|
1364
|
+
if (!node || now - (this.usedAt.get(node) ?? -Infinity) > USED_MS) return;
|
|
1365
|
+
if (!intersects(node.rect, this.o.view())) return;
|
|
1366
|
+
if (smallOnScreen(node, this.o.camera().zoom)) return;
|
|
1367
|
+
const ms = transitionMs(event.target) + 32;
|
|
1368
|
+
const end = now + ms;
|
|
1369
|
+
if ((this.transitioning.get(node) ?? 0) >= end) return;
|
|
1370
|
+
this.transitioning.set(node, end);
|
|
1371
|
+
node.dirty = true;
|
|
1372
|
+
this.o.schedule();
|
|
1373
|
+
setTimeout(() => {
|
|
1374
|
+
if (this.destroyed || this.transitioning.get(node) !== end) return;
|
|
1375
|
+
this.transitioning.delete(node);
|
|
1376
|
+
this.letGo(node);
|
|
1377
|
+
this.o.schedule();
|
|
1378
|
+
}, ms);
|
|
1379
|
+
};
|
|
1380
|
+
};
|
|
1381
|
+
var transitionMs = (element) => {
|
|
1382
|
+
const style = getComputedStyle(element);
|
|
1383
|
+
const seconds = (list) => list.split(",").map((value) => parseFloat(value) || 0);
|
|
1384
|
+
const durations = seconds(style.transitionDuration);
|
|
1385
|
+
const delays = seconds(style.transitionDelay);
|
|
1386
|
+
const longest = Math.max(...durations.map((d, i) => d + (delays[i % delays.length] ?? 0)));
|
|
1387
|
+
return Math.min(1e3, longest * 1e3);
|
|
1292
1388
|
};
|
|
1293
1389
|
|
|
1294
1390
|
// src/page.ts
|
|
@@ -1297,21 +1393,19 @@ var Page = class {
|
|
|
1297
1393
|
element;
|
|
1298
1394
|
width;
|
|
1299
1395
|
height;
|
|
1300
|
-
/** Nodes currently placed on this page. */
|
|
1301
|
-
count = 0;
|
|
1302
1396
|
/** Has the `drawable` attribute and is rendered; parked pages use `content-visibility: hidden`. */
|
|
1303
1397
|
drawable = false;
|
|
1304
1398
|
/** Frame in which the page became drawable. Its snapshot exists from that frame's paint on. */
|
|
1305
1399
|
promotedFrame = -1;
|
|
1306
|
-
/** Kept drawable for the live (hit-testable) or focused node. */
|
|
1307
|
-
pinned = false;
|
|
1308
1400
|
/** Registered for hit testing with `updateElementGeometry`. */
|
|
1309
1401
|
registered = false;
|
|
1310
|
-
|
|
1311
|
-
|
|
1402
|
+
/** The atlas slot of the page's last whole snapshot, reused when it is drawn again that size. */
|
|
1403
|
+
wholeSlot = null;
|
|
1404
|
+
shelves;
|
|
1312
1405
|
constructor(width, height) {
|
|
1313
1406
|
this.width = width;
|
|
1314
1407
|
this.height = height;
|
|
1408
|
+
this.shelves = new Shelves(width + GAP, height + GAP, 1.25);
|
|
1315
1409
|
this.element = document.createElement("div");
|
|
1316
1410
|
this.element.dataset.gpucanvasPage = "";
|
|
1317
1411
|
Object.assign(this.element.style, {
|
|
@@ -1330,25 +1424,8 @@ var Page = class {
|
|
|
1330
1424
|
}
|
|
1331
1425
|
/** Reserves a spot for a `width` x `height` node, or returns null when the page is full. */
|
|
1332
1426
|
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--;
|
|
1427
|
+
const at = this.shelves.place(Math.ceil(width) + GAP, Math.ceil(height) + GAP);
|
|
1428
|
+
return at && { page: this, ...at };
|
|
1352
1429
|
}
|
|
1353
1430
|
setDrawable(drawable, frame) {
|
|
1354
1431
|
if (this.drawable === drawable) return;
|
|
@@ -1366,229 +1443,172 @@ var Page = class {
|
|
|
1366
1443
|
}
|
|
1367
1444
|
};
|
|
1368
1445
|
|
|
1369
|
-
// src/gpu-
|
|
1446
|
+
// src/gpu-pages.ts
|
|
1447
|
+
var Pages = class {
|
|
1448
|
+
constructor(canvas, pageSize) {
|
|
1449
|
+
this.canvas = canvas;
|
|
1450
|
+
this.pageSize = pageSize;
|
|
1451
|
+
}
|
|
1452
|
+
canvas;
|
|
1453
|
+
pageSize;
|
|
1454
|
+
members = /* @__PURE__ */ new Map();
|
|
1455
|
+
byElement = /* @__PURE__ */ new Map();
|
|
1456
|
+
open = null;
|
|
1457
|
+
get size() {
|
|
1458
|
+
return this.members.size;
|
|
1459
|
+
}
|
|
1460
|
+
all() {
|
|
1461
|
+
return this.members.keys();
|
|
1462
|
+
}
|
|
1463
|
+
has(page) {
|
|
1464
|
+
return this.members.has(page);
|
|
1465
|
+
}
|
|
1466
|
+
nodesOf(page) {
|
|
1467
|
+
return this.members.get(page);
|
|
1468
|
+
}
|
|
1469
|
+
/** The page whose element is `element`. */
|
|
1470
|
+
pageOf(element) {
|
|
1471
|
+
return this.byElement.get(element);
|
|
1472
|
+
}
|
|
1473
|
+
/** Counts a node, already placed on its page, as one of the page's nodes. */
|
|
1474
|
+
join(node) {
|
|
1475
|
+
this.members.get(node.page).add(node);
|
|
1476
|
+
}
|
|
1477
|
+
release(node) {
|
|
1478
|
+
const members = this.members.get(node.page);
|
|
1479
|
+
members.delete(node);
|
|
1480
|
+
if (members.size === 0 && node.page !== this.open) this.drop(node.page);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Moves a node onto a page of its own and returns the page it left. Chrome rasterizes a
|
|
1484
|
+
* snapshot's whole source element at the requested scale before cropping, so zoomed in, a crop
|
|
1485
|
+
* from a shared page pays for every node on it: about 100 ms at 16x for two dozen app screens,
|
|
1486
|
+
* under 10 ms for one. `moveBefore` keeps focus, running transitions and other DOM state across
|
|
1487
|
+
* the move.
|
|
1488
|
+
*/
|
|
1489
|
+
lift(node) {
|
|
1490
|
+
const previous = node.page;
|
|
1491
|
+
const page = this.newPage(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
|
|
1492
|
+
page.place(node.rect.width, node.rect.height);
|
|
1493
|
+
const target = page.element;
|
|
1494
|
+
if (target.moveBefore) target.moveBefore(node.element, null);
|
|
1495
|
+
else target.append(node.element);
|
|
1496
|
+
node.element.style.left = "0px";
|
|
1497
|
+
node.element.style.top = "0px";
|
|
1498
|
+
this.release(node);
|
|
1499
|
+
node.page = page;
|
|
1500
|
+
node.pageX = 0;
|
|
1501
|
+
node.pageY = 0;
|
|
1502
|
+
node.solo = true;
|
|
1503
|
+
this.join(node);
|
|
1504
|
+
return previous;
|
|
1505
|
+
}
|
|
1506
|
+
/** Lays out `element` on the open page, or a new one when it is full or too small. */
|
|
1507
|
+
place(element, rect) {
|
|
1508
|
+
const { pageSize } = this;
|
|
1509
|
+
let slot = this.open?.place(rect.width, rect.height) ?? null;
|
|
1510
|
+
if (!slot) {
|
|
1511
|
+
const page = this.newPage(
|
|
1512
|
+
Math.max(pageSize, Math.ceil(rect.width)),
|
|
1513
|
+
Math.max(pageSize, Math.ceil(rect.height))
|
|
1514
|
+
);
|
|
1515
|
+
if (page.width === pageSize && page.height === pageSize) this.open = page;
|
|
1516
|
+
slot = page.place(rect.width, rect.height);
|
|
1517
|
+
}
|
|
1518
|
+
Object.assign(element.style, {
|
|
1519
|
+
position: "absolute",
|
|
1520
|
+
left: `${slot.x}px`,
|
|
1521
|
+
top: `${slot.y}px`,
|
|
1522
|
+
width: `${rect.width}px`,
|
|
1523
|
+
height: `${rect.height}px`
|
|
1524
|
+
});
|
|
1525
|
+
slot.page.element.append(element);
|
|
1526
|
+
return slot;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Parks drawable pages that nothing in this frame's plan needs: a page promoted for a camera
|
|
1530
|
+
* that has moved on would otherwise be painted by Chrome every frame.
|
|
1531
|
+
*/
|
|
1532
|
+
parkIdle(plans, frameNumber, isPinned) {
|
|
1533
|
+
for (const page of this.members.keys()) {
|
|
1534
|
+
if (page.drawable && !plans.has(page) && page.promotedFrame < frameNumber && !isPinned(page))
|
|
1535
|
+
page.setDrawable(false, frameNumber);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
newPage(width, height) {
|
|
1539
|
+
const page = new Page(width, height);
|
|
1540
|
+
this.members.set(page, /* @__PURE__ */ new Set());
|
|
1541
|
+
this.byElement.set(page.element, page);
|
|
1542
|
+
this.canvas.append(page.element);
|
|
1543
|
+
return page;
|
|
1544
|
+
}
|
|
1545
|
+
drop(page) {
|
|
1546
|
+
this.members.delete(page);
|
|
1547
|
+
this.byElement.delete(page.element);
|
|
1548
|
+
page.element.remove();
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
// src/gpu-plan.ts
|
|
1370
1553
|
var MIN_RASTER_SCALE = 1 / 64;
|
|
1371
1554
|
var MAX_RASTER_SCALE = 2;
|
|
1372
1555
|
var MAX_DETAIL_SCALE = 64;
|
|
1373
1556
|
var TILE_PIXELS = 512;
|
|
1374
1557
|
var CONTENT_SETTLE_MS = 150;
|
|
1375
|
-
var SMALL_NODE_PX = 200;
|
|
1376
|
-
var SMALL_REFRESH_MS = 250;
|
|
1377
|
-
var right = (rect) => rect.x + rect.width;
|
|
1378
|
-
var bottom = (rect) => rect.y + rect.height;
|
|
1379
|
-
var SETTLE_MS = 120;
|
|
1380
|
-
var USE_EVENTS = ["click", "input", "change", "keydown"];
|
|
1381
|
-
var USED_MS = 1e3;
|
|
1382
|
-
var LINGER_MS = 300;
|
|
1383
1558
|
var MAX_OVERSAMPLING = 4;
|
|
1384
|
-
var DEFAULT_PAGE_SIZE = 2048;
|
|
1385
|
-
var THUMBNAIL_SCALE = 1 / 16;
|
|
1386
|
-
var THUMBNAIL_REFRESH_MS = 1e3;
|
|
1387
|
-
var MAX_PROMOTIONS_PER_FRAME = 8;
|
|
1388
|
-
var SLOW_FRAME_MS = 22;
|
|
1389
|
-
var FAST_FRAME_MS = 12;
|
|
1390
1559
|
var MISSING = 0;
|
|
1391
1560
|
var DIRTY = 1;
|
|
1392
1561
|
var BLURRY = 2;
|
|
1393
1562
|
var OVERSAMPLED = 3;
|
|
1394
|
-
var CANVAS_TRANSFORM_HIT_TESTS = typeof HTMLCanvasElement !== "undefined" && "getElementImageDefaultSize" in HTMLCanvasElement.prototype;
|
|
1395
|
-
function supportsGpuCanvas() {
|
|
1396
|
-
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;
|
|
1397
|
-
}
|
|
1398
1563
|
function rasterScaleFor(deviceScale) {
|
|
1399
1564
|
const scale = 2 ** Math.ceil(Math.log2(deviceScale) - 0.14);
|
|
1400
1565
|
return Math.min(MAX_DETAIL_SCALE, Math.max(MIN_RASTER_SCALE, scale));
|
|
1401
1566
|
}
|
|
1402
|
-
var
|
|
1403
|
-
const
|
|
1404
|
-
const
|
|
1405
|
-
|
|
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;
|
|
1567
|
+
var distanceToCenter = (rect, view) => {
|
|
1568
|
+
const a = center(rect);
|
|
1569
|
+
const b = center(view);
|
|
1570
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
1408
1571
|
};
|
|
1409
|
-
var
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
const context = canvas.getContext("webgpu");
|
|
1442
|
-
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
1443
|
-
context.configure({ device, format, alphaMode: "opaque" });
|
|
1444
|
-
const atlasSize = Math.min(options.atlasSize ?? 4096, device.limits.maxTextureDimension2D);
|
|
1445
|
-
const atlas = new Atlas(
|
|
1446
|
-
device,
|
|
1447
|
-
atlasSize,
|
|
1448
|
-
Math.min(options.atlasLayers ?? 5, device.limits.maxTextureArrayLayers)
|
|
1449
|
-
);
|
|
1450
|
-
const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, atlasSize);
|
|
1451
|
-
const maxRasterCalls = options.maxRastersPerFrame ?? 12;
|
|
1452
|
-
const rasterTimeBudgetMs = options.rasterTimeBudgetMs ?? 6;
|
|
1453
|
-
const rasterPixelBudget = options.rasterPixelBudget ?? 24e6;
|
|
1454
|
-
const colorsFor = (css) => {
|
|
1455
|
-
const background = cssColorToRgba(css);
|
|
1456
|
-
const shade = background[0] + background[1] + background[2] > 1.5 ? 0.92 : 1.25;
|
|
1457
|
-
const placeholder = background.map(
|
|
1458
|
-
(channel) => Math.min(1, channel * shade + (shade > 1 ? 0.02 : 0))
|
|
1459
|
-
);
|
|
1460
|
-
placeholder[3] = 1;
|
|
1461
|
-
return [background, placeholder];
|
|
1462
|
-
};
|
|
1463
|
-
const renderer = new CompositeRenderer(
|
|
1464
|
-
device,
|
|
1465
|
-
context,
|
|
1466
|
-
format,
|
|
1467
|
-
atlas,
|
|
1468
|
-
...colorsFor(options.background ?? "#f4f4f5")
|
|
1469
|
-
);
|
|
1470
|
-
const selectionColor = premultiplied(
|
|
1471
|
-
cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
|
|
1472
|
-
);
|
|
1473
|
-
const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
|
|
1474
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
1475
|
-
const effects = /* @__PURE__ */ new Map();
|
|
1476
|
-
const startedAt = performance.now();
|
|
1477
|
-
const FAR_AWAY = { x: -1e7, y: -1e7 };
|
|
1478
|
-
renderer.onEffectReady = () => schedule();
|
|
1479
|
-
const runs = [];
|
|
1480
|
-
let effectsMoving = false;
|
|
1481
|
-
let selection = /* @__PURE__ */ new Set();
|
|
1482
|
-
let hovered = null;
|
|
1483
|
-
const ringRects = /* @__PURE__ */ new Map();
|
|
1484
|
-
const ringRect = (element) => {
|
|
1485
|
-
const own = nodes.get(element);
|
|
1486
|
-
if (own) return own.rect;
|
|
1487
|
-
const at = element.isConnected ? nodeOfElement(nodes, element) : null;
|
|
1488
|
-
if (!at) return null;
|
|
1489
|
-
const node = nodes.get(at);
|
|
1490
|
-
let rect = ringRects.get(element);
|
|
1491
|
-
if (!rect) ringRects.set(element, rect = rectInNode(at, node.rect, element));
|
|
1492
|
-
return rect;
|
|
1493
|
-
};
|
|
1494
|
-
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
|
-
const pageByElement = /* @__PURE__ */ new Map();
|
|
1509
|
-
let openPage = null;
|
|
1510
|
-
let camera = options.camera ?? DEFAULT_CAMERA;
|
|
1511
|
-
let lastCameraChange = -Infinity;
|
|
1512
|
-
let frameNumber = 0;
|
|
1513
|
-
let promotionsPerFrame = 2;
|
|
1514
|
-
let lastFrameAt = 0;
|
|
1515
|
-
let rafId = 0;
|
|
1516
|
-
let destroyed = false;
|
|
1517
|
-
let live = null;
|
|
1518
|
-
let livePositioned = false;
|
|
1519
|
-
let lingering = null;
|
|
1520
|
-
let lingerTimer = 0;
|
|
1521
|
-
let focused = null;
|
|
1522
|
-
let pointer = null;
|
|
1523
|
-
let settleTimer = 0;
|
|
1524
|
-
const stats = {
|
|
1525
|
-
visible: 0,
|
|
1526
|
-
pending: 0,
|
|
1527
|
-
rasterizedLastFrame: 0,
|
|
1528
|
-
rasterCallsLastFrame: 0,
|
|
1529
|
-
rasterMs: 0,
|
|
1530
|
-
frameMs: 0,
|
|
1531
|
-
frames: 0,
|
|
1532
|
-
lostPixels: 0,
|
|
1533
|
-
lostDetail: 0,
|
|
1534
|
-
onThumbnail: 0,
|
|
1535
|
-
/** Last plan's nodes per need class: missing, dirty, blurry, oversampled. */
|
|
1536
|
-
needs: [0, 0, 0, 0]
|
|
1537
|
-
};
|
|
1538
|
-
const visible = [];
|
|
1539
|
-
const plans = /* @__PURE__ */ new Map();
|
|
1540
|
-
const schedule = () => {
|
|
1541
|
-
if (rafId || destroyed) return;
|
|
1542
|
-
rafId = requestAnimationFrame(() => {
|
|
1543
|
-
rafId = 0;
|
|
1544
|
-
frameNumber++;
|
|
1545
|
-
frame(false);
|
|
1546
|
-
});
|
|
1547
|
-
};
|
|
1548
|
-
const resizeObserver = new ResizeObserver(([entry]) => {
|
|
1549
|
-
const size = entry.devicePixelContentBoxSize?.[0];
|
|
1550
|
-
canvas.width = Math.max(
|
|
1551
|
-
1,
|
|
1552
|
-
size ? size.inlineSize : Math.round(entry.contentRect.width * devicePixelRatio)
|
|
1553
|
-
);
|
|
1554
|
-
canvas.height = Math.max(
|
|
1555
|
-
1,
|
|
1556
|
-
size ? size.blockSize : Math.round(entry.contentRect.height * devicePixelRatio)
|
|
1557
|
-
);
|
|
1558
|
-
schedule();
|
|
1559
|
-
});
|
|
1560
|
-
resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
|
|
1561
|
-
const transitioning = /* @__PURE__ */ new Map();
|
|
1562
|
-
const captures = [];
|
|
1563
|
-
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);
|
|
1564
|
-
let detailScale = 1;
|
|
1565
|
-
let frameView = { x: 0, y: 0, width: 0, height: 0 };
|
|
1566
|
-
let contentTimer = 0;
|
|
1567
|
-
const scheduleAfterContentSettles = () => {
|
|
1568
|
-
clearTimeout(contentTimer);
|
|
1569
|
-
contentTimer = window.setTimeout(schedule, CONTENT_SETTLE_MS + 10);
|
|
1570
|
-
};
|
|
1571
|
-
let refreshTimer = 0;
|
|
1572
|
-
let refreshAt = Infinity;
|
|
1573
|
-
const refreshWait = (node, now) => Math.max(node.rect.width, node.rect.height) * camera.zoom < SMALL_NODE_PX ? node.refreshedAt + SMALL_REFRESH_MS - now : 0;
|
|
1574
|
-
const refreshLater = (wait) => {
|
|
1575
|
-
const at = performance.now() + wait;
|
|
1576
|
-
if (at >= refreshAt) return;
|
|
1577
|
-
clearTimeout(refreshTimer);
|
|
1578
|
-
refreshAt = at;
|
|
1579
|
-
refreshTimer = window.setTimeout(() => {
|
|
1580
|
-
refreshAt = Infinity;
|
|
1581
|
-
schedule();
|
|
1582
|
-
}, wait);
|
|
1583
|
-
};
|
|
1584
|
-
const planFrame = (view, desiredScale, deviceScale, moving) => {
|
|
1572
|
+
var Planner = class {
|
|
1573
|
+
constructor(atlas, lift, schedule) {
|
|
1574
|
+
this.atlas = atlas;
|
|
1575
|
+
this.lift = lift;
|
|
1576
|
+
this.schedule = schedule;
|
|
1577
|
+
}
|
|
1578
|
+
atlas;
|
|
1579
|
+
lift;
|
|
1580
|
+
schedule;
|
|
1581
|
+
/** This frame's texels per CSS pixel for tiles. */
|
|
1582
|
+
detailScale = 1;
|
|
1583
|
+
/** This frame's world rect in view. */
|
|
1584
|
+
view = { x: 0, y: 0, width: 0, height: 0 };
|
|
1585
|
+
visible = [];
|
|
1586
|
+
plans = /* @__PURE__ */ new Map();
|
|
1587
|
+
/** This frame's plans, most urgent first. */
|
|
1588
|
+
ordered = [];
|
|
1589
|
+
/** Last plan's nodes per need class: missing, dirty, blurry, oversampled. */
|
|
1590
|
+
needs = [0, 0, 0, 0];
|
|
1591
|
+
zoom = 1;
|
|
1592
|
+
contentTimer = 0;
|
|
1593
|
+
refreshTimer = 0;
|
|
1594
|
+
refreshAt = Infinity;
|
|
1595
|
+
/**
|
|
1596
|
+
* Builds this frame's raster plan: which visible nodes need pixels, grouped by page. `dpr` is
|
|
1597
|
+
* canvas pixels per CSS pixel; `moving` means the camera has not rested yet.
|
|
1598
|
+
*/
|
|
1599
|
+
plan(nodes, view, zoom, dpr, moving) {
|
|
1600
|
+
const { atlas, visible, plans } = this;
|
|
1601
|
+
const atlasSize = atlas.size;
|
|
1602
|
+
const deviceScale = zoom * dpr;
|
|
1603
|
+
const desiredScale = rasterScaleFor(deviceScale);
|
|
1585
1604
|
const now = performance.now();
|
|
1586
|
-
|
|
1587
|
-
|
|
1605
|
+
this.zoom = zoom;
|
|
1606
|
+
this.detailScale = Math.min(deviceScale, MAX_DETAIL_SCALE);
|
|
1607
|
+
this.view = view;
|
|
1588
1608
|
visible.length = 0;
|
|
1589
1609
|
plans.clear();
|
|
1590
|
-
|
|
1591
|
-
for (const node of nodes
|
|
1610
|
+
this.needs = [0, 0, 0, 0];
|
|
1611
|
+
for (const node of nodes) {
|
|
1592
1612
|
if (!intersects(node.rect, view)) continue;
|
|
1593
1613
|
visible.push(node);
|
|
1594
1614
|
const scale = Math.min(
|
|
@@ -1599,14 +1619,14 @@ async function createGpuCanvas(host, options) {
|
|
|
1599
1619
|
);
|
|
1600
1620
|
if (node.dirty) node.changedAt = now;
|
|
1601
1621
|
const thumbnailEnough = scale <= THUMBNAIL_SCALE;
|
|
1602
|
-
if (thumbnailEnough &&
|
|
1622
|
+
if (thumbnailEnough && hasFreshThumb(atlas, node) && !node.dirty && !node.wholeStale) {
|
|
1603
1623
|
node.slot = node.thumb;
|
|
1604
1624
|
node.slotScale = THUMBNAIL_SCALE;
|
|
1605
1625
|
}
|
|
1606
1626
|
const animating = now - node.changedAt < CONTENT_SETTLE_MS;
|
|
1607
1627
|
const deep = desiredScale > scale;
|
|
1608
|
-
if (deep && !node.solo) lift(node);
|
|
1609
|
-
const jobs = deep ? planDetail(node, view, animating, moving) : [];
|
|
1628
|
+
if (deep && !node.solo) this.lift(node);
|
|
1629
|
+
const jobs = deep ? this.planDetail(node, view, animating, moving) : [];
|
|
1610
1630
|
if (!deep) {
|
|
1611
1631
|
node.preview = null;
|
|
1612
1632
|
node.tiles.length = 0;
|
|
@@ -1616,12 +1636,12 @@ async function createGpuCanvas(host, options) {
|
|
|
1616
1636
|
if (!valid) need = atlas.isValid(node.thumb) ? DIRTY : MISSING;
|
|
1617
1637
|
else if (deep) need = node.slotScale < scale / (moving ? 4 : 2) ? BLURRY : -1;
|
|
1618
1638
|
else if (node.dirty || node.wholeStale) {
|
|
1619
|
-
const wait = refreshWait(node, now);
|
|
1620
|
-
if (wait > 0) refreshLater(wait);
|
|
1639
|
+
const wait = this.refreshWait(node, now);
|
|
1640
|
+
if (wait > 0) this.refreshLater(wait);
|
|
1621
1641
|
else need = DIRTY;
|
|
1622
1642
|
} else if (node.slotScale < (moving ? scale / 2 : scale)) need = BLURRY;
|
|
1623
1643
|
else if (!moving && node.slotScale > scale * MAX_OVERSAMPLING) need = OVERSAMPLED;
|
|
1624
|
-
if (need >= 0)
|
|
1644
|
+
if (need >= 0) this.needs[need]++;
|
|
1625
1645
|
if (need < 0 && !node.previewTarget && jobs.length === 0) continue;
|
|
1626
1646
|
node.targetScale = thumbnailEnough ? THUMBNAIL_SCALE : scale;
|
|
1627
1647
|
node.priority = (need < 0 ? BLURRY : need) * 1e12 + distanceToCenter(node.rect, view);
|
|
@@ -1636,8 +1656,33 @@ async function createGpuCanvas(host, options) {
|
|
|
1636
1656
|
plan.tiles.push(...jobs);
|
|
1637
1657
|
}
|
|
1638
1658
|
for (const plan of plans.values()) plan.tiles.sort((a, b) => a.priority - b.priority);
|
|
1639
|
-
|
|
1640
|
-
|
|
1659
|
+
this.ordered = [...plans.values()].sort((a, b) => a.priority - b.priority);
|
|
1660
|
+
}
|
|
1661
|
+
/** Atlas slots to keep when it is full: what is on screen, whole snapshots before sharp tiles. */
|
|
1662
|
+
*keep() {
|
|
1663
|
+
const { visible, detailScale, view } = this;
|
|
1664
|
+
const onScreen = (tile) => intersects(tile.rect, view);
|
|
1665
|
+
for (const node of visible) yield node.slot;
|
|
1666
|
+
for (const node of visible) {
|
|
1667
|
+
for (const tile of node.tiles) {
|
|
1668
|
+
if (isCurrentTile(tile, detailScale) && onScreen(tile)) yield tile.slot;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
for (const node of visible) {
|
|
1672
|
+
yield node.preview;
|
|
1673
|
+
for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
destroy() {
|
|
1677
|
+
clearTimeout(this.contentTimer);
|
|
1678
|
+
clearTimeout(this.refreshTimer);
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Plans a zoomed-in node: a preview while its content animates, otherwise the tiles in view
|
|
1682
|
+
* it is missing at this frame's scale.
|
|
1683
|
+
*/
|
|
1684
|
+
planDetail(node, view, animating, moving) {
|
|
1685
|
+
const { atlas, detailScale } = this;
|
|
1641
1686
|
const shown = intersection(node.rect, view);
|
|
1642
1687
|
node.previewTarget = null;
|
|
1643
1688
|
node.tiles = node.tiles.filter((tile) => atlas.isValid(tile.slot));
|
|
@@ -1647,19 +1692,19 @@ async function createGpuCanvas(host, options) {
|
|
|
1647
1692
|
const scale = detailScale / 2;
|
|
1648
1693
|
const stale = !node.preview || node.dirty || node.previewScale !== scale || !contains(node.previewRect, shown);
|
|
1649
1694
|
if (stale) {
|
|
1650
|
-
node.previewTarget = fitPreview(shown, scale);
|
|
1695
|
+
node.previewTarget = this.fitPreview(shown, scale);
|
|
1651
1696
|
node.previewTargetScale = scale;
|
|
1652
1697
|
}
|
|
1653
|
-
scheduleAfterContentSettles();
|
|
1698
|
+
this.scheduleAfterContentSettles();
|
|
1654
1699
|
return [];
|
|
1655
1700
|
}
|
|
1656
|
-
const current = node.tiles.filter((tile) => tile
|
|
1701
|
+
const current = node.tiles.filter((tile) => isCurrentTile(tile, detailScale));
|
|
1657
1702
|
if (moving && current.length === 0) return [];
|
|
1658
1703
|
const size = Math.max(1, Math.floor(TILE_PIXELS / detailScale));
|
|
1659
1704
|
const jobs = [];
|
|
1660
1705
|
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(
|
|
1706
|
+
for (let row = first(shown.y, node.rect.y); row <= first(shown.y + shown.height - 1e-6, node.rect.y); row++) {
|
|
1707
|
+
for (let column = first(shown.x, node.rect.x); column <= first(shown.x + shown.width - 1e-6, node.rect.x); column++) {
|
|
1663
1708
|
if (current.some((tile) => tile.column === column && tile.row === row)) continue;
|
|
1664
1709
|
const rect = intersection(node.rect, {
|
|
1665
1710
|
x: node.rect.x + column * size,
|
|
@@ -1682,61 +1727,107 @@ async function createGpuCanvas(host, options) {
|
|
|
1682
1727
|
node.preview = null;
|
|
1683
1728
|
}
|
|
1684
1729
|
return jobs;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1730
|
+
}
|
|
1731
|
+
/** The world rect a preview covers: `shown`, shrunk around its center to fit one layer. */
|
|
1732
|
+
fitPreview(shown, scale) {
|
|
1733
|
+
const limit = (this.atlas.size - 2) / scale;
|
|
1688
1734
|
const fit = (start, size) => size <= limit ? [start, size] : [start + (size - limit) / 2, limit];
|
|
1689
1735
|
const [x, width] = fit(shown.x, shown.width);
|
|
1690
1736
|
const [y, height] = fit(shown.y, shown.height);
|
|
1691
1737
|
return { x, y, width, height };
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1738
|
+
}
|
|
1739
|
+
scheduleAfterContentSettles() {
|
|
1740
|
+
clearTimeout(this.contentTimer);
|
|
1741
|
+
this.contentTimer = window.setTimeout(this.schedule, CONTENT_SETTLE_MS + 10);
|
|
1742
|
+
}
|
|
1743
|
+
/** How long a changed node waits before it is redrawn: only small ones wait. */
|
|
1744
|
+
refreshWait(node, now) {
|
|
1745
|
+
return smallOnScreen(node, this.zoom) ? node.refreshedAt + SMALL_REFRESH_MS - now : 0;
|
|
1746
|
+
}
|
|
1747
|
+
refreshLater(wait) {
|
|
1748
|
+
const at = performance.now() + wait;
|
|
1749
|
+
if (at >= this.refreshAt) return;
|
|
1750
|
+
clearTimeout(this.refreshTimer);
|
|
1751
|
+
this.refreshAt = at;
|
|
1752
|
+
this.refreshTimer = window.setTimeout(() => {
|
|
1753
|
+
this.refreshAt = Infinity;
|
|
1754
|
+
this.schedule();
|
|
1755
|
+
}, wait);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/capture.ts
|
|
1760
|
+
function captureRegion(device, source, region, width, height) {
|
|
1761
|
+
const texture = device.createTexture({
|
|
1762
|
+
size: { width, height },
|
|
1763
|
+
format: "rgba8unorm",
|
|
1764
|
+
usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
1765
|
+
});
|
|
1766
|
+
try {
|
|
1767
|
+
device.queue.drawElementImageToTexture(
|
|
1768
|
+
{
|
|
1769
|
+
source,
|
|
1770
|
+
sourceX: region.x,
|
|
1771
|
+
sourceY: region.y,
|
|
1772
|
+
sourceWidth: region.width,
|
|
1773
|
+
sourceHeight: region.height
|
|
1774
|
+
},
|
|
1775
|
+
{ texture, size: { width, height }, premultipliedAlpha: false }
|
|
1776
|
+
);
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
texture.destroy();
|
|
1779
|
+
throw error;
|
|
1780
|
+
}
|
|
1781
|
+
return readTexture(device, texture, width, height);
|
|
1782
|
+
}
|
|
1783
|
+
async function readTexture(device, texture, width, height) {
|
|
1784
|
+
const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
|
|
1785
|
+
const buffer = device.createBuffer({
|
|
1786
|
+
size: bytesPerRow * height,
|
|
1787
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
1788
|
+
});
|
|
1789
|
+
const encoder = device.createCommandEncoder();
|
|
1790
|
+
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
|
|
1791
|
+
device.queue.submit([encoder.finish()]);
|
|
1792
|
+
try {
|
|
1793
|
+
await buffer.mapAsync(GPUMapMode.READ);
|
|
1794
|
+
const rows = new Uint8Array(buffer.getMappedRange());
|
|
1795
|
+
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
1796
|
+
for (let y = 0; y < height; y++) {
|
|
1797
|
+
pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
1728
1798
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1799
|
+
return new ImageData(pixels, width, height);
|
|
1800
|
+
} finally {
|
|
1801
|
+
buffer.destroy();
|
|
1802
|
+
texture.destroy();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/gpu-raster.ts
|
|
1807
|
+
var THUMBNAIL_REFRESH_MS = 1e3;
|
|
1808
|
+
var MAX_CAPTURE_TRIES = 20;
|
|
1809
|
+
var Rasterizer = class {
|
|
1810
|
+
constructor(device, atlas, budget) {
|
|
1811
|
+
this.device = device;
|
|
1812
|
+
this.atlas = atlas;
|
|
1813
|
+
this.budget = budget;
|
|
1814
|
+
}
|
|
1815
|
+
device;
|
|
1816
|
+
atlas;
|
|
1817
|
+
budget;
|
|
1818
|
+
/**
|
|
1819
|
+
* Rasterizes planned pages, most urgent first, within the frame's budget. A page where many
|
|
1820
|
+
* nodes need pixels is drawn whole in one call; otherwise each node is cropped out. Pages that
|
|
1821
|
+
* end up with nothing left to draw are parked unless `isPinned` holds them.
|
|
1822
|
+
*/
|
|
1823
|
+
rasterize(ordered, pages, frameNumber, start, isPinned) {
|
|
1824
|
+
const { atlas, budget } = this;
|
|
1825
|
+
const atlasSize = atlas.size;
|
|
1734
1826
|
let calls = 0;
|
|
1735
1827
|
let pixels = 0;
|
|
1736
1828
|
let rasterized = 0;
|
|
1737
1829
|
let pending = 0;
|
|
1738
|
-
const overBudget = () => calls > 0 && (calls >=
|
|
1739
|
-
const ordered = [...plans.values()].sort((a, b) => a.priority - b.priority);
|
|
1830
|
+
const overBudget = () => calls > 0 && (calls >= budget.calls || pixels >= budget.pixels || performance.now() - start >= budget.ms);
|
|
1740
1831
|
for (const plan of ordered) {
|
|
1741
1832
|
const { page } = plan;
|
|
1742
1833
|
if (!page.drawable || page.promotedFrame >= frameNumber || overBudget()) {
|
|
@@ -1744,18 +1835,18 @@ async function createGpuCanvas(host, options) {
|
|
|
1744
1835
|
continue;
|
|
1745
1836
|
}
|
|
1746
1837
|
const scale = plan.nodes.reduce((max, node) => Math.max(max, node.targetScale), 0);
|
|
1747
|
-
const members = pages.
|
|
1838
|
+
const members = pages.nodesOf(page);
|
|
1748
1839
|
const neededArea = plan.nodes.reduce((sum, n) => sum + n.rect.width * n.rect.height, 0);
|
|
1749
1840
|
const wholeFits = page.width * scale <= atlasSize / 2 && page.height * scale <= atlasSize / 2;
|
|
1750
1841
|
const whole = wholeFits && (plan.nodes.length >= 6 || neededArea >= 0.25 * page.width * page.height);
|
|
1751
|
-
const wholeSlot = whole ? slotFor(
|
|
1752
|
-
|
|
1842
|
+
const wholeSlot = whole ? this.slotFor(
|
|
1843
|
+
page.wholeSlot,
|
|
1753
1844
|
Math.ceil(page.width * scale),
|
|
1754
1845
|
Math.ceil(page.height * scale),
|
|
1755
1846
|
scale === THUMBNAIL_SCALE
|
|
1756
1847
|
) : null;
|
|
1757
|
-
if (wholeSlot)
|
|
1758
|
-
if (wholeSlot && !
|
|
1848
|
+
if (wholeSlot) page.wholeSlot = wholeSlot;
|
|
1849
|
+
if (wholeSlot && !this.draw(page, null, wholeSlot)) {
|
|
1759
1850
|
pending += plan.nodes.length + plan.previews.length + plan.tiles.length;
|
|
1760
1851
|
continue;
|
|
1761
1852
|
}
|
|
@@ -1772,7 +1863,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1772
1863
|
width: node.rect.width * scale,
|
|
1773
1864
|
height: node.rect.height * scale
|
|
1774
1865
|
};
|
|
1775
|
-
|
|
1866
|
+
recordWhole(node, part, scale, start);
|
|
1776
1867
|
}
|
|
1777
1868
|
rasterized += members.size;
|
|
1778
1869
|
} else {
|
|
@@ -1782,52 +1873,37 @@ async function createGpuCanvas(host, options) {
|
|
|
1782
1873
|
continue;
|
|
1783
1874
|
}
|
|
1784
1875
|
const thumbnail = node.targetScale === THUMBNAIL_SCALE;
|
|
1785
|
-
const slot = slotFor(
|
|
1876
|
+
const slot = this.slotFor(
|
|
1786
1877
|
thumbnail ? node.thumb : node.slot,
|
|
1787
|
-
|
|
1788
|
-
|
|
1878
|
+
texels(node.rect.width, node.targetScale),
|
|
1879
|
+
texels(node.rect.height, node.targetScale),
|
|
1789
1880
|
thumbnail
|
|
1790
1881
|
);
|
|
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)) {
|
|
1882
|
+
if (!slot || !this.draw(page, nodeRegion(node), slot)) {
|
|
1798
1883
|
pending++;
|
|
1799
1884
|
continue;
|
|
1800
1885
|
}
|
|
1801
1886
|
calls++;
|
|
1802
1887
|
pixels += slot.width * slot.height;
|
|
1803
|
-
|
|
1888
|
+
recordWhole(node, slot, node.targetScale, start);
|
|
1804
1889
|
rasterized++;
|
|
1805
1890
|
}
|
|
1806
1891
|
}
|
|
1807
1892
|
const warm = /* @__PURE__ */ new Set();
|
|
1808
1893
|
const drawRegion = (node, rect, scale2, previous) => {
|
|
1809
1894
|
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
|
-
);
|
|
1895
|
+
const slot = this.slotFor(previous, texels(rect.width, scale2), texels(rect.height, scale2));
|
|
1815
1896
|
const region = {
|
|
1816
1897
|
x: node.pageX + rect.x - node.rect.x,
|
|
1817
1898
|
y: node.pageY + rect.y - node.rect.y,
|
|
1818
1899
|
width: rect.width,
|
|
1819
1900
|
height: rect.height
|
|
1820
1901
|
};
|
|
1821
|
-
if (!slot || !
|
|
1902
|
+
if (!slot || !this.draw(page, region, slot)) return null;
|
|
1822
1903
|
calls++;
|
|
1823
1904
|
warm.add(scale2);
|
|
1824
1905
|
pixels += slot.width * slot.height;
|
|
1825
|
-
|
|
1826
|
-
node.dirty = false;
|
|
1827
|
-
node.wholeStale = true;
|
|
1828
|
-
node.thumbStale = true;
|
|
1829
|
-
node.refreshedAt = start;
|
|
1830
|
-
}
|
|
1906
|
+
recordPartial(node, start);
|
|
1831
1907
|
return slot;
|
|
1832
1908
|
};
|
|
1833
1909
|
for (const node of plan.previews) {
|
|
@@ -1858,107 +1934,715 @@ async function createGpuCanvas(host, options) {
|
|
|
1858
1934
|
}
|
|
1859
1935
|
pending += tilesLeft;
|
|
1860
1936
|
for (const node of members) {
|
|
1861
|
-
if (
|
|
1937
|
+
if (hasFreshThumb(atlas, node) || node.dirty) continue;
|
|
1862
1938
|
if (atlas.isValid(node.thumb) && start - node.thumbAt < THUMBNAIL_REFRESH_MS) continue;
|
|
1863
|
-
const slot = slotFor(
|
|
1939
|
+
const slot = this.slotFor(
|
|
1864
1940
|
node.thumb,
|
|
1865
|
-
|
|
1866
|
-
|
|
1941
|
+
texels(node.rect.width, THUMBNAIL_SCALE),
|
|
1942
|
+
texels(node.rect.height, THUMBNAIL_SCALE),
|
|
1867
1943
|
true
|
|
1868
1944
|
);
|
|
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;
|
|
1945
|
+
if (!slot || !this.draw(page, nodeRegion(node), slot)) continue;
|
|
1876
1946
|
calls++;
|
|
1877
|
-
node
|
|
1878
|
-
node.thumbStale = false;
|
|
1879
|
-
node.thumbAt = start;
|
|
1947
|
+
setThumb(node, slot, start);
|
|
1880
1948
|
}
|
|
1881
1949
|
const done = plan.nodes.every((node) => !node.dirty && atlas.isValid(node.slot)) && plan.previews.every((node) => node.previewTarget === null) && tilesLeft === 0;
|
|
1882
1950
|
if (done && !isPinned(page)) page.setDrawable(false, frameNumber);
|
|
1883
1951
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1952
|
+
return { pending, rasterized, calls };
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Draws each node waiting for a snapshot into a texture of its own and reads it back, removing
|
|
1956
|
+
* it from `captures`. A page that just became drawable has no snapshot until the next paint
|
|
1957
|
+
* event, so a capture may wait a paint or two.
|
|
1958
|
+
*/
|
|
1959
|
+
capture(captures) {
|
|
1960
|
+
for (const capture of [...captures]) {
|
|
1961
|
+
const { node, scale } = capture;
|
|
1962
|
+
let image;
|
|
1963
|
+
try {
|
|
1964
|
+
image = captureRegion(
|
|
1965
|
+
this.device,
|
|
1966
|
+
node.page.element,
|
|
1967
|
+
nodeRegion(node),
|
|
1968
|
+
texels(node.rect.width, scale),
|
|
1969
|
+
texels(node.rect.height, scale)
|
|
1970
|
+
);
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
if (++capture.tries > MAX_CAPTURE_TRIES) {
|
|
1973
|
+
captures.splice(captures.indexOf(capture), 1);
|
|
1974
|
+
capture.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1975
|
+
}
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
captures.splice(captures.indexOf(capture), 1);
|
|
1979
|
+
image.then(capture.resolve, capture.reject);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Atlas space for a `width` x `height` snapshot. A redraw at the same size overwrites the slot
|
|
1984
|
+
* it replaces, so redraws (a hover, a click, an animation) never fill the atlas. Slots cut from
|
|
1985
|
+
* a whole-page snapshot start at fractional texels and belong to the page, so they are never
|
|
1986
|
+
* overwritten. Thumbnails go to the pinned layer while it has room.
|
|
1987
|
+
*/
|
|
1988
|
+
slotFor(previous, width, height, thumbnail = false) {
|
|
1989
|
+
const { atlas } = this;
|
|
1990
|
+
return 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);
|
|
1991
|
+
}
|
|
1992
|
+
/** Copies `source` of the page (all of it when null) into `slot`; false until it is recorded. */
|
|
1993
|
+
draw(page, source, slot) {
|
|
1994
|
+
try {
|
|
1995
|
+
this.device.queue.drawElementImageToTexture(
|
|
1996
|
+
source ? {
|
|
1997
|
+
source: page.element,
|
|
1998
|
+
sourceX: source.x,
|
|
1999
|
+
sourceY: source.y,
|
|
2000
|
+
sourceWidth: source.width,
|
|
2001
|
+
sourceHeight: source.height
|
|
2002
|
+
} : { source: page.element },
|
|
2003
|
+
{
|
|
2004
|
+
texture: this.atlas.texture,
|
|
2005
|
+
origin: { x: slot.x, y: slot.y, z: slot.layer },
|
|
2006
|
+
size: { width: slot.width, height: slot.height },
|
|
2007
|
+
premultipliedAlpha: true
|
|
2008
|
+
}
|
|
2009
|
+
);
|
|
2010
|
+
return true;
|
|
2011
|
+
} catch {
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
// src/composite-shader.ts
|
|
2018
|
+
var KIND = { snapshot: 0, placeholder: 1, ring: 2 };
|
|
2019
|
+
var prelude = (
|
|
2020
|
+
/* wgsl */
|
|
2021
|
+
`
|
|
2022
|
+
struct View {
|
|
2023
|
+
size: vec2f, // canvas size in CSS pixels
|
|
2024
|
+
origin: vec2f, // camera position, world units
|
|
2025
|
+
zoom: f32,
|
|
2026
|
+
dpr: f32,
|
|
2027
|
+
atlasSize: f32,
|
|
2028
|
+
time: f32, // seconds since the canvas started
|
|
2029
|
+
placeholder: vec4f, // premultiplied color for nodes without a snapshot
|
|
2030
|
+
pointer: vec4f, // pointer and last press in world units; far away when there is none
|
|
2031
|
+
pressTime: f32, // seconds since the last press
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
@group(0) @binding(0) var<uniform> view: View;
|
|
2035
|
+
@group(0) @binding(1) var atlas: texture_2d_array<f32>;
|
|
2036
|
+
@group(0) @binding(2) var atlasSampler: sampler;
|
|
2037
|
+
|
|
2038
|
+
struct Instance {
|
|
2039
|
+
@location(0) rect: vec4f, // world x, y, width, height
|
|
2040
|
+
@location(1) uv: vec4f, // atlas texels x0, y0, x1, y1; for a ring, its premultiplied color
|
|
2041
|
+
@location(2) info: vec4f, // layer, kind (0 snapshot, 1 placeholder, 2 ring), ring width or effect pad
|
|
2042
|
+
@location(3) params: vec4f, // an effect's parameters
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
struct Varyings {
|
|
2046
|
+
@builtin(position) position: vec4f,
|
|
2047
|
+
@location(0) texel: vec2f,
|
|
2048
|
+
@location(1) @interpolate(flat) bounds: vec4f,
|
|
2049
|
+
@location(2) @interpolate(flat) info: vec2f,
|
|
2050
|
+
@location(3) @interpolate(flat) color: vec4f,
|
|
2051
|
+
@location(4) uv: vec2f, // 0 to 1 across the node, past that in an effect's padding
|
|
2052
|
+
@location(5) @interpolate(flat) size: vec2f,
|
|
2053
|
+
@location(6) @interpolate(flat) params: vec4f,
|
|
2054
|
+
@location(7) @interpolate(flat) corner: vec2f, // the node's top left in world units
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
@vertex
|
|
2058
|
+
fn vs(@builtin(vertex_index) vertex: u32, instance: Instance) -> Varyings {
|
|
2059
|
+
let corner = vec2f(f32(vertex & 1u), f32(vertex >> 1u));
|
|
2060
|
+
// A ring's quad reaches past its element by the ring's width, an effect's by its padding.
|
|
2061
|
+
let ring = instance.info.y > ${KIND.ring - 0.5};
|
|
2062
|
+
let grow = select(instance.info.z, instance.info.z / view.zoom, ring);
|
|
2063
|
+
let start = instance.rect.xy - grow;
|
|
2064
|
+
let end = instance.rect.xy + instance.rect.zw + grow;
|
|
2065
|
+
// Snap edges to device pixels so a snapshot drawn at its raster scale stays crisp.
|
|
2066
|
+
let scale = view.zoom * view.dpr;
|
|
2067
|
+
let topLeft = round((start - view.origin) * scale) / view.dpr;
|
|
2068
|
+
let bottomRight = round((end - view.origin) * scale) / view.dpr;
|
|
2069
|
+
let point = mix(topLeft, bottomRight, corner);
|
|
2070
|
+
var out: Varyings;
|
|
2071
|
+
out.position = vec4f(point.x / view.size.x * 2.0 - 1.0, 1.0 - point.y / view.size.y * 2.0, 0.0, 1.0);
|
|
2072
|
+
if (ring) {
|
|
2073
|
+
// World units from the element's top left, and the element's own edges.
|
|
2074
|
+
out.texel = mix(vec2f(-grow), instance.rect.zw + grow, corner);
|
|
2075
|
+
out.bounds = vec4f(0.0, 0.0, instance.rect.zw);
|
|
2076
|
+
out.color = instance.uv;
|
|
2077
|
+
} else {
|
|
2078
|
+
let pad = grow * (instance.uv.zw - instance.uv.xy) / instance.rect.zw;
|
|
2079
|
+
out.texel = mix(instance.uv.xy - pad, instance.uv.zw + pad, corner);
|
|
2080
|
+
out.color = instance.uv;
|
|
2081
|
+
// Half a texel inside the node's rectangle: the outermost sample center that cannot blend
|
|
2082
|
+
// with whatever sits next to it in the atlas.
|
|
2083
|
+
out.bounds = vec4f(instance.uv.xy + 0.5, instance.uv.zw - 0.5);
|
|
2084
|
+
}
|
|
2085
|
+
out.info = instance.info.xy;
|
|
2086
|
+
out.uv = mix(-grow / instance.rect.zw, 1.0 + grow / instance.rect.zw, corner);
|
|
2087
|
+
out.size = instance.rect.zw;
|
|
2088
|
+
out.corner = instance.rect.xy;
|
|
2089
|
+
out.params = instance.params;
|
|
2090
|
+
return out;
|
|
2091
|
+
}
|
|
2092
|
+
`
|
|
2093
|
+
);
|
|
2094
|
+
var compositeShader = (
|
|
2095
|
+
/* wgsl */
|
|
2096
|
+
`${prelude}
|
|
2097
|
+
@fragment
|
|
2098
|
+
fn fs(in: Varyings) -> @location(0) vec4f {
|
|
2099
|
+
// Derivatives first: WGSL only allows them in uniform control flow.
|
|
2100
|
+
let dx = dpdx(in.texel);
|
|
2101
|
+
let dy = dpdy(in.texel);
|
|
2102
|
+
if (in.info.y > ${KIND.ring - 0.5}) {
|
|
2103
|
+
// The ring covers what lies outside the element; the element itself shows through.
|
|
2104
|
+
let outside = max(in.bounds.xy - in.texel, in.texel - in.bounds.zw);
|
|
2105
|
+
return select(vec4f(0.0), in.color, max(outside.x, outside.y) > 0.0);
|
|
2106
|
+
}
|
|
2107
|
+
if (in.info.y > ${KIND.placeholder - 0.5}) {
|
|
2108
|
+
return view.placeholder;
|
|
2109
|
+
}
|
|
2110
|
+
let layer = i32(in.info.x);
|
|
2111
|
+
// Texels covered by this pixel along each screen axis.
|
|
2112
|
+
let footprint = vec2f(length(vec2f(dx.x, dy.x)), length(vec2f(dx.y, dy.y)));
|
|
2113
|
+
let taps = vec2u(clamp(ceil(footprint), vec2f(1.0), vec2f(4.0)));
|
|
2114
|
+
var color = vec4f(0.0);
|
|
2115
|
+
for (var j = 0u; j < taps.y; j++) {
|
|
2116
|
+
for (var i = 0u; i < taps.x; i++) {
|
|
2117
|
+
let offset = (vec2f(f32(i), f32(j)) + 0.5) / vec2f(taps) - 0.5;
|
|
2118
|
+
let texel = clamp(in.texel + offset * footprint, in.bounds.xy, in.bounds.zw);
|
|
2119
|
+
color += textureSampleLevel(atlas, atlasSampler, texel / view.atlasSize, layer, 0.0);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return color / f32(taps.x * taps.y);
|
|
2123
|
+
}
|
|
2124
|
+
`
|
|
2125
|
+
);
|
|
2126
|
+
var effectShader = (wgsl) => (
|
|
2127
|
+
/* wgsl */
|
|
2128
|
+
`${prelude}
|
|
2129
|
+
struct EffectInput {
|
|
2130
|
+
uv: vec2f,
|
|
2131
|
+
size: vec2f,
|
|
2132
|
+
time: f32,
|
|
2133
|
+
zoom: f32,
|
|
2134
|
+
params: vec4f,
|
|
2135
|
+
pointer: vec2f,
|
|
2136
|
+
press: vec2f,
|
|
2137
|
+
sincePress: f32,
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
var<private> sourceRect: vec4f;
|
|
2141
|
+
var<private> sourceLayer: i32;
|
|
2142
|
+
|
|
2143
|
+
fn source(uv: vec2f) -> vec4f {
|
|
2144
|
+
if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) {
|
|
2145
|
+
return vec4f(0.0);
|
|
2146
|
+
}
|
|
2147
|
+
let texel = clamp(mix(sourceRect.xy, sourceRect.zw, uv), sourceRect.xy + 0.5, sourceRect.zw - 0.5);
|
|
2148
|
+
return textureSampleLevel(atlas, atlasSampler, texel / view.atlasSize, sourceLayer, 0.0);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
fn hash(p: vec2f) -> f32 {
|
|
2152
|
+
return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
fn noise(p: vec2f) -> f32 {
|
|
2156
|
+
let i = floor(p);
|
|
2157
|
+
let f = fract(p);
|
|
2158
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
2159
|
+
return mix(
|
|
2160
|
+
mix(hash(i), hash(i + vec2f(1.0, 0.0)), u.x),
|
|
2161
|
+
mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
|
|
2162
|
+
u.y,
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
${wgsl}
|
|
2167
|
+
|
|
2168
|
+
@fragment
|
|
2169
|
+
fn fs(in: Varyings) -> @location(0) vec4f {
|
|
2170
|
+
sourceRect = in.color;
|
|
2171
|
+
sourceLayer = i32(in.info.x);
|
|
2172
|
+
let pointer = (view.pointer.xy - in.corner) / in.size;
|
|
2173
|
+
let press = (view.pointer.zw - in.corner) / in.size;
|
|
2174
|
+
return effect(
|
|
2175
|
+
EffectInput(in.uv, in.size, view.time, view.zoom, in.params, pointer, press, view.pressTime),
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
`
|
|
2179
|
+
);
|
|
2180
|
+
|
|
2181
|
+
// src/gpu-renderer.ts
|
|
2182
|
+
var FLOATS_PER_INSTANCE = 16;
|
|
2183
|
+
var CompositeRenderer = class {
|
|
2184
|
+
constructor(device, context, format, atlas, background, placeholder, onEffectReady) {
|
|
2185
|
+
this.device = device;
|
|
2186
|
+
this.context = context;
|
|
2187
|
+
this.format = format;
|
|
2188
|
+
this.atlas = atlas;
|
|
2189
|
+
this.background = background;
|
|
2190
|
+
this.placeholder = placeholder;
|
|
2191
|
+
this.onEffectReady = onEffectReady;
|
|
2192
|
+
this.bindGroupLayout = device.createBindGroupLayout({
|
|
2193
|
+
entries: [
|
|
2194
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
|
|
2195
|
+
{
|
|
2196
|
+
binding: 1,
|
|
2197
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
2198
|
+
texture: { viewDimension: "2d-array" }
|
|
2199
|
+
},
|
|
2200
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
|
|
2201
|
+
]
|
|
2202
|
+
});
|
|
2203
|
+
this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
|
|
2204
|
+
this.pipeline = device.createRenderPipeline(
|
|
2205
|
+
this.pipelineDescriptor("gpucanvas composite", compositeShader)
|
|
2206
|
+
);
|
|
2207
|
+
this.uniforms = device.createBuffer({
|
|
2208
|
+
size: this.uniformData.byteLength,
|
|
2209
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
2210
|
+
});
|
|
2211
|
+
this.bindGroup = device.createBindGroup({
|
|
2212
|
+
layout: this.bindGroupLayout,
|
|
2213
|
+
entries: [
|
|
2214
|
+
{ binding: 0, resource: { buffer: this.uniforms } },
|
|
2215
|
+
{ binding: 1, resource: atlas.view },
|
|
2216
|
+
{ binding: 2, resource: this.sampler }
|
|
2217
|
+
]
|
|
2218
|
+
});
|
|
2219
|
+
this.instanceBuffer = this.createInstanceBuffer();
|
|
2220
|
+
}
|
|
2221
|
+
device;
|
|
2222
|
+
context;
|
|
2223
|
+
format;
|
|
2224
|
+
atlas;
|
|
2225
|
+
background;
|
|
2226
|
+
placeholder;
|
|
2227
|
+
onEffectReady;
|
|
2228
|
+
instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
|
|
2229
|
+
count = 0;
|
|
2230
|
+
runs = [];
|
|
2231
|
+
instanceBuffer;
|
|
2232
|
+
uniforms;
|
|
2233
|
+
uniformData = new Float32Array(20);
|
|
2234
|
+
pipeline;
|
|
2235
|
+
bindGroup;
|
|
2236
|
+
bindGroupLayout;
|
|
2237
|
+
sampler;
|
|
2238
|
+
/** Effect pipelines, null while one compiles. Compiling never blocks a frame. */
|
|
2239
|
+
effects = /* @__PURE__ */ new Map();
|
|
2240
|
+
failed = /* @__PURE__ */ new Set();
|
|
2241
|
+
pipelineDescriptor(label, code) {
|
|
2242
|
+
const module = this.device.createShaderModule({ label, code });
|
|
2243
|
+
return {
|
|
2244
|
+
label,
|
|
2245
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bindGroupLayout] }),
|
|
2246
|
+
vertex: {
|
|
2247
|
+
module,
|
|
2248
|
+
entryPoint: "vs",
|
|
2249
|
+
buffers: [
|
|
2250
|
+
{
|
|
2251
|
+
arrayStride: FLOATS_PER_INSTANCE * 4,
|
|
2252
|
+
stepMode: "instance",
|
|
2253
|
+
attributes: [
|
|
2254
|
+
{ shaderLocation: 0, offset: 0, format: "float32x4" },
|
|
2255
|
+
{ shaderLocation: 1, offset: 16, format: "float32x4" },
|
|
2256
|
+
{ shaderLocation: 2, offset: 32, format: "float32x4" },
|
|
2257
|
+
{ shaderLocation: 3, offset: 48, format: "float32x4" }
|
|
2258
|
+
]
|
|
2259
|
+
}
|
|
2260
|
+
]
|
|
2261
|
+
},
|
|
2262
|
+
fragment: {
|
|
2263
|
+
module,
|
|
2264
|
+
entryPoint: "fs",
|
|
2265
|
+
targets: [
|
|
2266
|
+
{
|
|
2267
|
+
format: this.format,
|
|
2268
|
+
// Snapshots are requested premultiplied.
|
|
2269
|
+
blend: {
|
|
2270
|
+
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
|
|
2271
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
]
|
|
2275
|
+
},
|
|
2276
|
+
primitive: { topology: "triangle-strip" }
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
/** Whether `effect` can draw this frame. The first ask starts compiling it. */
|
|
2280
|
+
hasEffect(effect) {
|
|
2281
|
+
if (this.effects.has(effect)) return this.effects.get(effect) !== null;
|
|
2282
|
+
this.effects.set(effect, null);
|
|
2283
|
+
this.device.createRenderPipelineAsync(
|
|
2284
|
+
this.pipelineDescriptor(`gpucanvas effect ${effect.name}`, effectShader(effect.wgsl))
|
|
2285
|
+
).then((pipeline) => {
|
|
2286
|
+
this.effects.set(effect, pipeline);
|
|
2287
|
+
this.onEffectReady();
|
|
2288
|
+
}).catch((error) => {
|
|
2289
|
+
this.failed.add(effect);
|
|
2290
|
+
console.error(`[gpucanvas] effect "${effect.name}" failed`, error);
|
|
2291
|
+
});
|
|
2292
|
+
return false;
|
|
2293
|
+
}
|
|
2294
|
+
/** Whether `effect`'s pipeline failed to compile; it never draws then. */
|
|
2295
|
+
effectFailed(effect) {
|
|
2296
|
+
return this.failed.has(effect);
|
|
2297
|
+
}
|
|
2298
|
+
/** Starts a frame's list of quads. */
|
|
2299
|
+
begin() {
|
|
2300
|
+
this.count = 0;
|
|
2301
|
+
this.runs.length = 0;
|
|
2302
|
+
}
|
|
2303
|
+
/** Room for the next quad, in the run of `effect`; returns its first float. */
|
|
2304
|
+
next(effect) {
|
|
2305
|
+
if ((this.count + 1) * FLOATS_PER_INSTANCE > this.instances.length) {
|
|
2306
|
+
const larger = new Float32Array(this.instances.length * 2);
|
|
2307
|
+
larger.set(this.instances);
|
|
2308
|
+
this.instances = larger;
|
|
2309
|
+
this.instanceBuffer.destroy();
|
|
2310
|
+
this.instanceBuffer = this.createInstanceBuffer();
|
|
2311
|
+
}
|
|
2312
|
+
const last = this.runs.at(-1);
|
|
2313
|
+
if (last && last.effect === effect) last.count++;
|
|
2314
|
+
else this.runs.push({ effect, start: this.count, count: 1 });
|
|
2315
|
+
return this.count++ * FLOATS_PER_INSTANCE;
|
|
2316
|
+
}
|
|
2317
|
+
writeRect(offset, rect) {
|
|
2318
|
+
const data = this.instances;
|
|
2319
|
+
data[offset] = rect.x;
|
|
2320
|
+
data[offset + 1] = rect.y;
|
|
2321
|
+
data[offset + 2] = rect.width;
|
|
2322
|
+
data[offset + 3] = rect.height;
|
|
2323
|
+
}
|
|
2324
|
+
writeSlot(offset, slot) {
|
|
2325
|
+
const data = this.instances;
|
|
2326
|
+
this.atlas.touch(slot.layer);
|
|
2327
|
+
data[offset + 4] = slot.x;
|
|
2328
|
+
data[offset + 5] = slot.y;
|
|
2329
|
+
data[offset + 6] = slot.x + slot.width;
|
|
2330
|
+
data[offset + 7] = slot.y + slot.height;
|
|
2331
|
+
data[offset + 8] = slot.layer;
|
|
2332
|
+
data[offset + 9] = KIND.snapshot;
|
|
2333
|
+
}
|
|
2334
|
+
/** `rect` showing the snapshot in `slot`, or the placeholder color while there is none. */
|
|
2335
|
+
quad(rect, slot) {
|
|
2336
|
+
const offset = this.next(null);
|
|
2337
|
+
this.writeRect(offset, rect);
|
|
2338
|
+
if (slot) this.writeSlot(offset, slot);
|
|
2339
|
+
else this.instances[offset + 9] = KIND.placeholder;
|
|
2340
|
+
this.instances[offset + 10] = 0;
|
|
2341
|
+
}
|
|
2342
|
+
/** `rect` drawn through `effect` from the snapshot in `slot`, reaching `pad` past its edges. */
|
|
2343
|
+
effectQuad(rect, slot, effect, pad, params) {
|
|
2344
|
+
const offset = this.next(effect);
|
|
2345
|
+
this.writeRect(offset, rect);
|
|
2346
|
+
this.writeSlot(offset, slot);
|
|
2347
|
+
this.instances[offset + 10] = pad;
|
|
2348
|
+
this.instances.set(params, offset + 12);
|
|
2349
|
+
}
|
|
2350
|
+
/** An outline `width` CSS pixels wide just outside `rect`, in a premultiplied color. */
|
|
2351
|
+
ring(rect, color, width) {
|
|
2352
|
+
const offset = this.next(null);
|
|
2353
|
+
this.writeRect(offset, rect);
|
|
2354
|
+
this.instances.set(color, offset + 4);
|
|
2355
|
+
this.instances[offset + 9] = KIND.ring;
|
|
2356
|
+
this.instances[offset + 10] = width;
|
|
2357
|
+
}
|
|
2358
|
+
/** Draws the quads listed since `begin`. */
|
|
2359
|
+
draw(camera, cssWidth, cssHeight, dpr, effects) {
|
|
2360
|
+
const { device, count, runs } = this;
|
|
2361
|
+
this.uniformData.set([
|
|
2362
|
+
cssWidth,
|
|
2363
|
+
cssHeight,
|
|
2364
|
+
camera.x,
|
|
2365
|
+
camera.y,
|
|
2366
|
+
camera.zoom,
|
|
2367
|
+
dpr,
|
|
2368
|
+
this.atlas.size,
|
|
2369
|
+
effects.time,
|
|
2370
|
+
...this.placeholder,
|
|
2371
|
+
effects.pointer.x,
|
|
2372
|
+
effects.pointer.y,
|
|
2373
|
+
effects.press.x,
|
|
2374
|
+
effects.press.y,
|
|
2375
|
+
effects.sincePress
|
|
2376
|
+
]);
|
|
2377
|
+
device.queue.writeBuffer(this.uniforms, 0, this.uniformData);
|
|
2378
|
+
if (count > 0) {
|
|
2379
|
+
device.queue.writeBuffer(
|
|
2380
|
+
this.instanceBuffer,
|
|
2381
|
+
0,
|
|
2382
|
+
this.instances,
|
|
2383
|
+
0,
|
|
2384
|
+
count * FLOATS_PER_INSTANCE
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const [r, g, b] = this.background;
|
|
2388
|
+
const encoder = device.createCommandEncoder();
|
|
2389
|
+
const pass = encoder.beginRenderPass({
|
|
2390
|
+
colorAttachments: [
|
|
2391
|
+
{
|
|
2392
|
+
view: this.context.getCurrentTexture().createView(),
|
|
2393
|
+
clearValue: { r, g, b, a: 1 },
|
|
2394
|
+
loadOp: "clear",
|
|
2395
|
+
storeOp: "store"
|
|
2396
|
+
}
|
|
2397
|
+
]
|
|
2398
|
+
});
|
|
2399
|
+
if (count > 0) {
|
|
2400
|
+
pass.setBindGroup(0, this.bindGroup);
|
|
2401
|
+
pass.setVertexBuffer(0, this.instanceBuffer);
|
|
2402
|
+
let current = null;
|
|
2403
|
+
for (const run of runs) {
|
|
2404
|
+
const pipeline = run.effect && this.effects.get(run.effect) || this.pipeline;
|
|
2405
|
+
if (pipeline !== current) pass.setPipeline(current = pipeline);
|
|
2406
|
+
pass.draw(4, run.count, 0, run.start);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
pass.end();
|
|
2410
|
+
device.queue.submit([encoder.finish()]);
|
|
2411
|
+
}
|
|
2412
|
+
destroy() {
|
|
2413
|
+
this.instanceBuffer.destroy();
|
|
2414
|
+
this.uniforms.destroy();
|
|
2415
|
+
}
|
|
2416
|
+
createInstanceBuffer() {
|
|
2417
|
+
return this.device.createBuffer({
|
|
2418
|
+
label: "gpucanvas instances",
|
|
2419
|
+
size: this.instances.byteLength,
|
|
2420
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2425
|
+
// src/gpu-backend.ts
|
|
2426
|
+
var SETTLE_MS = 120;
|
|
2427
|
+
var DEFAULT_PAGE_SIZE = 2048;
|
|
2428
|
+
var MAX_PROMOTIONS_PER_FRAME = 8;
|
|
2429
|
+
var SLOW_FRAME_MS = 22;
|
|
2430
|
+
var FAST_FRAME_MS = 12;
|
|
2431
|
+
function supportsGpuCanvas() {
|
|
2432
|
+
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;
|
|
2433
|
+
}
|
|
2434
|
+
function cssColorToRgba(color) {
|
|
2435
|
+
const context = document.createElement("canvas").getContext("2d");
|
|
2436
|
+
context.fillStyle = color;
|
|
2437
|
+
context.fillRect(0, 0, 1, 1);
|
|
2438
|
+
const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
|
|
2439
|
+
return [r / 255, g / 255, b / 255, a / 255];
|
|
2440
|
+
}
|
|
2441
|
+
var premultiplied = ([r, g, b, a]) => [r * a, g * a, b * a, a];
|
|
2442
|
+
async function createGpuCanvas(host, options) {
|
|
2443
|
+
const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
2444
|
+
if (!adapter) throw new Error("gpucanvas: no WebGPU adapter");
|
|
2445
|
+
const device = await adapter.requestDevice();
|
|
2446
|
+
const canvas = document.createElement("canvas");
|
|
2447
|
+
canvas.setAttribute("content", "drawable");
|
|
2448
|
+
markSurface(canvas);
|
|
2449
|
+
Object.assign(canvas.style, { display: "block", width: "100%", height: "100%" });
|
|
2450
|
+
const context = canvas.getContext("webgpu");
|
|
2451
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
2452
|
+
try {
|
|
2453
|
+
if (!context) throw new Error("gpucanvas: no WebGPU canvas context");
|
|
2454
|
+
context.configure({ device, format, alphaMode: "opaque" });
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
device.destroy();
|
|
2457
|
+
throw error;
|
|
2458
|
+
}
|
|
2459
|
+
const base = createBase(
|
|
2460
|
+
host,
|
|
2461
|
+
options,
|
|
2462
|
+
() => {
|
|
2463
|
+
lastCameraChange = performance.now();
|
|
2464
|
+
live.set(null);
|
|
2465
|
+
clearTimeout(settleTimer);
|
|
2466
|
+
settleTimer = window.setTimeout(() => {
|
|
2467
|
+
live.pick();
|
|
2468
|
+
schedule();
|
|
2469
|
+
}, SETTLE_MS);
|
|
2470
|
+
schedule();
|
|
2471
|
+
},
|
|
2472
|
+
{
|
|
2473
|
+
add(element, rect) {
|
|
2474
|
+
element.style.pointerEvents = "none";
|
|
2475
|
+
const node = createGpuNode(element, rect, pages.place(element, rect));
|
|
2476
|
+
pages.join(node);
|
|
2477
|
+
schedule();
|
|
2478
|
+
return node;
|
|
2479
|
+
},
|
|
2480
|
+
update(node, rect) {
|
|
2481
|
+
ringRects.clear();
|
|
2482
|
+
if (node.rect.width === rect.width && node.rect.height === rect.height) {
|
|
2483
|
+
translateNode(node, rect);
|
|
2484
|
+
live.moved(node);
|
|
2485
|
+
return schedule();
|
|
2486
|
+
}
|
|
2487
|
+
node.rect = rect;
|
|
2488
|
+
relocate(node, () => {
|
|
2489
|
+
pages.release(node);
|
|
2490
|
+
placeNode(node, pages.place(node.element, rect));
|
|
2491
|
+
pages.join(node);
|
|
2492
|
+
});
|
|
2493
|
+
schedule();
|
|
2494
|
+
},
|
|
2495
|
+
remove(node) {
|
|
2496
|
+
live.forget(node);
|
|
2497
|
+
effects.delete(node.element);
|
|
2498
|
+
selection.delete(node.element);
|
|
2499
|
+
pages.release(node);
|
|
2500
|
+
schedule();
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
);
|
|
2504
|
+
const { surface, nodes } = base;
|
|
2505
|
+
surface.append(canvas);
|
|
2506
|
+
const atlasSize = Math.min(options.atlasSize ?? 4096, device.limits.maxTextureDimension2D);
|
|
2507
|
+
const atlas = new Atlas(
|
|
2508
|
+
device,
|
|
2509
|
+
atlasSize,
|
|
2510
|
+
Math.min(options.atlasLayers ?? 5, device.limits.maxTextureArrayLayers),
|
|
2511
|
+
() => planner.keep()
|
|
2512
|
+
);
|
|
2513
|
+
const pages = new Pages(canvas, Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, atlasSize));
|
|
2514
|
+
const rasterizer = new Rasterizer(device, atlas, {
|
|
2515
|
+
calls: options.maxRastersPerFrame ?? 12,
|
|
2516
|
+
ms: options.rasterTimeBudgetMs ?? 6,
|
|
2517
|
+
pixels: options.rasterPixelBudget ?? 24e6
|
|
2518
|
+
});
|
|
2519
|
+
const colorsFor = (css) => {
|
|
2520
|
+
const background = cssColorToRgba(css);
|
|
2521
|
+
const shade = background[0] + background[1] + background[2] > 1.5 ? 0.92 : 1.25;
|
|
2522
|
+
const placeholder = background.map(
|
|
2523
|
+
(channel) => Math.min(1, channel * shade + (shade > 1 ? 0.02 : 0))
|
|
2524
|
+
);
|
|
2525
|
+
placeholder[3] = 1;
|
|
2526
|
+
return [background, placeholder];
|
|
2527
|
+
};
|
|
2528
|
+
const renderer = new CompositeRenderer(
|
|
2529
|
+
device,
|
|
2530
|
+
context,
|
|
2531
|
+
format,
|
|
2532
|
+
atlas,
|
|
2533
|
+
...colorsFor(options.background ?? "#f4f4f5"),
|
|
2534
|
+
() => schedule()
|
|
2535
|
+
);
|
|
2536
|
+
const selectionColor = premultiplied(
|
|
2537
|
+
cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
|
|
2538
|
+
);
|
|
2539
|
+
const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
|
|
2540
|
+
const effects = /* @__PURE__ */ new Map();
|
|
2541
|
+
const startedAt = performance.now();
|
|
2542
|
+
let effectsMoving = false;
|
|
2543
|
+
let selection = /* @__PURE__ */ new Set();
|
|
2544
|
+
let hovered = null;
|
|
2545
|
+
const ringRects = /* @__PURE__ */ new Map();
|
|
2546
|
+
const ringRect = (element) => {
|
|
2547
|
+
const own = nodes.get(element);
|
|
2548
|
+
if (own) return own.rect;
|
|
2549
|
+
const at = element.isConnected ? nodeOfElement(nodes, element) : null;
|
|
2550
|
+
if (!at) return null;
|
|
2551
|
+
const node = nodes.get(at);
|
|
2552
|
+
let rect = ringRects.get(element);
|
|
2553
|
+
if (!rect) ringRects.set(element, rect = rectInNode(at, node.rect, element));
|
|
2554
|
+
return rect;
|
|
2555
|
+
};
|
|
2556
|
+
let lastCameraChange = -Infinity;
|
|
2557
|
+
let frameNumber = 0;
|
|
2558
|
+
let promotionsPerFrame = 2;
|
|
2559
|
+
let lastFrameAt = 0;
|
|
2560
|
+
let rafId = 0;
|
|
2561
|
+
let destroyed = false;
|
|
2562
|
+
let settleTimer = 0;
|
|
2563
|
+
let awaitingPaint = false;
|
|
2564
|
+
const requestPaint = () => {
|
|
2565
|
+
awaitingPaint = true;
|
|
2566
|
+
canvas.requestPaint();
|
|
2567
|
+
};
|
|
2568
|
+
const stats = {
|
|
2569
|
+
visible: 0,
|
|
2570
|
+
pending: 0,
|
|
2571
|
+
rasterizedLastFrame: 0,
|
|
2572
|
+
rasterCallsLastFrame: 0,
|
|
2573
|
+
rasterMs: 0,
|
|
2574
|
+
frameMs: 0,
|
|
2575
|
+
frames: 0,
|
|
2576
|
+
lostPixels: 0,
|
|
2577
|
+
lostDetail: 0,
|
|
2578
|
+
onThumbnail: 0
|
|
2579
|
+
};
|
|
2580
|
+
const schedule = () => {
|
|
2581
|
+
if (rafId || destroyed) return;
|
|
2582
|
+
rafId = requestAnimationFrame(() => {
|
|
2583
|
+
rafId = 0;
|
|
2584
|
+
frameNumber++;
|
|
2585
|
+
frame(false);
|
|
2586
|
+
});
|
|
2587
|
+
};
|
|
2588
|
+
const canvasDpr = () => canvas.width / Math.max(1, canvas.clientWidth);
|
|
2589
|
+
const resizeObserver = new ResizeObserver(([entry]) => {
|
|
2590
|
+
const size = entry.devicePixelContentBoxSize?.[0];
|
|
2591
|
+
canvas.width = Math.max(
|
|
2592
|
+
1,
|
|
2593
|
+
size ? size.inlineSize : Math.round(entry.contentRect.width * devicePixelRatio)
|
|
2594
|
+
);
|
|
2595
|
+
canvas.height = Math.max(
|
|
2596
|
+
1,
|
|
2597
|
+
size ? size.blockSize : Math.round(entry.contentRect.height * devicePixelRatio)
|
|
2598
|
+
);
|
|
2599
|
+
schedule();
|
|
2600
|
+
});
|
|
2601
|
+
resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
|
|
2602
|
+
const captures = [];
|
|
2603
|
+
const captured = (page) => captures.some((capture) => capture.node.page === page);
|
|
2604
|
+
const isPinned = (page) => captured(page) || live.holds(page);
|
|
2605
|
+
const live = new Live({
|
|
2606
|
+
canvas,
|
|
2607
|
+
surface,
|
|
2608
|
+
nodes,
|
|
2609
|
+
camera: base.camera,
|
|
2610
|
+
view: base.view,
|
|
2611
|
+
nodeAt: base.nodeAt,
|
|
2612
|
+
frameNumber: () => frameNumber,
|
|
2613
|
+
canvasDpr,
|
|
2614
|
+
schedule,
|
|
2615
|
+
isPinned,
|
|
2616
|
+
onPress: () => {
|
|
2617
|
+
if (effects.size) schedule();
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
const relocate = (node, move) => {
|
|
2621
|
+
const wasLive = live.current === node;
|
|
2622
|
+
if (wasLive) live.set(null);
|
|
2623
|
+
const previous = node.page;
|
|
2624
|
+
move();
|
|
2625
|
+
if (pages.has(previous) && !isPinned(previous)) {
|
|
2626
|
+
live.unpin(previous);
|
|
2627
|
+
previous.setDrawable(false, frameNumber);
|
|
2628
|
+
}
|
|
2629
|
+
if (wasLive) live.pick();
|
|
1887
2630
|
};
|
|
2631
|
+
const lift = (node) => relocate(node, () => pages.lift(node));
|
|
2632
|
+
const planner = new Planner(atlas, lift, schedule);
|
|
1888
2633
|
const composite = () => {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
instances2 += selection.size + 1;
|
|
1892
|
-
renderer.reserve(instances2);
|
|
1893
|
-
const data = renderer.instances;
|
|
1894
|
-
let count = 0;
|
|
1895
|
-
runs.length = 0;
|
|
2634
|
+
const { visible, detailScale, view } = planner;
|
|
2635
|
+
renderer.begin();
|
|
1896
2636
|
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
2637
|
const pushRing = (element, color, width) => {
|
|
1944
2638
|
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;
|
|
2639
|
+
if (rect && intersects(rect, view)) renderer.ring(rect, color, width);
|
|
1956
2640
|
};
|
|
1957
2641
|
let onThumbnail = 0;
|
|
1958
2642
|
for (const node of visible) {
|
|
1959
|
-
const
|
|
1960
|
-
if (
|
|
1961
|
-
const hasSlot =
|
|
2643
|
+
const image = atlas.isValid(node.slot) ? node.slot : atlas.isValid(node.thumb) ? node.thumb : null;
|
|
2644
|
+
if (image && image !== node.slot) onThumbnail++;
|
|
2645
|
+
const hasSlot = image !== null;
|
|
1962
2646
|
const sharp = node.tiles.length > 0 || node.preview !== null && atlas.isValid(node.preview);
|
|
1963
2647
|
const shown = sharp ? 3 : hasSlot ? 2 : 1;
|
|
1964
2648
|
const before = node.shownFrame === stats.frames - 1 ? node.shown : 0;
|
|
@@ -1967,44 +2651,47 @@ async function createGpuCanvas(host, options) {
|
|
|
1967
2651
|
node.shown = shown;
|
|
1968
2652
|
node.shownFrame = stats.frames;
|
|
1969
2653
|
const worn = effects.size ? effects.get(node.element) : void 0;
|
|
1970
|
-
if (worn) {
|
|
2654
|
+
if (worn && !renderer.effectFailed(worn.effect)) {
|
|
1971
2655
|
if (worn.effect.animated) effectsMoving = true;
|
|
1972
|
-
if (
|
|
1973
|
-
|
|
2656
|
+
if (image && renderer.hasEffect(worn.effect)) {
|
|
2657
|
+
renderer.effectQuad(node.rect, image, worn.effect, worn.pad, worn.params);
|
|
1974
2658
|
continue;
|
|
1975
2659
|
}
|
|
1976
2660
|
effectsMoving = true;
|
|
1977
2661
|
}
|
|
1978
|
-
|
|
1979
|
-
const current = (tile) => tile
|
|
2662
|
+
renderer.quad(node.rect, image);
|
|
2663
|
+
const current = (tile) => isCurrentTile(tile, detailScale);
|
|
1980
2664
|
for (const tile of node.tiles)
|
|
1981
|
-
if (!current(tile) && atlas.isValid(tile.slot))
|
|
1982
|
-
if (node.preview && atlas.isValid(node.preview))
|
|
2665
|
+
if (!current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
|
|
2666
|
+
if (node.preview && atlas.isValid(node.preview))
|
|
2667
|
+
renderer.quad(node.previewRect, node.preview);
|
|
1983
2668
|
for (const tile of node.tiles)
|
|
1984
|
-
if (current(tile) && atlas.isValid(tile.slot))
|
|
2669
|
+
if (current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
|
|
1985
2670
|
}
|
|
1986
2671
|
stats.onThumbnail = onThumbnail;
|
|
1987
2672
|
for (const element of selection) pushRing(element, selectionColor, SELECTION_WIDTH);
|
|
1988
2673
|
if (hovered && !selection.has(hovered)) pushRing(hovered, hoverColor, HOVER_WIDTH);
|
|
1989
2674
|
const cssWidth = canvas.clientWidth;
|
|
1990
|
-
const dpr =
|
|
2675
|
+
const dpr = canvasDpr();
|
|
1991
2676
|
const now = performance.now();
|
|
1992
|
-
|
|
2677
|
+
const camera = base.camera();
|
|
2678
|
+
renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, {
|
|
1993
2679
|
time: (now - startedAt) / 1e3,
|
|
1994
|
-
pointer: pointer ? screenToWorld(camera, pointer) : FAR_AWAY,
|
|
1995
|
-
press,
|
|
1996
|
-
sincePress: Math.min((now - pressedAt) / 1e3, 1e4)
|
|
2680
|
+
pointer: live.pointer ? screenToWorld(camera, live.pointer) : FAR_AWAY,
|
|
2681
|
+
press: live.press,
|
|
2682
|
+
sincePress: Math.min((now - live.pressedAt) / 1e3, 1e4)
|
|
1997
2683
|
});
|
|
1998
2684
|
};
|
|
1999
2685
|
const frame = (inPaint) => {
|
|
2000
2686
|
if (destroyed) return;
|
|
2001
2687
|
const start = performance.now();
|
|
2002
2688
|
atlas.nextFrame();
|
|
2003
|
-
const
|
|
2004
|
-
const
|
|
2005
|
-
const view =
|
|
2689
|
+
const dpr = canvasDpr();
|
|
2690
|
+
const camera = base.camera();
|
|
2691
|
+
const view = base.view();
|
|
2006
2692
|
const moving = start - lastCameraChange < SETTLE_MS;
|
|
2007
|
-
|
|
2693
|
+
planner.plan(nodes.values(), view, camera.zoom, dpr, moving);
|
|
2694
|
+
const { plans, ordered } = planner;
|
|
2008
2695
|
if (!inPaint && plans.size > 0) {
|
|
2009
2696
|
const interval = start - lastFrameAt;
|
|
2010
2697
|
if (interval < 100) {
|
|
@@ -2014,99 +2701,44 @@ async function createGpuCanvas(host, options) {
|
|
|
2014
2701
|
}
|
|
2015
2702
|
lastFrameAt = start;
|
|
2016
2703
|
let promoted = 0;
|
|
2017
|
-
for (const plan of
|
|
2704
|
+
for (const plan of ordered) {
|
|
2018
2705
|
if (plan.page.drawable) continue;
|
|
2019
2706
|
if (promoted++ >= promotionsPerFrame) break;
|
|
2020
2707
|
plan.page.setDrawable(true, frameNumber);
|
|
2021
2708
|
}
|
|
2022
|
-
|
|
2023
|
-
canvas.requestPaint();
|
|
2709
|
+
requestPaint();
|
|
2024
2710
|
return;
|
|
2025
2711
|
}
|
|
2026
2712
|
let pending = 0;
|
|
2027
2713
|
stats.rasterizedLastFrame = 0;
|
|
2028
2714
|
stats.rasterCallsLastFrame = 0;
|
|
2029
|
-
if (inPaint && plans.size > 0)
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2715
|
+
if (inPaint && plans.size > 0) {
|
|
2716
|
+
const drawn = rasterizer.rasterize(ordered, pages, frameNumber, start, isPinned);
|
|
2717
|
+
pending = drawn.pending;
|
|
2718
|
+
stats.rasterizedLastFrame = drawn.rasterized;
|
|
2719
|
+
stats.rasterCallsLastFrame = drawn.calls;
|
|
2033
2720
|
}
|
|
2721
|
+
pages.parkIdle(plans, frameNumber, isPinned);
|
|
2034
2722
|
stats.rasterMs = performance.now() - start;
|
|
2035
2723
|
composite();
|
|
2036
2724
|
stats.frames++;
|
|
2037
|
-
|
|
2038
|
-
stats.visible = visible.length;
|
|
2725
|
+
live.position();
|
|
2726
|
+
stats.visible = planner.visible.length;
|
|
2039
2727
|
stats.pending = pending;
|
|
2040
2728
|
stats.frameMs = performance.now() - start;
|
|
2041
2729
|
if (pending > 0 || effectsMoving) schedule();
|
|
2042
2730
|
};
|
|
2043
|
-
|
|
2044
|
-
const drawCaptures = () => {
|
|
2045
|
-
for (const capture of [...captures]) {
|
|
2046
|
-
const { node, scale } = capture;
|
|
2047
|
-
const width = Math.max(1, Math.round(node.rect.width * scale));
|
|
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
|
-
});
|
|
2054
|
-
try {
|
|
2055
|
-
device.queue.drawElementImageToTexture(
|
|
2056
|
-
{
|
|
2057
|
-
source: node.page.element,
|
|
2058
|
-
sourceX: node.pageX,
|
|
2059
|
-
sourceY: node.pageY,
|
|
2060
|
-
sourceWidth: node.rect.width,
|
|
2061
|
-
sourceHeight: node.rect.height
|
|
2062
|
-
},
|
|
2063
|
-
{ texture, size: { width, height }, premultipliedAlpha: false }
|
|
2064
|
-
);
|
|
2065
|
-
} catch (error) {
|
|
2066
|
-
texture.destroy();
|
|
2067
|
-
if (++capture.tries > 20) {
|
|
2068
|
-
captures.splice(captures.indexOf(capture), 1);
|
|
2069
|
-
capture.reject(error instanceof Error ? error : new Error(String(error)));
|
|
2070
|
-
}
|
|
2071
|
-
continue;
|
|
2072
|
-
}
|
|
2073
|
-
captures.splice(captures.indexOf(capture), 1);
|
|
2074
|
-
readTexture(texture, width, height).then(capture.resolve, capture.reject);
|
|
2075
|
-
}
|
|
2731
|
+
canvas.onpaint = (event) => {
|
|
2076
2732
|
if (captures.length) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
};
|
|
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();
|
|
2733
|
+
rasterizer.capture(captures);
|
|
2734
|
+
if (captures.length) requestPaint();
|
|
2735
|
+
else schedule();
|
|
2101
2736
|
}
|
|
2102
|
-
};
|
|
2103
|
-
canvas.onpaint = (event) => {
|
|
2104
|
-
if (captures.length) drawCaptures();
|
|
2105
2737
|
let changed = false;
|
|
2106
2738
|
for (const element of event.changedElements) {
|
|
2107
|
-
const page =
|
|
2739
|
+
const page = pages.pageOf(element);
|
|
2108
2740
|
if (!page) continue;
|
|
2109
|
-
for (const node of
|
|
2741
|
+
for (const node of live.held()) {
|
|
2110
2742
|
if (node?.page === page) {
|
|
2111
2743
|
node.dirty = true;
|
|
2112
2744
|
changed = true;
|
|
@@ -2118,26 +2750,18 @@ async function createGpuCanvas(host, options) {
|
|
|
2118
2750
|
awaitingPaint = false;
|
|
2119
2751
|
frame(true);
|
|
2120
2752
|
};
|
|
2121
|
-
const viewNow = () => visibleWorldRect(camera, canvas.clientWidth, canvas.clientHeight);
|
|
2122
|
-
const nodeOf = (target) => {
|
|
2123
|
-
let element = target instanceof HTMLElement ? target : target.parentElement;
|
|
2124
|
-
while (element?.parentElement && !pageByElement.has(element.parentElement)) {
|
|
2125
|
-
element = element.parentElement;
|
|
2126
|
-
}
|
|
2127
|
-
return element ? nodes.get(element) : void 0;
|
|
2128
|
-
};
|
|
2129
2753
|
const mutations = new MutationObserver((records) => {
|
|
2130
2754
|
ringRects.clear();
|
|
2131
2755
|
let changed = false;
|
|
2132
2756
|
for (const record of records) {
|
|
2133
2757
|
const target = record.target;
|
|
2134
|
-
if (record.type === "attributes" && (nodes.has(target) ||
|
|
2758
|
+
if (record.type === "attributes" && (nodes.has(target) || pages.pageOf(target) || target === canvas)) {
|
|
2135
2759
|
continue;
|
|
2136
2760
|
}
|
|
2137
|
-
const node = nodeOf(record.target);
|
|
2761
|
+
const node = nodeOf(nodes, record.target);
|
|
2138
2762
|
if (node && !node.dirty) {
|
|
2139
2763
|
node.dirty = true;
|
|
2140
|
-
if (!changed) changed = intersects(node.rect,
|
|
2764
|
+
if (!changed) changed = intersects(node.rect, base.view());
|
|
2141
2765
|
}
|
|
2142
2766
|
}
|
|
2143
2767
|
if (changed) schedule();
|
|
@@ -2148,150 +2772,8 @@ async function createGpuCanvas(host, options) {
|
|
|
2148
2772
|
characterData: true,
|
|
2149
2773
|
attributes: true
|
|
2150
2774
|
});
|
|
2151
|
-
const unpin = (page) => {
|
|
2152
|
-
if (isPinned(page)) return;
|
|
2153
|
-
if (page.registered) canvas.clearElementGeometry(page.element);
|
|
2154
|
-
page.registered = false;
|
|
2155
|
-
page.element.style.transform = "";
|
|
2156
|
-
schedule();
|
|
2157
|
-
};
|
|
2158
|
-
const positionLivePage = () => {
|
|
2159
|
-
if (!live || livePositioned) return;
|
|
2160
|
-
const { zoom } = camera;
|
|
2161
|
-
const x = (live.rect.x - camera.x - live.pageX) * zoom;
|
|
2162
|
-
const y = (live.rect.y - camera.y - live.pageY) * zoom;
|
|
2163
|
-
try {
|
|
2164
|
-
if (CANVAS_TRANSFORM_HIT_TESTS) {
|
|
2165
|
-
canvas.updateElementGeometry(live.page.element, {
|
|
2166
|
-
canvasTransform: new DOMMatrix([zoom, 0, 0, zoom, x, y])
|
|
2167
|
-
});
|
|
2168
|
-
} else {
|
|
2169
|
-
const dpr = canvas.width / Math.max(1, canvas.clientWidth);
|
|
2170
|
-
const drawn2 = new DOMMatrix([zoom, 0, 0, zoom, x * dpr, y * dpr]);
|
|
2171
|
-
live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
|
|
2172
|
-
}
|
|
2173
|
-
live.element.style.pointerEvents = "auto";
|
|
2174
|
-
livePositioned = true;
|
|
2175
|
-
} catch {
|
|
2176
|
-
schedule();
|
|
2177
|
-
}
|
|
2178
|
-
};
|
|
2179
|
-
const setLive = (node) => {
|
|
2180
|
-
if (node === live) return;
|
|
2181
|
-
const previous = live;
|
|
2182
|
-
live = node;
|
|
2183
|
-
if (previous) {
|
|
2184
|
-
previous.element.style.pointerEvents = "none";
|
|
2185
|
-
clearTimeout(lingerTimer);
|
|
2186
|
-
const earlier = lingering;
|
|
2187
|
-
lingering = previous;
|
|
2188
|
-
if (earlier && earlier !== previous) {
|
|
2189
|
-
earlier.dirty = true;
|
|
2190
|
-
unpin(earlier.page);
|
|
2191
|
-
}
|
|
2192
|
-
lingerTimer = window.setTimeout(() => {
|
|
2193
|
-
const last = lingering;
|
|
2194
|
-
if (!last) return;
|
|
2195
|
-
lingering = null;
|
|
2196
|
-
last.dirty = true;
|
|
2197
|
-
unpin(last.page);
|
|
2198
|
-
}, LINGER_MS);
|
|
2199
|
-
unpin(previous.page);
|
|
2200
|
-
}
|
|
2201
|
-
if (!node) return;
|
|
2202
|
-
const { page } = node;
|
|
2203
|
-
page.setDrawable(true, frameNumber);
|
|
2204
|
-
if (!page.registered) {
|
|
2205
|
-
canvas.updateElementGeometry(page.element, {});
|
|
2206
|
-
page.registered = true;
|
|
2207
|
-
}
|
|
2208
|
-
livePositioned = false;
|
|
2209
|
-
positionLivePage();
|
|
2210
|
-
schedule();
|
|
2211
|
-
};
|
|
2212
|
-
const pickLive = () => {
|
|
2213
|
-
if (!pointer) return setLive(null);
|
|
2214
|
-
const point = screenToWorld(camera, pointer);
|
|
2215
|
-
let hit = null;
|
|
2216
|
-
for (const node of nodes.values()) if (containsPoint(node.rect, point)) hit = node;
|
|
2217
|
-
setLive(hit);
|
|
2218
|
-
};
|
|
2219
|
-
const onPointerMove = (event) => {
|
|
2220
|
-
const bounds = canvas.getBoundingClientRect();
|
|
2221
|
-
pointer = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
2222
|
-
if (event.buttons === 0) pickLive();
|
|
2223
|
-
};
|
|
2224
|
-
const onPointerLeave = () => {
|
|
2225
|
-
pointer = null;
|
|
2226
|
-
setLive(null);
|
|
2227
|
-
};
|
|
2228
|
-
let press = FAR_AWAY;
|
|
2229
|
-
let pressedAt = -Infinity;
|
|
2230
|
-
let tapped = false;
|
|
2231
|
-
const onPointerDown = (event) => {
|
|
2232
|
-
const bounds = canvas.getBoundingClientRect();
|
|
2233
|
-
const at = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
2234
|
-
press = screenToWorld(camera, at);
|
|
2235
|
-
pressedAt = performance.now();
|
|
2236
|
-
if (event.pointerType !== "mouse") {
|
|
2237
|
-
pointer = at;
|
|
2238
|
-
pickLive();
|
|
2239
|
-
tapped = true;
|
|
2240
|
-
}
|
|
2241
|
-
if (effects.size) schedule();
|
|
2242
|
-
};
|
|
2243
|
-
const onClick = (event) => {
|
|
2244
|
-
if (!tapped) return;
|
|
2245
|
-
tapped = false;
|
|
2246
|
-
const bounds = canvas.getBoundingClientRect();
|
|
2247
|
-
const point = screenToWorld(camera, {
|
|
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;
|
|
2253
|
-
if (!top || top.element.contains(event.target)) return;
|
|
2254
|
-
let target = elementInNode(top.element, top.rect, point);
|
|
2255
|
-
while (target && !(target instanceof HTMLElement)) target = target.parentElement;
|
|
2256
|
-
if (!(target instanceof HTMLElement)) return;
|
|
2257
|
-
target.focus({ preventScroll: true });
|
|
2258
|
-
target.dispatchEvent(
|
|
2259
|
-
new MouseEvent("click", {
|
|
2260
|
-
bubbles: true,
|
|
2261
|
-
cancelable: true,
|
|
2262
|
-
composed: true,
|
|
2263
|
-
view: window,
|
|
2264
|
-
clientX: event.clientX,
|
|
2265
|
-
clientY: event.clientY
|
|
2266
|
-
})
|
|
2267
|
-
);
|
|
2268
|
-
};
|
|
2269
|
-
const onFocusIn = (event) => {
|
|
2270
|
-
const node = nodeOf(event.target) ?? null;
|
|
2271
|
-
if (node === focused) return;
|
|
2272
|
-
const previous = focused;
|
|
2273
|
-
focused = node;
|
|
2274
|
-
if (previous) {
|
|
2275
|
-
previous.dirty = true;
|
|
2276
|
-
unpin(previous.page);
|
|
2277
|
-
}
|
|
2278
|
-
if (node) {
|
|
2279
|
-
node.page.setDrawable(true, frameNumber);
|
|
2280
|
-
node.dirty = true;
|
|
2281
|
-
schedule();
|
|
2282
|
-
}
|
|
2283
|
-
};
|
|
2284
|
-
const onFocusOut = (event) => {
|
|
2285
|
-
if (event.relatedTarget && canvas.contains(event.relatedTarget)) return;
|
|
2286
|
-
const previous = focused;
|
|
2287
|
-
focused = null;
|
|
2288
|
-
if (previous) {
|
|
2289
|
-
previous.dirty = true;
|
|
2290
|
-
unpin(previous.page);
|
|
2291
|
-
}
|
|
2292
|
-
};
|
|
2293
2775
|
const onResourceLoad = (event) => {
|
|
2294
|
-
const node = nodeOf(event.target);
|
|
2776
|
+
const node = nodeOf(nodes, event.target);
|
|
2295
2777
|
if (!node) return;
|
|
2296
2778
|
node.dirty = true;
|
|
2297
2779
|
schedule();
|
|
@@ -2302,212 +2784,13 @@ async function createGpuCanvas(host, options) {
|
|
|
2302
2784
|
schedule();
|
|
2303
2785
|
};
|
|
2304
2786
|
document.fonts.addEventListener("loadingdone", onFontsLoaded);
|
|
2305
|
-
surface.addEventListener("pointermove", onPointerMove);
|
|
2306
|
-
surface.addEventListener("pointerleave", onPointerLeave);
|
|
2307
|
-
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
2308
|
-
surface.addEventListener("click", onClick);
|
|
2309
|
-
canvas.addEventListener("focusin", onFocusIn);
|
|
2310
|
-
canvas.addEventListener("focusout", onFocusOut);
|
|
2311
|
-
const usedAt = /* @__PURE__ */ new WeakMap();
|
|
2312
|
-
const onUse = (event) => {
|
|
2313
|
-
const node = nodeOf(event.target);
|
|
2314
|
-
if (node) usedAt.set(node, performance.now());
|
|
2315
|
-
};
|
|
2316
|
-
const transitionMs = (element) => {
|
|
2317
|
-
const style = getComputedStyle(element);
|
|
2318
|
-
const seconds = (list) => list.split(",").map((value) => parseFloat(value) || 0);
|
|
2319
|
-
const durations = seconds(style.transitionDuration);
|
|
2320
|
-
const delays = seconds(style.transitionDelay);
|
|
2321
|
-
const longest = Math.max(...durations.map((d, i) => d + (delays[i % delays.length] ?? 0)));
|
|
2322
|
-
return Math.min(1e3, longest * 1e3);
|
|
2323
|
-
};
|
|
2324
|
-
const onTransitionRun = (event) => {
|
|
2325
|
-
const node = nodeOf(event.target);
|
|
2326
|
-
const now = performance.now();
|
|
2327
|
-
if (!node || now - (usedAt.get(node) ?? -Infinity) > USED_MS) return;
|
|
2328
|
-
if (!intersects(node.rect, viewNow())) return;
|
|
2329
|
-
if (Math.max(node.rect.width, node.rect.height) * camera.zoom < SMALL_NODE_PX) return;
|
|
2330
|
-
const ms = transitionMs(event.target) + 32;
|
|
2331
|
-
const end = now + ms;
|
|
2332
|
-
if ((transitioning.get(node) ?? 0) >= end) return;
|
|
2333
|
-
transitioning.set(node, end);
|
|
2334
|
-
node.dirty = true;
|
|
2335
|
-
schedule();
|
|
2336
|
-
setTimeout(() => {
|
|
2337
|
-
if (transitioning.get(node) !== end) return;
|
|
2338
|
-
transitioning.delete(node);
|
|
2339
|
-
node.dirty = true;
|
|
2340
|
-
unpin(node.page);
|
|
2341
|
-
schedule();
|
|
2342
|
-
}, ms);
|
|
2343
|
-
};
|
|
2344
|
-
for (const type of USE_EVENTS) canvas.addEventListener(type, onUse, true);
|
|
2345
|
-
canvas.addEventListener("transitionrun", onTransitionRun);
|
|
2346
|
-
const setCamera = (next) => {
|
|
2347
|
-
camera = next;
|
|
2348
|
-
lastCameraChange = performance.now();
|
|
2349
|
-
setLive(null);
|
|
2350
|
-
clearTimeout(settleTimer);
|
|
2351
|
-
settleTimer = window.setTimeout(() => {
|
|
2352
|
-
pickLive();
|
|
2353
|
-
schedule();
|
|
2354
|
-
}, SETTLE_MS);
|
|
2355
|
-
schedule();
|
|
2356
|
-
options.onCameraChange?.(camera);
|
|
2357
|
-
};
|
|
2358
|
-
const unbindInput = bindCameraInput(surface, () => camera, setCamera);
|
|
2359
|
-
const placeNode = (element, rect) => {
|
|
2360
|
-
let slot = openPage?.place(rect.width, rect.height) ?? null;
|
|
2361
|
-
if (!slot) {
|
|
2362
|
-
const page = new Page(
|
|
2363
|
-
Math.max(pageSize, Math.ceil(rect.width)),
|
|
2364
|
-
Math.max(pageSize, Math.ceil(rect.height))
|
|
2365
|
-
);
|
|
2366
|
-
pages.set(page, /* @__PURE__ */ new Set());
|
|
2367
|
-
pageByElement.set(page.element, page);
|
|
2368
|
-
canvas.append(page.element);
|
|
2369
|
-
if (page.width === pageSize && page.height === pageSize) openPage = page;
|
|
2370
|
-
slot = page.place(rect.width, rect.height);
|
|
2371
|
-
}
|
|
2372
|
-
Object.assign(element.style, {
|
|
2373
|
-
position: "absolute",
|
|
2374
|
-
left: `${slot.x}px`,
|
|
2375
|
-
top: `${slot.y}px`,
|
|
2376
|
-
width: `${rect.width}px`,
|
|
2377
|
-
height: `${rect.height}px`
|
|
2378
|
-
});
|
|
2379
|
-
slot.page.element.append(element);
|
|
2380
|
-
return slot;
|
|
2381
|
-
};
|
|
2382
|
-
const releaseNode = (node) => {
|
|
2383
|
-
const members = pages.get(node.page);
|
|
2384
|
-
members.delete(node);
|
|
2385
|
-
node.page.release();
|
|
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
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
const lift = (node) => {
|
|
2393
|
-
const previous = node.page;
|
|
2394
|
-
const wasLive = live === node;
|
|
2395
|
-
if (wasLive) setLive(null);
|
|
2396
|
-
const page = new Page(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
|
|
2397
|
-
pages.set(page, /* @__PURE__ */ new Set());
|
|
2398
|
-
pageByElement.set(page.element, page);
|
|
2399
|
-
canvas.append(page.element);
|
|
2400
|
-
page.place(node.rect.width, node.rect.height);
|
|
2401
|
-
const target = page.element;
|
|
2402
|
-
if (target.moveBefore) target.moveBefore(node.element, null);
|
|
2403
|
-
else target.append(node.element);
|
|
2404
|
-
node.element.style.left = "0px";
|
|
2405
|
-
node.element.style.top = "0px";
|
|
2406
|
-
releaseNode(node);
|
|
2407
|
-
node.page = page;
|
|
2408
|
-
node.pageX = 0;
|
|
2409
|
-
node.pageY = 0;
|
|
2410
|
-
node.solo = true;
|
|
2411
|
-
pages.get(page).add(node);
|
|
2412
|
-
if (pages.has(previous) && !isPinned(previous)) {
|
|
2413
|
-
unpin(previous);
|
|
2414
|
-
previous.setDrawable(false, frameNumber);
|
|
2415
|
-
}
|
|
2416
|
-
if (wasLive) pickLive();
|
|
2417
|
-
};
|
|
2418
|
-
const attach = (node) => {
|
|
2419
|
-
const slot = placeNode(node.element, node.rect);
|
|
2420
|
-
node.page = slot.page;
|
|
2421
|
-
node.pageX = slot.x;
|
|
2422
|
-
node.pageY = slot.y;
|
|
2423
|
-
node.slot = null;
|
|
2424
|
-
node.thumb = null;
|
|
2425
|
-
node.solo = false;
|
|
2426
|
-
node.dirty = true;
|
|
2427
|
-
pages.get(slot.page).add(node);
|
|
2428
|
-
};
|
|
2429
2787
|
return {
|
|
2788
|
+
...base.api,
|
|
2430
2789
|
mode: "gpu",
|
|
2431
|
-
surface,
|
|
2432
|
-
addNode(element, rect) {
|
|
2433
|
-
Object.assign(element.style, {
|
|
2434
|
-
boxSizing: "border-box",
|
|
2435
|
-
contain: "strict",
|
|
2436
|
-
overflow: "hidden",
|
|
2437
|
-
pointerEvents: "none"
|
|
2438
|
-
});
|
|
2439
|
-
const node = {
|
|
2440
|
-
element,
|
|
2441
|
-
rect,
|
|
2442
|
-
slotScale: 0,
|
|
2443
|
-
thumb: null,
|
|
2444
|
-
thumbStale: false,
|
|
2445
|
-
thumbAt: -Infinity,
|
|
2446
|
-
shown: 0,
|
|
2447
|
-
shownFrame: -1,
|
|
2448
|
-
targetScale: 0,
|
|
2449
|
-
tiles: [],
|
|
2450
|
-
preview: null,
|
|
2451
|
-
previewRect: null,
|
|
2452
|
-
previewScale: 0,
|
|
2453
|
-
previewTarget: null,
|
|
2454
|
-
previewTargetScale: 0,
|
|
2455
|
-
changedAt: -Infinity,
|
|
2456
|
-
refreshedAt: -Infinity,
|
|
2457
|
-
wholeStale: false,
|
|
2458
|
-
solo: false,
|
|
2459
|
-
priority: 0
|
|
2460
|
-
};
|
|
2461
|
-
attach(node);
|
|
2462
|
-
nodes.set(element, node);
|
|
2463
|
-
schedule();
|
|
2464
|
-
},
|
|
2465
|
-
updateNode(element, rect) {
|
|
2466
|
-
const node = nodes.get(element);
|
|
2467
|
-
if (!node) return;
|
|
2468
|
-
const resized = node.rect.width !== rect.width || node.rect.height !== rect.height;
|
|
2469
|
-
node.rect = rect;
|
|
2470
|
-
ringRects.clear();
|
|
2471
|
-
if (resized) {
|
|
2472
|
-
if (live === node) setLive(null);
|
|
2473
|
-
releaseNode(node);
|
|
2474
|
-
attach(node);
|
|
2475
|
-
}
|
|
2476
|
-
schedule();
|
|
2477
|
-
},
|
|
2478
|
-
removeNode(element) {
|
|
2479
|
-
const node = nodes.get(element);
|
|
2480
|
-
if (!node) return;
|
|
2481
|
-
if (live === node) setLive(null);
|
|
2482
|
-
if (lingering === node) lingering = null;
|
|
2483
|
-
if (focused === node) focused = null;
|
|
2484
|
-
transitioning.delete(node);
|
|
2485
|
-
effects.delete(element);
|
|
2486
|
-
selection.delete(element);
|
|
2487
|
-
nodes.delete(element);
|
|
2488
|
-
releaseNode(node);
|
|
2489
|
-
element.remove();
|
|
2490
|
-
schedule();
|
|
2491
|
-
},
|
|
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
2790
|
getElementRect(element) {
|
|
2503
2791
|
const rect = ringRect(element);
|
|
2504
2792
|
return rect ? { ...rect } : null;
|
|
2505
2793
|
},
|
|
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
2794
|
setSelection(elements) {
|
|
2512
2795
|
selection = new Set(elements);
|
|
2513
2796
|
ringRects.clear();
|
|
@@ -2527,10 +2810,6 @@ async function createGpuCanvas(host, options) {
|
|
|
2527
2810
|
} else effects.delete(element);
|
|
2528
2811
|
schedule();
|
|
2529
2812
|
},
|
|
2530
|
-
screenToWorld: (point) => screenToWorld(camera, point),
|
|
2531
|
-
worldToScreen: (point) => worldToScreen(camera, point),
|
|
2532
|
-
getCamera: () => camera,
|
|
2533
|
-
setCamera,
|
|
2534
2813
|
invalidate: onFontsLoaded,
|
|
2535
2814
|
snapshot(element, options2 = {}) {
|
|
2536
2815
|
const node = nodes.get(element);
|
|
@@ -2541,18 +2820,13 @@ async function createGpuCanvas(host, options) {
|
|
|
2541
2820
|
return new Promise((resolve, reject) => {
|
|
2542
2821
|
captures.push({ node, scale, resolve, reject, tries: 0 });
|
|
2543
2822
|
node.page.setDrawable(true, frameNumber);
|
|
2544
|
-
|
|
2545
|
-
canvas.requestPaint();
|
|
2823
|
+
requestPaint();
|
|
2546
2824
|
});
|
|
2547
2825
|
},
|
|
2548
2826
|
setBackground(color) {
|
|
2549
2827
|
[renderer.background, renderer.placeholder] = colorsFor(color);
|
|
2550
2828
|
schedule();
|
|
2551
2829
|
},
|
|
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
2830
|
getStats: () => ({
|
|
2557
2831
|
mode: "gpu",
|
|
2558
2832
|
nodes: nodes.size,
|
|
@@ -2562,7 +2836,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2562
2836
|
rasterCallsLastFrame: stats.rasterCallsLastFrame,
|
|
2563
2837
|
rasterMs: stats.rasterMs,
|
|
2564
2838
|
pages: pages.size,
|
|
2565
|
-
drawablePages: [...pages.
|
|
2839
|
+
drawablePages: [...pages.all()].filter((page) => page.drawable).length,
|
|
2566
2840
|
atlasLayersUsed: atlas.usedLayers,
|
|
2567
2841
|
frameMs: stats.frameMs,
|
|
2568
2842
|
frames: stats.frames,
|
|
@@ -2570,29 +2844,20 @@ async function createGpuCanvas(host, options) {
|
|
|
2570
2844
|
lostDetail: stats.lostDetail,
|
|
2571
2845
|
thumbnailFill: atlas.pinnedFill,
|
|
2572
2846
|
onThumbnail: stats.onThumbnail,
|
|
2573
|
-
needs:
|
|
2847
|
+
needs: planner.needs
|
|
2574
2848
|
}),
|
|
2575
2849
|
destroy() {
|
|
2576
2850
|
destroyed = true;
|
|
2577
2851
|
cancelAnimationFrame(rafId);
|
|
2578
2852
|
clearTimeout(settleTimer);
|
|
2579
|
-
|
|
2580
|
-
|
|
2853
|
+
live.destroy();
|
|
2854
|
+
planner.destroy();
|
|
2581
2855
|
mutations.disconnect();
|
|
2582
2856
|
document.fonts.removeEventListener("loadingdone", onFontsLoaded);
|
|
2583
2857
|
canvas.removeEventListener("load", onResourceLoad, true);
|
|
2584
|
-
surface.removeEventListener("pointermove", onPointerMove);
|
|
2585
|
-
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
2586
|
-
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
2587
|
-
surface.removeEventListener("click", onClick);
|
|
2588
|
-
canvas.removeEventListener("focusin", onFocusIn);
|
|
2589
|
-
canvas.removeEventListener("focusout", onFocusOut);
|
|
2590
|
-
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
2591
|
-
for (const type of USE_EVENTS) canvas.removeEventListener(type, onUse, true);
|
|
2592
|
-
unbindInput();
|
|
2593
2858
|
resizeObserver.disconnect();
|
|
2594
2859
|
canvas.onpaint = null;
|
|
2595
|
-
|
|
2860
|
+
base.destroy();
|
|
2596
2861
|
renderer.destroy();
|
|
2597
2862
|
atlas.destroy();
|
|
2598
2863
|
device.destroy();
|
|
@@ -2651,13 +2916,13 @@ var labelText = (element) => {
|
|
|
2651
2916
|
}
|
|
2652
2917
|
return text.replace(/\s+/g, " ").trim();
|
|
2653
2918
|
};
|
|
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
2919
|
var round = (rect) => ({
|
|
2656
2920
|
x: Math.round(rect.x),
|
|
2657
2921
|
y: Math.round(rect.y),
|
|
2658
2922
|
width: Math.round(rect.width),
|
|
2659
2923
|
height: Math.round(rect.height)
|
|
2660
2924
|
});
|
|
2925
|
+
var MAX_CAPTURE_PX = 8192;
|
|
2661
2926
|
function createAgentView(canvas) {
|
|
2662
2927
|
const ids = /* @__PURE__ */ new WeakMap();
|
|
2663
2928
|
const elements = /* @__PURE__ */ new Map();
|
|
@@ -2703,11 +2968,7 @@ function createAgentView(canvas) {
|
|
|
2703
2968
|
return {
|
|
2704
2969
|
/** What is in `rect` (default: the view), node by node, as plain JSON. */
|
|
2705
2970
|
describe(options = {}) {
|
|
2706
|
-
const area = options.rect ??
|
|
2707
|
-
canvas.getCamera(),
|
|
2708
|
-
canvas.surface.clientWidth,
|
|
2709
|
-
canvas.surface.clientHeight
|
|
2710
|
-
);
|
|
2971
|
+
const area = options.rect ?? viewOf(canvas);
|
|
2711
2972
|
return canvas.getNodesIn(area).flatMap((node) => read(node, options.depth ?? 6, options));
|
|
2712
2973
|
},
|
|
2713
2974
|
/** The element behind an id from `describe`, or null once it is gone. */
|
|
@@ -2725,9 +2986,9 @@ function createAgentView(canvas) {
|
|
|
2725
2986
|
* `scale` texels per world unit. GPU backend only.
|
|
2726
2987
|
*/
|
|
2727
2988
|
async capture(target, { scale = 1, background } = {}) {
|
|
2728
|
-
const area = typeof target === "
|
|
2989
|
+
const area = typeof target === "string" || target instanceof Element ? this.rect(target) : target;
|
|
2729
2990
|
if (!area) throw new Error("Nothing to capture");
|
|
2730
|
-
const fit = Math.min(scale,
|
|
2991
|
+
const fit = Math.min(scale, MAX_CAPTURE_PX / area.width, MAX_CAPTURE_PX / area.height);
|
|
2731
2992
|
const width = Math.max(1, Math.round(area.width * fit));
|
|
2732
2993
|
const height = Math.max(1, Math.round(area.height * fit));
|
|
2733
2994
|
const out = new OffscreenCanvas(width, height);
|
|
@@ -2742,7 +3003,13 @@ function createAgentView(canvas) {
|
|
|
2742
3003
|
const rect = canvas.getNodeRect(node);
|
|
2743
3004
|
const bitmap = new OffscreenCanvas(images[i].width, images[i].height);
|
|
2744
3005
|
bitmap.getContext("2d").putImageData(images[i], 0, 0);
|
|
2745
|
-
context.drawImage(
|
|
3006
|
+
context.drawImage(
|
|
3007
|
+
bitmap,
|
|
3008
|
+
(rect.x - area.x) * fit,
|
|
3009
|
+
(rect.y - area.y) * fit,
|
|
3010
|
+
rect.width * fit,
|
|
3011
|
+
rect.height * fit
|
|
3012
|
+
);
|
|
2746
3013
|
});
|
|
2747
3014
|
return context.getImageData(0, 0, width, height);
|
|
2748
3015
|
}
|
|
@@ -2752,25 +3019,7 @@ function createAgentView(canvas) {
|
|
|
2752
3019
|
// src/layers.ts
|
|
2753
3020
|
var sameLayer = (a, b) => !!a && !!b && a[0] === b[0];
|
|
2754
3021
|
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;
|
|
3022
|
+
var layerRect = (canvas, layer) => unionOf(layer.map((element) => canvas.getElementRect(element)));
|
|
2774
3023
|
function createLayers(canvas, pick) {
|
|
2775
3024
|
const chainOf = (element) => {
|
|
2776
3025
|
const node = canvas.getNodeOf(element);
|
|
@@ -2789,7 +3038,7 @@ function createLayers(canvas, pick) {
|
|
|
2789
3038
|
let previous = canvas.getNodeRect(node);
|
|
2790
3039
|
for (const layer of inner) {
|
|
2791
3040
|
const rect = layerRect(canvas, layer);
|
|
2792
|
-
if (
|
|
3041
|
+
if (sameRect(rect, previous)) continue;
|
|
2793
3042
|
chain.push(layer);
|
|
2794
3043
|
previous = rect;
|
|
2795
3044
|
}
|
|
@@ -2807,7 +3056,7 @@ function createLayers(canvas, pick) {
|
|
|
2807
3056
|
for (const child of element.children) {
|
|
2808
3057
|
const rect = canvas.getElementRect(child);
|
|
2809
3058
|
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
2810
|
-
if (
|
|
3059
|
+
if (sameRect(rect, outer)) visit(child, outer);
|
|
2811
3060
|
else children.push([child]);
|
|
2812
3061
|
}
|
|
2813
3062
|
};
|
|
@@ -2834,6 +3083,16 @@ function createLayers(canvas, pick) {
|
|
|
2834
3083
|
|
|
2835
3084
|
// src/overlay.ts
|
|
2836
3085
|
var HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
3086
|
+
var HANDLE_ANCHORS = {
|
|
3087
|
+
nw: { x: 0, y: 0 },
|
|
3088
|
+
n: { x: 0.5, y: 0 },
|
|
3089
|
+
ne: { x: 1, y: 0 },
|
|
3090
|
+
e: { x: 1, y: 0.5 },
|
|
3091
|
+
se: { x: 1, y: 1 },
|
|
3092
|
+
s: { x: 0.5, y: 1 },
|
|
3093
|
+
sw: { x: 0, y: 1 },
|
|
3094
|
+
w: { x: 0, y: 0.5 }
|
|
3095
|
+
};
|
|
2837
3096
|
var HANDLE_PX = 8;
|
|
2838
3097
|
var CURSORS = {
|
|
2839
3098
|
nw: "nwse-resize",
|
|
@@ -2855,14 +3114,14 @@ var pill = (color) => ({
|
|
|
2855
3114
|
whiteSpace: "nowrap",
|
|
2856
3115
|
pointerEvents: "none"
|
|
2857
3116
|
});
|
|
2858
|
-
function createOverlay(surface, color
|
|
3117
|
+
function createOverlay(surface, color) {
|
|
2859
3118
|
const root = document.createElement("div");
|
|
2860
3119
|
Object.assign(root.style, {
|
|
2861
3120
|
position: "absolute",
|
|
2862
3121
|
inset: "0",
|
|
2863
3122
|
overflow: "hidden",
|
|
2864
3123
|
pointerEvents: "none",
|
|
2865
|
-
zIndex:
|
|
3124
|
+
zIndex: SURFACE_Z.overlay
|
|
2866
3125
|
});
|
|
2867
3126
|
const make = (style) => {
|
|
2868
3127
|
const element = document.createElement("div");
|
|
@@ -2870,7 +3129,7 @@ function createOverlay(surface, color, hoverColor) {
|
|
|
2870
3129
|
root.append(element);
|
|
2871
3130
|
return element;
|
|
2872
3131
|
};
|
|
2873
|
-
const hoverName = make(pill(
|
|
3132
|
+
const hoverName = make(pill(color));
|
|
2874
3133
|
const name = make(pill(color));
|
|
2875
3134
|
const size = make(pill(color));
|
|
2876
3135
|
const bounds = make({ position: "absolute", border: `1px solid ${color}` });
|
|
@@ -2928,12 +3187,11 @@ function createOverlay(surface, color, hoverColor) {
|
|
|
2928
3187
|
}
|
|
2929
3188
|
if (state.handles) {
|
|
2930
3189
|
HANDLES.forEach((handle, i) => {
|
|
2931
|
-
const
|
|
2932
|
-
const fy = handle.includes("n") ? 0 : handle.includes("s") ? 1 : 0.5;
|
|
3190
|
+
const anchor = HANDLE_ANCHORS[handle];
|
|
2933
3191
|
place(
|
|
2934
3192
|
handles[i],
|
|
2935
|
-
rect.x + rect.width *
|
|
2936
|
-
rect.y + rect.height *
|
|
3193
|
+
rect.x + rect.width * anchor.x - HANDLE_PX / 2,
|
|
3194
|
+
rect.y + rect.height * anchor.y - HANDLE_PX / 2
|
|
2937
3195
|
);
|
|
2938
3196
|
});
|
|
2939
3197
|
}
|
|
@@ -2955,9 +3213,10 @@ function createOverlay(surface, color, hoverColor) {
|
|
|
2955
3213
|
}
|
|
2956
3214
|
};
|
|
2957
3215
|
}
|
|
2958
|
-
var
|
|
2959
|
-
|
|
2960
|
-
const
|
|
3216
|
+
var handleAt = (target) => target instanceof Element ? target.closest("[data-gpucanvas-handle]")?.dataset.gpucanvasHandle ?? null : null;
|
|
3217
|
+
var guideRect = (guide, camera) => {
|
|
3218
|
+
const a = worldToScreen(camera, { x: guide.x1, y: guide.y1 });
|
|
3219
|
+
const b = worldToScreen(camera, { x: guide.x2, y: guide.y2 });
|
|
2961
3220
|
return {
|
|
2962
3221
|
x: Math.min(a.x, b.x),
|
|
2963
3222
|
y: Math.min(a.y, b.y),
|
|
@@ -3004,22 +3263,22 @@ function guidesFor(rect, targets, edges) {
|
|
|
3004
3263
|
const hits = targets.filter((target) => anchorsX(target).some((at) => close(at, x)));
|
|
3005
3264
|
if (!hits.length) continue;
|
|
3006
3265
|
const top = Math.min(rect.y, ...hits.map((t) => t.y));
|
|
3007
|
-
const
|
|
3008
|
-
guides.push({ x1: x, y1: top, x2: x, y2:
|
|
3266
|
+
const bottom = Math.max(rect.y + rect.height, ...hits.map((t) => t.y + t.height));
|
|
3267
|
+
guides.push({ x1: x, y1: top, x2: x, y2: bottom });
|
|
3009
3268
|
}
|
|
3010
3269
|
for (const f of edges.y) {
|
|
3011
3270
|
const y = rect.y + rect.height * f;
|
|
3012
3271
|
const hits = targets.filter((target) => anchorsY(target).some((at) => close(at, y)));
|
|
3013
3272
|
if (!hits.length) continue;
|
|
3014
3273
|
const left = Math.min(rect.x, ...hits.map((t) => t.x));
|
|
3015
|
-
const
|
|
3016
|
-
guides.push({ x1: left, y1: y, x2:
|
|
3274
|
+
const right = Math.max(rect.x + rect.width, ...hits.map((t) => t.x + t.width));
|
|
3275
|
+
guides.push({ x1: left, y1: y, x2: right, y2: y });
|
|
3017
3276
|
}
|
|
3018
3277
|
return guides;
|
|
3019
3278
|
}
|
|
3020
3279
|
|
|
3021
3280
|
// src/editor.ts
|
|
3022
|
-
function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR
|
|
3281
|
+
function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR) {
|
|
3023
3282
|
const box = document.createElement("div");
|
|
3024
3283
|
Object.assign(box.style, {
|
|
3025
3284
|
position: "absolute",
|
|
@@ -3027,11 +3286,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
|
|
|
3027
3286
|
pointerEvents: "none",
|
|
3028
3287
|
border: `1px solid ${color}`,
|
|
3029
3288
|
background: `color-mix(in srgb, ${color} 12%, transparent)`,
|
|
3030
|
-
zIndex:
|
|
3289
|
+
zIndex: SURFACE_Z.marquee
|
|
3031
3290
|
});
|
|
3032
3291
|
canvas.surface.append(box);
|
|
3033
3292
|
return {
|
|
3034
|
-
/** Spans the box from `a` to `b` (surface pixels) and
|
|
3293
|
+
/** Spans the box from `a` to `b` (surface pixels) and returns the world rect it covers. */
|
|
3035
3294
|
update(a, b) {
|
|
3036
3295
|
const x = Math.min(a.x, b.x);
|
|
3037
3296
|
const y = Math.min(a.y, b.y);
|
|
@@ -3045,13 +3304,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
|
|
|
3045
3304
|
height: `${height}px`
|
|
3046
3305
|
});
|
|
3047
3306
|
const camera = canvas.getCamera();
|
|
3048
|
-
|
|
3307
|
+
return {
|
|
3049
3308
|
...screenToWorld(camera, { x, y }),
|
|
3050
3309
|
width: width / camera.zoom,
|
|
3051
3310
|
height: height / camera.zoom
|
|
3052
|
-
}
|
|
3053
|
-
canvas.setSelection(hits);
|
|
3054
|
-
return hits;
|
|
3311
|
+
};
|
|
3055
3312
|
},
|
|
3056
3313
|
hide() {
|
|
3057
3314
|
box.style.display = "none";
|
|
@@ -3061,9 +3318,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
|
|
|
3061
3318
|
}
|
|
3062
3319
|
};
|
|
3063
3320
|
}
|
|
3321
|
+
var defaultLabel = (canvas) => (layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName;
|
|
3064
3322
|
var CLICK_SLOP = 4;
|
|
3065
3323
|
var SNAP_PX = 6;
|
|
3066
3324
|
var MAX_NAMED = 1;
|
|
3325
|
+
var EVERYWHERE = { x: -1e9, y: -1e9, width: 2e9, height: 2e9 };
|
|
3067
3326
|
var isEditable = (element) => !!element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable);
|
|
3068
3327
|
function bindEditor(canvas, options = {}) {
|
|
3069
3328
|
const {
|
|
@@ -3077,16 +3336,12 @@ function bindEditor(canvas, options = {}) {
|
|
|
3077
3336
|
onChange,
|
|
3078
3337
|
onTransform
|
|
3079
3338
|
} = options;
|
|
3080
|
-
const label = options.label ?? (
|
|
3339
|
+
const label = options.label ?? defaultLabel(canvas);
|
|
3081
3340
|
const { surface } = canvas;
|
|
3082
3341
|
const layers = createLayers(canvas, pick);
|
|
3083
3342
|
const transforms = !!onTransform && trigger === "click";
|
|
3084
|
-
const overlay = createOverlay(surface, color
|
|
3085
|
-
|
|
3086
|
-
const box = marquee ? createMarquee(canvas, color, (world) => {
|
|
3087
|
-
marqueeLayers = layers.layersIn(scopes.at(-1) ?? null, world);
|
|
3088
|
-
return marqueeLayers.flat();
|
|
3089
|
-
}) : null;
|
|
3343
|
+
const overlay = createOverlay(surface, color);
|
|
3344
|
+
const box = marquee ? createMarquee(canvas, color) : null;
|
|
3090
3345
|
let selection = [];
|
|
3091
3346
|
let scopes = [];
|
|
3092
3347
|
let hovered = null;
|
|
@@ -3096,7 +3351,8 @@ function bindEditor(canvas, options = {}) {
|
|
|
3096
3351
|
let swallowClick = false;
|
|
3097
3352
|
let keepFocus = false;
|
|
3098
3353
|
const commit = (next, nextScopes = scopes) => {
|
|
3099
|
-
|
|
3354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3355
|
+
selection = next.filter((layer) => !seen.has(layer[0]) && !!seen.add(layer[0]));
|
|
3100
3356
|
scopes = selection.length ? nextScopes : [];
|
|
3101
3357
|
canvas.setSelection(selection.flat());
|
|
3102
3358
|
onChange?.(selection);
|
|
@@ -3108,10 +3364,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3108
3364
|
canvas.setHover(layer?.[0] ?? null);
|
|
3109
3365
|
draw();
|
|
3110
3366
|
};
|
|
3111
|
-
const at = (event) =>
|
|
3112
|
-
const bounds = surface.getBoundingClientRect();
|
|
3113
|
-
return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
3114
|
-
};
|
|
3367
|
+
const at = (event) => pointIn(surface, event);
|
|
3115
3368
|
const chainAt = (point) => layers.chainAt(canvas.screenToWorld(point));
|
|
3116
3369
|
const fieldAt = (point) => {
|
|
3117
3370
|
const element = canvas.getElementAt(canvas.screenToWorld(point));
|
|
@@ -3140,15 +3393,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3140
3393
|
const draw = () => {
|
|
3141
3394
|
if (!frame) frame = requestAnimationFrame(render);
|
|
3142
3395
|
};
|
|
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
|
-
);
|
|
3396
|
+
const toScreen = (rect) => rectToScreen(canvas.getCamera(), rect);
|
|
3152
3397
|
const render = () => {
|
|
3153
3398
|
frame = 0;
|
|
3154
3399
|
const transforming = gesture?.kind === "move" || gesture?.kind === "resize" ? gesture : null;
|
|
@@ -3164,28 +3409,24 @@ function bindEditor(canvas, options = {}) {
|
|
|
3164
3409
|
} : null,
|
|
3165
3410
|
bounds: selection.length > 1 || !!transforming,
|
|
3166
3411
|
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
|
-
) : []
|
|
3412
|
+
guides: transforming ? transforming.guides.map((guide) => guideRect(guide, canvas.getCamera())) : []
|
|
3170
3413
|
});
|
|
3171
3414
|
if (selection.length || hovered || gesture) draw();
|
|
3172
3415
|
};
|
|
3173
|
-
const emit = (phase) => {
|
|
3174
|
-
if (gesture?.kind !== "move" && gesture?.kind !== "resize") return;
|
|
3416
|
+
const emit = (transform, phase) => {
|
|
3175
3417
|
onTransform?.({
|
|
3176
3418
|
phase,
|
|
3177
|
-
kind:
|
|
3178
|
-
layers:
|
|
3179
|
-
from:
|
|
3180
|
-
to: phase === "cancel" ?
|
|
3419
|
+
kind: transform.kind,
|
|
3420
|
+
layers: transform.layers,
|
|
3421
|
+
from: transform.from,
|
|
3422
|
+
to: phase === "cancel" ? transform.from : transform.to
|
|
3181
3423
|
});
|
|
3182
3424
|
};
|
|
3183
3425
|
const snapTargets = (moving) => {
|
|
3184
3426
|
if (!snap) return [];
|
|
3185
3427
|
const scope = scopes.at(-1);
|
|
3186
3428
|
if (!scope) {
|
|
3187
|
-
|
|
3188
|
-
return canvas.getNodesIn(view).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
|
|
3429
|
+
return canvas.getNodesIn(viewOf(canvas)).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
|
|
3189
3430
|
}
|
|
3190
3431
|
const rects = layers.childrenOf(scope).filter((layer) => !layerIn(layer, moving)).map((layer) => layerRect(canvas, layer));
|
|
3191
3432
|
return [layerRect(canvas, scope), ...rects].filter((rect) => !!rect);
|
|
@@ -3194,7 +3435,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3194
3435
|
const from = selection.map((layer) => layerRect(canvas, layer));
|
|
3195
3436
|
if (from.some((rect) => !rect)) return;
|
|
3196
3437
|
const rects = from;
|
|
3197
|
-
|
|
3438
|
+
const transform = {
|
|
3198
3439
|
kind: handle ? "resize" : "move",
|
|
3199
3440
|
start,
|
|
3200
3441
|
handle,
|
|
@@ -3205,8 +3446,9 @@ function bindEditor(canvas, options = {}) {
|
|
|
3205
3446
|
targets: snapTargets(selection),
|
|
3206
3447
|
guides: []
|
|
3207
3448
|
};
|
|
3449
|
+
gesture = transform;
|
|
3208
3450
|
setHovered(null);
|
|
3209
|
-
emit("start");
|
|
3451
|
+
emit(transform, "start");
|
|
3210
3452
|
};
|
|
3211
3453
|
const updateTransform = (point, keepRatio) => {
|
|
3212
3454
|
if (gesture?.kind !== "move" && gesture?.kind !== "resize") return;
|
|
@@ -3225,29 +3467,30 @@ function bindEditor(canvas, options = {}) {
|
|
|
3225
3467
|
} else {
|
|
3226
3468
|
let left = bounds.x;
|
|
3227
3469
|
let top = bounds.y;
|
|
3228
|
-
let
|
|
3229
|
-
let
|
|
3230
|
-
|
|
3231
|
-
if (
|
|
3232
|
-
if (
|
|
3233
|
-
if (
|
|
3234
|
-
|
|
3235
|
-
|
|
3470
|
+
let right = bounds.x + bounds.width;
|
|
3471
|
+
let bottom = bounds.y + bounds.height;
|
|
3472
|
+
const anchor = HANDLE_ANCHORS[handle];
|
|
3473
|
+
if (anchor.x === 0) left = Math.min(left + dx, right - 1);
|
|
3474
|
+
if (anchor.x === 1) right = Math.max(right + dx, left + 1);
|
|
3475
|
+
if (anchor.y === 0) top = Math.min(top + dy, bottom - 1);
|
|
3476
|
+
if (anchor.y === 1) bottom = Math.max(bottom + dy, top + 1);
|
|
3477
|
+
next = { x: left, y: top, width: right - left, height: bottom - top };
|
|
3478
|
+
const corner = anchor.x !== 0.5 && anchor.y !== 0.5;
|
|
3236
3479
|
if (keepRatio && corner) {
|
|
3237
3480
|
const scale = Math.max(next.width / bounds.width, next.height / bounds.height);
|
|
3238
3481
|
const width = bounds.width * scale;
|
|
3239
3482
|
const height = bounds.height * scale;
|
|
3240
3483
|
next = {
|
|
3241
|
-
x:
|
|
3242
|
-
y:
|
|
3484
|
+
x: anchor.x === 0 ? right - width : left,
|
|
3485
|
+
y: anchor.y === 0 ? bottom - height : top,
|
|
3243
3486
|
width,
|
|
3244
3487
|
height
|
|
3245
3488
|
};
|
|
3246
3489
|
gesture.guides = [];
|
|
3247
3490
|
} else {
|
|
3248
3491
|
const edges = {
|
|
3249
|
-
x:
|
|
3250
|
-
y:
|
|
3492
|
+
x: anchor.x === 0.5 ? [] : [anchor.x],
|
|
3493
|
+
y: anchor.y === 0.5 ? [] : [anchor.y]
|
|
3251
3494
|
};
|
|
3252
3495
|
const snapped = snapRect(next, gesture.targets, threshold, edges);
|
|
3253
3496
|
if (edges.x[0] === 0)
|
|
@@ -3267,7 +3510,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3267
3510
|
width: rect.width * sx,
|
|
3268
3511
|
height: rect.height * sy
|
|
3269
3512
|
}));
|
|
3270
|
-
emit("move");
|
|
3513
|
+
emit(gesture, "move");
|
|
3271
3514
|
draw();
|
|
3272
3515
|
};
|
|
3273
3516
|
const onPointerDown = (event) => {
|
|
@@ -3287,16 +3530,16 @@ function bindEditor(canvas, options = {}) {
|
|
|
3287
3530
|
swallowClick = true;
|
|
3288
3531
|
surface.setPointerCapture(event.pointerId);
|
|
3289
3532
|
};
|
|
3290
|
-
const handle = event.target
|
|
3533
|
+
const handle = handleAt(event.target);
|
|
3291
3534
|
if (handle && transforms) {
|
|
3292
3535
|
take();
|
|
3293
|
-
return beginTransform(point, handle
|
|
3536
|
+
return beginTransform(point, handle);
|
|
3294
3537
|
}
|
|
3295
3538
|
const target = targetIn(chainAt(point));
|
|
3296
3539
|
if (!target) {
|
|
3297
3540
|
if (event.shiftKey && box) {
|
|
3298
3541
|
take();
|
|
3299
|
-
gesture = { kind: "marquee", start: point };
|
|
3542
|
+
gesture = { kind: "marquee", start: point, scopes };
|
|
3300
3543
|
return;
|
|
3301
3544
|
}
|
|
3302
3545
|
if (selection.length) commit([], []);
|
|
@@ -3320,35 +3563,27 @@ function bindEditor(canvas, options = {}) {
|
|
|
3320
3563
|
if (Math.hypot(pointer.x - gesture.start.x, pointer.y - gesture.start.y) <= CLICK_SLOP)
|
|
3321
3564
|
return;
|
|
3322
3565
|
if (gesture.inside || trigger !== "click" && box) {
|
|
3323
|
-
gesture = { kind: "marquee", start: gesture.start };
|
|
3566
|
+
gesture = { kind: "marquee", start: gesture.start, scopes };
|
|
3324
3567
|
} else if (trigger === "click" && transforms) beginTransform(gesture.start, null);
|
|
3325
3568
|
else return;
|
|
3326
3569
|
}
|
|
3327
3570
|
if (gesture?.kind === "marquee") {
|
|
3328
|
-
box.update(gesture.start, pointer);
|
|
3329
|
-
|
|
3330
|
-
onChange?.(selection);
|
|
3331
|
-
draw();
|
|
3571
|
+
const world = box.update(gesture.start, pointer);
|
|
3572
|
+
commit(layers.layersIn(gesture.scopes.at(-1) ?? null, world), gesture.scopes);
|
|
3332
3573
|
} else updateTransform(pointer, event.shiftKey);
|
|
3333
3574
|
};
|
|
3334
3575
|
const onPointerUp = () => {
|
|
3335
3576
|
const done = gesture;
|
|
3336
3577
|
gesture = null;
|
|
3337
3578
|
if (!done) return;
|
|
3338
|
-
if (done.kind === "marquee")
|
|
3339
|
-
|
|
3340
|
-
commit(marqueeLayers);
|
|
3341
|
-
} else if (done.kind === "press") {
|
|
3579
|
+
if (done.kind === "marquee") box.hide();
|
|
3580
|
+
else if (done.kind === "press") {
|
|
3342
3581
|
const { layer, scopes: inside } = done.target;
|
|
3343
3582
|
if (!done.additive) commit([layer], inside);
|
|
3344
3583
|
else if (layerIn(layer, selection))
|
|
3345
3584
|
commit(selection.filter((other) => !sameLayer(other, layer)));
|
|
3346
3585
|
else commit([...selection, layer], inside);
|
|
3347
|
-
} else
|
|
3348
|
-
gesture = done;
|
|
3349
|
-
emit("end");
|
|
3350
|
-
gesture = null;
|
|
3351
|
-
}
|
|
3586
|
+
} else emit(done, "end");
|
|
3352
3587
|
updateHover();
|
|
3353
3588
|
draw();
|
|
3354
3589
|
};
|
|
@@ -3412,7 +3647,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3412
3647
|
}
|
|
3413
3648
|
if (event.key === "Escape") {
|
|
3414
3649
|
if (gesture?.kind === "move" || gesture?.kind === "resize") {
|
|
3415
|
-
emit("cancel");
|
|
3650
|
+
emit(gesture, "cancel");
|
|
3416
3651
|
gesture = null;
|
|
3417
3652
|
} else if (scopes.length) up();
|
|
3418
3653
|
else if (selection.length) commit([], []);
|
|
@@ -3427,7 +3662,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3427
3662
|
} else if (event.key === "Tab") {
|
|
3428
3663
|
event.preventDefault();
|
|
3429
3664
|
const scope = scopes.at(-1);
|
|
3430
|
-
const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn(
|
|
3665
|
+
const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn(EVERYWHERE).map((node) => [node]);
|
|
3431
3666
|
const index = siblings.findIndex((layer) => sameLayer(layer, selection[0]));
|
|
3432
3667
|
const step = event.shiftKey ? -1 : 1;
|
|
3433
3668
|
const next = siblings[(index + step + siblings.length) % siblings.length];
|
|
@@ -3487,7 +3722,7 @@ var pillStyle = (color) => ({
|
|
|
3487
3722
|
whiteSpace: "nowrap",
|
|
3488
3723
|
pointerEvents: "none"
|
|
3489
3724
|
});
|
|
3490
|
-
function createMarks(canvas, { color =
|
|
3725
|
+
function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
|
|
3491
3726
|
const id = `gpucanvas-marks-${++instances}`;
|
|
3492
3727
|
const root = document.createElement("div");
|
|
3493
3728
|
Object.assign(root.style, {
|
|
@@ -3495,7 +3730,7 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
|
|
|
3495
3730
|
inset: "0",
|
|
3496
3731
|
overflow: "hidden",
|
|
3497
3732
|
pointerEvents: "none",
|
|
3498
|
-
zIndex:
|
|
3733
|
+
zIndex: SURFACE_Z.marks
|
|
3499
3734
|
});
|
|
3500
3735
|
const svg = document.createElementNS(SVG, "svg");
|
|
3501
3736
|
Object.assign(svg.style, { position: "absolute", inset: "0", width: "100%", height: "100%" });
|
|
@@ -3530,11 +3765,7 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
|
|
|
3530
3765
|
return ref;
|
|
3531
3766
|
};
|
|
3532
3767
|
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
|
-
};
|
|
3768
|
+
const screenRect = (rect) => rectToScreen(canvas.getCamera(), rect);
|
|
3538
3769
|
const worldRect = (target) => target instanceof Element ? canvas.getElementRect(target) : { x: target.x, y: target.y, width: 0, height: 0 };
|
|
3539
3770
|
const place = (element, x, y) => {
|
|
3540
3771
|
const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
|
@@ -3551,21 +3782,22 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
|
|
|
3551
3782
|
root.append(element);
|
|
3552
3783
|
return element;
|
|
3553
3784
|
};
|
|
3554
|
-
const marks = /* @__PURE__ */ new
|
|
3785
|
+
const marks = /* @__PURE__ */ new Map();
|
|
3555
3786
|
let frame = 0;
|
|
3556
3787
|
const loop = () => {
|
|
3557
3788
|
frame = marks.size ? requestAnimationFrame(loop) : 0;
|
|
3558
|
-
for (const draw of marks) draw();
|
|
3789
|
+
for (const draw of marks.keys()) draw();
|
|
3559
3790
|
};
|
|
3560
3791
|
const add = (draw, remove) => {
|
|
3561
|
-
|
|
3562
|
-
if (!frame) frame = requestAnimationFrame(loop);
|
|
3563
|
-
return {
|
|
3792
|
+
const mark = {
|
|
3564
3793
|
remove() {
|
|
3565
3794
|
marks.delete(draw);
|
|
3566
3795
|
remove();
|
|
3567
3796
|
}
|
|
3568
3797
|
};
|
|
3798
|
+
marks.set(draw, mark.remove);
|
|
3799
|
+
if (!frame) frame = requestAnimationFrame(loop);
|
|
3800
|
+
return mark;
|
|
3569
3801
|
};
|
|
3570
3802
|
return {
|
|
3571
3803
|
/** An arrow from one element (or world point) to another, with an optional label. */
|
|
@@ -3716,9 +3948,7 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
|
|
|
3716
3948
|
},
|
|
3717
3949
|
/** Removes every mark. */
|
|
3718
3950
|
clear() {
|
|
3719
|
-
for (const
|
|
3720
|
-
root.querySelectorAll(":scope > div").forEach((element) => element.remove());
|
|
3721
|
-
svg.querySelectorAll(":scope > path").forEach((element) => element.remove());
|
|
3951
|
+
for (const remove of [...marks.values()]) remove();
|
|
3722
3952
|
},
|
|
3723
3953
|
destroy() {
|
|
3724
3954
|
cancelAnimationFrame(frame);
|
|
@@ -3787,6 +4017,7 @@ function recordFilmstrip(canvas, node, {
|
|
|
3787
4017
|
const frames = [];
|
|
3788
4018
|
let stopped = false;
|
|
3789
4019
|
let timer = 0;
|
|
4020
|
+
let warned = false;
|
|
3790
4021
|
const take = () => {
|
|
3791
4022
|
canvas.snapshot(node, { scale }).then((image) => {
|
|
3792
4023
|
if (stopped) return;
|
|
@@ -3794,7 +4025,10 @@ function recordFilmstrip(canvas, node, {
|
|
|
3794
4025
|
frames.push(frame);
|
|
3795
4026
|
if (frames.length > max) frames.shift();
|
|
3796
4027
|
onFrame?.(frame, frames);
|
|
3797
|
-
}).catch(() => {
|
|
4028
|
+
}).catch((error) => {
|
|
4029
|
+
if (warned) return;
|
|
4030
|
+
warned = true;
|
|
4031
|
+
console.warn("gpucanvas: filmstrip snapshot failed", error);
|
|
3798
4032
|
});
|
|
3799
4033
|
};
|
|
3800
4034
|
const observer = new MutationObserver((records) => {
|
|
@@ -3867,6 +4101,7 @@ async function createGpuCanvas2(host, options = {}) {
|
|
|
3867
4101
|
return await createGpuCanvas(host, options);
|
|
3868
4102
|
} catch (error) {
|
|
3869
4103
|
if (mode === "gpu") throw error;
|
|
4104
|
+
console.warn("gpucanvas: the GPU backend failed to start, using DOM", error);
|
|
3870
4105
|
return createDomCanvas(host, options);
|
|
3871
4106
|
}
|
|
3872
4107
|
}
|
|
@@ -3880,6 +4115,7 @@ export {
|
|
|
3880
4115
|
EFFECTS,
|
|
3881
4116
|
supportsGpuCanvas,
|
|
3882
4117
|
createAgentView,
|
|
4118
|
+
defaultLabel,
|
|
3883
4119
|
bindEditor,
|
|
3884
4120
|
createMarks,
|
|
3885
4121
|
toBlob,
|
|
@@ -3890,4 +4126,4 @@ export {
|
|
|
3890
4126
|
swapNodes,
|
|
3891
4127
|
createGpuCanvas2 as createGpuCanvas
|
|
3892
4128
|
};
|
|
3893
|
-
//# sourceMappingURL=chunk-
|
|
4129
|
+
//# sourceMappingURL=chunk-IIRGNX76.js.map
|