gpucanvas 0.4.4 → 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-IJ3R6X75.js → chunk-IIRGNX76.js} +1445 -1177
- package/dist/chunk-IIRGNX76.js.map +1 -0
- package/dist/{editor-B5nVs54L.d.ts → editor-BuoBLRhF.d.ts} +4 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +21 -7
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-IJ3R6X75.js.map +0 -1
|
@@ -219,12 +219,13 @@ var sameRect = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y
|
|
|
219
219
|
// src/types.ts
|
|
220
220
|
var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
|
|
221
221
|
var DEFAULT_SELECTION_COLOR = "#4f8cff";
|
|
222
|
+
var SURFACE_Z = { marks: "1", marquee: "1", overlay: "2" };
|
|
222
223
|
var SELECTION_WIDTH = 2;
|
|
223
224
|
var HOVER_WIDTH = 1;
|
|
224
225
|
|
|
225
226
|
// src/base.ts
|
|
226
227
|
var styleNode = (element) => Object.assign(element.style, { boxSizing: "border-box", contain: "strict", overflow: "hidden" });
|
|
227
|
-
function createBase(host, options, onCamera) {
|
|
228
|
+
function createBase(host, options, onCamera, hooks) {
|
|
228
229
|
const surface = document.createElement("div");
|
|
229
230
|
markSurface(surface);
|
|
230
231
|
Object.assign(surface.style, {
|
|
@@ -250,6 +251,22 @@ function createBase(host, options, onCamera) {
|
|
|
250
251
|
};
|
|
251
252
|
const api = {
|
|
252
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
|
+
},
|
|
253
270
|
getNodeRect: (element) => {
|
|
254
271
|
const node = nodes.get(element);
|
|
255
272
|
return node ? { ...node.rect } : null;
|
|
@@ -679,7 +696,30 @@ function createDomCanvas(host, options) {
|
|
|
679
696
|
world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
|
|
680
697
|
frameMs = performance.now() - start;
|
|
681
698
|
};
|
|
682
|
-
const
|
|
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
|
+
}
|
|
722
|
+
});
|
|
683
723
|
const { surface, nodes } = base;
|
|
684
724
|
surface.style.background = options.background ?? "transparent";
|
|
685
725
|
const world = document.createElement("div");
|
|
@@ -779,32 +819,10 @@ function createDomCanvas(host, options) {
|
|
|
779
819
|
element.animate(keyframes, { duration: period * 1e3, iterations: Infinity })
|
|
780
820
|
);
|
|
781
821
|
};
|
|
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`;
|
|
788
|
-
};
|
|
789
822
|
apply(base.camera());
|
|
790
823
|
return {
|
|
791
824
|
...base.api,
|
|
792
825
|
mode: "dom",
|
|
793
|
-
addNode(element, rect) {
|
|
794
|
-
styleNode(element);
|
|
795
|
-
element.style.position = "absolute";
|
|
796
|
-
updateNode(element, rect);
|
|
797
|
-
world.append(element);
|
|
798
|
-
},
|
|
799
|
-
updateNode,
|
|
800
|
-
removeNode(element) {
|
|
801
|
-
clearEffect(element);
|
|
802
|
-
nodes.delete(element);
|
|
803
|
-
selection.delete(element);
|
|
804
|
-
outlinedNodes.delete(element);
|
|
805
|
-
element.style.outline = "";
|
|
806
|
-
element.remove();
|
|
807
|
-
},
|
|
808
826
|
getElementRect: elementRect,
|
|
809
827
|
setSelection(elements) {
|
|
810
828
|
selection = new Set(elements);
|
|
@@ -1019,458 +1037,354 @@ var Atlas = class {
|
|
|
1019
1037
|
}
|
|
1020
1038
|
};
|
|
1021
1039
|
|
|
1022
|
-
// src/
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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;
|
|
1045
1094
|
}
|
|
1046
|
-
|
|
1047
|
-
const
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
}
|
|
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;
|
|
1067
1103
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
/* wgsl */
|
|
1073
|
-
`
|
|
1074
|
-
struct View {
|
|
1075
|
-
size: vec2f, // canvas size in CSS pixels
|
|
1076
|
-
origin: vec2f, // camera position, world units
|
|
1077
|
-
zoom: f32,
|
|
1078
|
-
dpr: f32,
|
|
1079
|
-
atlasSize: f32,
|
|
1080
|
-
time: f32, // seconds since the canvas started
|
|
1081
|
-
placeholder: vec4f, // premultiplied color for nodes without a snapshot
|
|
1082
|
-
pointer: vec4f, // pointer and last press in world units; far away when there is none
|
|
1083
|
-
pressTime: f32, // seconds since the last press
|
|
1104
|
+
function setThumb(node, slot, now) {
|
|
1105
|
+
node.thumb = slot;
|
|
1106
|
+
node.thumbStale = false;
|
|
1107
|
+
node.thumbAt = now;
|
|
1084
1108
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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;
|
|
1117
|
+
}
|
|
1118
|
+
node.dirty = false;
|
|
1119
|
+
node.wholeStale = false;
|
|
1095
1120
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
@location(3) @interpolate(flat) color: vec4f,
|
|
1103
|
-
@location(4) uv: vec2f, // 0 to 1 across the node, past that in an effect's padding
|
|
1104
|
-
@location(5) @interpolate(flat) size: vec2f,
|
|
1105
|
-
@location(6) @interpolate(flat) params: vec4f,
|
|
1106
|
-
@location(7) @interpolate(flat) corner: vec2f, // the node's top left in world units
|
|
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;
|
|
1107
1127
|
}
|
|
1128
|
+
var isCurrentTile = (tile, detailScale) => tile.scale === detailScale && !tile.stale;
|
|
1129
|
+
var hasFreshThumb = (atlas, node) => atlas.isValid(node.thumb) && !node.thumbStale;
|
|
1108
1130
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
out.bounds = vec4f(0.0, 0.0, instance.rect.zw);
|
|
1128
|
-
out.color = instance.uv;
|
|
1129
|
-
} else {
|
|
1130
|
-
let pad = grow * (instance.uv.zw - instance.uv.xy) / instance.rect.zw;
|
|
1131
|
-
out.texel = mix(instance.uv.xy - pad, instance.uv.zw + pad, corner);
|
|
1132
|
-
out.color = instance.uv;
|
|
1133
|
-
// Half a texel inside the node's rectangle: the outermost sample center that cannot blend
|
|
1134
|
-
// with whatever sits next to it in the atlas.
|
|
1135
|
-
out.bounds = vec4f(instance.uv.xy + 0.5, instance.uv.zw - 0.5);
|
|
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);
|
|
1136
1149
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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);
|
|
1158
1175
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
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()];
|
|
1161
1179
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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;
|
|
1172
1210
|
}
|
|
1211
|
+
this.positioned = false;
|
|
1212
|
+
this.position();
|
|
1213
|
+
schedule();
|
|
1173
1214
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
);
|
|
1178
|
-
var effectShader = (wgsl) => (
|
|
1179
|
-
/* wgsl */
|
|
1180
|
-
`${prelude}
|
|
1181
|
-
struct EffectInput {
|
|
1182
|
-
uv: vec2f,
|
|
1183
|
-
size: vec2f,
|
|
1184
|
-
time: f32,
|
|
1185
|
-
zoom: f32,
|
|
1186
|
-
params: vec4f,
|
|
1187
|
-
pointer: vec2f,
|
|
1188
|
-
press: vec2f,
|
|
1189
|
-
sincePress: f32,
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
var<private> sourceRect: vec4f;
|
|
1193
|
-
var<private> sourceLayer: i32;
|
|
1194
|
-
|
|
1195
|
-
fn source(uv: vec2f) -> vec4f {
|
|
1196
|
-
if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) {
|
|
1197
|
-
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)));
|
|
1198
1219
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
);
|
|
1229
|
-
}
|
|
1230
|
-
`
|
|
1231
|
-
);
|
|
1232
|
-
|
|
1233
|
-
// src/gpu-renderer.ts
|
|
1234
|
-
var FLOATS_PER_INSTANCE = 16;
|
|
1235
|
-
var CompositeRenderer = class {
|
|
1236
|
-
constructor(device, context, format, atlas, background, placeholder, onEffectReady) {
|
|
1237
|
-
this.device = device;
|
|
1238
|
-
this.context = context;
|
|
1239
|
-
this.format = format;
|
|
1240
|
-
this.atlas = atlas;
|
|
1241
|
-
this.background = background;
|
|
1242
|
-
this.placeholder = placeholder;
|
|
1243
|
-
this.onEffectReady = onEffectReady;
|
|
1244
|
-
this.bindGroupLayout = device.createBindGroupLayout({
|
|
1245
|
-
entries: [
|
|
1246
|
-
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
|
|
1247
|
-
{
|
|
1248
|
-
binding: 1,
|
|
1249
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
1250
|
-
texture: { viewDimension: "2d-array" }
|
|
1251
|
-
},
|
|
1252
|
-
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
|
|
1253
|
-
]
|
|
1254
|
-
});
|
|
1255
|
-
this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
|
|
1256
|
-
this.pipeline = device.createRenderPipeline(
|
|
1257
|
-
this.pipelineDescriptor("gpucanvas composite", compositeShader)
|
|
1258
|
-
);
|
|
1259
|
-
this.uniforms = device.createBuffer({
|
|
1260
|
-
size: this.uniformData.byteLength,
|
|
1261
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
1262
|
-
});
|
|
1263
|
-
this.bindGroup = device.createBindGroup({
|
|
1264
|
-
layout: this.bindGroupLayout,
|
|
1265
|
-
entries: [
|
|
1266
|
-
{ binding: 0, resource: { buffer: this.uniforms } },
|
|
1267
|
-
{ binding: 1, resource: atlas.view },
|
|
1268
|
-
{ binding: 2, resource: this.sampler }
|
|
1269
|
-
]
|
|
1270
|
-
});
|
|
1271
|
-
this.instanceBuffer = this.createInstanceBuffer();
|
|
1272
|
-
}
|
|
1273
|
-
device;
|
|
1274
|
-
context;
|
|
1275
|
-
format;
|
|
1276
|
-
atlas;
|
|
1277
|
-
background;
|
|
1278
|
-
placeholder;
|
|
1279
|
-
onEffectReady;
|
|
1280
|
-
instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
|
|
1281
|
-
count = 0;
|
|
1282
|
-
runs = [];
|
|
1283
|
-
instanceBuffer;
|
|
1284
|
-
uniforms;
|
|
1285
|
-
uniformData = new Float32Array(20);
|
|
1286
|
-
pipeline;
|
|
1287
|
-
bindGroup;
|
|
1288
|
-
bindGroupLayout;
|
|
1289
|
-
sampler;
|
|
1290
|
-
/** Effect pipelines, null while one compiles. Compiling never blocks a frame. */
|
|
1291
|
-
effects = /* @__PURE__ */ new Map();
|
|
1292
|
-
failed = /* @__PURE__ */ new Set();
|
|
1293
|
-
pipelineDescriptor(label, code) {
|
|
1294
|
-
const module = this.device.createShaderModule({ label, code });
|
|
1295
|
-
return {
|
|
1296
|
-
label,
|
|
1297
|
-
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bindGroupLayout] }),
|
|
1298
|
-
vertex: {
|
|
1299
|
-
module,
|
|
1300
|
-
entryPoint: "vs",
|
|
1301
|
-
buffers: [
|
|
1302
|
-
{
|
|
1303
|
-
arrayStride: FLOATS_PER_INSTANCE * 4,
|
|
1304
|
-
stepMode: "instance",
|
|
1305
|
-
attributes: [
|
|
1306
|
-
{ shaderLocation: 0, offset: 0, format: "float32x4" },
|
|
1307
|
-
{ shaderLocation: 1, offset: 16, format: "float32x4" },
|
|
1308
|
-
{ shaderLocation: 2, offset: 32, format: "float32x4" },
|
|
1309
|
-
{ shaderLocation: 3, offset: 48, format: "float32x4" }
|
|
1310
|
-
]
|
|
1311
|
-
}
|
|
1312
|
-
]
|
|
1313
|
-
},
|
|
1314
|
-
fragment: {
|
|
1315
|
-
module,
|
|
1316
|
-
entryPoint: "fs",
|
|
1317
|
-
targets: [
|
|
1318
|
-
{
|
|
1319
|
-
format: this.format,
|
|
1320
|
-
// Snapshots are requested premultiplied.
|
|
1321
|
-
blend: {
|
|
1322
|
-
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
|
|
1323
|
-
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
]
|
|
1327
|
-
},
|
|
1328
|
-
primitive: { topology: "triangle-strip" }
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
/** Whether `effect` can draw this frame. The first ask starts compiling it. */
|
|
1332
|
-
hasEffect(effect) {
|
|
1333
|
-
if (this.effects.has(effect)) return this.effects.get(effect) !== null;
|
|
1334
|
-
this.effects.set(effect, null);
|
|
1335
|
-
this.device.createRenderPipelineAsync(
|
|
1336
|
-
this.pipelineDescriptor(`gpucanvas effect ${effect.name}`, effectShader(effect.wgsl))
|
|
1337
|
-
).then((pipeline) => {
|
|
1338
|
-
this.effects.set(effect, pipeline);
|
|
1339
|
-
this.onEffectReady();
|
|
1340
|
-
}).catch((error) => {
|
|
1341
|
-
this.failed.add(effect);
|
|
1342
|
-
console.error(`[gpucanvas] effect "${effect.name}" failed`, error);
|
|
1343
|
-
});
|
|
1344
|
-
return false;
|
|
1345
|
-
}
|
|
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();
|
|
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();
|
|
1357
1249
|
}
|
|
1358
|
-
this.count = 0;
|
|
1359
|
-
this.runs.length = 0;
|
|
1360
1250
|
}
|
|
1361
|
-
/**
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
return this.count++ * FLOATS_PER_INSTANCE;
|
|
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();
|
|
1367
1256
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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();
|
|
1374
1264
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
this.
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
data[offset + 7] = slot.y + slot.height;
|
|
1382
|
-
data[offset + 8] = slot.layer;
|
|
1383
|
-
data[offset + 9] = KIND.snapshot;
|
|
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);
|
|
1384
1271
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
this.
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
this.
|
|
1272
|
+
destroy() {
|
|
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);
|
|
1392
1284
|
}
|
|
1393
|
-
/**
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
this.
|
|
1397
|
-
this.writeSlot(offset, slot);
|
|
1398
|
-
this.instances[offset + 10] = pad;
|
|
1399
|
-
this.instances.set(params, offset + 12);
|
|
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);
|
|
1400
1289
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
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;
|
|
1290
|
+
nodeOf(target) {
|
|
1291
|
+
return nodeOf(this.o.nodes, target);
|
|
1408
1292
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
effects.press.y,
|
|
1426
|
-
effects.sincePress
|
|
1427
|
-
]);
|
|
1428
|
-
device.queue.writeBuffer(this.uniforms, 0, this.uniformData);
|
|
1429
|
-
if (count > 0) {
|
|
1430
|
-
device.queue.writeBuffer(
|
|
1431
|
-
this.instanceBuffer,
|
|
1432
|
-
0,
|
|
1433
|
-
this.instances,
|
|
1434
|
-
0,
|
|
1435
|
-
count * FLOATS_PER_INSTANCE
|
|
1436
|
-
);
|
|
1437
|
-
}
|
|
1438
|
-
const [r, g, b] = this.background;
|
|
1439
|
-
const encoder = device.createCommandEncoder();
|
|
1440
|
-
const pass = encoder.beginRenderPass({
|
|
1441
|
-
colorAttachments: [
|
|
1442
|
-
{
|
|
1443
|
-
view: this.context.getCurrentTexture().createView(),
|
|
1444
|
-
clearValue: { r, g, b, a: 1 },
|
|
1445
|
-
loadOp: "clear",
|
|
1446
|
-
storeOp: "store"
|
|
1447
|
-
}
|
|
1448
|
-
]
|
|
1449
|
-
});
|
|
1450
|
-
if (count > 0) {
|
|
1451
|
-
pass.setBindGroup(0, this.bindGroup);
|
|
1452
|
-
pass.setVertexBuffer(0, this.instanceBuffer);
|
|
1453
|
-
let current = null;
|
|
1454
|
-
for (const run of runs) {
|
|
1455
|
-
const pipeline = run.effect && this.effects.get(run.effect) || this.pipeline;
|
|
1456
|
-
if (pipeline !== current) pass.setPipeline(current = pipeline);
|
|
1457
|
-
pass.draw(4, run.count, 0, run.start);
|
|
1458
|
-
}
|
|
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;
|
|
1459
1309
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
this.
|
|
1465
|
-
this.
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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);
|
|
1474
1388
|
};
|
|
1475
1389
|
|
|
1476
1390
|
// src/page.ts
|
|
@@ -1485,6 +1399,8 @@ var Page = class {
|
|
|
1485
1399
|
promotedFrame = -1;
|
|
1486
1400
|
/** Registered for hit testing with `updateElementGeometry`. */
|
|
1487
1401
|
registered = false;
|
|
1402
|
+
/** The atlas slot of the page's last whole snapshot, reused when it is drawn again that size. */
|
|
1403
|
+
wholeSlot = null;
|
|
1488
1404
|
shelves;
|
|
1489
1405
|
constructor(width, height) {
|
|
1490
1406
|
this.width = width;
|
|
@@ -1527,228 +1443,172 @@ var Page = class {
|
|
|
1527
1443
|
}
|
|
1528
1444
|
};
|
|
1529
1445
|
|
|
1530
|
-
// 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
|
|
1531
1553
|
var MIN_RASTER_SCALE = 1 / 64;
|
|
1532
1554
|
var MAX_RASTER_SCALE = 2;
|
|
1533
1555
|
var MAX_DETAIL_SCALE = 64;
|
|
1534
1556
|
var TILE_PIXELS = 512;
|
|
1535
1557
|
var CONTENT_SETTLE_MS = 150;
|
|
1536
|
-
var SMALL_NODE_PX = 200;
|
|
1537
|
-
var SMALL_REFRESH_MS = 250;
|
|
1538
|
-
var SETTLE_MS = 120;
|
|
1539
|
-
var USE_EVENTS = ["click", "input", "change", "keydown"];
|
|
1540
|
-
var USED_MS = 1e3;
|
|
1541
|
-
var LINGER_MS = 300;
|
|
1542
1558
|
var MAX_OVERSAMPLING = 4;
|
|
1543
|
-
var DEFAULT_PAGE_SIZE = 2048;
|
|
1544
|
-
var THUMBNAIL_SCALE = 1 / 16;
|
|
1545
|
-
var THUMBNAIL_REFRESH_MS = 1e3;
|
|
1546
|
-
var MAX_PROMOTIONS_PER_FRAME = 8;
|
|
1547
|
-
var SLOW_FRAME_MS = 22;
|
|
1548
|
-
var FAST_FRAME_MS = 12;
|
|
1549
|
-
var MAX_CAPTURE_TRIES = 20;
|
|
1550
1559
|
var MISSING = 0;
|
|
1551
1560
|
var DIRTY = 1;
|
|
1552
1561
|
var BLURRY = 2;
|
|
1553
1562
|
var OVERSAMPLED = 3;
|
|
1554
|
-
var CANVAS_TRANSFORM_HIT_TESTS = typeof HTMLCanvasElement !== "undefined" && "getElementImageDefaultSize" in HTMLCanvasElement.prototype;
|
|
1555
|
-
function supportsGpuCanvas() {
|
|
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;
|
|
1557
|
-
}
|
|
1558
1563
|
function rasterScaleFor(deviceScale) {
|
|
1559
1564
|
const scale = 2 ** Math.ceil(Math.log2(deviceScale) - 0.14);
|
|
1560
1565
|
return Math.min(MAX_DETAIL_SCALE, Math.max(MIN_RASTER_SCALE, scale));
|
|
1561
1566
|
}
|
|
1562
|
-
function cssColorToRgba(color) {
|
|
1563
|
-
const context = document.createElement("canvas").getContext("2d");
|
|
1564
|
-
context.fillStyle = color;
|
|
1565
|
-
context.fillRect(0, 0, 1, 1);
|
|
1566
|
-
const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
|
|
1567
|
-
return [r / 255, g / 255, b / 255, a / 255];
|
|
1568
|
-
}
|
|
1569
|
-
var premultiplied = ([r, g, b, a]) => [r * a, g * a, b * a, a];
|
|
1570
1567
|
var distanceToCenter = (rect, view) => {
|
|
1571
1568
|
const a = center(rect);
|
|
1572
1569
|
const b = center(view);
|
|
1573
1570
|
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
1574
1571
|
};
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
canvas.setAttribute("content", "drawable");
|
|
1581
|
-
markSurface(canvas);
|
|
1582
|
-
Object.assign(canvas.style, { display: "block", width: "100%", height: "100%" });
|
|
1583
|
-
const context = canvas.getContext("webgpu");
|
|
1584
|
-
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
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;
|
|
1572
|
+
var Planner = class {
|
|
1573
|
+
constructor(atlas, lift, schedule) {
|
|
1574
|
+
this.atlas = atlas;
|
|
1575
|
+
this.lift = lift;
|
|
1576
|
+
this.schedule = schedule;
|
|
1591
1577
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
yield node.preview;
|
|
1619
|
-
for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
);
|
|
1623
|
-
const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, atlasSize);
|
|
1624
|
-
const maxRasterCalls = options.maxRastersPerFrame ?? 12;
|
|
1625
|
-
const rasterTimeBudgetMs = options.rasterTimeBudgetMs ?? 6;
|
|
1626
|
-
const rasterPixelBudget = options.rasterPixelBudget ?? 24e6;
|
|
1627
|
-
const colorsFor = (css) => {
|
|
1628
|
-
const background = cssColorToRgba(css);
|
|
1629
|
-
const shade = background[0] + background[1] + background[2] > 1.5 ? 0.92 : 1.25;
|
|
1630
|
-
const placeholder = background.map(
|
|
1631
|
-
(channel) => Math.min(1, channel * shade + (shade > 1 ? 0.02 : 0))
|
|
1632
|
-
);
|
|
1633
|
-
placeholder[3] = 1;
|
|
1634
|
-
return [background, placeholder];
|
|
1635
|
-
};
|
|
1636
|
-
const renderer = new CompositeRenderer(
|
|
1637
|
-
device,
|
|
1638
|
-
context,
|
|
1639
|
-
format,
|
|
1640
|
-
atlas,
|
|
1641
|
-
...colorsFor(options.background ?? "#f4f4f5"),
|
|
1642
|
-
() => schedule()
|
|
1643
|
-
);
|
|
1644
|
-
const selectionColor = premultiplied(
|
|
1645
|
-
cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
|
|
1646
|
-
);
|
|
1647
|
-
const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
|
|
1648
|
-
const effects = /* @__PURE__ */ new Map();
|
|
1649
|
-
const startedAt = performance.now();
|
|
1650
|
-
const FAR_AWAY = { x: -1e7, y: -1e7 };
|
|
1651
|
-
let effectsMoving = false;
|
|
1652
|
-
let selection = /* @__PURE__ */ new Set();
|
|
1653
|
-
let hovered = null;
|
|
1654
|
-
const ringRects = /* @__PURE__ */ new Map();
|
|
1655
|
-
const ringRect = (element) => {
|
|
1656
|
-
const own = nodes.get(element);
|
|
1657
|
-
if (own) return own.rect;
|
|
1658
|
-
const at = element.isConnected ? nodeOfElement(nodes, element) : null;
|
|
1659
|
-
if (!at) return null;
|
|
1660
|
-
const node = nodes.get(at);
|
|
1661
|
-
let rect = ringRects.get(element);
|
|
1662
|
-
if (!rect) ringRects.set(element, rect = rectInNode(at, node.rect, element));
|
|
1663
|
-
return rect;
|
|
1664
|
-
};
|
|
1665
|
-
const pages = /* @__PURE__ */ new Map();
|
|
1666
|
-
const pageByElement = /* @__PURE__ */ new Map();
|
|
1667
|
-
let openPage = null;
|
|
1668
|
-
let lastCameraChange = -Infinity;
|
|
1669
|
-
let frameNumber = 0;
|
|
1670
|
-
let promotionsPerFrame = 2;
|
|
1671
|
-
let lastFrameAt = 0;
|
|
1672
|
-
let rafId = 0;
|
|
1673
|
-
let destroyed = false;
|
|
1674
|
-
let live = null;
|
|
1675
|
-
let livePositioned = false;
|
|
1676
|
-
let lingering = null;
|
|
1677
|
-
let lingerTimer = 0;
|
|
1678
|
-
let focused = null;
|
|
1679
|
-
let pointer = null;
|
|
1680
|
-
let settleTimer = 0;
|
|
1681
|
-
const stats = {
|
|
1682
|
-
visible: 0,
|
|
1683
|
-
pending: 0,
|
|
1684
|
-
rasterizedLastFrame: 0,
|
|
1685
|
-
rasterCallsLastFrame: 0,
|
|
1686
|
-
rasterMs: 0,
|
|
1687
|
-
frameMs: 0,
|
|
1688
|
-
frames: 0,
|
|
1689
|
-
lostPixels: 0,
|
|
1690
|
-
lostDetail: 0,
|
|
1691
|
-
onThumbnail: 0,
|
|
1692
|
-
/** Last plan's nodes per need class: missing, dirty, blurry, oversampled. */
|
|
1693
|
-
needs: [0, 0, 0, 0]
|
|
1694
|
-
};
|
|
1695
|
-
const visible = [];
|
|
1696
|
-
const plans = /* @__PURE__ */ new Map();
|
|
1697
|
-
const schedule = () => {
|
|
1698
|
-
if (rafId || destroyed) return;
|
|
1699
|
-
rafId = requestAnimationFrame(() => {
|
|
1700
|
-
rafId = 0;
|
|
1701
|
-
frameNumber++;
|
|
1702
|
-
frame(false);
|
|
1703
|
-
});
|
|
1704
|
-
};
|
|
1705
|
-
const canvasDpr = () => canvas.width / Math.max(1, canvas.clientWidth);
|
|
1706
|
-
const resizeObserver = new ResizeObserver(([entry]) => {
|
|
1707
|
-
const size = entry.devicePixelContentBoxSize?.[0];
|
|
1708
|
-
canvas.width = Math.max(
|
|
1709
|
-
1,
|
|
1710
|
-
size ? size.inlineSize : Math.round(entry.contentRect.width * devicePixelRatio)
|
|
1711
|
-
);
|
|
1712
|
-
canvas.height = Math.max(
|
|
1713
|
-
1,
|
|
1714
|
-
size ? size.blockSize : Math.round(entry.contentRect.height * devicePixelRatio)
|
|
1715
|
-
);
|
|
1716
|
-
schedule();
|
|
1717
|
-
});
|
|
1718
|
-
resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
|
|
1719
|
-
const transitioning = /* @__PURE__ */ new Map();
|
|
1720
|
-
const captures = [];
|
|
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);
|
|
1723
|
-
let detailScale = 1;
|
|
1724
|
-
let frameView = { x: 0, y: 0, width: 0, height: 0 };
|
|
1725
|
-
let contentTimer = 0;
|
|
1726
|
-
const scheduleAfterContentSettles = () => {
|
|
1727
|
-
clearTimeout(contentTimer);
|
|
1728
|
-
contentTimer = window.setTimeout(schedule, CONTENT_SETTLE_MS + 10);
|
|
1729
|
-
};
|
|
1730
|
-
let refreshTimer = 0;
|
|
1731
|
-
let refreshAt = Infinity;
|
|
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;
|
|
1734
|
-
const refreshLater = (wait) => {
|
|
1735
|
-
const at = performance.now() + wait;
|
|
1736
|
-
if (at >= refreshAt) return;
|
|
1737
|
-
clearTimeout(refreshTimer);
|
|
1738
|
-
refreshAt = at;
|
|
1739
|
-
refreshTimer = window.setTimeout(() => {
|
|
1740
|
-
refreshAt = Infinity;
|
|
1741
|
-
schedule();
|
|
1742
|
-
}, wait);
|
|
1743
|
-
};
|
|
1744
|
-
const planFrame = (view, desiredScale, deviceScale, moving) => {
|
|
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);
|
|
1745
1604
|
const now = performance.now();
|
|
1746
|
-
|
|
1747
|
-
|
|
1605
|
+
this.zoom = zoom;
|
|
1606
|
+
this.detailScale = Math.min(deviceScale, MAX_DETAIL_SCALE);
|
|
1607
|
+
this.view = view;
|
|
1748
1608
|
visible.length = 0;
|
|
1749
1609
|
plans.clear();
|
|
1750
|
-
|
|
1751
|
-
for (const node of nodes
|
|
1610
|
+
this.needs = [0, 0, 0, 0];
|
|
1611
|
+
for (const node of nodes) {
|
|
1752
1612
|
if (!intersects(node.rect, view)) continue;
|
|
1753
1613
|
visible.push(node);
|
|
1754
1614
|
const scale = Math.min(
|
|
@@ -1759,14 +1619,14 @@ async function createGpuCanvas(host, options) {
|
|
|
1759
1619
|
);
|
|
1760
1620
|
if (node.dirty) node.changedAt = now;
|
|
1761
1621
|
const thumbnailEnough = scale <= THUMBNAIL_SCALE;
|
|
1762
|
-
if (thumbnailEnough &&
|
|
1622
|
+
if (thumbnailEnough && hasFreshThumb(atlas, node) && !node.dirty && !node.wholeStale) {
|
|
1763
1623
|
node.slot = node.thumb;
|
|
1764
1624
|
node.slotScale = THUMBNAIL_SCALE;
|
|
1765
1625
|
}
|
|
1766
1626
|
const animating = now - node.changedAt < CONTENT_SETTLE_MS;
|
|
1767
1627
|
const deep = desiredScale > scale;
|
|
1768
|
-
if (deep && !node.solo) lift(node);
|
|
1769
|
-
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) : [];
|
|
1770
1630
|
if (!deep) {
|
|
1771
1631
|
node.preview = null;
|
|
1772
1632
|
node.tiles.length = 0;
|
|
@@ -1776,12 +1636,12 @@ async function createGpuCanvas(host, options) {
|
|
|
1776
1636
|
if (!valid) need = atlas.isValid(node.thumb) ? DIRTY : MISSING;
|
|
1777
1637
|
else if (deep) need = node.slotScale < scale / (moving ? 4 : 2) ? BLURRY : -1;
|
|
1778
1638
|
else if (node.dirty || node.wholeStale) {
|
|
1779
|
-
const wait = refreshWait(node, now);
|
|
1780
|
-
if (wait > 0) refreshLater(wait);
|
|
1639
|
+
const wait = this.refreshWait(node, now);
|
|
1640
|
+
if (wait > 0) this.refreshLater(wait);
|
|
1781
1641
|
else need = DIRTY;
|
|
1782
1642
|
} else if (node.slotScale < (moving ? scale / 2 : scale)) need = BLURRY;
|
|
1783
1643
|
else if (!moving && node.slotScale > scale * MAX_OVERSAMPLING) need = OVERSAMPLED;
|
|
1784
|
-
if (need >= 0)
|
|
1644
|
+
if (need >= 0) this.needs[need]++;
|
|
1785
1645
|
if (need < 0 && !node.previewTarget && jobs.length === 0) continue;
|
|
1786
1646
|
node.targetScale = thumbnailEnough ? THUMBNAIL_SCALE : scale;
|
|
1787
1647
|
node.priority = (need < 0 ? BLURRY : need) * 1e12 + distanceToCenter(node.rect, view);
|
|
@@ -1796,8 +1656,33 @@ async function createGpuCanvas(host, options) {
|
|
|
1796
1656
|
plan.tiles.push(...jobs);
|
|
1797
1657
|
}
|
|
1798
1658
|
for (const plan of plans.values()) plan.tiles.sort((a, b) => a.priority - b.priority);
|
|
1799
|
-
|
|
1800
|
-
|
|
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;
|
|
1801
1686
|
const shown = intersection(node.rect, view);
|
|
1802
1687
|
node.previewTarget = null;
|
|
1803
1688
|
node.tiles = node.tiles.filter((tile) => atlas.isValid(tile.slot));
|
|
@@ -1807,13 +1692,13 @@ async function createGpuCanvas(host, options) {
|
|
|
1807
1692
|
const scale = detailScale / 2;
|
|
1808
1693
|
const stale = !node.preview || node.dirty || node.previewScale !== scale || !contains(node.previewRect, shown);
|
|
1809
1694
|
if (stale) {
|
|
1810
|
-
node.previewTarget = fitPreview(shown, scale);
|
|
1695
|
+
node.previewTarget = this.fitPreview(shown, scale);
|
|
1811
1696
|
node.previewTargetScale = scale;
|
|
1812
1697
|
}
|
|
1813
|
-
scheduleAfterContentSettles();
|
|
1698
|
+
this.scheduleAfterContentSettles();
|
|
1814
1699
|
return [];
|
|
1815
1700
|
}
|
|
1816
|
-
const current = node.tiles.filter((tile) => tile
|
|
1701
|
+
const current = node.tiles.filter((tile) => isCurrentTile(tile, detailScale));
|
|
1817
1702
|
if (moving && current.length === 0) return [];
|
|
1818
1703
|
const size = Math.max(1, Math.floor(TILE_PIXELS / detailScale));
|
|
1819
1704
|
const jobs = [];
|
|
@@ -1842,70 +1727,107 @@ async function createGpuCanvas(host, options) {
|
|
|
1842
1727
|
node.preview = null;
|
|
1843
1728
|
}
|
|
1844
1729
|
return jobs;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
|
|
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;
|
|
1848
1734
|
const fit = (start, size) => size <= limit ? [start, size] : [start + (size - limit) / 2, limit];
|
|
1849
1735
|
const [x, width] = fit(shown.x, shown.width);
|
|
1850
1736
|
const [y, height] = fit(shown.y, shown.height);
|
|
1851
1737
|
return { x, y, width, height };
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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);
|
|
1897
1798
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
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;
|
|
1903
1826
|
let calls = 0;
|
|
1904
1827
|
let pixels = 0;
|
|
1905
1828
|
let rasterized = 0;
|
|
1906
1829
|
let pending = 0;
|
|
1907
|
-
const overBudget = () => calls > 0 && (calls >=
|
|
1908
|
-
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);
|
|
1909
1831
|
for (const plan of ordered) {
|
|
1910
1832
|
const { page } = plan;
|
|
1911
1833
|
if (!page.drawable || page.promotedFrame >= frameNumber || overBudget()) {
|
|
@@ -1913,18 +1835,18 @@ async function createGpuCanvas(host, options) {
|
|
|
1913
1835
|
continue;
|
|
1914
1836
|
}
|
|
1915
1837
|
const scale = plan.nodes.reduce((max, node) => Math.max(max, node.targetScale), 0);
|
|
1916
|
-
const members = pages.
|
|
1838
|
+
const members = pages.nodesOf(page);
|
|
1917
1839
|
const neededArea = plan.nodes.reduce((sum, n) => sum + n.rect.width * n.rect.height, 0);
|
|
1918
1840
|
const wholeFits = page.width * scale <= atlasSize / 2 && page.height * scale <= atlasSize / 2;
|
|
1919
1841
|
const whole = wholeFits && (plan.nodes.length >= 6 || neededArea >= 0.25 * page.width * page.height);
|
|
1920
|
-
const wholeSlot = whole ? slotFor(
|
|
1921
|
-
|
|
1842
|
+
const wholeSlot = whole ? this.slotFor(
|
|
1843
|
+
page.wholeSlot,
|
|
1922
1844
|
Math.ceil(page.width * scale),
|
|
1923
1845
|
Math.ceil(page.height * scale),
|
|
1924
1846
|
scale === THUMBNAIL_SCALE
|
|
1925
1847
|
) : null;
|
|
1926
|
-
if (wholeSlot)
|
|
1927
|
-
if (wholeSlot && !
|
|
1848
|
+
if (wholeSlot) page.wholeSlot = wholeSlot;
|
|
1849
|
+
if (wholeSlot && !this.draw(page, null, wholeSlot)) {
|
|
1928
1850
|
pending += plan.nodes.length + plan.previews.length + plan.tiles.length;
|
|
1929
1851
|
continue;
|
|
1930
1852
|
}
|
|
@@ -1941,7 +1863,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1941
1863
|
width: node.rect.width * scale,
|
|
1942
1864
|
height: node.rect.height * scale
|
|
1943
1865
|
};
|
|
1944
|
-
|
|
1866
|
+
recordWhole(node, part, scale, start);
|
|
1945
1867
|
}
|
|
1946
1868
|
rasterized += members.size;
|
|
1947
1869
|
} else {
|
|
@@ -1951,42 +1873,37 @@ async function createGpuCanvas(host, options) {
|
|
|
1951
1873
|
continue;
|
|
1952
1874
|
}
|
|
1953
1875
|
const thumbnail = node.targetScale === THUMBNAIL_SCALE;
|
|
1954
|
-
const slot = slotFor(
|
|
1876
|
+
const slot = this.slotFor(
|
|
1955
1877
|
thumbnail ? node.thumb : node.slot,
|
|
1956
1878
|
texels(node.rect.width, node.targetScale),
|
|
1957
1879
|
texels(node.rect.height, node.targetScale),
|
|
1958
1880
|
thumbnail
|
|
1959
1881
|
);
|
|
1960
|
-
if (!slot || !
|
|
1882
|
+
if (!slot || !this.draw(page, nodeRegion(node), slot)) {
|
|
1961
1883
|
pending++;
|
|
1962
1884
|
continue;
|
|
1963
1885
|
}
|
|
1964
1886
|
calls++;
|
|
1965
1887
|
pixels += slot.width * slot.height;
|
|
1966
|
-
|
|
1888
|
+
recordWhole(node, slot, node.targetScale, start);
|
|
1967
1889
|
rasterized++;
|
|
1968
1890
|
}
|
|
1969
1891
|
}
|
|
1970
1892
|
const warm = /* @__PURE__ */ new Set();
|
|
1971
1893
|
const drawRegion = (node, rect, scale2, previous) => {
|
|
1972
1894
|
if (!warm.has(scale2) && overBudget()) return null;
|
|
1973
|
-
const slot = slotFor(previous, texels(rect.width, scale2), texels(rect.height, scale2));
|
|
1895
|
+
const slot = this.slotFor(previous, texels(rect.width, scale2), texels(rect.height, scale2));
|
|
1974
1896
|
const region = {
|
|
1975
1897
|
x: node.pageX + rect.x - node.rect.x,
|
|
1976
1898
|
y: node.pageY + rect.y - node.rect.y,
|
|
1977
1899
|
width: rect.width,
|
|
1978
1900
|
height: rect.height
|
|
1979
1901
|
};
|
|
1980
|
-
if (!slot || !
|
|
1902
|
+
if (!slot || !this.draw(page, region, slot)) return null;
|
|
1981
1903
|
calls++;
|
|
1982
1904
|
warm.add(scale2);
|
|
1983
1905
|
pixels += slot.width * slot.height;
|
|
1984
|
-
|
|
1985
|
-
node.dirty = false;
|
|
1986
|
-
node.wholeStale = true;
|
|
1987
|
-
node.thumbStale = true;
|
|
1988
|
-
node.refreshedAt = start;
|
|
1989
|
-
}
|
|
1906
|
+
recordPartial(node, start);
|
|
1990
1907
|
return slot;
|
|
1991
1908
|
};
|
|
1992
1909
|
for (const node of plan.previews) {
|
|
@@ -2017,34 +1934,709 @@ async function createGpuCanvas(host, options) {
|
|
|
2017
1934
|
}
|
|
2018
1935
|
pending += tilesLeft;
|
|
2019
1936
|
for (const node of members) {
|
|
2020
|
-
if (
|
|
1937
|
+
if (hasFreshThumb(atlas, node) || node.dirty) continue;
|
|
2021
1938
|
if (atlas.isValid(node.thumb) && start - node.thumbAt < THUMBNAIL_REFRESH_MS) continue;
|
|
2022
|
-
const slot = slotFor(
|
|
1939
|
+
const slot = this.slotFor(
|
|
2023
1940
|
node.thumb,
|
|
2024
1941
|
texels(node.rect.width, THUMBNAIL_SCALE),
|
|
2025
1942
|
texels(node.rect.height, THUMBNAIL_SCALE),
|
|
2026
1943
|
true
|
|
2027
1944
|
);
|
|
2028
|
-
if (!slot || !
|
|
1945
|
+
if (!slot || !this.draw(page, nodeRegion(node), slot)) continue;
|
|
2029
1946
|
calls++;
|
|
2030
1947
|
setThumb(node, slot, start);
|
|
2031
1948
|
}
|
|
2032
1949
|
const done = plan.nodes.every((node) => !node.dirty && atlas.isValid(node.slot)) && plan.previews.every((node) => node.previewTarget === null) && tilesLeft === 0;
|
|
2033
1950
|
if (done && !isPinned(page)) page.setDrawable(false, frameNumber);
|
|
2034
1951
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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();
|
|
2038
2630
|
};
|
|
2631
|
+
const lift = (node) => relocate(node, () => pages.lift(node));
|
|
2632
|
+
const planner = new Planner(atlas, lift, schedule);
|
|
2039
2633
|
const composite = () => {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
instances2 += selection.size + 1;
|
|
2043
|
-
renderer.begin(instances2);
|
|
2634
|
+
const { visible, detailScale, view } = planner;
|
|
2635
|
+
renderer.begin();
|
|
2044
2636
|
effectsMoving = false;
|
|
2045
2637
|
const pushRing = (element, color, width) => {
|
|
2046
2638
|
const rect = ringRect(element);
|
|
2047
|
-
if (rect && intersects(rect,
|
|
2639
|
+
if (rect && intersects(rect, view)) renderer.ring(rect, color, width);
|
|
2048
2640
|
};
|
|
2049
2641
|
let onThumbnail = 0;
|
|
2050
2642
|
for (const node of visible) {
|
|
@@ -2068,7 +2660,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2068
2660
|
effectsMoving = true;
|
|
2069
2661
|
}
|
|
2070
2662
|
renderer.quad(node.rect, image);
|
|
2071
|
-
const current = (tile) => tile
|
|
2663
|
+
const current = (tile) => isCurrentTile(tile, detailScale);
|
|
2072
2664
|
for (const tile of node.tiles)
|
|
2073
2665
|
if (!current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
|
|
2074
2666
|
if (node.preview && atlas.isValid(node.preview))
|
|
@@ -2085,9 +2677,9 @@ async function createGpuCanvas(host, options) {
|
|
|
2085
2677
|
const camera = base.camera();
|
|
2086
2678
|
renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, {
|
|
2087
2679
|
time: (now - startedAt) / 1e3,
|
|
2088
|
-
pointer: pointer ? screenToWorld(camera, pointer) : FAR_AWAY,
|
|
2089
|
-
press,
|
|
2090
|
-
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)
|
|
2091
2683
|
});
|
|
2092
2684
|
};
|
|
2093
2685
|
const frame = (inPaint) => {
|
|
@@ -2098,7 +2690,8 @@ async function createGpuCanvas(host, options) {
|
|
|
2098
2690
|
const camera = base.camera();
|
|
2099
2691
|
const view = base.view();
|
|
2100
2692
|
const moving = start - lastCameraChange < SETTLE_MS;
|
|
2101
|
-
|
|
2693
|
+
planner.plan(nodes.values(), view, camera.zoom, dpr, moving);
|
|
2694
|
+
const { plans, ordered } = planner;
|
|
2102
2695
|
if (!inPaint && plans.size > 0) {
|
|
2103
2696
|
const interval = start - lastFrameAt;
|
|
2104
2697
|
if (interval < 100) {
|
|
@@ -2108,67 +2701,44 @@ async function createGpuCanvas(host, options) {
|
|
|
2108
2701
|
}
|
|
2109
2702
|
lastFrameAt = start;
|
|
2110
2703
|
let promoted = 0;
|
|
2111
|
-
for (const plan of
|
|
2704
|
+
for (const plan of ordered) {
|
|
2112
2705
|
if (plan.page.drawable) continue;
|
|
2113
2706
|
if (promoted++ >= promotionsPerFrame) break;
|
|
2114
2707
|
plan.page.setDrawable(true, frameNumber);
|
|
2115
2708
|
}
|
|
2116
|
-
|
|
2117
|
-
canvas.requestPaint();
|
|
2709
|
+
requestPaint();
|
|
2118
2710
|
return;
|
|
2119
2711
|
}
|
|
2120
2712
|
let pending = 0;
|
|
2121
2713
|
stats.rasterizedLastFrame = 0;
|
|
2122
2714
|
stats.rasterCallsLastFrame = 0;
|
|
2123
|
-
if (inPaint && plans.size > 0)
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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;
|
|
2127
2720
|
}
|
|
2721
|
+
pages.parkIdle(plans, frameNumber, isPinned);
|
|
2128
2722
|
stats.rasterMs = performance.now() - start;
|
|
2129
2723
|
composite();
|
|
2130
2724
|
stats.frames++;
|
|
2131
|
-
|
|
2132
|
-
stats.visible = visible.length;
|
|
2725
|
+
live.position();
|
|
2726
|
+
stats.visible = planner.visible.length;
|
|
2133
2727
|
stats.pending = pending;
|
|
2134
2728
|
stats.frameMs = performance.now() - start;
|
|
2135
2729
|
if (pending > 0 || effectsMoving) schedule();
|
|
2136
2730
|
};
|
|
2137
|
-
let awaitingPaint = false;
|
|
2138
|
-
const drawCaptures = () => {
|
|
2139
|
-
for (const capture of [...captures]) {
|
|
2140
|
-
const { node, scale } = capture;
|
|
2141
|
-
let image;
|
|
2142
|
-
try {
|
|
2143
|
-
image = captureRegion(
|
|
2144
|
-
device,
|
|
2145
|
-
node.page.element,
|
|
2146
|
-
nodeRegion(node),
|
|
2147
|
-
texels(node.rect.width, scale),
|
|
2148
|
-
texels(node.rect.height, scale)
|
|
2149
|
-
);
|
|
2150
|
-
} catch (error) {
|
|
2151
|
-
if (++capture.tries > MAX_CAPTURE_TRIES) {
|
|
2152
|
-
captures.splice(captures.indexOf(capture), 1);
|
|
2153
|
-
capture.reject(error instanceof Error ? error : new Error(String(error)));
|
|
2154
|
-
}
|
|
2155
|
-
continue;
|
|
2156
|
-
}
|
|
2157
|
-
captures.splice(captures.indexOf(capture), 1);
|
|
2158
|
-
image.then(capture.resolve, capture.reject);
|
|
2159
|
-
}
|
|
2160
|
-
if (captures.length) {
|
|
2161
|
-
awaitingPaint = true;
|
|
2162
|
-
canvas.requestPaint();
|
|
2163
|
-
} else schedule();
|
|
2164
|
-
};
|
|
2165
2731
|
canvas.onpaint = (event) => {
|
|
2166
|
-
if (captures.length)
|
|
2732
|
+
if (captures.length) {
|
|
2733
|
+
rasterizer.capture(captures);
|
|
2734
|
+
if (captures.length) requestPaint();
|
|
2735
|
+
else schedule();
|
|
2736
|
+
}
|
|
2167
2737
|
let changed = false;
|
|
2168
2738
|
for (const element of event.changedElements) {
|
|
2169
|
-
const page =
|
|
2739
|
+
const page = pages.pageOf(element);
|
|
2170
2740
|
if (!page) continue;
|
|
2171
|
-
for (const node of
|
|
2741
|
+
for (const node of live.held()) {
|
|
2172
2742
|
if (node?.page === page) {
|
|
2173
2743
|
node.dirty = true;
|
|
2174
2744
|
changed = true;
|
|
@@ -2180,20 +2750,15 @@ async function createGpuCanvas(host, options) {
|
|
|
2180
2750
|
awaitingPaint = false;
|
|
2181
2751
|
frame(true);
|
|
2182
2752
|
};
|
|
2183
|
-
const nodeOf = (target) => {
|
|
2184
|
-
const element = target instanceof Element ? target : target.parentElement;
|
|
2185
|
-
const node = element && nodeOfElement(nodes, element);
|
|
2186
|
-
return node ? nodes.get(node) : void 0;
|
|
2187
|
-
};
|
|
2188
2753
|
const mutations = new MutationObserver((records) => {
|
|
2189
2754
|
ringRects.clear();
|
|
2190
2755
|
let changed = false;
|
|
2191
2756
|
for (const record of records) {
|
|
2192
2757
|
const target = record.target;
|
|
2193
|
-
if (record.type === "attributes" && (nodes.has(target) ||
|
|
2758
|
+
if (record.type === "attributes" && (nodes.has(target) || pages.pageOf(target) || target === canvas)) {
|
|
2194
2759
|
continue;
|
|
2195
2760
|
}
|
|
2196
|
-
const node = nodeOf(record.target);
|
|
2761
|
+
const node = nodeOf(nodes, record.target);
|
|
2197
2762
|
if (node && !node.dirty) {
|
|
2198
2763
|
node.dirty = true;
|
|
2199
2764
|
if (!changed) changed = intersects(node.rect, base.view());
|
|
@@ -2207,135 +2772,8 @@ async function createGpuCanvas(host, options) {
|
|
|
2207
2772
|
characterData: true,
|
|
2208
2773
|
attributes: true
|
|
2209
2774
|
});
|
|
2210
|
-
const unpin = (page) => {
|
|
2211
|
-
if (isPinned(page)) return;
|
|
2212
|
-
if (page.registered) canvas.clearElementGeometry(page.element);
|
|
2213
|
-
page.registered = false;
|
|
2214
|
-
page.element.style.transform = "";
|
|
2215
|
-
schedule();
|
|
2216
|
-
};
|
|
2217
|
-
const letGo = (node) => {
|
|
2218
|
-
node.dirty = true;
|
|
2219
|
-
unpin(node.page);
|
|
2220
|
-
};
|
|
2221
|
-
const positionLivePage = () => {
|
|
2222
|
-
if (!live || livePositioned) return;
|
|
2223
|
-
const camera = base.camera();
|
|
2224
|
-
const { zoom } = camera;
|
|
2225
|
-
const x = (live.rect.x - camera.x - live.pageX) * zoom;
|
|
2226
|
-
const y = (live.rect.y - camera.y - live.pageY) * zoom;
|
|
2227
|
-
try {
|
|
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
|
-
}
|
|
2237
|
-
live.element.style.pointerEvents = "auto";
|
|
2238
|
-
livePositioned = true;
|
|
2239
|
-
} catch {
|
|
2240
|
-
schedule();
|
|
2241
|
-
}
|
|
2242
|
-
};
|
|
2243
|
-
const setLive = (node) => {
|
|
2244
|
-
if (node === live) return;
|
|
2245
|
-
const previous = live;
|
|
2246
|
-
live = node;
|
|
2247
|
-
if (previous) {
|
|
2248
|
-
previous.element.style.pointerEvents = "none";
|
|
2249
|
-
clearTimeout(lingerTimer);
|
|
2250
|
-
const earlier = lingering;
|
|
2251
|
-
lingering = previous;
|
|
2252
|
-
if (earlier && earlier !== previous) letGo(earlier);
|
|
2253
|
-
lingerTimer = window.setTimeout(() => {
|
|
2254
|
-
const last = lingering;
|
|
2255
|
-
if (!last) return;
|
|
2256
|
-
lingering = null;
|
|
2257
|
-
letGo(last);
|
|
2258
|
-
}, LINGER_MS);
|
|
2259
|
-
unpin(previous.page);
|
|
2260
|
-
}
|
|
2261
|
-
if (!node) return;
|
|
2262
|
-
const { page } = node;
|
|
2263
|
-
page.setDrawable(true, frameNumber);
|
|
2264
|
-
if (!page.registered) {
|
|
2265
|
-
canvas.updateElementGeometry(page.element, {});
|
|
2266
|
-
page.registered = true;
|
|
2267
|
-
}
|
|
2268
|
-
livePositioned = false;
|
|
2269
|
-
positionLivePage();
|
|
2270
|
-
schedule();
|
|
2271
|
-
};
|
|
2272
|
-
const pickLive = () => {
|
|
2273
|
-
if (!pointer) return setLive(null);
|
|
2274
|
-
setLive(base.nodeAt(screenToWorld(base.camera(), pointer)));
|
|
2275
|
-
};
|
|
2276
|
-
const onPointerMove = (event) => {
|
|
2277
|
-
pointer = pointIn(canvas, event);
|
|
2278
|
-
if (event.buttons === 0) pickLive();
|
|
2279
|
-
};
|
|
2280
|
-
const onPointerLeave = () => {
|
|
2281
|
-
pointer = null;
|
|
2282
|
-
setLive(null);
|
|
2283
|
-
};
|
|
2284
|
-
let press = FAR_AWAY;
|
|
2285
|
-
let pressedAt = -Infinity;
|
|
2286
|
-
let tapped = false;
|
|
2287
|
-
const onPointerDown = (event) => {
|
|
2288
|
-
const at = pointIn(canvas, event);
|
|
2289
|
-
press = screenToWorld(base.camera(), at);
|
|
2290
|
-
pressedAt = performance.now();
|
|
2291
|
-
if (event.pointerType !== "mouse") {
|
|
2292
|
-
pointer = at;
|
|
2293
|
-
pickLive();
|
|
2294
|
-
tapped = true;
|
|
2295
|
-
}
|
|
2296
|
-
if (effects.size) schedule();
|
|
2297
|
-
};
|
|
2298
|
-
const onClick = (event) => {
|
|
2299
|
-
if (!tapped) return;
|
|
2300
|
-
tapped = false;
|
|
2301
|
-
const point = screenToWorld(base.camera(), pointIn(canvas, event));
|
|
2302
|
-
const top = base.nodeAt(point);
|
|
2303
|
-
if (!top || top.element.contains(event.target)) return;
|
|
2304
|
-
let target = elementInNode(top.element, top.rect, point);
|
|
2305
|
-
while (target && !(target instanceof HTMLElement)) target = target.parentElement;
|
|
2306
|
-
if (!(target instanceof HTMLElement)) return;
|
|
2307
|
-
target.focus({ preventScroll: true });
|
|
2308
|
-
target.dispatchEvent(
|
|
2309
|
-
new MouseEvent("click", {
|
|
2310
|
-
bubbles: true,
|
|
2311
|
-
cancelable: true,
|
|
2312
|
-
composed: true,
|
|
2313
|
-
view: window,
|
|
2314
|
-
clientX: event.clientX,
|
|
2315
|
-
clientY: event.clientY
|
|
2316
|
-
})
|
|
2317
|
-
);
|
|
2318
|
-
};
|
|
2319
|
-
const onFocusIn = (event) => {
|
|
2320
|
-
const node = nodeOf(event.target) ?? null;
|
|
2321
|
-
if (node === focused) return;
|
|
2322
|
-
const previous = focused;
|
|
2323
|
-
focused = node;
|
|
2324
|
-
if (previous) letGo(previous);
|
|
2325
|
-
if (node) {
|
|
2326
|
-
node.page.setDrawable(true, frameNumber);
|
|
2327
|
-
node.dirty = true;
|
|
2328
|
-
schedule();
|
|
2329
|
-
}
|
|
2330
|
-
};
|
|
2331
|
-
const onFocusOut = (event) => {
|
|
2332
|
-
if (event.relatedTarget && canvas.contains(event.relatedTarget)) return;
|
|
2333
|
-
const previous = focused;
|
|
2334
|
-
focused = null;
|
|
2335
|
-
if (previous) letGo(previous);
|
|
2336
|
-
};
|
|
2337
2775
|
const onResourceLoad = (event) => {
|
|
2338
|
-
const node = nodeOf(event.target);
|
|
2776
|
+
const node = nodeOf(nodes, event.target);
|
|
2339
2777
|
if (!node) return;
|
|
2340
2778
|
node.dirty = true;
|
|
2341
2779
|
schedule();
|
|
@@ -2346,177 +2784,9 @@ async function createGpuCanvas(host, options) {
|
|
|
2346
2784
|
schedule();
|
|
2347
2785
|
};
|
|
2348
2786
|
document.fonts.addEventListener("loadingdone", onFontsLoaded);
|
|
2349
|
-
surface.addEventListener("pointermove", onPointerMove);
|
|
2350
|
-
surface.addEventListener("pointerleave", onPointerLeave);
|
|
2351
|
-
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
2352
|
-
surface.addEventListener("click", onClick);
|
|
2353
|
-
canvas.addEventListener("focusin", onFocusIn);
|
|
2354
|
-
canvas.addEventListener("focusout", onFocusOut);
|
|
2355
|
-
const usedAt = /* @__PURE__ */ new WeakMap();
|
|
2356
|
-
const onUse = (event) => {
|
|
2357
|
-
const node = nodeOf(event.target);
|
|
2358
|
-
if (node) usedAt.set(node, performance.now());
|
|
2359
|
-
};
|
|
2360
|
-
const transitionMs = (element) => {
|
|
2361
|
-
const style = getComputedStyle(element);
|
|
2362
|
-
const seconds = (list) => list.split(",").map((value) => parseFloat(value) || 0);
|
|
2363
|
-
const durations = seconds(style.transitionDuration);
|
|
2364
|
-
const delays = seconds(style.transitionDelay);
|
|
2365
|
-
const longest = Math.max(...durations.map((d, i) => d + (delays[i % delays.length] ?? 0)));
|
|
2366
|
-
return Math.min(1e3, longest * 1e3);
|
|
2367
|
-
};
|
|
2368
|
-
const onTransitionRun = (event) => {
|
|
2369
|
-
const node = nodeOf(event.target);
|
|
2370
|
-
const now = performance.now();
|
|
2371
|
-
if (!node || now - (usedAt.get(node) ?? -Infinity) > USED_MS) return;
|
|
2372
|
-
if (!intersects(node.rect, base.view())) return;
|
|
2373
|
-
if (smallOnScreen(node)) return;
|
|
2374
|
-
const ms = transitionMs(event.target) + 32;
|
|
2375
|
-
const end = now + ms;
|
|
2376
|
-
if ((transitioning.get(node) ?? 0) >= end) return;
|
|
2377
|
-
transitioning.set(node, end);
|
|
2378
|
-
node.dirty = true;
|
|
2379
|
-
schedule();
|
|
2380
|
-
setTimeout(() => {
|
|
2381
|
-
if (destroyed || transitioning.get(node) !== end) return;
|
|
2382
|
-
transitioning.delete(node);
|
|
2383
|
-
letGo(node);
|
|
2384
|
-
schedule();
|
|
2385
|
-
}, ms);
|
|
2386
|
-
};
|
|
2387
|
-
for (const type of USE_EVENTS) canvas.addEventListener(type, onUse, true);
|
|
2388
|
-
canvas.addEventListener("transitionrun", onTransitionRun);
|
|
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();
|
|
2401
|
-
};
|
|
2402
|
-
const placeNode = (element, rect) => {
|
|
2403
|
-
let slot = openPage?.place(rect.width, rect.height) ?? null;
|
|
2404
|
-
if (!slot) {
|
|
2405
|
-
const page = newPage(
|
|
2406
|
-
Math.max(pageSize, Math.ceil(rect.width)),
|
|
2407
|
-
Math.max(pageSize, Math.ceil(rect.height))
|
|
2408
|
-
);
|
|
2409
|
-
if (page.width === pageSize && page.height === pageSize) openPage = page;
|
|
2410
|
-
slot = page.place(rect.width, rect.height);
|
|
2411
|
-
}
|
|
2412
|
-
Object.assign(element.style, {
|
|
2413
|
-
position: "absolute",
|
|
2414
|
-
left: `${slot.x}px`,
|
|
2415
|
-
top: `${slot.y}px`,
|
|
2416
|
-
width: `${rect.width}px`,
|
|
2417
|
-
height: `${rect.height}px`
|
|
2418
|
-
});
|
|
2419
|
-
slot.page.element.append(element);
|
|
2420
|
-
return slot;
|
|
2421
|
-
};
|
|
2422
|
-
const releaseNode = (node) => {
|
|
2423
|
-
const members = pages.get(node.page);
|
|
2424
|
-
members.delete(node);
|
|
2425
|
-
if (members.size === 0 && node.page !== openPage) dropPage(node.page);
|
|
2426
|
-
};
|
|
2427
|
-
const lift = (node) => {
|
|
2428
|
-
const previous = node.page;
|
|
2429
|
-
const wasLive = live === node;
|
|
2430
|
-
if (wasLive) setLive(null);
|
|
2431
|
-
const page = newPage(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
|
|
2432
|
-
page.place(node.rect.width, node.rect.height);
|
|
2433
|
-
const target = page.element;
|
|
2434
|
-
if (target.moveBefore) target.moveBefore(node.element, null);
|
|
2435
|
-
else target.append(node.element);
|
|
2436
|
-
node.element.style.left = "0px";
|
|
2437
|
-
node.element.style.top = "0px";
|
|
2438
|
-
releaseNode(node);
|
|
2439
|
-
node.page = page;
|
|
2440
|
-
node.pageX = 0;
|
|
2441
|
-
node.pageY = 0;
|
|
2442
|
-
node.solo = true;
|
|
2443
|
-
pages.get(page).add(node);
|
|
2444
|
-
if (pages.has(previous) && !isPinned(previous)) {
|
|
2445
|
-
unpin(previous);
|
|
2446
|
-
previous.setDrawable(false, frameNumber);
|
|
2447
|
-
}
|
|
2448
|
-
if (wasLive) pickLive();
|
|
2449
|
-
};
|
|
2450
|
-
const attach = (node) => {
|
|
2451
|
-
const slot = placeNode(node.element, node.rect);
|
|
2452
|
-
node.page = slot.page;
|
|
2453
|
-
node.pageX = slot.x;
|
|
2454
|
-
node.pageY = slot.y;
|
|
2455
|
-
node.slot = null;
|
|
2456
|
-
node.thumb = null;
|
|
2457
|
-
node.solo = false;
|
|
2458
|
-
node.dirty = true;
|
|
2459
|
-
pages.get(slot.page).add(node);
|
|
2460
|
-
};
|
|
2461
2787
|
return {
|
|
2462
2788
|
...base.api,
|
|
2463
2789
|
mode: "gpu",
|
|
2464
|
-
addNode(element, rect) {
|
|
2465
|
-
styleNode(element);
|
|
2466
|
-
element.style.pointerEvents = "none";
|
|
2467
|
-
const node = {
|
|
2468
|
-
element,
|
|
2469
|
-
rect,
|
|
2470
|
-
slotScale: 0,
|
|
2471
|
-
thumb: null,
|
|
2472
|
-
thumbStale: false,
|
|
2473
|
-
thumbAt: -Infinity,
|
|
2474
|
-
shown: 0,
|
|
2475
|
-
shownFrame: -1,
|
|
2476
|
-
targetScale: 0,
|
|
2477
|
-
tiles: [],
|
|
2478
|
-
preview: null,
|
|
2479
|
-
previewRect: null,
|
|
2480
|
-
previewScale: 0,
|
|
2481
|
-
previewTarget: null,
|
|
2482
|
-
previewTargetScale: 0,
|
|
2483
|
-
changedAt: -Infinity,
|
|
2484
|
-
refreshedAt: -Infinity,
|
|
2485
|
-
wholeStale: false,
|
|
2486
|
-
solo: false,
|
|
2487
|
-
priority: 0
|
|
2488
|
-
};
|
|
2489
|
-
attach(node);
|
|
2490
|
-
nodes.set(element, node);
|
|
2491
|
-
schedule();
|
|
2492
|
-
},
|
|
2493
|
-
updateNode(element, rect) {
|
|
2494
|
-
const node = nodes.get(element);
|
|
2495
|
-
if (!node) return;
|
|
2496
|
-
const resized = node.rect.width !== rect.width || node.rect.height !== rect.height;
|
|
2497
|
-
node.rect = rect;
|
|
2498
|
-
ringRects.clear();
|
|
2499
|
-
if (resized) {
|
|
2500
|
-
if (live === node) setLive(null);
|
|
2501
|
-
releaseNode(node);
|
|
2502
|
-
attach(node);
|
|
2503
|
-
}
|
|
2504
|
-
schedule();
|
|
2505
|
-
},
|
|
2506
|
-
removeNode(element) {
|
|
2507
|
-
const node = nodes.get(element);
|
|
2508
|
-
if (!node) return;
|
|
2509
|
-
if (live === node) setLive(null);
|
|
2510
|
-
if (lingering === node) lingering = null;
|
|
2511
|
-
if (focused === node) focused = null;
|
|
2512
|
-
transitioning.delete(node);
|
|
2513
|
-
effects.delete(element);
|
|
2514
|
-
selection.delete(element);
|
|
2515
|
-
nodes.delete(element);
|
|
2516
|
-
releaseNode(node);
|
|
2517
|
-
element.remove();
|
|
2518
|
-
schedule();
|
|
2519
|
-
},
|
|
2520
2790
|
getElementRect(element) {
|
|
2521
2791
|
const rect = ringRect(element);
|
|
2522
2792
|
return rect ? { ...rect } : null;
|
|
@@ -2550,8 +2820,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2550
2820
|
return new Promise((resolve, reject) => {
|
|
2551
2821
|
captures.push({ node, scale, resolve, reject, tries: 0 });
|
|
2552
2822
|
node.page.setDrawable(true, frameNumber);
|
|
2553
|
-
|
|
2554
|
-
canvas.requestPaint();
|
|
2823
|
+
requestPaint();
|
|
2555
2824
|
});
|
|
2556
2825
|
},
|
|
2557
2826
|
setBackground(color) {
|
|
@@ -2567,7 +2836,7 @@ async function createGpuCanvas(host, options) {
|
|
|
2567
2836
|
rasterCallsLastFrame: stats.rasterCallsLastFrame,
|
|
2568
2837
|
rasterMs: stats.rasterMs,
|
|
2569
2838
|
pages: pages.size,
|
|
2570
|
-
drawablePages: [...pages.
|
|
2839
|
+
drawablePages: [...pages.all()].filter((page) => page.drawable).length,
|
|
2571
2840
|
atlasLayersUsed: atlas.usedLayers,
|
|
2572
2841
|
frameMs: stats.frameMs,
|
|
2573
2842
|
frames: stats.frames,
|
|
@@ -2575,26 +2844,17 @@ async function createGpuCanvas(host, options) {
|
|
|
2575
2844
|
lostDetail: stats.lostDetail,
|
|
2576
2845
|
thumbnailFill: atlas.pinnedFill,
|
|
2577
2846
|
onThumbnail: stats.onThumbnail,
|
|
2578
|
-
needs:
|
|
2847
|
+
needs: planner.needs
|
|
2579
2848
|
}),
|
|
2580
2849
|
destroy() {
|
|
2581
2850
|
destroyed = true;
|
|
2582
2851
|
cancelAnimationFrame(rafId);
|
|
2583
2852
|
clearTimeout(settleTimer);
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
clearTimeout(refreshTimer);
|
|
2853
|
+
live.destroy();
|
|
2854
|
+
planner.destroy();
|
|
2587
2855
|
mutations.disconnect();
|
|
2588
2856
|
document.fonts.removeEventListener("loadingdone", onFontsLoaded);
|
|
2589
2857
|
canvas.removeEventListener("load", onResourceLoad, true);
|
|
2590
|
-
surface.removeEventListener("pointermove", onPointerMove);
|
|
2591
|
-
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
2592
|
-
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
2593
|
-
surface.removeEventListener("click", onClick);
|
|
2594
|
-
canvas.removeEventListener("focusin", onFocusIn);
|
|
2595
|
-
canvas.removeEventListener("focusout", onFocusOut);
|
|
2596
|
-
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
2597
|
-
for (const type of USE_EVENTS) canvas.removeEventListener(type, onUse, true);
|
|
2598
2858
|
resizeObserver.disconnect();
|
|
2599
2859
|
canvas.onpaint = null;
|
|
2600
2860
|
base.destroy();
|
|
@@ -2662,6 +2922,7 @@ var round = (rect) => ({
|
|
|
2662
2922
|
width: Math.round(rect.width),
|
|
2663
2923
|
height: Math.round(rect.height)
|
|
2664
2924
|
});
|
|
2925
|
+
var MAX_CAPTURE_PX = 8192;
|
|
2665
2926
|
function createAgentView(canvas) {
|
|
2666
2927
|
const ids = /* @__PURE__ */ new WeakMap();
|
|
2667
2928
|
const elements = /* @__PURE__ */ new Map();
|
|
@@ -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
|
}
|
|
@@ -2816,6 +3083,16 @@ function createLayers(canvas, pick) {
|
|
|
2816
3083
|
|
|
2817
3084
|
// src/overlay.ts
|
|
2818
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
|
+
};
|
|
2819
3096
|
var HANDLE_PX = 8;
|
|
2820
3097
|
var CURSORS = {
|
|
2821
3098
|
nw: "nwse-resize",
|
|
@@ -2844,7 +3121,7 @@ function createOverlay(surface, color) {
|
|
|
2844
3121
|
inset: "0",
|
|
2845
3122
|
overflow: "hidden",
|
|
2846
3123
|
pointerEvents: "none",
|
|
2847
|
-
zIndex:
|
|
3124
|
+
zIndex: SURFACE_Z.overlay
|
|
2848
3125
|
});
|
|
2849
3126
|
const make = (style) => {
|
|
2850
3127
|
const element = document.createElement("div");
|
|
@@ -2910,12 +3187,11 @@ function createOverlay(surface, color) {
|
|
|
2910
3187
|
}
|
|
2911
3188
|
if (state.handles) {
|
|
2912
3189
|
HANDLES.forEach((handle, i) => {
|
|
2913
|
-
const
|
|
2914
|
-
const fy = handle.includes("n") ? 0 : handle.includes("s") ? 1 : 0.5;
|
|
3190
|
+
const anchor = HANDLE_ANCHORS[handle];
|
|
2915
3191
|
place(
|
|
2916
3192
|
handles[i],
|
|
2917
|
-
rect.x + rect.width *
|
|
2918
|
-
rect.y + rect.height *
|
|
3193
|
+
rect.x + rect.width * anchor.x - HANDLE_PX / 2,
|
|
3194
|
+
rect.y + rect.height * anchor.y - HANDLE_PX / 2
|
|
2919
3195
|
);
|
|
2920
3196
|
});
|
|
2921
3197
|
}
|
|
@@ -3002,7 +3278,7 @@ function guidesFor(rect, targets, edges) {
|
|
|
3002
3278
|
}
|
|
3003
3279
|
|
|
3004
3280
|
// src/editor.ts
|
|
3005
|
-
function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR
|
|
3281
|
+
function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR) {
|
|
3006
3282
|
const box = document.createElement("div");
|
|
3007
3283
|
Object.assign(box.style, {
|
|
3008
3284
|
position: "absolute",
|
|
@@ -3010,11 +3286,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
|
|
|
3010
3286
|
pointerEvents: "none",
|
|
3011
3287
|
border: `1px solid ${color}`,
|
|
3012
3288
|
background: `color-mix(in srgb, ${color} 12%, transparent)`,
|
|
3013
|
-
zIndex:
|
|
3289
|
+
zIndex: SURFACE_Z.marquee
|
|
3014
3290
|
});
|
|
3015
3291
|
canvas.surface.append(box);
|
|
3016
3292
|
return {
|
|
3017
|
-
/** 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. */
|
|
3018
3294
|
update(a, b) {
|
|
3019
3295
|
const x = Math.min(a.x, b.x);
|
|
3020
3296
|
const y = Math.min(a.y, b.y);
|
|
@@ -3028,13 +3304,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
|
|
|
3028
3304
|
height: `${height}px`
|
|
3029
3305
|
});
|
|
3030
3306
|
const camera = canvas.getCamera();
|
|
3031
|
-
|
|
3307
|
+
return {
|
|
3032
3308
|
...screenToWorld(camera, { x, y }),
|
|
3033
3309
|
width: width / camera.zoom,
|
|
3034
3310
|
height: height / camera.zoom
|
|
3035
|
-
}
|
|
3036
|
-
canvas.setSelection(hits);
|
|
3037
|
-
return hits;
|
|
3311
|
+
};
|
|
3038
3312
|
},
|
|
3039
3313
|
hide() {
|
|
3040
3314
|
box.style.display = "none";
|
|
@@ -3067,11 +3341,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3067
3341
|
const layers = createLayers(canvas, pick);
|
|
3068
3342
|
const transforms = !!onTransform && trigger === "click";
|
|
3069
3343
|
const overlay = createOverlay(surface, color);
|
|
3070
|
-
|
|
3071
|
-
const box = marquee ? createMarquee(canvas, color, (world) => {
|
|
3072
|
-
marqueeLayers = layers.layersIn(scopes.at(-1) ?? null, world);
|
|
3073
|
-
return marqueeLayers.flat();
|
|
3074
|
-
}) : null;
|
|
3344
|
+
const box = marquee ? createMarquee(canvas, color) : null;
|
|
3075
3345
|
let selection = [];
|
|
3076
3346
|
let scopes = [];
|
|
3077
3347
|
let hovered = null;
|
|
@@ -3081,7 +3351,8 @@ function bindEditor(canvas, options = {}) {
|
|
|
3081
3351
|
let swallowClick = false;
|
|
3082
3352
|
let keepFocus = false;
|
|
3083
3353
|
const commit = (next, nextScopes = scopes) => {
|
|
3084
|
-
|
|
3354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3355
|
+
selection = next.filter((layer) => !seen.has(layer[0]) && !!seen.add(layer[0]));
|
|
3085
3356
|
scopes = selection.length ? nextScopes : [];
|
|
3086
3357
|
canvas.setSelection(selection.flat());
|
|
3087
3358
|
onChange?.(selection);
|
|
@@ -3142,14 +3413,13 @@ function bindEditor(canvas, options = {}) {
|
|
|
3142
3413
|
});
|
|
3143
3414
|
if (selection.length || hovered || gesture) draw();
|
|
3144
3415
|
};
|
|
3145
|
-
const emit = (phase) => {
|
|
3146
|
-
if (gesture?.kind !== "move" && gesture?.kind !== "resize") return;
|
|
3416
|
+
const emit = (transform, phase) => {
|
|
3147
3417
|
onTransform?.({
|
|
3148
3418
|
phase,
|
|
3149
|
-
kind:
|
|
3150
|
-
layers:
|
|
3151
|
-
from:
|
|
3152
|
-
to: phase === "cancel" ?
|
|
3419
|
+
kind: transform.kind,
|
|
3420
|
+
layers: transform.layers,
|
|
3421
|
+
from: transform.from,
|
|
3422
|
+
to: phase === "cancel" ? transform.from : transform.to
|
|
3153
3423
|
});
|
|
3154
3424
|
};
|
|
3155
3425
|
const snapTargets = (moving) => {
|
|
@@ -3165,7 +3435,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3165
3435
|
const from = selection.map((layer) => layerRect(canvas, layer));
|
|
3166
3436
|
if (from.some((rect) => !rect)) return;
|
|
3167
3437
|
const rects = from;
|
|
3168
|
-
|
|
3438
|
+
const transform = {
|
|
3169
3439
|
kind: handle ? "resize" : "move",
|
|
3170
3440
|
start,
|
|
3171
3441
|
handle,
|
|
@@ -3176,8 +3446,9 @@ function bindEditor(canvas, options = {}) {
|
|
|
3176
3446
|
targets: snapTargets(selection),
|
|
3177
3447
|
guides: []
|
|
3178
3448
|
};
|
|
3449
|
+
gesture = transform;
|
|
3179
3450
|
setHovered(null);
|
|
3180
|
-
emit("start");
|
|
3451
|
+
emit(transform, "start");
|
|
3181
3452
|
};
|
|
3182
3453
|
const updateTransform = (point, keepRatio) => {
|
|
3183
3454
|
if (gesture?.kind !== "move" && gesture?.kind !== "resize") return;
|
|
@@ -3198,27 +3469,28 @@ function bindEditor(canvas, options = {}) {
|
|
|
3198
3469
|
let top = bounds.y;
|
|
3199
3470
|
let right = bounds.x + bounds.width;
|
|
3200
3471
|
let bottom = bounds.y + bounds.height;
|
|
3201
|
-
|
|
3202
|
-
if (
|
|
3203
|
-
if (
|
|
3204
|
-
if (
|
|
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);
|
|
3205
3477
|
next = { x: left, y: top, width: right - left, height: bottom - top };
|
|
3206
|
-
const corner =
|
|
3478
|
+
const corner = anchor.x !== 0.5 && anchor.y !== 0.5;
|
|
3207
3479
|
if (keepRatio && corner) {
|
|
3208
3480
|
const scale = Math.max(next.width / bounds.width, next.height / bounds.height);
|
|
3209
3481
|
const width = bounds.width * scale;
|
|
3210
3482
|
const height = bounds.height * scale;
|
|
3211
3483
|
next = {
|
|
3212
|
-
x:
|
|
3213
|
-
y:
|
|
3484
|
+
x: anchor.x === 0 ? right - width : left,
|
|
3485
|
+
y: anchor.y === 0 ? bottom - height : top,
|
|
3214
3486
|
width,
|
|
3215
3487
|
height
|
|
3216
3488
|
};
|
|
3217
3489
|
gesture.guides = [];
|
|
3218
3490
|
} else {
|
|
3219
3491
|
const edges = {
|
|
3220
|
-
x:
|
|
3221
|
-
y:
|
|
3492
|
+
x: anchor.x === 0.5 ? [] : [anchor.x],
|
|
3493
|
+
y: anchor.y === 0.5 ? [] : [anchor.y]
|
|
3222
3494
|
};
|
|
3223
3495
|
const snapped = snapRect(next, gesture.targets, threshold, edges);
|
|
3224
3496
|
if (edges.x[0] === 0)
|
|
@@ -3238,7 +3510,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3238
3510
|
width: rect.width * sx,
|
|
3239
3511
|
height: rect.height * sy
|
|
3240
3512
|
}));
|
|
3241
|
-
emit("move");
|
|
3513
|
+
emit(gesture, "move");
|
|
3242
3514
|
draw();
|
|
3243
3515
|
};
|
|
3244
3516
|
const onPointerDown = (event) => {
|
|
@@ -3267,7 +3539,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3267
3539
|
if (!target) {
|
|
3268
3540
|
if (event.shiftKey && box) {
|
|
3269
3541
|
take();
|
|
3270
|
-
gesture = { kind: "marquee", start: point };
|
|
3542
|
+
gesture = { kind: "marquee", start: point, scopes };
|
|
3271
3543
|
return;
|
|
3272
3544
|
}
|
|
3273
3545
|
if (selection.length) commit([], []);
|
|
@@ -3291,35 +3563,27 @@ function bindEditor(canvas, options = {}) {
|
|
|
3291
3563
|
if (Math.hypot(pointer.x - gesture.start.x, pointer.y - gesture.start.y) <= CLICK_SLOP)
|
|
3292
3564
|
return;
|
|
3293
3565
|
if (gesture.inside || trigger !== "click" && box) {
|
|
3294
|
-
gesture = { kind: "marquee", start: gesture.start };
|
|
3566
|
+
gesture = { kind: "marquee", start: gesture.start, scopes };
|
|
3295
3567
|
} else if (trigger === "click" && transforms) beginTransform(gesture.start, null);
|
|
3296
3568
|
else return;
|
|
3297
3569
|
}
|
|
3298
3570
|
if (gesture?.kind === "marquee") {
|
|
3299
|
-
box.update(gesture.start, pointer);
|
|
3300
|
-
|
|
3301
|
-
onChange?.(selection);
|
|
3302
|
-
draw();
|
|
3571
|
+
const world = box.update(gesture.start, pointer);
|
|
3572
|
+
commit(layers.layersIn(gesture.scopes.at(-1) ?? null, world), gesture.scopes);
|
|
3303
3573
|
} else updateTransform(pointer, event.shiftKey);
|
|
3304
3574
|
};
|
|
3305
3575
|
const onPointerUp = () => {
|
|
3306
3576
|
const done = gesture;
|
|
3307
3577
|
gesture = null;
|
|
3308
3578
|
if (!done) return;
|
|
3309
|
-
if (done.kind === "marquee")
|
|
3310
|
-
|
|
3311
|
-
commit(marqueeLayers);
|
|
3312
|
-
} else if (done.kind === "press") {
|
|
3579
|
+
if (done.kind === "marquee") box.hide();
|
|
3580
|
+
else if (done.kind === "press") {
|
|
3313
3581
|
const { layer, scopes: inside } = done.target;
|
|
3314
3582
|
if (!done.additive) commit([layer], inside);
|
|
3315
3583
|
else if (layerIn(layer, selection))
|
|
3316
3584
|
commit(selection.filter((other) => !sameLayer(other, layer)));
|
|
3317
3585
|
else commit([...selection, layer], inside);
|
|
3318
|
-
} else
|
|
3319
|
-
gesture = done;
|
|
3320
|
-
emit("end");
|
|
3321
|
-
gesture = null;
|
|
3322
|
-
}
|
|
3586
|
+
} else emit(done, "end");
|
|
3323
3587
|
updateHover();
|
|
3324
3588
|
draw();
|
|
3325
3589
|
};
|
|
@@ -3383,7 +3647,7 @@ function bindEditor(canvas, options = {}) {
|
|
|
3383
3647
|
}
|
|
3384
3648
|
if (event.key === "Escape") {
|
|
3385
3649
|
if (gesture?.kind === "move" || gesture?.kind === "resize") {
|
|
3386
|
-
emit("cancel");
|
|
3650
|
+
emit(gesture, "cancel");
|
|
3387
3651
|
gesture = null;
|
|
3388
3652
|
} else if (scopes.length) up();
|
|
3389
3653
|
else if (selection.length) commit([], []);
|
|
@@ -3466,7 +3730,7 @@ function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
|
|
|
3466
3730
|
inset: "0",
|
|
3467
3731
|
overflow: "hidden",
|
|
3468
3732
|
pointerEvents: "none",
|
|
3469
|
-
zIndex:
|
|
3733
|
+
zIndex: SURFACE_Z.marks
|
|
3470
3734
|
});
|
|
3471
3735
|
const svg = document.createElementNS(SVG, "svg");
|
|
3472
3736
|
Object.assign(svg.style, { position: "absolute", inset: "0", width: "100%", height: "100%" });
|
|
@@ -3518,21 +3782,22 @@ function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
|
|
|
3518
3782
|
root.append(element);
|
|
3519
3783
|
return element;
|
|
3520
3784
|
};
|
|
3521
|
-
const marks = /* @__PURE__ */ new
|
|
3785
|
+
const marks = /* @__PURE__ */ new Map();
|
|
3522
3786
|
let frame = 0;
|
|
3523
3787
|
const loop = () => {
|
|
3524
3788
|
frame = marks.size ? requestAnimationFrame(loop) : 0;
|
|
3525
|
-
for (const draw of marks) draw();
|
|
3789
|
+
for (const draw of marks.keys()) draw();
|
|
3526
3790
|
};
|
|
3527
3791
|
const add = (draw, remove) => {
|
|
3528
|
-
|
|
3529
|
-
if (!frame) frame = requestAnimationFrame(loop);
|
|
3530
|
-
return {
|
|
3792
|
+
const mark = {
|
|
3531
3793
|
remove() {
|
|
3532
3794
|
marks.delete(draw);
|
|
3533
3795
|
remove();
|
|
3534
3796
|
}
|
|
3535
3797
|
};
|
|
3798
|
+
marks.set(draw, mark.remove);
|
|
3799
|
+
if (!frame) frame = requestAnimationFrame(loop);
|
|
3800
|
+
return mark;
|
|
3536
3801
|
};
|
|
3537
3802
|
return {
|
|
3538
3803
|
/** An arrow from one element (or world point) to another, with an optional label. */
|
|
@@ -3683,9 +3948,7 @@ function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
|
|
|
3683
3948
|
},
|
|
3684
3949
|
/** Removes every mark. */
|
|
3685
3950
|
clear() {
|
|
3686
|
-
for (const
|
|
3687
|
-
root.querySelectorAll(":scope > div").forEach((element) => element.remove());
|
|
3688
|
-
svg.querySelectorAll(":scope > path").forEach((element) => element.remove());
|
|
3951
|
+
for (const remove of [...marks.values()]) remove();
|
|
3689
3952
|
},
|
|
3690
3953
|
destroy() {
|
|
3691
3954
|
cancelAnimationFrame(frame);
|
|
@@ -3754,6 +4017,7 @@ function recordFilmstrip(canvas, node, {
|
|
|
3754
4017
|
const frames = [];
|
|
3755
4018
|
let stopped = false;
|
|
3756
4019
|
let timer = 0;
|
|
4020
|
+
let warned = false;
|
|
3757
4021
|
const take = () => {
|
|
3758
4022
|
canvas.snapshot(node, { scale }).then((image) => {
|
|
3759
4023
|
if (stopped) return;
|
|
@@ -3761,7 +4025,10 @@ function recordFilmstrip(canvas, node, {
|
|
|
3761
4025
|
frames.push(frame);
|
|
3762
4026
|
if (frames.length > max) frames.shift();
|
|
3763
4027
|
onFrame?.(frame, frames);
|
|
3764
|
-
}).catch(() => {
|
|
4028
|
+
}).catch((error) => {
|
|
4029
|
+
if (warned) return;
|
|
4030
|
+
warned = true;
|
|
4031
|
+
console.warn("gpucanvas: filmstrip snapshot failed", error);
|
|
3765
4032
|
});
|
|
3766
4033
|
};
|
|
3767
4034
|
const observer = new MutationObserver((records) => {
|
|
@@ -3834,6 +4101,7 @@ async function createGpuCanvas2(host, options = {}) {
|
|
|
3834
4101
|
return await createGpuCanvas(host, options);
|
|
3835
4102
|
} catch (error) {
|
|
3836
4103
|
if (mode === "gpu") throw error;
|
|
4104
|
+
console.warn("gpucanvas: the GPU backend failed to start, using DOM", error);
|
|
3837
4105
|
return createDomCanvas(host, options);
|
|
3838
4106
|
}
|
|
3839
4107
|
}
|
|
@@ -3858,4 +4126,4 @@ export {
|
|
|
3858
4126
|
swapNodes,
|
|
3859
4127
|
createGpuCanvas2 as createGpuCanvas
|
|
3860
4128
|
};
|
|
3861
|
-
//# sourceMappingURL=chunk-
|
|
4129
|
+
//# sourceMappingURL=chunk-IIRGNX76.js.map
|