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