dsh-browser-playwright-codex 0.7.8 → 0.8.1
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/CHANGELOG.md +92 -0
- package/NOTICE.md +26 -5
- package/README.md +33 -10
- package/assets/swarm-rebuild.js +3864 -0
- package/lib/browser-lifecycle.js +27 -1
- package/lib/challenge.d.ts +7 -4
- package/lib/challenge.js +28 -6
- package/lib/client.js +1245 -647
- package/lib/config.d.ts +26 -0
- package/lib/config.js +19 -0
- package/lib/errors.d.ts +17 -0
- package/lib/errors.js +28 -0
- package/lib/gates-patch.d.ts +41 -1
- package/lib/gates-patch.js +103 -5
- package/lib/injected.js +45 -0
- package/lib/page-actions.d.ts +24 -0
- package/lib/page-actions.js +73 -11
- package/lib/provider.js +8 -0
- package/lib/read-deadline.d.ts +5 -1
- package/lib/read-deadline.js +10 -3
- package/lib/runtime-state.d.ts +26 -0
- package/lib/runtime-state.js +100 -0
- package/lib/session.d.ts +11 -0
- package/lib/session.js +80 -11
- package/lib/settings-card.js +123 -0
- package/lib/snapshot-render.js +52 -7
- package/lib/swarm-contract.d.ts +232 -0
- package/lib/swarm-contract.js +215 -0
- package/lib/swarm-curtain.d.ts +22 -0
- package/lib/swarm-curtain.js +115 -0
- package/lib/swarm-intro.d.ts +66 -0
- package/lib/swarm-intro.js +829 -0
- package/lib/tool-types.d.ts +10 -0
- package/lib/tool.js +20 -6
- package/lib/types.d.ts +6 -0
- package/package.json +18 -13
|
@@ -0,0 +1,3864 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Swarm Rebuild
|
|
3
|
+
*
|
|
4
|
+
* Snapshots the current page into a blueprint (boxes, text blocks, media,
|
|
5
|
+
* borders, repeated components), blanks the page, then lets a swarm of
|
|
6
|
+
* multiplayer cursors rebuild it on a canvas: crosshair-drawn rects, typed
|
|
7
|
+
* text, pen-tool lines, ruler guides with snapping, alt-drag duplicates.
|
|
8
|
+
*
|
|
9
|
+
* Injected by background.js; injecting again toggles it off.
|
|
10
|
+
*/
|
|
11
|
+
(() => {
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const KEY = "__swarmRebuild";
|
|
15
|
+
if (window[KEY]) {
|
|
16
|
+
window[KEY].toggle();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------- constants
|
|
21
|
+
|
|
22
|
+
const NAMES = [
|
|
23
|
+
"Ada", "Linus", "Grace", "Alan", "Margaret", "Ken", "Barbara", "Dennis",
|
|
24
|
+
"Radia", "Guido", "Frances", "Bjarne", "Hedy", "Tim", "Katherine", "Edsger",
|
|
25
|
+
"Joan", "Donald", "Sophie", "Niklaus", "Anita", "Brian", "Mary", "Rob",
|
|
26
|
+
"Ida", "Yukihiro", "Shafi", "Leslie", "Jean", "Vint", "Lynn", "Bram",
|
|
27
|
+
];
|
|
28
|
+
const COLORS = [
|
|
29
|
+
"#F24E1E", "#A259FF", "#1ABCFE", "#0ACF83", "#FF7262", "#FFC700",
|
|
30
|
+
"#9747FF", "#E8467C", "#14AE5C", "#0D99FF", "#FF8A00", "#00B5CE",
|
|
31
|
+
];
|
|
32
|
+
const BLUE = "#0D99FF";
|
|
33
|
+
const RED = "#F24822";
|
|
34
|
+
const RULER = 20;
|
|
35
|
+
const MIN_AGENTS = 8;
|
|
36
|
+
const MAX_AGENTS = 48;
|
|
37
|
+
const FLOCK_AGENTS = 36;
|
|
38
|
+
const INTRO_AGENTS = 18;
|
|
39
|
+
const MAX_ITEMS = 4000;
|
|
40
|
+
const MAX_WORLD_H = 40000;
|
|
41
|
+
const SVGNS = "http://www.w3.org/2000/svg";
|
|
42
|
+
|
|
43
|
+
const rand = (a, b) => a + Math.random() * (b - a);
|
|
44
|
+
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
45
|
+
const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
|
|
46
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
47
|
+
const bump = (it) => {
|
|
48
|
+
it.v++;
|
|
49
|
+
};
|
|
50
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
51
|
+
const nextFrame = () => new Promise((r) => requestAnimationFrame(() => r()));
|
|
52
|
+
const f1 = (n) => Math.round(n * 10) / 10;
|
|
53
|
+
|
|
54
|
+
function alphaOf(c) {
|
|
55
|
+
if (!c || c === "transparent") return 0;
|
|
56
|
+
const slash = c.match(/\/\s*([\d.]+%?)\s*\)/);
|
|
57
|
+
if (slash) return slash[1].endsWith("%") ? parseFloat(slash[1]) / 100 : parseFloat(slash[1]);
|
|
58
|
+
const m = c.match(/^rgba\(([^)]+)\)/);
|
|
59
|
+
if (m) {
|
|
60
|
+
const p = m[1].split(",");
|
|
61
|
+
return p.length >= 4 ? parseFloat(p[3]) : 1;
|
|
62
|
+
}
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function intersect(a, b) {
|
|
67
|
+
if (!a || !b) return null;
|
|
68
|
+
const l = Math.max(a.l, b.l);
|
|
69
|
+
const t = Math.max(a.t, b.t);
|
|
70
|
+
const r = Math.min(a.r, b.r);
|
|
71
|
+
const bt = Math.min(a.b, b.b);
|
|
72
|
+
return r > l && bt > t ? { l, t, r, b: bt } : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ltrb = (r) => ({ l: r.left, t: r.top, r: r.right, b: r.bottom });
|
|
76
|
+
|
|
77
|
+
// ----------------------------------------------------------- cursor shapes
|
|
78
|
+
|
|
79
|
+
const ARROW_D =
|
|
80
|
+
"M4.6 3.9 C4.2 3.1 4.9 2.4 5.7 2.8 L20.6 9.4 C21.5 9.8 21.4 11.1 20.4 11.3 L13.9 12.9 C13.4 13 13 13.4 12.9 13.9 L11.3 20.4 C11.1 21.4 9.8 21.5 9.4 20.6 Z";
|
|
81
|
+
const HOT = {
|
|
82
|
+
arrow: [5, 3.5],
|
|
83
|
+
dup: [5, 3.5],
|
|
84
|
+
pen: [3, 3],
|
|
85
|
+
"pen-add": [3, 3],
|
|
86
|
+
"pen-close": [3, 3],
|
|
87
|
+
cross: [12, 12],
|
|
88
|
+
ibeam: [12, 12],
|
|
89
|
+
ew: [12, 12],
|
|
90
|
+
ns: [12, 12],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function cursorSVG(shape, c) {
|
|
94
|
+
const st = 'stroke="#fff" stroke-width="1.5" stroke-linejoin="round"';
|
|
95
|
+
switch (shape) {
|
|
96
|
+
case "arrow":
|
|
97
|
+
return `<path d="${ARROW_D}" fill="${c}" ${st}/>`;
|
|
98
|
+
case "dup":
|
|
99
|
+
return (
|
|
100
|
+
`<path d="${ARROW_D}" transform="translate(4 3.5)" fill="#fff" stroke="${c}" stroke-width="1.3" stroke-linejoin="round"/>` +
|
|
101
|
+
`<path d="${ARROW_D}" fill="${c}" ${st}/>`
|
|
102
|
+
);
|
|
103
|
+
case "pen":
|
|
104
|
+
case "pen-add":
|
|
105
|
+
case "pen-close": {
|
|
106
|
+
let s =
|
|
107
|
+
`<path d="M17.4 10.6 L20.6 13.8 L13.8 20.6 L10.6 17.4 Z" fill="${c}" ${st}/>` +
|
|
108
|
+
`<path d="M3 3 L13.1 6.3 L17.4 10.6 L10.6 17.4 L6.3 13.1 Z" fill="${c}" ${st}/>` +
|
|
109
|
+
`<path d="M3.6 3.6 L8.9 8.9" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/>` +
|
|
110
|
+
`<circle cx="10.2" cy="10.2" r="1.75" fill="none" stroke="#fff" stroke-width="1.2"/>`;
|
|
111
|
+
if (shape === "pen-add")
|
|
112
|
+
s +=
|
|
113
|
+
`<g stroke-linecap="round"><path d="M23 19.5 V26.5 M19.5 23 H26.5" stroke="#fff" stroke-width="3.4"/>` +
|
|
114
|
+
`<path d="M23 19.5 V26.5 M19.5 23 H26.5" stroke="#1E1E1E" stroke-width="1.3"/></g>`;
|
|
115
|
+
if (shape === "pen-close")
|
|
116
|
+
s +=
|
|
117
|
+
`<circle cx="23" cy="23" r="2.8" fill="none" stroke="#fff" stroke-width="3.2"/>` +
|
|
118
|
+
`<circle cx="23" cy="23" r="2.8" fill="none" stroke="#1E1E1E" stroke-width="1.2"/>`;
|
|
119
|
+
return s;
|
|
120
|
+
}
|
|
121
|
+
case "cross":
|
|
122
|
+
return (
|
|
123
|
+
`<g stroke-linecap="round"><path d="M12 3.5 V20.5 M3.5 12 H20.5" stroke="#fff" stroke-width="3.6"/>` +
|
|
124
|
+
`<path d="M12 3.5 V20.5 M3.5 12 H20.5" stroke="${c}" stroke-width="1.5"/></g>`
|
|
125
|
+
);
|
|
126
|
+
case "ibeam": {
|
|
127
|
+
const d =
|
|
128
|
+
"M8.5 4.5 H10.5 Q12 4.5 12 6 Q12 4.5 13.5 4.5 H15.5 M12 6 V18 M8.5 19.5 H10.5 Q12 19.5 12 18 Q12 19.5 13.5 19.5 H15.5";
|
|
129
|
+
return (
|
|
130
|
+
`<g fill="none" stroke-linecap="round"><path d="${d}" stroke="#fff" stroke-width="3.6"/>` +
|
|
131
|
+
`<path d="${d}" stroke="${c}" stroke-width="1.5"/></g>`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
case "ew":
|
|
135
|
+
case "ns":
|
|
136
|
+
return `<path d="M2.5 12 L7 7.5 V10.4 H17 V7.5 L21.5 12 L17 16.5 V13.6 H7 V16.5 Z"${
|
|
137
|
+
shape === "ns" ? ' transform="rotate(90 12 12)"' : ""
|
|
138
|
+
} fill="${c}" ${st}/>`;
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const IMG_ICON =
|
|
144
|
+
'<svg class="ph" viewBox="0 0 24 24" fill="none" stroke="#8A8A8A" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.8"/><path d="M4 18 L10 13 L14 16 L17 13.5 L21 17"/></svg>';
|
|
145
|
+
|
|
146
|
+
// ------------------------------------------------------------------ capture
|
|
147
|
+
|
|
148
|
+
const SKIP = new Set([
|
|
149
|
+
"script", "style", "noscript", "template", "head", "meta", "link", "title",
|
|
150
|
+
"base", "datalist", "area", "map", "source", "track", "param", "swarm-rebuild",
|
|
151
|
+
]);
|
|
152
|
+
const DROP_IN_CLONE = new Set(["script", "style", "noscript", "template", "iframe", "object", "embed", "link", "meta"]);
|
|
153
|
+
const REPLACED = new Set(["img", "svg", "video", "canvas", "iframe", "embed", "object", "picture"]);
|
|
154
|
+
const FORMS = new Set(["input", "textarea", "select"]);
|
|
155
|
+
const ATOMIC = new Set(["img", "svg", "video", "canvas", "input", "select", "textarea", "iframe", "picture", "button"]);
|
|
156
|
+
|
|
157
|
+
const TEXT_PROPS = [
|
|
158
|
+
"color", "font-family", "font-size", "font-weight", "font-style", "font-variant",
|
|
159
|
+
"font-stretch", "font-feature-settings", "font-variation-settings", "font-kerning",
|
|
160
|
+
"font-optical-sizing", "line-height", "letter-spacing", "word-spacing",
|
|
161
|
+
"text-transform", "text-align", "text-indent", "text-decoration-line",
|
|
162
|
+
"text-decoration-color", "text-decoration-style", "text-decoration-thickness",
|
|
163
|
+
"text-underline-offset", "text-shadow", "white-space", "white-space-collapse",
|
|
164
|
+
"text-wrap-mode", "text-wrap-style", "direction", "overflow-wrap", "word-break",
|
|
165
|
+
"hyphens", "tab-size", "-webkit-text-fill-color", "-webkit-text-stroke-width",
|
|
166
|
+
"-webkit-text-stroke-color", "text-rendering", "-webkit-font-smoothing",
|
|
167
|
+
"list-style-type", "list-style-position", "list-style-image", "writing-mode",
|
|
168
|
+
];
|
|
169
|
+
const BOX_PROPS = [
|
|
170
|
+
"display", "box-sizing", "width", "height", "min-width", "min-height", "max-width",
|
|
171
|
+
"max-height", "padding-top", "padding-right", "padding-bottom", "padding-left",
|
|
172
|
+
"margin-top", "margin-right", "margin-bottom", "margin-left", "border-top-width",
|
|
173
|
+
"border-right-width", "border-bottom-width", "border-left-width", "border-top-style",
|
|
174
|
+
"border-right-style", "border-bottom-style", "border-left-style", "border-top-color",
|
|
175
|
+
"border-right-color", "border-bottom-color", "border-left-color",
|
|
176
|
+
"border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius",
|
|
177
|
+
"border-bottom-left-radius", "background-color", "background-image",
|
|
178
|
+
"background-size", "background-position", "background-repeat", "background-clip",
|
|
179
|
+
"-webkit-background-clip", "box-shadow", "opacity", "vertical-align", "position",
|
|
180
|
+
"top", "right", "bottom", "left", "flex-direction", "flex-wrap", "align-items",
|
|
181
|
+
"justify-content", "gap", "flex-grow", "flex-shrink", "flex-basis", "align-self",
|
|
182
|
+
"object-fit", "object-position", "overflow-x", "overflow-y", "text-overflow",
|
|
183
|
+
"-webkit-line-clamp", "-webkit-box-orient", "visibility", "transform", "filter",
|
|
184
|
+
"float", "fill", "stroke", "clip-path", "mix-blend-mode",
|
|
185
|
+
"-webkit-mask-image", "-webkit-mask-size", "-webkit-mask-position", "-webkit-mask-repeat",
|
|
186
|
+
];
|
|
187
|
+
const ELEM_PROPS = [...TEXT_PROPS, ...BOX_PROPS];
|
|
188
|
+
const SVG_PROPS = [
|
|
189
|
+
"fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin",
|
|
190
|
+
"stroke-dasharray", "stroke-dashoffset", "stroke-miterlimit", "fill-opacity",
|
|
191
|
+
"stroke-opacity", "fill-rule", "clip-rule", "opacity", "color", "stop-color",
|
|
192
|
+
"stop-opacity", "display", "visibility", "font-family", "font-size", "font-weight",
|
|
193
|
+
"text-anchor", "dominant-baseline", "letter-spacing", "mix-blend-mode", "filter",
|
|
194
|
+
];
|
|
195
|
+
const SVG_OUTER_PROPS = [
|
|
196
|
+
"margin-top", "margin-right", "margin-bottom", "margin-left", "vertical-align", "display",
|
|
197
|
+
"position", "top", "right", "bottom", "left", "transform", "transform-origin",
|
|
198
|
+
"flex-shrink", "flex-grow", "align-self", "order", "overflow",
|
|
199
|
+
];
|
|
200
|
+
const RECT_PROPS = [
|
|
201
|
+
"background-color", "background-image", "background-size", "background-position",
|
|
202
|
+
"background-repeat", "background-origin", "background-clip", "border-top-width",
|
|
203
|
+
"border-right-width", "border-bottom-width", "border-left-width", "border-top-style",
|
|
204
|
+
"border-right-style", "border-bottom-style", "border-left-style", "border-top-color",
|
|
205
|
+
"border-right-color", "border-bottom-color", "border-left-color",
|
|
206
|
+
"border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius",
|
|
207
|
+
"border-bottom-left-radius", "box-shadow", "backdrop-filter", "filter", "clip-path",
|
|
208
|
+
"-webkit-mask-image", "-webkit-mask-size", "-webkit-mask-position", "-webkit-mask-repeat",
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
function styleString(cs, props) {
|
|
212
|
+
let s = "";
|
|
213
|
+
for (const p of props) {
|
|
214
|
+
const v = cs.getPropertyValue(p);
|
|
215
|
+
if (v !== "") s += `${p}:${v};`;
|
|
216
|
+
}
|
|
217
|
+
return s;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lhPx = (cs) => {
|
|
221
|
+
const v = parseFloat(cs.lineHeight);
|
|
222
|
+
return isNaN(v) ? (parseFloat(cs.fontSize) || 16) * 1.2 : v;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function kidsOf(el) {
|
|
226
|
+
if (el.shadowRoot) return [...el.shadowRoot.childNodes];
|
|
227
|
+
if (el.localName === "slot") {
|
|
228
|
+
const a = el.assignedNodes({ flatten: true });
|
|
229
|
+
return a.length ? a : [...el.childNodes];
|
|
230
|
+
}
|
|
231
|
+
return [...el.childNodes];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Copy one element's computed style onto its clone, fix up media sources.
|
|
235
|
+
// ::before / ::after become real spans so icons, chevrons and separators survive.
|
|
236
|
+
function pseudoClone(src, which) {
|
|
237
|
+
const ps = getComputedStyle(src, which);
|
|
238
|
+
const c = ps.content;
|
|
239
|
+
if (!c || c === "none" || c === "normal" || ps.display === "none") return null;
|
|
240
|
+
const sp = document.createElement("span");
|
|
241
|
+
const str = c.match(/^"((?:[^"\\]|\\.)*)"/);
|
|
242
|
+
if (str) sp.textContent = str[1].replace(/\\(.)/g, "$1");
|
|
243
|
+
else if (/^url\(/.test(c)) {
|
|
244
|
+
const u = c.match(/^url\(\s*["']?([^"')]+)["']?\s*\)/);
|
|
245
|
+
if (u) {
|
|
246
|
+
const im = document.createElement("img");
|
|
247
|
+
im.src = u[1];
|
|
248
|
+
im.setAttribute("style", "display:inline-block;vertical-align:middle;");
|
|
249
|
+
sp.appendChild(im);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
sp.setAttribute("style", styleString(ps, ELEM_PROPS));
|
|
253
|
+
return sp;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function addPseudos(src, dst) {
|
|
257
|
+
const b = pseudoClone(src, "::before");
|
|
258
|
+
if (b) dst.insertBefore(b, dst.firstChild);
|
|
259
|
+
const a = pseudoClone(src, "::after");
|
|
260
|
+
if (a) dst.appendChild(a);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function prepClone(src, dst) {
|
|
264
|
+
const tag = src.localName;
|
|
265
|
+
if (DROP_IN_CLONE.has(tag)) {
|
|
266
|
+
dst.remove();
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const cs = getComputedStyle(src);
|
|
270
|
+
if (cs.display === "none") {
|
|
271
|
+
dst.remove();
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const isSvg = src instanceof SVGElement;
|
|
275
|
+
let st = styleString(cs, isSvg ? SVG_PROPS : ELEM_PROPS);
|
|
276
|
+
if (isSvg && tag === "svg" && !src.ownerSVGElement) {
|
|
277
|
+
dst.setAttribute("width", src.clientWidth || src.getBoundingClientRect().width);
|
|
278
|
+
dst.setAttribute("height", src.clientHeight || src.getBoundingClientRect().height);
|
|
279
|
+
st += styleString(cs, SVG_OUTER_PROPS);
|
|
280
|
+
}
|
|
281
|
+
dst.setAttribute("style", st);
|
|
282
|
+
for (const a of [...dst.attributes]) {
|
|
283
|
+
const n = a.name;
|
|
284
|
+
if (n.startsWith("on") || n === "contenteditable" || n === "tabindex" || n === "autofocus" || n === "srcset" || n === "sizes" || n === "loading")
|
|
285
|
+
dst.removeAttribute(n);
|
|
286
|
+
}
|
|
287
|
+
if (tag === "img") {
|
|
288
|
+
dst.setAttribute("src", imgSrc(src));
|
|
289
|
+
dst.setAttribute("decoding", "async");
|
|
290
|
+
}
|
|
291
|
+
if (tag === "svg") fixSvgRefs(src, dst);
|
|
292
|
+
// Clones live in the page's document: custom elements would be upgraded by the
|
|
293
|
+
// site's own JS (and often wipe their content), media would play. Swap in inert stand-ins.
|
|
294
|
+
if (tag === "audio") {
|
|
295
|
+
dst.remove();
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (tag === "video" || tag === "canvas" || (!isSvg && tag.includes("-"))) {
|
|
299
|
+
let rep;
|
|
300
|
+
const vurl = tag === "video" ? videoUrl(src) : null;
|
|
301
|
+
if (vurl) {
|
|
302
|
+
rep = document.createElement("video");
|
|
303
|
+
for (const [k, v] of [["src", vurl], ["muted", ""], ["playsinline", ""], ["preload", "auto"]]) rep.setAttribute(k, v);
|
|
304
|
+
if (!src.paused || src.autoplay) rep.setAttribute("autoplay", "");
|
|
305
|
+
if (src.loop || src.autoplay || !src.paused) rep.setAttribute("loop", "");
|
|
306
|
+
if (src.poster) rep.setAttribute("poster", src.poster);
|
|
307
|
+
} else if (tag === "video" || tag === "canvas") {
|
|
308
|
+
let url = tag === "video" ? src.poster : canvasPixels(src);
|
|
309
|
+
rep = document.createElement(url ? "img" : "span");
|
|
310
|
+
if (url) rep.setAttribute("src", url);
|
|
311
|
+
else st += "background:#1b1b1f;";
|
|
312
|
+
} else {
|
|
313
|
+
rep = document.createElement("span");
|
|
314
|
+
while (dst.firstChild) rep.appendChild(dst.firstChild);
|
|
315
|
+
}
|
|
316
|
+
rep.setAttribute("style", st);
|
|
317
|
+
if (dst.parentNode) dst.replaceWith(rep);
|
|
318
|
+
dst = rep;
|
|
319
|
+
}
|
|
320
|
+
if (!isSvg && !REPLACED.has(tag) && !FORMS.has(tag) && tag !== "img") addPseudos(src, dst);
|
|
321
|
+
return dst;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Clones are made in a document without a custom-element registry so the
|
|
325
|
+
// site's component code never runs on them.
|
|
326
|
+
let inertDoc = null;
|
|
327
|
+
const inert = () => inertDoc || (inertDoc = document.implementation.createHTMLDocument(""));
|
|
328
|
+
|
|
329
|
+
function cloneStyled(src) {
|
|
330
|
+
const dst = inert().importNode(src, true);
|
|
331
|
+
const a = [...src.querySelectorAll("*")];
|
|
332
|
+
const b = [...dst.querySelectorAll("*")];
|
|
333
|
+
const root = prepClone(src, dst) || document.createElement("span");
|
|
334
|
+
for (let i = 0; i < a.length && i < b.length; i++) prepClone(a[i], b[i]);
|
|
335
|
+
return root;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function cloneDescendantsStyled(src, dst) {
|
|
339
|
+
const a = src.querySelectorAll("*");
|
|
340
|
+
const b = dst.querySelectorAll("*");
|
|
341
|
+
for (let i = 0; i < a.length && i < b.length; i++) prepClone(a[i], b[i]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function imgSrc(img) {
|
|
345
|
+
let s = img.currentSrc || img.src || "";
|
|
346
|
+
const lazy = img.getAttribute("data-src") || img.getAttribute("data-lazy-src") || img.getAttribute("data-original");
|
|
347
|
+
if (lazy && (!s || s.startsWith("data:") || !img.complete || !img.naturalWidth)) {
|
|
348
|
+
try {
|
|
349
|
+
s = new URL(lazy, location.href).href;
|
|
350
|
+
} catch (_) {}
|
|
351
|
+
}
|
|
352
|
+
return s;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Shadow trees can't see document-level <symbol>/<linearGradient> ids, so
|
|
356
|
+
// inline anything the clone references.
|
|
357
|
+
function fixSvgRefs(src, dst) {
|
|
358
|
+
for (const u of dst.querySelectorAll("use")) {
|
|
359
|
+
const href = u.getAttribute("href") || u.getAttribute("xlink:href");
|
|
360
|
+
if (!href || href[0] !== "#") continue;
|
|
361
|
+
const ref = document.getElementById(href.slice(1));
|
|
362
|
+
if (!ref) continue;
|
|
363
|
+
const isSym = ref.localName === "symbol";
|
|
364
|
+
const g = document.createElementNS(SVGNS, isSym ? "svg" : "g");
|
|
365
|
+
if (isSym) {
|
|
366
|
+
for (const at of ["viewBox", "preserveAspectRatio"]) if (ref.getAttribute(at)) g.setAttribute(at, ref.getAttribute(at));
|
|
367
|
+
g.setAttribute("width", u.getAttribute("width") || "100%");
|
|
368
|
+
g.setAttribute("height", u.getAttribute("height") || "100%");
|
|
369
|
+
g.setAttribute("x", u.getAttribute("x") || 0);
|
|
370
|
+
g.setAttribute("y", u.getAttribute("y") || 0);
|
|
371
|
+
g.setAttribute("overflow", "visible");
|
|
372
|
+
} else if (u.getAttribute("x") || u.getAttribute("y")) {
|
|
373
|
+
g.setAttribute("transform", `translate(${u.getAttribute("x") || 0} ${u.getAttribute("y") || 0})`);
|
|
374
|
+
}
|
|
375
|
+
const us = u.getAttribute("style");
|
|
376
|
+
if (us) g.setAttribute("style", us);
|
|
377
|
+
for (const ch of ref.childNodes) {
|
|
378
|
+
if (ch.nodeType !== 1) {
|
|
379
|
+
g.appendChild(ch.cloneNode(true));
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
g.appendChild(cloneStyled(ch));
|
|
383
|
+
}
|
|
384
|
+
u.replaceWith(g);
|
|
385
|
+
}
|
|
386
|
+
const html = dst.outerHTML;
|
|
387
|
+
const ids = new Set();
|
|
388
|
+
html.replace(/url\(\s*["']?#([^"')\s]+)["']?\s*\)/g, (_, id) => ids.add(id));
|
|
389
|
+
if (!ids.size) return;
|
|
390
|
+
let defs = null;
|
|
391
|
+
for (const id of ids) {
|
|
392
|
+
if (dst.querySelector(`[id="${CSS.escape(id)}"]`)) continue;
|
|
393
|
+
const ref = document.getElementById(id);
|
|
394
|
+
if (!ref) continue;
|
|
395
|
+
if (!defs) {
|
|
396
|
+
defs = document.createElementNS(SVGNS, "defs");
|
|
397
|
+
dst.insertBefore(defs, dst.firstChild);
|
|
398
|
+
}
|
|
399
|
+
defs.appendChild(ref.cloneNode(true));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Pixels for things the DOM can't give back (WebGL canvases, playing video,
|
|
404
|
+
// cross-origin iframes) come from a screenshot taken when the user clicked.
|
|
405
|
+
function cropShot(shot, r) {
|
|
406
|
+
if (!shot || !shot.valid) return null;
|
|
407
|
+
if (r.height > innerHeight * 3 || r.width > innerWidth * 2) return null;
|
|
408
|
+
const vis = intersect(ltrb(r), { l: 0, t: 0, r: innerWidth, b: innerHeight });
|
|
409
|
+
if (!vis) return null;
|
|
410
|
+
const k = shot.scale;
|
|
411
|
+
const c = document.createElement("canvas");
|
|
412
|
+
c.width = Math.max(1, Math.round(r.width * k));
|
|
413
|
+
c.height = Math.max(1, Math.round(r.height * k));
|
|
414
|
+
const w = (vis.r - vis.l) * k;
|
|
415
|
+
const h = (vis.b - vis.t) * k;
|
|
416
|
+
c.getContext("2d").drawImage(shot.img, vis.l * k, vis.t * k, w, h, (vis.l - r.left) * k, (vis.t - r.top) * k, w, h);
|
|
417
|
+
const full = vis.l <= r.left + 1 && vis.t <= r.top + 1 && vis.r >= r.right - 1 && vis.b >= r.bottom - 1;
|
|
418
|
+
try {
|
|
419
|
+
return c.toDataURL(full ? "image/jpeg" : "image/webp", 0.9);
|
|
420
|
+
} catch (_) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// A URL another <video> can play: the chosen source, a <source>, or a lazy-load data-src.
|
|
426
|
+
// (blob:/MediaSource streams are tied to the original element and can't be reused.)
|
|
427
|
+
function videoUrl(el) {
|
|
428
|
+
const cands = [el.currentSrc, el.getAttribute("src"), el.getAttribute("data-src")];
|
|
429
|
+
for (const s of el.querySelectorAll("source")) cands.push(s.getAttribute("src"), s.getAttribute("data-src"));
|
|
430
|
+
for (const c of cands) {
|
|
431
|
+
if (!c || /^(blob|mediasource):/i.test(c)) continue;
|
|
432
|
+
try {
|
|
433
|
+
return new URL(c, location.href).href;
|
|
434
|
+
} catch (_) {}
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function canvasPixels(el) {
|
|
440
|
+
try {
|
|
441
|
+
const probe = document.createElement("canvas");
|
|
442
|
+
probe.width = probe.height = 12;
|
|
443
|
+
const g = probe.getContext("2d");
|
|
444
|
+
g.drawImage(el, 0, 0, 12, 12);
|
|
445
|
+
const d = g.getImageData(0, 0, 12, 12).data;
|
|
446
|
+
let a = 0;
|
|
447
|
+
for (let i = 3; i < d.length; i += 4) a += d[i];
|
|
448
|
+
return a > 0 ? el.toDataURL() : null;
|
|
449
|
+
} catch (_) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function boxyStyle(st) {
|
|
455
|
+
if (alphaOf(st.backgroundColor) > 0.02) return true;
|
|
456
|
+
if (st.backgroundImage && st.backgroundImage !== "none") return true;
|
|
457
|
+
if (st.boxShadow && st.boxShadow !== "none") return true;
|
|
458
|
+
for (const s of ["Top", "Right", "Bottom", "Left"]) {
|
|
459
|
+
if (parseFloat(st["border" + s + "Width"]) > 0 && st["border" + s + "Style"] !== "none" && alphaOf(st["border" + s + "Color"]) > 0.02) return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function sameColor(a, b) {
|
|
465
|
+
return (a || "").replace(/\s/g, "") === (b || "").replace(/\s/g, "");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function pageBackground() {
|
|
469
|
+
const h = document.documentElement;
|
|
470
|
+
const b = document.body;
|
|
471
|
+
const hs = getComputedStyle(h);
|
|
472
|
+
const bs = b ? getComputedStyle(b) : null;
|
|
473
|
+
const pick = (cs) => ({
|
|
474
|
+
color: cs.backgroundColor,
|
|
475
|
+
image: cs.backgroundImage,
|
|
476
|
+
size: cs.backgroundSize,
|
|
477
|
+
position: cs.backgroundPosition,
|
|
478
|
+
repeat: cs.backgroundRepeat,
|
|
479
|
+
});
|
|
480
|
+
if (alphaOf(hs.backgroundColor) > 0.01 || hs.backgroundImage !== "none") return { ...pick(hs), from: h };
|
|
481
|
+
if (bs && (alphaOf(bs.backgroundColor) > 0.01 || bs.backgroundImage !== "none")) return { ...pick(bs), from: b };
|
|
482
|
+
const dark = /dark/.test(hs.colorScheme) && matchMedia("(prefers-color-scheme: dark)").matches;
|
|
483
|
+
return { color: dark ? "rgb(18, 18, 18)" : "rgb(255, 255, 255)", image: "none", from: null };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Walk the live DOM and produce a blueprint the swarm can rebuild.
|
|
488
|
+
* Items live in "world" coords (scroller content space) or, for fixed UI,
|
|
489
|
+
* in screen coords on an "over"/"under" layer.
|
|
490
|
+
*/
|
|
491
|
+
function captureModel(env, shot) {
|
|
492
|
+
const { ox, oy, sx, sy, vw, vh } = env.geom();
|
|
493
|
+
const docW = Math.max(env.scrollWidth(), vw);
|
|
494
|
+
const docH = Math.min(Math.max(env.scrollHeight(), vh), MAX_WORLD_H);
|
|
495
|
+
const items = [];
|
|
496
|
+
const roots = [];
|
|
497
|
+
let order = 0;
|
|
498
|
+
const csCache = new Map();
|
|
499
|
+
const css = (el) => {
|
|
500
|
+
let c = csCache.get(el);
|
|
501
|
+
if (!c) {
|
|
502
|
+
c = getComputedStyle(el);
|
|
503
|
+
csCache.set(el, c);
|
|
504
|
+
}
|
|
505
|
+
return c;
|
|
506
|
+
};
|
|
507
|
+
const inlineCache = new Map();
|
|
508
|
+
const fullClip = { l: -1e9, t: -1e9, r: 1e9, b: 1e9 };
|
|
509
|
+
const viewClip = { l: 0, t: 0, r: innerWidth, b: innerHeight };
|
|
510
|
+
const pageBg = env.pageBg;
|
|
511
|
+
const floats = [];
|
|
512
|
+
const bars = [];
|
|
513
|
+
let portKey = null;
|
|
514
|
+
const html = document.documentElement;
|
|
515
|
+
const body = document.body;
|
|
516
|
+
|
|
517
|
+
const conv = (r, layer) =>
|
|
518
|
+
layer === "world"
|
|
519
|
+
? { x: r.left - ox + sx, y: r.top - oy + sy, w: r.width, h: r.height }
|
|
520
|
+
: { x: r.left, y: r.top, w: r.width, h: r.height };
|
|
521
|
+
const convClip = (c, layer) =>
|
|
522
|
+
layer === "world" ? { l: c.l - ox + sx, t: c.t - oy + sy, r: c.r - ox + sx, b: c.b - oy + sy } : { ...c };
|
|
523
|
+
const clipFor = (r, clip, layer) => {
|
|
524
|
+
if (r.left >= clip.l - 0.5 && r.top >= clip.t - 0.5 && r.right <= clip.r + 0.5 && r.bottom <= clip.b + 0.5) return null;
|
|
525
|
+
return convClip(clip, layer);
|
|
526
|
+
};
|
|
527
|
+
const inDoc = (b, layer) =>
|
|
528
|
+
layer === "world"
|
|
529
|
+
? b.x + b.w > 0 && b.x < docW && b.y + b.h > 0 && b.y < docH
|
|
530
|
+
: b.x + b.w > 0 && b.x < innerWidth && b.y + b.h > 0 && b.y < innerHeight;
|
|
531
|
+
|
|
532
|
+
function add(it) {
|
|
533
|
+
it.id = items.length;
|
|
534
|
+
it.v = 1;
|
|
535
|
+
it.rv = 0;
|
|
536
|
+
it.vis = false;
|
|
537
|
+
it.dx = 0;
|
|
538
|
+
it.dy = 0;
|
|
539
|
+
it.scale = 1;
|
|
540
|
+
it.mode = "own";
|
|
541
|
+
items.push(it);
|
|
542
|
+
return it;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function allInline(el) {
|
|
546
|
+
let v = inlineCache.get(el);
|
|
547
|
+
if (v !== undefined) return v;
|
|
548
|
+
v = !el.shadowRoot;
|
|
549
|
+
if (v)
|
|
550
|
+
for (const c of el.children) {
|
|
551
|
+
const tag = c.localName;
|
|
552
|
+
if (SKIP.has(tag) || tag === "br" || tag === "wbr") continue;
|
|
553
|
+
if (c instanceof SVGElement) continue;
|
|
554
|
+
const d = css(c).display;
|
|
555
|
+
if (d === "none") continue;
|
|
556
|
+
if (d === "inline") {
|
|
557
|
+
if (!allInline(c)) {
|
|
558
|
+
v = false;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
} else if (!d.startsWith("inline")) {
|
|
562
|
+
v = false;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
inlineCache.set(el, v);
|
|
567
|
+
return v;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function rootStyle(cs, isRun) {
|
|
571
|
+
let s = styleString(cs, TEXT_PROPS);
|
|
572
|
+
if (isRun) return s + "display:block;padding:0;margin:0;box-sizing:border-box;width:100%;height:100%;position:relative;";
|
|
573
|
+
const pad = (side) =>
|
|
574
|
+
(parseFloat(cs.getPropertyValue(`padding-${side}`)) || 0) + (parseFloat(cs.getPropertyValue(`border-${side}-width`)) || 0);
|
|
575
|
+
let d = cs.display;
|
|
576
|
+
if (/flex/.test(d)) {
|
|
577
|
+
d = "flex";
|
|
578
|
+
s += styleString(cs, ["flex-direction", "flex-wrap", "align-items", "justify-content", "align-content", "row-gap", "column-gap"]);
|
|
579
|
+
} else if (/grid/.test(d)) {
|
|
580
|
+
d = "grid";
|
|
581
|
+
s += styleString(cs, [
|
|
582
|
+
"grid-template-columns", "grid-template-rows", "grid-auto-flow", "grid-auto-columns",
|
|
583
|
+
"grid-auto-rows", "align-items", "justify-items", "justify-content", "align-content",
|
|
584
|
+
"row-gap", "column-gap",
|
|
585
|
+
]);
|
|
586
|
+
} else if (d === "list-item" || d === "-webkit-box") {
|
|
587
|
+
// keep
|
|
588
|
+
} else d = "block";
|
|
589
|
+
s += `display:${d};padding:${pad("top")}px ${pad("right")}px ${pad("bottom")}px ${pad("left")}px;`;
|
|
590
|
+
if (cs.overflowX !== "visible" || cs.overflowY !== "visible" || cs.textOverflow === "ellipsis" || (cs.webkitLineClamp && cs.webkitLineClamp !== "none")) {
|
|
591
|
+
s += styleString(cs, ["overflow-x", "overflow-y", "text-overflow", "-webkit-line-clamp", "-webkit-box-orient"]);
|
|
592
|
+
}
|
|
593
|
+
return s + "margin:0;box-sizing:border-box;width:100%;height:100%;position:relative;";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function textItem(base, root, tb, cs, layer, extra) {
|
|
597
|
+
const fs = parseFloat(cs.fontSize) || 16;
|
|
598
|
+
const text = root.textContent.replace(/\s+/g, " ").trim();
|
|
599
|
+
return add({
|
|
600
|
+
kind: "text",
|
|
601
|
+
...base,
|
|
602
|
+
tb,
|
|
603
|
+
full: root,
|
|
604
|
+
fs,
|
|
605
|
+
lh: lhPx(cs),
|
|
606
|
+
align: cs.textAlign,
|
|
607
|
+
text,
|
|
608
|
+
sig: text,
|
|
609
|
+
...extra,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function emitLeaf(el, cs, r, ctx, key) {
|
|
614
|
+
const root = document.createElement("div");
|
|
615
|
+
root.setAttribute("style", rootStyle(cs, false));
|
|
616
|
+
const clone = inert().importNode(el, true);
|
|
617
|
+
cloneDescendantsStyled(el, clone);
|
|
618
|
+
while (clone.firstChild) root.appendChild(clone.firstChild);
|
|
619
|
+
addPseudos(el, root);
|
|
620
|
+
const rg = document.createRange();
|
|
621
|
+
rg.selectNodeContents(el);
|
|
622
|
+
const tr = rg.getBoundingClientRect();
|
|
623
|
+
// table cells center their content vertically; reproduce the offset
|
|
624
|
+
if (cs.display === "table-cell" && tr.height) root.style.paddingTop = Math.max(0, tr.top - r.top) + "px";
|
|
625
|
+
// floats outside the leaf (figures, infoboxes) shape its line boxes: add invisible shims
|
|
626
|
+
if (/^(block|list-item)$/.test(root.style.display)) {
|
|
627
|
+
const n = (p) => parseFloat(cs.getPropertyValue(p)) || 0;
|
|
628
|
+
const cl = r.left + n("border-left-width") + n("padding-left");
|
|
629
|
+
const ct = r.top + n("border-top-width") + n("padding-top");
|
|
630
|
+
const cr = r.right - n("border-right-width") - n("padding-right");
|
|
631
|
+
for (const f of floats) {
|
|
632
|
+
if (f.b <= r.top + 1 || f.t >= r.bottom - 1 || f.r <= r.left + 1 || f.l >= r.right - 1) continue;
|
|
633
|
+
if (f.el.contains(el)) continue;
|
|
634
|
+
const shim = document.createElement("div");
|
|
635
|
+
const top = Math.max(0, f.t - ct);
|
|
636
|
+
const w = f.side === "left" ? f.r - cl : cr - f.l;
|
|
637
|
+
if (w <= 0) continue;
|
|
638
|
+
shim.setAttribute("style", `float:${f.side};clear:${f.side};width:${w}px;height:${Math.max(0, f.b - Math.max(f.t, ct))}px;margin-top:${top}px;`);
|
|
639
|
+
root.insertBefore(shim, root.firstChild);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const box = conv(r, ctx.layer);
|
|
643
|
+
const tb = tr.width > 0 && tr.height > 0 ? conv(tr, ctx.layer) : { ...box };
|
|
644
|
+
tb.x = clamp(tb.x, box.x - 4, box.x + box.w);
|
|
645
|
+
tb.y = clamp(tb.y, box.y - 4, box.y + box.h);
|
|
646
|
+
textItem(
|
|
647
|
+
{ ...box, layer: ctx.layer, key, op: ctx.op, clip: clipFor(r, ctx.clip, ctx.layer), parentBox: ctx.boxItem, blend: cs.mixBlendMode !== "normal" ? cs.mixBlendMode : ctx.blend || "" },
|
|
648
|
+
root,
|
|
649
|
+
tb,
|
|
650
|
+
cs,
|
|
651
|
+
ctx.layer,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function emitRun(el, cs, run, ctx) {
|
|
656
|
+
const rg = document.createRange();
|
|
657
|
+
rg.setStartBefore(run[0]);
|
|
658
|
+
rg.setEndAfter(run[run.length - 1]);
|
|
659
|
+
const r = rg.getBoundingClientRect();
|
|
660
|
+
if (r.width < 1 || r.height < 1) return;
|
|
661
|
+
if (!intersect(ltrb(r), ctx.clip)) return;
|
|
662
|
+
const root = document.createElement("div");
|
|
663
|
+
root.setAttribute("style", rootStyle(cs, true));
|
|
664
|
+
const single = r.height < lhPx(cs) * 1.6;
|
|
665
|
+
if (single) root.style.whiteSpace = "nowrap";
|
|
666
|
+
for (const n of run) {
|
|
667
|
+
if (n.nodeType === 3) root.appendChild(document.createTextNode(n.data));
|
|
668
|
+
else root.appendChild(cloneStyled(n));
|
|
669
|
+
}
|
|
670
|
+
const box = conv(r, ctx.layer);
|
|
671
|
+
if (!inDoc(box, ctx.layer)) return;
|
|
672
|
+
box.w += 1;
|
|
673
|
+
textItem(
|
|
674
|
+
{ ...box, layer: ctx.layer, key: ctx.key(), op: ctx.op, clip: clipFor(r, ctx.clip, ctx.layer), parentBox: ctx.boxItem, blend: ctx.blend || "" },
|
|
675
|
+
root,
|
|
676
|
+
{ ...box },
|
|
677
|
+
cs,
|
|
678
|
+
ctx.layer,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function lineItem(pts, stroke, sw, dash, ctx, key) {
|
|
683
|
+
const xs = pts.map((p) => p.x);
|
|
684
|
+
const ys = pts.map((p) => p.y);
|
|
685
|
+
return add({
|
|
686
|
+
kind: "line",
|
|
687
|
+
x: Math.min(...xs),
|
|
688
|
+
y: Math.min(...ys),
|
|
689
|
+
w: Math.max(...xs) - Math.min(...xs),
|
|
690
|
+
h: Math.max(...ys) - Math.min(...ys),
|
|
691
|
+
pts,
|
|
692
|
+
stroke,
|
|
693
|
+
sw,
|
|
694
|
+
dash,
|
|
695
|
+
placed: 0,
|
|
696
|
+
shut: false,
|
|
697
|
+
edit: false,
|
|
698
|
+
owner: null,
|
|
699
|
+
layer: ctx.layer,
|
|
700
|
+
key,
|
|
701
|
+
op: ctx.op,
|
|
702
|
+
clip: null,
|
|
703
|
+
parentBox: ctx.boxItem,
|
|
704
|
+
sig: pts.length + stroke,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function emitBox(el, cs, r, ctx, key, forceRect) {
|
|
709
|
+
if (el === html) return null;
|
|
710
|
+
const tag = el.localName;
|
|
711
|
+
const bgc = cs.backgroundColor;
|
|
712
|
+
const bgi = cs.backgroundImage;
|
|
713
|
+
const bgVisible = (alphaOf(bgc) > 0.01 && !sameColor(bgc, pageBg.color)) || bgi !== "none";
|
|
714
|
+
if (el === body && pageBg.from === body) return null;
|
|
715
|
+
const sides = ["top", "right", "bottom", "left"].map((s) => {
|
|
716
|
+
const w = parseFloat(cs.getPropertyValue(`border-${s}-width`)) || 0;
|
|
717
|
+
const st = cs.getPropertyValue(`border-${s}-style`);
|
|
718
|
+
const c = cs.getPropertyValue(`border-${s}-color`);
|
|
719
|
+
return w > 0 && st !== "none" && st !== "hidden" && alphaOf(c) > 0.02 ? { s, w, st, c } : null;
|
|
720
|
+
});
|
|
721
|
+
const nb = sides.filter(Boolean).length;
|
|
722
|
+
const shadow = cs.boxShadow && cs.boxShadow !== "none";
|
|
723
|
+
const backdrop = cs.backdropFilter && cs.backdropFilter !== "none";
|
|
724
|
+
const b = conv(r, ctx.layer);
|
|
725
|
+
if (tag === "hr") {
|
|
726
|
+
const c = sides[0] ? sides[0].c : alphaOf(bgc) > 0.01 ? bgc : "rgb(200,200,200)";
|
|
727
|
+
const y = b.y + b.h / 2;
|
|
728
|
+
lineItem([{ x: b.x, y }, { x: b.x + b.w, y }], c, Math.max(1, Math.min(b.h, 2)), null, ctx, key);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
const sameAsPage = alphaOf(bgc) > 0.01 && sameColor(bgc, pageBg.color) && bgi === "none";
|
|
732
|
+
if (!forceRect && !bgVisible && !shadow && !backdrop) {
|
|
733
|
+
if (nb === 0) return null;
|
|
734
|
+
if (nb < 4) {
|
|
735
|
+
for (const sd of sides) {
|
|
736
|
+
if (!sd) continue;
|
|
737
|
+
const hw = sd.w / 2;
|
|
738
|
+
let pts;
|
|
739
|
+
if (sd.s === "top") pts = [{ x: b.x, y: b.y + hw }, { x: b.x + b.w, y: b.y + hw }];
|
|
740
|
+
else if (sd.s === "bottom") pts = [{ x: b.x, y: b.y + b.h - hw }, { x: b.x + b.w, y: b.y + b.h - hw }];
|
|
741
|
+
else if (sd.s === "left") pts = [{ x: b.x + hw, y: b.y }, { x: b.x + hw, y: b.y + b.h }];
|
|
742
|
+
else pts = [{ x: b.x + b.w - hw, y: b.y }, { x: b.x + b.w - hw, y: b.y + b.h }];
|
|
743
|
+
const dash = sd.st === "dashed" ? `${sd.w * 3} ${sd.w * 2}` : sd.st === "dotted" ? `${sd.w} ${sd.w * 1.5}` : null;
|
|
744
|
+
lineItem(pts, sd.c, sd.w, dash, ctx, key);
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (sameAsPage && nb === 0 && !shadow && !backdrop && !forceRect) return null;
|
|
750
|
+
const cssText = styleString(cs, RECT_PROPS);
|
|
751
|
+
return add({
|
|
752
|
+
kind: "rect",
|
|
753
|
+
blend: cs.mixBlendMode !== "normal" ? cs.mixBlendMode : ctx.blend || "",
|
|
754
|
+
big: b.w * b.h > vw * vh * 0.4,
|
|
755
|
+
...b,
|
|
756
|
+
css: cssText,
|
|
757
|
+
radius: cs.borderRadius,
|
|
758
|
+
draw: null,
|
|
759
|
+
styled: false,
|
|
760
|
+
layer: ctx.layer,
|
|
761
|
+
key,
|
|
762
|
+
op: ctx.op,
|
|
763
|
+
clip: clipFor(r, ctx.clip, ctx.layer),
|
|
764
|
+
parentBox: ctx.boxItem,
|
|
765
|
+
sig: cssText,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Decorative ::before/::after on containers (overlays, glows, standalone icons).
|
|
770
|
+
// Their boxes aren't exposed, so derive them from the host box and the pseudo's insets.
|
|
771
|
+
const PSEUDO_EL = { localName: "pseudo" };
|
|
772
|
+
function emitPseudoBox(el, cs, r, ctx, which, empty) {
|
|
773
|
+
const ps = getComputedStyle(el, which);
|
|
774
|
+
const c = ps.content;
|
|
775
|
+
if (!c || c === "none" || c === "normal" || ps.display === "none" || ps.visibility !== "visible") return;
|
|
776
|
+
const pop = parseFloat(ps.opacity);
|
|
777
|
+
if (!(pop > 0.03)) return;
|
|
778
|
+
const n = (v) => parseFloat(v) || 0;
|
|
779
|
+
const w = parseFloat(ps.width);
|
|
780
|
+
const h = parseFloat(ps.height);
|
|
781
|
+
if (!(w > 0 && h > 0)) return;
|
|
782
|
+
const pad = {
|
|
783
|
+
l: r.left + n(cs.borderLeftWidth), t: r.top + n(cs.borderTopWidth),
|
|
784
|
+
r: r.right - n(cs.borderRightWidth), b: r.bottom - n(cs.borderBottomWidth),
|
|
785
|
+
};
|
|
786
|
+
let L;
|
|
787
|
+
let T;
|
|
788
|
+
if (ps.position === "absolute") {
|
|
789
|
+
if (cs.position === "static") return; // containing block is some other ancestor
|
|
790
|
+
const bw = w + n(ps.paddingLeft) + n(ps.paddingRight) + n(ps.borderLeftWidth) + n(ps.borderRightWidth);
|
|
791
|
+
const bh = h + n(ps.paddingTop) + n(ps.paddingBottom) + n(ps.borderTopWidth) + n(ps.borderBottomWidth);
|
|
792
|
+
L = ps.left !== "auto" ? pad.l + n(ps.left) : ps.right !== "auto" ? pad.r - n(ps.right) - bw : pad.l;
|
|
793
|
+
T = ps.top !== "auto" ? pad.t + n(ps.top) : ps.bottom !== "auto" ? pad.b - n(ps.bottom) - bh : pad.t;
|
|
794
|
+
const m = ps.transform.match(/^matrix\(([^)]+)\)$/);
|
|
795
|
+
if (m) {
|
|
796
|
+
const k = m[1].split(",").map(Number);
|
|
797
|
+
L += k[4];
|
|
798
|
+
T += k[5];
|
|
799
|
+
}
|
|
800
|
+
const fake = { left: L, top: T, right: L + bw, bottom: T + bh, width: bw, height: bh };
|
|
801
|
+
if (!intersect(ltrb(fake), ctx.clip)) return;
|
|
802
|
+
emitBox(PSEUDO_EL, ps, fake, { ...ctx, op: ctx.op * pop }, ctx.key(), false);
|
|
803
|
+
} else {
|
|
804
|
+
const cl = pad.l + n(cs.paddingLeft);
|
|
805
|
+
const ct = pad.t + n(cs.paddingTop);
|
|
806
|
+
const cw = pad.r - n(cs.paddingRight) - cl;
|
|
807
|
+
const ch = pad.b - n(cs.paddingBottom) - ct;
|
|
808
|
+
const align = (ps.alignSelf !== "auto" ? ps.alignSelf : cs.alignItems) || "";
|
|
809
|
+
const vAlign = () => (/center/.test(align) ? ct + (ch - h) / 2 : /end/.test(align) ? ct + ch - h : ct);
|
|
810
|
+
if (empty) {
|
|
811
|
+
// the only content of an icon-ish element: centre it in the content box
|
|
812
|
+
L = cl + Math.max(0, (cw - w) / 2);
|
|
813
|
+
T = ct + Math.max(0, (ch - h) / 2);
|
|
814
|
+
} else if (/flex/.test(cs.display) && /^row/.test(cs.flexDirection)) {
|
|
815
|
+
// icon before the first / after the last item of a flex row (chevrons, arrows)
|
|
816
|
+
const kidsR = [...el.children]
|
|
817
|
+
.filter((k) => css(k).display !== "none" && css(k).position !== "absolute" && css(k).position !== "fixed")
|
|
818
|
+
.map((k) => k.getBoundingClientRect())
|
|
819
|
+
.filter((k) => k.width || k.height);
|
|
820
|
+
if (!kidsR.length) return;
|
|
821
|
+
const gap = n(cs.columnGap);
|
|
822
|
+
if (which === "::after") L = kidsR[kidsR.length - 1].right + gap + n(ps.marginLeft);
|
|
823
|
+
else L = kidsR[0].left - gap - n(ps.marginRight) - w;
|
|
824
|
+
T = vAlign();
|
|
825
|
+
} else return;
|
|
826
|
+
const fake = { left: L, top: T, right: L + w, bottom: T + h, width: w, height: h };
|
|
827
|
+
if (!intersect(ltrb(fake), ctx.clip)) return;
|
|
828
|
+
emitBox(PSEUDO_EL, ps, fake, { ...ctx, op: ctx.op * pop }, ctx.key(), false);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function mediaFor(el, cs, r) {
|
|
833
|
+
const tag = el.localName;
|
|
834
|
+
const fit = `object-fit:${cs.objectFit};object-position:${cs.objectPosition};`;
|
|
835
|
+
if (tag === "img") {
|
|
836
|
+
const src = imgSrc(el);
|
|
837
|
+
if (!src) return null;
|
|
838
|
+
const m = document.createElement("img");
|
|
839
|
+
m.src = src;
|
|
840
|
+
m.decoding = "async";
|
|
841
|
+
m.setAttribute("style", fit + "display:block;");
|
|
842
|
+
m.draggable = false;
|
|
843
|
+
return { el: m, sig: src };
|
|
844
|
+
}
|
|
845
|
+
if (tag === "svg") {
|
|
846
|
+
const m = cloneStyled(el);
|
|
847
|
+
m.setAttribute("width", "100%");
|
|
848
|
+
m.setAttribute("height", "100%");
|
|
849
|
+
m.style.display = "block";
|
|
850
|
+
m.style.overflow = "visible";
|
|
851
|
+
m.style.margin = "0";
|
|
852
|
+
m.style.position = "static";
|
|
853
|
+
// the box we draw is the transformed bounding box; keep only a pure rotation/flip
|
|
854
|
+
const mt = cs.transform.match(/^matrix\(([^)]+)\)$/);
|
|
855
|
+
const k = mt ? mt[1].split(",").map(Number) : null;
|
|
856
|
+
m.style.transform = k && Math.abs(Math.hypot(k[0], k[1]) - 1) < 0.02 && Math.abs(Math.hypot(k[2], k[3]) - 1) < 0.02 ? `matrix(${k[0]},${k[1]},${k[2]},${k[3]},0,0)` : "none";
|
|
857
|
+
m.style.transformOrigin = "50% 50%";
|
|
858
|
+
const h = m.outerHTML;
|
|
859
|
+
return { el: m, sig: h.length + ":" + h.slice(0, 160) };
|
|
860
|
+
}
|
|
861
|
+
const imgOf = (src, extra) => {
|
|
862
|
+
const m = document.createElement("img");
|
|
863
|
+
m.src = src;
|
|
864
|
+
m.setAttribute("style", (extra || "") + "display:block;width:100%;height:100%;");
|
|
865
|
+
m.draggable = false;
|
|
866
|
+
return { el: m, sig: src.slice(0, 120) + src.length };
|
|
867
|
+
};
|
|
868
|
+
if (tag === "canvas" || tag === "iframe" || tag === "embed" || tag === "object") {
|
|
869
|
+
const crop = cropShot(shot, r);
|
|
870
|
+
if (crop) return imgOf(crop);
|
|
871
|
+
}
|
|
872
|
+
if (tag === "video") {
|
|
873
|
+
// Cross-origin video can't be read back as pixels once it's off-screen, so
|
|
874
|
+
// rebuild it as a real (muted) video in the state the original was in.
|
|
875
|
+
const crop = cropShot(shot, r);
|
|
876
|
+
const url = videoUrl(el);
|
|
877
|
+
if (url) {
|
|
878
|
+
const v = document.createElement("video");
|
|
879
|
+
v.setAttribute("src", url);
|
|
880
|
+
v.setAttribute("muted", "");
|
|
881
|
+
v.setAttribute("playsinline", "");
|
|
882
|
+
v.setAttribute("preload", "auto");
|
|
883
|
+
if (el.loop || el.autoplay || !el.paused) v.setAttribute("loop", "");
|
|
884
|
+
if (!el.paused || el.autoplay) v.setAttribute("data-play", "");
|
|
885
|
+
if (el.crossOrigin) v.setAttribute("crossorigin", el.crossOrigin);
|
|
886
|
+
if (el.currentTime > 0) v.setAttribute("data-t", String(el.currentTime));
|
|
887
|
+
const poster = crop || el.poster;
|
|
888
|
+
if (poster) v.setAttribute("poster", poster);
|
|
889
|
+
v.setAttribute("style", fit + "display:block;width:100%;height:100%;");
|
|
890
|
+
return { el: v, sig: url };
|
|
891
|
+
}
|
|
892
|
+
if (crop) return imgOf(crop);
|
|
893
|
+
let src = el.poster || "";
|
|
894
|
+
if (!src && el.videoWidth) {
|
|
895
|
+
try {
|
|
896
|
+
const c = document.createElement("canvas");
|
|
897
|
+
c.width = el.videoWidth;
|
|
898
|
+
c.height = el.videoHeight;
|
|
899
|
+
c.getContext("2d").drawImage(el, 0, 0);
|
|
900
|
+
src = c.toDataURL("image/jpeg", 0.82);
|
|
901
|
+
} catch (_) {}
|
|
902
|
+
}
|
|
903
|
+
if (src) {
|
|
904
|
+
const m = document.createElement("img");
|
|
905
|
+
m.src = src;
|
|
906
|
+
m.setAttribute("style", fit + "display:block;");
|
|
907
|
+
return { el: m, sig: src.slice(0, 200) };
|
|
908
|
+
}
|
|
909
|
+
const d = document.createElement("div");
|
|
910
|
+
d.setAttribute("style", "background:#111;");
|
|
911
|
+
return { el: d, sig: "video" };
|
|
912
|
+
}
|
|
913
|
+
if (tag === "canvas") {
|
|
914
|
+
const src = canvasPixels(el);
|
|
915
|
+
if (src) return imgOf(src);
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
// iframe / embed / object
|
|
919
|
+
const d = document.createElement("div");
|
|
920
|
+
let host = "";
|
|
921
|
+
try {
|
|
922
|
+
host = new URL(el.src || el.data || "", location.href).hostname.replace(/^www\./, "");
|
|
923
|
+
} catch (_) {}
|
|
924
|
+
d.setAttribute(
|
|
925
|
+
"style",
|
|
926
|
+
"background:#F1F2F4;display:flex;align-items:center;justify-content:center;font:500 12px/1.2 system-ui,sans-serif;color:#8A8F98;letter-spacing:.01em;",
|
|
927
|
+
);
|
|
928
|
+
d.textContent = host || "embed";
|
|
929
|
+
return { el: d, sig: "frame:" + host };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function emitMedia(el, cs, r, ctx, key) {
|
|
933
|
+
let src = el;
|
|
934
|
+
let scs = cs;
|
|
935
|
+
if (el.localName === "picture") {
|
|
936
|
+
src = el.querySelector("img");
|
|
937
|
+
if (!src) return;
|
|
938
|
+
scs = css(src);
|
|
939
|
+
}
|
|
940
|
+
const m = mediaFor(src, scs, r);
|
|
941
|
+
if (!m) return;
|
|
942
|
+
const b = conv(r, ctx.layer);
|
|
943
|
+
add({
|
|
944
|
+
kind: "image",
|
|
945
|
+
...b,
|
|
946
|
+
blend: cs.mixBlendMode !== "normal" ? cs.mixBlendMode : ctx.blend || "",
|
|
947
|
+
media: m.el,
|
|
948
|
+
frame: styleString(cs, ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width", "border-top-style", "border-right-style", "border-bottom-style", "border-left-style", "border-top-color", "border-right-color", "border-bottom-color", "border-left-color"]),
|
|
949
|
+
radius: cs.borderRadius,
|
|
950
|
+
shadow: cs.boxShadow !== "none" ? cs.boxShadow : "",
|
|
951
|
+
bg: alphaOf(cs.backgroundColor) > 0.01 ? cs.backgroundColor : "",
|
|
952
|
+
draw: null,
|
|
953
|
+
styled: false,
|
|
954
|
+
layer: ctx.layer,
|
|
955
|
+
key,
|
|
956
|
+
op: ctx.op,
|
|
957
|
+
clip: clipFor(r, ctx.clip, ctx.layer),
|
|
958
|
+
parentBox: ctx.boxItem,
|
|
959
|
+
sig: m.sig,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function emitForm(el, cs, r, ctx, key) {
|
|
964
|
+
const tag = el.localName;
|
|
965
|
+
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
966
|
+
if (type === "hidden") return;
|
|
967
|
+
const b = conv(r, ctx.layer);
|
|
968
|
+
if (tag === "input" && (type === "checkbox" || type === "radio")) {
|
|
969
|
+
const accent = cs.accentColor && cs.accentColor !== "auto" ? cs.accentColor : "#0B6CFF";
|
|
970
|
+
const s = document.createElementNS(SVGNS, "svg");
|
|
971
|
+
s.setAttribute("viewBox", "0 0 16 16");
|
|
972
|
+
s.setAttribute("width", "100%");
|
|
973
|
+
s.setAttribute("height", "100%");
|
|
974
|
+
const on = el.checked;
|
|
975
|
+
s.innerHTML =
|
|
976
|
+
type === "radio"
|
|
977
|
+
? `<circle cx="8" cy="8" r="7" fill="#fff" stroke="${on ? accent : "#767676"}" stroke-width="1.2"/>${on ? `<circle cx="8" cy="8" r="4" fill="${accent}"/>` : ""}`
|
|
978
|
+
: `<rect x="1" y="1" width="14" height="14" rx="2.5" fill="${on ? accent : "#fff"}" stroke="${on ? accent : "#767676"}" stroke-width="1.2"/>${on ? '<path d="M4.2 8.3 L7 11 L12 5.4" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' : ""}`;
|
|
979
|
+
add({
|
|
980
|
+
kind: "image", ...b, media: s, frame: "", radius: "0px", shadow: "", bg: "",
|
|
981
|
+
draw: null, styled: false, layer: ctx.layer, key, op: ctx.op, clip: null,
|
|
982
|
+
parentBox: ctx.boxItem, sig: type + on,
|
|
983
|
+
});
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const boxIt = emitBox(el, cs, r, ctx, key, false);
|
|
987
|
+
let text = "";
|
|
988
|
+
let ph = false;
|
|
989
|
+
if (tag === "select") text = (el.selectedOptions && el.selectedOptions[0] && el.selectedOptions[0].text) || "";
|
|
990
|
+
else if (["submit", "button", "reset"].includes(type)) text = el.value || (type === "submit" ? "Submit" : type === "reset" ? "Reset" : "");
|
|
991
|
+
else if (type === "password") text = "•".repeat((el.value || "").length);
|
|
992
|
+
else if (["range", "color", "file", "image"].includes(type)) text = "";
|
|
993
|
+
else text = el.value || "";
|
|
994
|
+
if (!text && el.placeholder) {
|
|
995
|
+
text = el.placeholder;
|
|
996
|
+
ph = true;
|
|
997
|
+
}
|
|
998
|
+
if (!text.trim()) return;
|
|
999
|
+
const root = document.createElement("div");
|
|
1000
|
+
const pad = (side) =>
|
|
1001
|
+
(parseFloat(cs.getPropertyValue(`padding-${side}`)) || 0) + (parseFloat(cs.getPropertyValue(`border-${side}-width`)) || 0);
|
|
1002
|
+
const isButton = ["submit", "button", "reset"].includes(type);
|
|
1003
|
+
let st = styleString(cs, TEXT_PROPS);
|
|
1004
|
+
st += `padding:${pad("top")}px ${pad("right")}px ${pad("bottom")}px ${pad("left")}px;margin:0;box-sizing:border-box;width:100%;height:100%;overflow:hidden;`;
|
|
1005
|
+
st += tag === "textarea" ? "display:block;white-space:pre-wrap;" : `display:flex;align-items:center;white-space:pre;${isButton ? "justify-content:center;" : ""}`;
|
|
1006
|
+
if (ph) st += `color:${getComputedStyle(el, "::placeholder").color};-webkit-text-fill-color:currentColor;`;
|
|
1007
|
+
root.setAttribute("style", st);
|
|
1008
|
+
root.textContent = text;
|
|
1009
|
+
textItem(
|
|
1010
|
+
{ ...b, layer: ctx.layer, key: key + 0.5, op: ctx.op, clip: clipFor(r, ctx.clip, ctx.layer), parentBox: boxIt || ctx.boxItem },
|
|
1011
|
+
root,
|
|
1012
|
+
{ ...b },
|
|
1013
|
+
cs,
|
|
1014
|
+
ctx.layer,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function walk(el, ctx, parent) {
|
|
1019
|
+
if (items.length >= MAX_ITEMS) return;
|
|
1020
|
+
const tag = el.localName;
|
|
1021
|
+
if (SKIP.has(tag) || el === env.host) return;
|
|
1022
|
+
const cs = css(el);
|
|
1023
|
+
const disp = cs.display;
|
|
1024
|
+
if (disp === "none") return;
|
|
1025
|
+
if (disp === "contents") {
|
|
1026
|
+
// transparent wrapper (incl. <slot>): its loose text still renders, styled by it
|
|
1027
|
+
let run = [];
|
|
1028
|
+
const flushRun = () => {
|
|
1029
|
+
if (ctx.key && run.some((n) => /\S/.test(n.data))) emitRun(el, cs, run, ctx);
|
|
1030
|
+
run = [];
|
|
1031
|
+
};
|
|
1032
|
+
for (const c of kidsOf(el)) {
|
|
1033
|
+
if (c.nodeType === 3) run.push(c);
|
|
1034
|
+
else if (c.nodeType === 1) {
|
|
1035
|
+
flushRun();
|
|
1036
|
+
walk(c, ctx, parent);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
flushRun();
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const op = ctx.op * parseFloat(cs.opacity);
|
|
1043
|
+
if (!(op > 0.03)) return;
|
|
1044
|
+
const r = el.getBoundingClientRect();
|
|
1045
|
+
// "visually hidden" accessibility text: 1px boxes, clip:rect(0 0 0 0), clip-path:inset(50%)
|
|
1046
|
+
// (html/body overflow belongs to the viewport, so a 0px-tall body clips nothing)
|
|
1047
|
+
const clipsSelf = el !== html && el !== body && el !== env.scroller && cs.overflowX !== "visible" && cs.overflowY !== "visible";
|
|
1048
|
+
if (clipsSelf && (r.width <= 2 || r.height <= 2)) return;
|
|
1049
|
+
if (cs.position === "absolute" && /rect\(0px,? 0px,? 0px,? 0px\)/.test(cs.clip)) return;
|
|
1050
|
+
if (/inset\(50%\)|circle\(0px/.test(cs.clipPath)) return;
|
|
1051
|
+
if (cs.float !== "none" && r.width > 1 && r.height > 1) {
|
|
1052
|
+
const n = (p) => parseFloat(cs.getPropertyValue(p)) || 0;
|
|
1053
|
+
floats.push({ el, side: cs.float, l: r.left - n("margin-left"), t: r.top - n("margin-top"), r: r.right + n("margin-right"), b: r.bottom + n("margin-bottom") });
|
|
1054
|
+
}
|
|
1055
|
+
let layer = ctx.layer;
|
|
1056
|
+
let clip = ctx.clip;
|
|
1057
|
+
let zTop = ctx.zTop;
|
|
1058
|
+
let posd = ctx.posd;
|
|
1059
|
+
const pos = cs.position;
|
|
1060
|
+
if (pos === "fixed" && layer !== "over") {
|
|
1061
|
+
layer = "over";
|
|
1062
|
+
clip = viewClip;
|
|
1063
|
+
} else if (pos === "sticky" && layer === "world") {
|
|
1064
|
+
// stuck (or sitting exactly where it would stick) at the top of its scroller
|
|
1065
|
+
const top = parseFloat(cs.top);
|
|
1066
|
+
if (!isNaN(top) && Math.abs(r.top - oy - top) < 2 && r.height < vh * 0.4) {
|
|
1067
|
+
layer = "over";
|
|
1068
|
+
clip = viewClip;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// z-index counts for positioned boxes and for flex/grid items (even when static)
|
|
1072
|
+
const pe = el.parentElement;
|
|
1073
|
+
const flexItem = pos === "static" && pe && /flex|grid/.test(css(pe).display);
|
|
1074
|
+
if (pos !== "static" || flexItem) {
|
|
1075
|
+
if (cs.zIndex !== "auto") {
|
|
1076
|
+
if (zTop == null) zTop = parseInt(cs.zIndex, 10) || 0;
|
|
1077
|
+
} else if (pos !== "static") posd = true;
|
|
1078
|
+
}
|
|
1079
|
+
const keyFn = () => (zTop || 0) * 1e7 + (posd ? 5e6 : 0) + order++;
|
|
1080
|
+
if (layer === "over" && ctx.layer !== "over" && r.top <= 4 && r.width >= vw * 0.8 && r.height > 20 && r.height <= 200) {
|
|
1081
|
+
bars.push({ node: null, r, start: items.length, key: (zTop || 0) * 1e7 + (posd ? 5e6 : 0) + order - 0.5 });
|
|
1082
|
+
}
|
|
1083
|
+
const isScroller = !env.isRoot && el === env.scroller;
|
|
1084
|
+
const kidLayer = isScroller ? "world" : layer;
|
|
1085
|
+
const box = conv(r, layer);
|
|
1086
|
+
const visible = cs.visibility === "visible";
|
|
1087
|
+
const sized = r.width >= 1 && r.height >= 1;
|
|
1088
|
+
const emit = visible && sized && !!intersect(ltrb(r), clip) && inDoc(box, layer);
|
|
1089
|
+
|
|
1090
|
+
const node = {
|
|
1091
|
+
el, tag, cls: (el.getAttribute && el.getAttribute("class")) || "", layer,
|
|
1092
|
+
start: items.length, end: items.length, kids: [], box,
|
|
1093
|
+
};
|
|
1094
|
+
(parent ? parent.kids : roots).push(node);
|
|
1095
|
+
|
|
1096
|
+
if (REPLACED.has(tag) || FORMS.has(tag)) {
|
|
1097
|
+
if (emit) {
|
|
1098
|
+
if (FORMS.has(tag)) emitForm(el, cs, r, { ...ctx, layer, clip, op }, keyFn());
|
|
1099
|
+
else emitMedia(el, cs, r, { ...ctx, layer, clip, op }, keyFn());
|
|
1100
|
+
}
|
|
1101
|
+
node.end = items.length;
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
let boxItem = ctx.boxItem;
|
|
1106
|
+
if (emit) {
|
|
1107
|
+
const b = emitBox(el, cs, r, { ...ctx, layer, clip, op }, keyFn(), false);
|
|
1108
|
+
if (b) boxItem = b;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
let kidClip = clip;
|
|
1112
|
+
if (isScroller) {
|
|
1113
|
+
kidClip = fullClip;
|
|
1114
|
+
portKey = keyFn();
|
|
1115
|
+
}
|
|
1116
|
+
else if (el !== html && el !== body && (cs.overflowX !== "visible" || cs.overflowY !== "visible")) {
|
|
1117
|
+
const bl = parseFloat(cs.borderLeftWidth) || 0;
|
|
1118
|
+
const bt = parseFloat(cs.borderTopWidth) || 0;
|
|
1119
|
+
const br = parseFloat(cs.borderRightWidth) || 0;
|
|
1120
|
+
const bb = parseFloat(cs.borderBottomWidth) || 0;
|
|
1121
|
+
kidClip = intersect(clip, { l: r.left + bl, t: r.top + bt, r: r.right - br, b: r.bottom - bb });
|
|
1122
|
+
if (!kidClip) {
|
|
1123
|
+
node.end = items.length;
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const blend = cs.mixBlendMode !== "normal" ? cs.mixBlendMode : ctx.blend;
|
|
1128
|
+
const kctx = { op, layer: kidLayer, clip: kidClip, zTop, posd, boxItem, key: keyFn, blend };
|
|
1129
|
+
|
|
1130
|
+
// Decide: one text leaf, or a mix of text runs and nested blocks.
|
|
1131
|
+
// Closed <details> and content-visibility:hidden still report real boxes for
|
|
1132
|
+
// content that isn't painted, so only the summary (or nothing) gets built.
|
|
1133
|
+
if (cs.contentVisibility === "hidden") {
|
|
1134
|
+
node.end = items.length;
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
let kids = kidsOf(el);
|
|
1138
|
+
if (tag === "details" && !el.open) {
|
|
1139
|
+
const sum = kids.find((n) => n.nodeType === 1 && n.localName === "summary");
|
|
1140
|
+
kids = sum ? [sum] : [];
|
|
1141
|
+
}
|
|
1142
|
+
const emptyEl = !kids.some((k) => (k.nodeType === 3 && /\S/.test(k.data)) || k.nodeType === 1);
|
|
1143
|
+
const pctx = { ...kctx, layer, clip };
|
|
1144
|
+
const directText = kids.some((n) => n.nodeType === 3 && /\S/.test(n.data));
|
|
1145
|
+
const kinds = [];
|
|
1146
|
+
let allRun = !el.shadowRoot;
|
|
1147
|
+
let anyText = directText;
|
|
1148
|
+
for (const n of kids) {
|
|
1149
|
+
if (n.nodeType === 3) {
|
|
1150
|
+
kinds.push(1);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (n.nodeType !== 1 || SKIP.has(n.localName)) {
|
|
1154
|
+
kinds.push(0);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const d = css(n).display;
|
|
1158
|
+
if (d === "none") {
|
|
1159
|
+
kinds.push(0);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
let inl = false;
|
|
1163
|
+
const nt = n.localName;
|
|
1164
|
+
if (n.shadowRoot || nt === "slot") inl = false;
|
|
1165
|
+
else if (nt === "br" || nt === "wbr") inl = true;
|
|
1166
|
+
else if (d === "inline") inl = allInline(n);
|
|
1167
|
+
else if (directText && d.startsWith("inline")) inl = true;
|
|
1168
|
+
if (inl && !anyText && /\S/.test(n.textContent)) anyText = true;
|
|
1169
|
+
kinds.push(inl ? 1 : 2);
|
|
1170
|
+
if (!inl) allRun = false;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (allRun && anyText && !isScroller) {
|
|
1174
|
+
if (visible && sized && emit) emitLeaf(el, cs, r, { ...kctx, layer }, keyFn());
|
|
1175
|
+
node.end = items.length;
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
// (text leaves carry their pseudos inside the clone instead)
|
|
1179
|
+
if (emit && visible) emitPseudoBox(el, cs, r, pctx, "::before", emptyEl);
|
|
1180
|
+
const afterPseudo = () => {
|
|
1181
|
+
if (emit && visible) emitPseudoBox(el, cs, r, pctx, "::after", emptyEl);
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
let run = [];
|
|
1185
|
+
const flush = () => {
|
|
1186
|
+
if (!run.length) return;
|
|
1187
|
+
const hasText = run.some((n) => (n.nodeType === 3 ? /\S/.test(n.data) : /\S/.test(n.textContent)));
|
|
1188
|
+
if (hasText) {
|
|
1189
|
+
if (visible) emitRun(el, cs, run, kctx);
|
|
1190
|
+
} else {
|
|
1191
|
+
for (const n of run) if (n.nodeType === 1) walk(n, kctx, node);
|
|
1192
|
+
}
|
|
1193
|
+
run = [];
|
|
1194
|
+
};
|
|
1195
|
+
kids.forEach((n, i) => {
|
|
1196
|
+
if (kinds[i] === 1) run.push(n);
|
|
1197
|
+
else if (kinds[i] === 2) {
|
|
1198
|
+
flush();
|
|
1199
|
+
walk(n, kctx, node);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
flush();
|
|
1203
|
+
afterPseudo();
|
|
1204
|
+
node.end = items.length;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const rootLayer = env.isRoot ? "world" : "under";
|
|
1208
|
+
const rootClip = env.isRoot ? fullClip : viewClip;
|
|
1209
|
+
walk(html, { op: 1, layer: rootLayer, clip: rootClip, zTop: null, posd: false, boxItem: null }, null);
|
|
1210
|
+
|
|
1211
|
+
// --------------------------------------------- repeated components (dup)
|
|
1212
|
+
const groups = [];
|
|
1213
|
+
const kindsSig = (n) => {
|
|
1214
|
+
let s = "";
|
|
1215
|
+
for (let i = n.start; i < n.end; i++) s += items[i].kind[0];
|
|
1216
|
+
return s;
|
|
1217
|
+
};
|
|
1218
|
+
const similar = (a, b, loose) =>
|
|
1219
|
+
a.w > 0 && b.w > 0 && a.h > 0 && b.h > 0 && a.h / b.h < 1.8 && b.h / a.h < 1.8 && (loose || (a.w / b.w < 1.6 && b.w / a.w < 1.6));
|
|
1220
|
+
function findGroups(node, inCopy) {
|
|
1221
|
+
if (inCopy) return;
|
|
1222
|
+
const buckets = new Map();
|
|
1223
|
+
for (const k of node.kids) {
|
|
1224
|
+
const n = k.end - k.start;
|
|
1225
|
+
if (n < 1 || n > 48) continue;
|
|
1226
|
+
// long single paragraphs read better typed than duplicated
|
|
1227
|
+
if (n === 1 && items[k.start].kind === "text" && items[k.start].text.length > 40) continue;
|
|
1228
|
+
const sig = `${k.tag}|${k.cls}|${k.layer}|${kindsSig(k)}`;
|
|
1229
|
+
if (!buckets.has(sig)) buckets.set(sig, []);
|
|
1230
|
+
buckets.get(sig).push(k);
|
|
1231
|
+
}
|
|
1232
|
+
const copies = new Set();
|
|
1233
|
+
for (const list of buckets.values()) {
|
|
1234
|
+
if (list.length < 2) continue;
|
|
1235
|
+
const m = list[0];
|
|
1236
|
+
const loose = m.end - m.start === 1 && items[m.start].kind === "text";
|
|
1237
|
+
const members = list.filter((k) => similar(k.box, m.box, loose));
|
|
1238
|
+
if (members.length < 2) continue;
|
|
1239
|
+
const g = { id: groups.length, master: members[0], copies: members.slice(1) };
|
|
1240
|
+
groups.push(g);
|
|
1241
|
+
for (const c of g.copies) copies.add(c);
|
|
1242
|
+
}
|
|
1243
|
+
for (const k of node.kids) findGroups(k, copies.has(k));
|
|
1244
|
+
}
|
|
1245
|
+
for (const r of roots) findGroups(r, false);
|
|
1246
|
+
|
|
1247
|
+
for (const g of groups) {
|
|
1248
|
+
for (const c of g.copies) {
|
|
1249
|
+
for (let i = c.start; i < c.end; i++) {
|
|
1250
|
+
const it = items[i];
|
|
1251
|
+
if (it.copyOf) continue;
|
|
1252
|
+
it.copyOf = items[g.master.start + (i - c.start)];
|
|
1253
|
+
it.group = g;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const twinKey = new Map();
|
|
1259
|
+
for (const it of items) {
|
|
1260
|
+
if (it.kind !== "text" || it.copyOf || !it.text) continue;
|
|
1261
|
+
const k = `${it.layer}|${Math.round(it.tb.x / 4)}|${Math.round(it.tb.y / 4)}|${it.text}`;
|
|
1262
|
+
const m = twinKey.get(k);
|
|
1263
|
+
if (m) it.twinOf = m;
|
|
1264
|
+
else twinKey.set(k, it);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// ------------------------------------------------------------- tasks
|
|
1268
|
+
const tasks = [];
|
|
1269
|
+
const mk = (y, spec, deps, layer) => {
|
|
1270
|
+
const t = { id: tasks.length, y, spec, deps, state: "wait", doneAt: 0, layer };
|
|
1271
|
+
tasks.push(t);
|
|
1272
|
+
return t;
|
|
1273
|
+
};
|
|
1274
|
+
const KIND_TASK = { rect: "rect", image: "image", text: "text", line: "line" };
|
|
1275
|
+
for (const it of items) {
|
|
1276
|
+
if (it.copyOf) continue;
|
|
1277
|
+
if (it.twinOf) {
|
|
1278
|
+
it.final = it.twinOf.final;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const deps = it.parentBox && it.parentBox.final ? it.parentBox.final : [];
|
|
1282
|
+
const y = it.kind === "text" ? it.tb.y : it.y;
|
|
1283
|
+
it.task = mk(y, { type: KIND_TASK[it.kind], it }, deps, it.layer);
|
|
1284
|
+
it.final = [it.task];
|
|
1285
|
+
}
|
|
1286
|
+
// inner groups first so outer copies can wait for them
|
|
1287
|
+
for (let gi = groups.length - 1; gi >= 0; gi--) {
|
|
1288
|
+
const g = groups[gi];
|
|
1289
|
+
const mItems = items.slice(g.master.start, g.master.end);
|
|
1290
|
+
const masterFinals = [];
|
|
1291
|
+
for (const it of mItems) if (it.final) masterFinals.push(...it.final);
|
|
1292
|
+
if (g.master.start < g.master.end && items[g.master.start].parentBox && items[g.master.start].parentBox.final)
|
|
1293
|
+
masterFinals.push(...items[g.master.start].parentBox.final);
|
|
1294
|
+
g.src = { items: mItems, box: g.master.box };
|
|
1295
|
+
g.dst = g.copies.map((c, k) => {
|
|
1296
|
+
const its = items.slice(c.start, c.end).filter((it) => it.group === g);
|
|
1297
|
+
const minY = Math.min(...its.map((it) => (it.kind === "text" ? it.tb.y : it.y)));
|
|
1298
|
+
const task = mk(minY, { type: "dup", g, k }, masterFinals, c.layer);
|
|
1299
|
+
for (const it of its) {
|
|
1300
|
+
it.dupTask = task;
|
|
1301
|
+
it.final = [task];
|
|
1302
|
+
const m = it.copyOf;
|
|
1303
|
+
let type = null;
|
|
1304
|
+
if (it.kind === "text" && it.sig !== m.sig) type = "edit";
|
|
1305
|
+
else if (it.kind === "image" && it.sig !== m.sig) type = "swap";
|
|
1306
|
+
else if (it.kind === "rect" && it.sig !== m.sig) type = "restyle";
|
|
1307
|
+
if (type) {
|
|
1308
|
+
it.editTask = mk(it.kind === "text" ? it.tb.y : it.y, { type, it }, [task], it.layer);
|
|
1309
|
+
it.final = [task, it.editTask];
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return { items: its, box: c.box, task };
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// -------------------------------------------------- guides & snapping
|
|
1317
|
+
const guides = [];
|
|
1318
|
+
const texts = items.filter((it) => it.kind === "text" && it.task && it.layer === "world" && it.fs >= 13 && it.tb.w > 30);
|
|
1319
|
+
const BAND = 900;
|
|
1320
|
+
const bands = new Map();
|
|
1321
|
+
for (const it of texts) {
|
|
1322
|
+
const k = Math.floor(it.tb.y / BAND);
|
|
1323
|
+
if (!bands.has(k)) bands.set(k, []);
|
|
1324
|
+
bands.get(k).push(it);
|
|
1325
|
+
}
|
|
1326
|
+
for (const [, list] of [...bands.entries()].sort((a, b) => a[0] - b[0])) {
|
|
1327
|
+
if (guides.length >= 16) break;
|
|
1328
|
+
const byX = new Map();
|
|
1329
|
+
for (const it of list) {
|
|
1330
|
+
const x = Math.round(it.tb.x);
|
|
1331
|
+
if (!byX.has(x)) byX.set(x, []);
|
|
1332
|
+
byX.get(x).push(it);
|
|
1333
|
+
}
|
|
1334
|
+
let best = null;
|
|
1335
|
+
for (const [x, l] of byX) if (l.length >= 3 && (!best || l.length > best[1].length)) best = [x, l];
|
|
1336
|
+
if (best) {
|
|
1337
|
+
const top = Math.min(...best[1].map((m) => m.tb.y));
|
|
1338
|
+
const members = best[1].filter((m) => m.tb.y < top + 450).sort((a, b) => b.fs - a.fs).slice(0, 2);
|
|
1339
|
+
const g = { axis: "x", pos: best[0], cur: best[0], op: 1, flash: 0, dragging: false, vis: false };
|
|
1340
|
+
guides.push(g);
|
|
1341
|
+
const gy = Math.min(...members.map((m) => m.tb.y)) - 30;
|
|
1342
|
+
g.task = mk(gy, { type: "guide", g }, [], "world");
|
|
1343
|
+
g.deps = [];
|
|
1344
|
+
for (const m of members) {
|
|
1345
|
+
m.task.spec.snap = g;
|
|
1346
|
+
m.task.spec.snapDx = m.tb.x - best[0];
|
|
1347
|
+
g.deps.push(m.task);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const byY = new Map();
|
|
1351
|
+
for (const it of list) {
|
|
1352
|
+
const y = Math.round(it.tb.y);
|
|
1353
|
+
if (!byY.has(y)) byY.set(y, []);
|
|
1354
|
+
byY.get(y).push(it);
|
|
1355
|
+
}
|
|
1356
|
+
let row = null;
|
|
1357
|
+
for (const [y, l] of byY) if (l.length >= 3 && (!row || l.length > row[1].length)) row = [y, l];
|
|
1358
|
+
if (row) {
|
|
1359
|
+
const m = row[1].find((it) => !it.task.spec.snap);
|
|
1360
|
+
if (m) {
|
|
1361
|
+
const g = { axis: "y", pos: row[0], cur: row[0], op: 1, flash: 0, dragging: false, vis: false };
|
|
1362
|
+
guides.push(g);
|
|
1363
|
+
g.task = mk(row[0] - 30, { type: "guide", g }, [], "world");
|
|
1364
|
+
g.deps = [m.task];
|
|
1365
|
+
m.task.spec.snap = g;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
for (const it of items) if (it.kind === "text" && it.task && it.fs >= 28 && it.tb.h < 400) it.task.spec.grow = true;
|
|
1370
|
+
// deps always have lower ids, so one reverse pass propagates the earliest need
|
|
1371
|
+
for (const t of tasks) t.gate = t.y;
|
|
1372
|
+
for (let i = tasks.length - 1; i >= 0; i--) for (const d of tasks[i].deps) if (tasks[i].gate < d.gate) d.gate = tasks[i].gate;
|
|
1373
|
+
|
|
1374
|
+
// A sticky header that is transparent at the top of the page still needs
|
|
1375
|
+
// something behind it once content scrolls underneath (sites add it on scroll).
|
|
1376
|
+
const backdrops = [];
|
|
1377
|
+
if (pageBg.image === "none")
|
|
1378
|
+
for (const b of bars) {
|
|
1379
|
+
const area = b.r.width * b.r.height;
|
|
1380
|
+
const covered = items.some(
|
|
1381
|
+
(it) => it.id >= b.start && it.layer === "over" && it.kind === "rect" && it.w * it.h >= area * 0.85 && /rgba?\(/.test(it.css) && !/background-color:\s*rgba\([^)]*,\s*0(\.\d+)?\)/.test(it.css),
|
|
1382
|
+
);
|
|
1383
|
+
if (!covered) backdrops.push({ x: b.r.left, y: b.r.top, w: b.r.width, h: b.r.height, key: b.key, color: pageBg.color });
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return { items, tasks, groups, guides, docW, docH, backdrops, portKey };
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// -------------------------------------------------------------- text reveal
|
|
1390
|
+
|
|
1391
|
+
function wsOf(n, root) {
|
|
1392
|
+
for (let e = n; e && e !== root.parentNode; e = e.parentNode) {
|
|
1393
|
+
const w = e.style && e.style.whiteSpace;
|
|
1394
|
+
if (w) return w;
|
|
1395
|
+
}
|
|
1396
|
+
return "normal";
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function prepText(it) {
|
|
1400
|
+
const live = it.full.cloneNode(true);
|
|
1401
|
+
const units = [];
|
|
1402
|
+
let seq = "";
|
|
1403
|
+
const boxes = [];
|
|
1404
|
+
(function rec(n, box) {
|
|
1405
|
+
for (const c of [...n.childNodes]) {
|
|
1406
|
+
if (c.nodeType === 3) {
|
|
1407
|
+
let s = c.data;
|
|
1408
|
+
const ws = wsOf(n, live);
|
|
1409
|
+
if (!/^pre|break-spaces/.test(ws)) s = s.replace(/[\t\n\r ]+/g, " ");
|
|
1410
|
+
if (!s.length) {
|
|
1411
|
+
c.remove();
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
units.push({ t: c, s, len: s.length, box });
|
|
1415
|
+
seq += s;
|
|
1416
|
+
} else if (c.nodeType === 1) {
|
|
1417
|
+
// media, and empty styled boxes (mask icons, dots, swatches) appear in one keystroke
|
|
1418
|
+
if (ATOMIC.has(c.localName) || c instanceof SVGElement || (!/\S/.test(c.textContent) && c.style && boxyStyle(c.style))) {
|
|
1419
|
+
units.push({ e: c, len: 1, box, on: true });
|
|
1420
|
+
seq += "□";
|
|
1421
|
+
} else {
|
|
1422
|
+
const b = c.style && boxyStyle(c.style) ? c : box;
|
|
1423
|
+
if (b === c) boxes.push(c);
|
|
1424
|
+
rec(c, b);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
})(live, null);
|
|
1429
|
+
for (const u of units) {
|
|
1430
|
+
if (u.t) u.t.data = "";
|
|
1431
|
+
else {
|
|
1432
|
+
u.e.style.visibility = "hidden";
|
|
1433
|
+
u.on = false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
for (const b of boxes) {
|
|
1437
|
+
b.style.visibility = "hidden";
|
|
1438
|
+
b.__on = false;
|
|
1439
|
+
}
|
|
1440
|
+
it.live = live;
|
|
1441
|
+
it.units = units;
|
|
1442
|
+
it.seq = seq;
|
|
1443
|
+
it.total = seq.length;
|
|
1444
|
+
it.shown = 0;
|
|
1445
|
+
it.rshown = 0;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function reveal(it, n) {
|
|
1449
|
+
let left = n;
|
|
1450
|
+
let last = null;
|
|
1451
|
+
for (const u of it.units) {
|
|
1452
|
+
const k = left <= 0 ? 0 : left >= u.len ? u.len : left;
|
|
1453
|
+
left -= k;
|
|
1454
|
+
if (u.t) {
|
|
1455
|
+
const want = k === u.len ? u.s : u.s.slice(0, k);
|
|
1456
|
+
if (u.t.data !== want) u.t.data = want;
|
|
1457
|
+
if (k) last = u.t;
|
|
1458
|
+
} else {
|
|
1459
|
+
const on = k > 0;
|
|
1460
|
+
if (u.on !== on) {
|
|
1461
|
+
u.e.style.visibility = on ? "" : "hidden";
|
|
1462
|
+
u.on = on;
|
|
1463
|
+
}
|
|
1464
|
+
if (on) last = u.e;
|
|
1465
|
+
}
|
|
1466
|
+
if (k && u.box && u.box.__on === false) {
|
|
1467
|
+
for (let b = u.box; b && b !== it.live; b = b.parentNode)
|
|
1468
|
+
if (b.__on === false) {
|
|
1469
|
+
b.style.visibility = "";
|
|
1470
|
+
b.__on = true;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return last;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// ------------------------------------------------------------------ styles
|
|
1478
|
+
|
|
1479
|
+
const STYLE = `
|
|
1480
|
+
:host{all:initial}
|
|
1481
|
+
*{box-sizing:border-box}
|
|
1482
|
+
.bg{position:absolute;inset:0;opacity:0;transition:opacity .5s ease}
|
|
1483
|
+
.bg.on{opacity:1}
|
|
1484
|
+
.layer,.port{position:absolute;overflow:hidden;pointer-events:none}
|
|
1485
|
+
.layer{inset:0}
|
|
1486
|
+
.world{position:absolute;left:0;top:0;will-change:transform}
|
|
1487
|
+
.it{position:absolute;left:0;top:0;transform-origin:0 0}
|
|
1488
|
+
.it.settle{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
|
|
1489
|
+
.r{background-color:var(--ph,#D9D9D9);transition:background-color .22s ease,border-radius .22s ease}
|
|
1490
|
+
.r.st{background-color:transparent}
|
|
1491
|
+
.r>.f{position:absolute;inset:0;opacity:0;transition:opacity .22s ease,background-color .35s ease,border-radius .35s ease,box-shadow .35s ease,border-color .35s ease}
|
|
1492
|
+
.r.st>.f{opacity:1}
|
|
1493
|
+
.m{background-color:var(--ph,#D9D9D9);overflow:hidden}
|
|
1494
|
+
.m>.ph{position:absolute;left:50%;top:50%;width:22px;height:22px;transform:translate(-50%,-50%);opacity:.7;transition:opacity .2s}
|
|
1495
|
+
.m>.md{position:absolute;inset:0;width:100%;height:100%;opacity:0;clip-path:inset(0 0 100% 0);transition:opacity .35s ease,clip-path .6s cubic-bezier(.2,.75,.2,1)}
|
|
1496
|
+
.m.st{background-color:transparent;transition:background-color .3s ease .3s}
|
|
1497
|
+
.m.st>.ph{opacity:0}
|
|
1498
|
+
.m.st>.md{opacity:1;clip-path:inset(0 0 0 0)}
|
|
1499
|
+
.m>.md.swap{animation:swap .45s ease}
|
|
1500
|
+
@keyframes swap{from{opacity:0;filter:blur(6px)}}
|
|
1501
|
+
.m>.fr{position:absolute;inset:0;pointer-events:none}
|
|
1502
|
+
.l svg{position:absolute;left:0;top:0;overflow:visible}
|
|
1503
|
+
.bd{position:absolute;opacity:0;-webkit-backdrop-filter:saturate(180%) blur(14px);backdrop-filter:saturate(180%) blur(14px);box-shadow:0 1px 0 rgba(0,0,0,.06);pointer-events:none}
|
|
1504
|
+
.caret{display:inline-block;width:1.5px;height:1.1em;margin-left:1px;margin-right:-2.5px;vertical-align:text-bottom;background:currentColor;animation:blink 1s steps(1) infinite}
|
|
1505
|
+
@keyframes blink{50%{opacity:0}}
|
|
1506
|
+
.sa{background-color:rgba(13,153,255,.28)!important}
|
|
1507
|
+
.ov{position:absolute;left:0;top:0;width:100%;height:100%;overflow:visible;pointer-events:none}
|
|
1508
|
+
.badges,.cursors{position:absolute;inset:0;pointer-events:none;overflow:hidden}
|
|
1509
|
+
.badge{position:absolute;transform:translate(-50%);padding:1px 4px;border-radius:2px;color:#fff;font:600 11px/16px Inter,system-ui,-apple-system,"Segoe UI",sans-serif;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
1510
|
+
.cursor{position:absolute;left:0;top:0;will-change:transform}
|
|
1511
|
+
.cursor svg{position:absolute;overflow:visible;filter:drop-shadow(0 1px 1.5px rgba(0,0,0,.28))}
|
|
1512
|
+
.rulers{position:absolute;inset:0;pointer-events:none}
|
|
1513
|
+
.ruler{position:absolute;background:#2c2c2c;pointer-events:none}
|
|
1514
|
+
.ruler svg{display:block}
|
|
1515
|
+
.ruler-top{top:0;left:0;right:0;height:20px;border-bottom:1px solid #3a3a3a}
|
|
1516
|
+
.ruler-left{top:0;left:0;bottom:0;width:20px;border-right:1px solid #3a3a3a}
|
|
1517
|
+
.ruler-corner{position:absolute;top:0;left:0;width:20px;height:20px;background:#2c2c2c}
|
|
1518
|
+
.rtext{font:500 9px Inter,system-ui,-apple-system,sans-serif;fill:#8c8c8c}
|
|
1519
|
+
.rsel{fill:#0d99ff}
|
|
1520
|
+
.intro{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(.92);opacity:0;transition:opacity .35s ease,transform .45s cubic-bezier(.2,.8,.2,1);pointer-events:none}
|
|
1521
|
+
.intro.on{opacity:1;transform:translate(-50%,-50%)}
|
|
1522
|
+
.intro.gone{opacity:0;transform:translate(-50%,-50%) scale(.9)}
|
|
1523
|
+
.intro img{display:block;width:60px;height:60px;object-fit:contain;filter:grayscale(1);opacity:.42;border-radius:14px}
|
|
1524
|
+
.intro .mono{width:60px;height:60px;border-radius:16px;background:#D7DADE;color:#fff;font:700 30px/60px Inter,system-ui,sans-serif;text-align:center}
|
|
1525
|
+
.hud{position:absolute;left:50%;bottom:20px;transform:translate(-50%,18px);opacity:0;display:flex;align-items:center;gap:10px;padding:6px 6px 6px 10px;background:rgba(30,30,30,.94);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);color:#fff;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.28),inset 0 0 0 1px rgba(255,255,255,.07);font:500 12px/1 Inter,system-ui,-apple-system,"Segoe UI",sans-serif;pointer-events:auto;transition:opacity .45s ease,transform .45s cubic-bezier(.2,.8,.2,1);white-space:nowrap;user-select:none;z-index:5}
|
|
1526
|
+
.hud.on{opacity:1;transform:translate(-50%,0)}
|
|
1527
|
+
.hud .dots{display:flex;padding-left:5px}
|
|
1528
|
+
.hud .dots i{display:block;width:18px;height:18px;margin-left:-5px;border-radius:50%;border:2px solid #1e1e1e;font:700 9px/14px Inter,system-ui,sans-serif;font-style:normal;text-align:center;color:#fff;transition:background .3s}
|
|
1529
|
+
.hud .txt,.hud .status{display:flex;align-items:center;gap:7px;color:#cfcfcf;font-variant-numeric:tabular-nums}
|
|
1530
|
+
.hud .txt b{color:#fff;font-weight:600}
|
|
1531
|
+
.hud .sep{width:1px;height:12px;background:#444}
|
|
1532
|
+
.hud .bar{width:64px;height:3px;border-radius:2px;background:#3a3a3a;overflow:hidden}
|
|
1533
|
+
.hud .bar i{display:block;height:100%;width:0;background:#0D99FF;transition:width .35s ease}
|
|
1534
|
+
.hud button{all:unset;cursor:pointer;padding:7px 10px;border-radius:8px;background:#2c2c2c;color:#fff;font:500 12px/1 Inter,system-ui,-apple-system,sans-serif;transition:background .15s}
|
|
1535
|
+
.hud button:hover{background:#3a3a3a}
|
|
1536
|
+
.hud button.pri{background:#0D99FF}
|
|
1537
|
+
.hud button.pri:hover{background:#2aa6ff}
|
|
1538
|
+
.hud button.on{background:rgba(13,153,255,.22);color:#8fd0ff}
|
|
1539
|
+
.hud button[hidden]{display:none}
|
|
1540
|
+
.hud .x{padding:7px 9px;color:#aaa}
|
|
1541
|
+
.hud button.snd{display:flex;align-items:center;gap:6px;padding:6px 8px}
|
|
1542
|
+
.hud button.snd.off{color:#8a8a8a}
|
|
1543
|
+
.hud button.snd.tap{background:#0D99FF;animation:hudpulse 1.6s ease-in-out infinite}
|
|
1544
|
+
@keyframes hudpulse{50%{box-shadow:0 0 0 4px rgba(13,153,255,.28)}}
|
|
1545
|
+
`;
|
|
1546
|
+
|
|
1547
|
+
// -------------------------------------------------------------------- env
|
|
1548
|
+
|
|
1549
|
+
function detectEnv() {
|
|
1550
|
+
const se = document.scrollingElement || document.documentElement;
|
|
1551
|
+
let scroller = se;
|
|
1552
|
+
let isRoot = true;
|
|
1553
|
+
if (se.scrollHeight <= innerHeight + 4) {
|
|
1554
|
+
let best = null;
|
|
1555
|
+
let bestArea = 0;
|
|
1556
|
+
const all = document.body ? document.body.getElementsByTagName("*") : [];
|
|
1557
|
+
for (let i = 0; i < all.length && i < 20000; i++) {
|
|
1558
|
+
const el = all[i];
|
|
1559
|
+
if (el.scrollHeight <= el.clientHeight + 40 || el.clientHeight < innerHeight * 0.4) continue;
|
|
1560
|
+
const oy = getComputedStyle(el).overflowY;
|
|
1561
|
+
if (!/(auto|scroll|overlay)/.test(oy)) continue;
|
|
1562
|
+
const r = el.getBoundingClientRect();
|
|
1563
|
+
const area = r.width * r.height;
|
|
1564
|
+
if (area > bestArea && r.width > innerWidth * 0.35) {
|
|
1565
|
+
best = el;
|
|
1566
|
+
bestArea = area;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (best) {
|
|
1570
|
+
scroller = best;
|
|
1571
|
+
isRoot = false;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const env = {
|
|
1575
|
+
scroller,
|
|
1576
|
+
isRoot,
|
|
1577
|
+
host: null,
|
|
1578
|
+
pageBg: pageBackground(),
|
|
1579
|
+
geom() {
|
|
1580
|
+
if (isRoot) {
|
|
1581
|
+
return {
|
|
1582
|
+
ox: 0,
|
|
1583
|
+
oy: 0,
|
|
1584
|
+
sx: window.scrollX,
|
|
1585
|
+
sy: window.scrollY,
|
|
1586
|
+
vw: document.documentElement.clientWidth,
|
|
1587
|
+
vh: window.innerHeight,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
const r = scroller.getBoundingClientRect();
|
|
1591
|
+
return {
|
|
1592
|
+
ox: r.left + scroller.clientLeft,
|
|
1593
|
+
oy: r.top + scroller.clientTop,
|
|
1594
|
+
sx: scroller.scrollLeft,
|
|
1595
|
+
sy: scroller.scrollTop,
|
|
1596
|
+
vw: scroller.clientWidth,
|
|
1597
|
+
vh: scroller.clientHeight,
|
|
1598
|
+
};
|
|
1599
|
+
},
|
|
1600
|
+
scrollHeight: () => (isRoot ? Math.max(se.scrollHeight, document.body ? document.body.scrollHeight : 0) : scroller.scrollHeight),
|
|
1601
|
+
scrollWidth: () => (isRoot ? se.scrollWidth : scroller.scrollWidth),
|
|
1602
|
+
scrollTo(y, x) {
|
|
1603
|
+
const opts = { top: y, behavior: "instant" };
|
|
1604
|
+
if (x != null) opts.left = x;
|
|
1605
|
+
if (isRoot) window.scrollTo(opts);
|
|
1606
|
+
else scroller.scrollTo(opts);
|
|
1607
|
+
},
|
|
1608
|
+
scrollBy(dy) {
|
|
1609
|
+
if (isRoot) window.scrollBy({ top: dy, behavior: "instant" });
|
|
1610
|
+
else scroller.scrollBy({ top: dy, behavior: "instant" });
|
|
1611
|
+
},
|
|
1612
|
+
};
|
|
1613
|
+
return env;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// ------------------------------------------------------------------- view
|
|
1617
|
+
|
|
1618
|
+
class View {
|
|
1619
|
+
constructor(env) {
|
|
1620
|
+
this.env = env;
|
|
1621
|
+
this.frozen = null;
|
|
1622
|
+
for (const old of document.querySelectorAll("swarm-rebuild")) old.remove();
|
|
1623
|
+
const host = document.createElement("swarm-rebuild");
|
|
1624
|
+
host.style.cssText =
|
|
1625
|
+
"all:initial !important;position:fixed !important;inset:0 !important;z-index:2147483647 !important;display:block !important;pointer-events:auto !important;contain:strict !important;opacity:1;transition:opacity .6s ease;";
|
|
1626
|
+
env.host = host;
|
|
1627
|
+
this.host = host;
|
|
1628
|
+
const root = host.attachShadow({ mode: "open" });
|
|
1629
|
+
this.root = root;
|
|
1630
|
+
root.innerHTML = `<style>${STYLE}</style>
|
|
1631
|
+
<div class="bg"></div>
|
|
1632
|
+
<div class="layer under"></div>
|
|
1633
|
+
<div class="port"><div class="world"></div></div>
|
|
1634
|
+
<div class="layer over"></div>
|
|
1635
|
+
<div class="intro"></div>
|
|
1636
|
+
<svg class="ov"></svg>
|
|
1637
|
+
<div class="badges"></div>
|
|
1638
|
+
<div class="cursors"></div>
|
|
1639
|
+
<div class="rulers"></div>
|
|
1640
|
+
`;
|
|
1641
|
+
const $ = (s) => root.querySelector(s);
|
|
1642
|
+
this.bg = $(".bg");
|
|
1643
|
+
this.under = $(".under");
|
|
1644
|
+
this.over = $(".over");
|
|
1645
|
+
this.port = $(".port");
|
|
1646
|
+
this.world = $(".world");
|
|
1647
|
+
this.intro = $(".intro");
|
|
1648
|
+
this.ov = $(".ov");
|
|
1649
|
+
this.badges = $(".badges");
|
|
1650
|
+
this.cursorsEl = $(".cursors");
|
|
1651
|
+
this.rulers = $(".rulers");
|
|
1652
|
+
// 控制条已移除(见 scripts/swarm-patch.mjs)。下面四个字段在各处被写,
|
|
1653
|
+
// 换成不挂在文档里的哑元素:写入照常成功,但屏幕上什么都不会出现。
|
|
1654
|
+
this.hud = $(".hud") || document.createElement("div");
|
|
1655
|
+
this.status = $(".status") || document.createElement("span");
|
|
1656
|
+
this.barFill = $(".bar i") || document.createElement("i");
|
|
1657
|
+
this.dots = $(".dots") || document.createElement("div");
|
|
1658
|
+
this.cursorEls = new Map();
|
|
1659
|
+
this._ovS = "";
|
|
1660
|
+
this._ovB = "";
|
|
1661
|
+
this._rulersOn = false;
|
|
1662
|
+
|
|
1663
|
+
const pb = env.pageBg;
|
|
1664
|
+
this.bg.style.backgroundColor = pb.color;
|
|
1665
|
+
const rgb = (pb.color.match(/[\d.]+/g) || [255, 255, 255]).map(Number);
|
|
1666
|
+
const lum = (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
|
|
1667
|
+
if (lum < 0.4) this.host.style.setProperty("--ph", "#3B3D42");
|
|
1668
|
+
if (pb.image && pb.image !== "none") {
|
|
1669
|
+
this.bg.style.backgroundImage = pb.image;
|
|
1670
|
+
this.bg.style.backgroundSize = pb.size;
|
|
1671
|
+
this.bg.style.backgroundPosition = pb.position;
|
|
1672
|
+
this.bg.style.backgroundRepeat = pb.repeat;
|
|
1673
|
+
}
|
|
1674
|
+
this.makeIntroLogo();
|
|
1675
|
+
document.documentElement.appendChild(host);
|
|
1676
|
+
this.measure();
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
makeIntroLogo() {
|
|
1680
|
+
const links = [...document.querySelectorAll('link[rel~="icon"], link[rel="apple-touch-icon"], link[rel="shortcut icon"]')];
|
|
1681
|
+
const sized = links
|
|
1682
|
+
.map((l) => ({ href: l.href, s: parseInt((l.getAttribute("sizes") || "0").split("x")[0], 10) || (l.rel.includes("apple") ? 180 : 32) }))
|
|
1683
|
+
.filter((l) => l.href)
|
|
1684
|
+
.sort((a, b) => b.s - a.s);
|
|
1685
|
+
const label = (document.title || location.hostname || "?").replace(/^www\./, "").trim();
|
|
1686
|
+
const mono = () => {
|
|
1687
|
+
this.intro.innerHTML = "";
|
|
1688
|
+
const d = document.createElement("div");
|
|
1689
|
+
d.className = "mono";
|
|
1690
|
+
d.textContent = (label[0] || "?").toUpperCase();
|
|
1691
|
+
this.intro.appendChild(d);
|
|
1692
|
+
};
|
|
1693
|
+
const href = sized.length ? sized[0].href : location.origin + "/favicon.ico";
|
|
1694
|
+
const img = document.createElement("img");
|
|
1695
|
+
img.onerror = mono;
|
|
1696
|
+
img.src = href;
|
|
1697
|
+
this.intro.appendChild(img);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
measure() {
|
|
1701
|
+
this.W = innerWidth;
|
|
1702
|
+
this.H = innerHeight;
|
|
1703
|
+
this.g = this.env.geom();
|
|
1704
|
+
const g = this.g;
|
|
1705
|
+
this.port.style.left = g.ox + "px";
|
|
1706
|
+
this.port.style.top = g.oy + "px";
|
|
1707
|
+
this.port.style.width = g.vw + "px";
|
|
1708
|
+
this.port.style.height = g.vh + "px";
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
get vp() {
|
|
1712
|
+
if (this.frozen) return this.frozen;
|
|
1713
|
+
const g = this.env.geom();
|
|
1714
|
+
return { ...g, ox: this.g.ox, oy: this.g.oy };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
syncWorld() {
|
|
1718
|
+
const vp = this.vp;
|
|
1719
|
+
this.world.style.transform = `translate(${-vp.sx}px,${-vp.sy}px)`;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
mount(model) {
|
|
1723
|
+
this.world.style.width = model.docW + "px";
|
|
1724
|
+
this.world.style.height = model.docH + "px";
|
|
1725
|
+
const by = { world: [], under: [], over: [] };
|
|
1726
|
+
for (const it of model.items) by[it.layer].push(it);
|
|
1727
|
+
if (model.portKey != null) {
|
|
1728
|
+
// inner scroller: its scrolling content sits at the scroller's own place in
|
|
1729
|
+
// the stacking order (above its dialog/panel background, below later UI)
|
|
1730
|
+
const screen = [...by.under, ...by.over].sort((a, b) => a.key - b.key);
|
|
1731
|
+
const frag = document.createDocumentFragment();
|
|
1732
|
+
let placed = false;
|
|
1733
|
+
for (const it of screen) {
|
|
1734
|
+
if (!placed && it.key > model.portKey) {
|
|
1735
|
+
frag.appendChild(this.port);
|
|
1736
|
+
placed = true;
|
|
1737
|
+
}
|
|
1738
|
+
frag.appendChild(this.makeNode(it));
|
|
1739
|
+
}
|
|
1740
|
+
if (!placed) frag.appendChild(this.port);
|
|
1741
|
+
this.over.appendChild(frag);
|
|
1742
|
+
const wl = by.world.sort((a, b) => a.key - b.key);
|
|
1743
|
+
const wf = document.createDocumentFragment();
|
|
1744
|
+
for (const it of wl) wf.appendChild(this.makeNode(it));
|
|
1745
|
+
this.world.appendChild(wf);
|
|
1746
|
+
} else
|
|
1747
|
+
for (const layer of ["under", "world", "over"]) {
|
|
1748
|
+
const list = by[layer].sort((a, b) => a.key - b.key);
|
|
1749
|
+
const frag = document.createDocumentFragment();
|
|
1750
|
+
for (const it of list) frag.appendChild(this.makeNode(it));
|
|
1751
|
+
(layer === "world" ? this.world : this[layer]).appendChild(frag);
|
|
1752
|
+
}
|
|
1753
|
+
this.backdrops = (model.backdrops || []).map((b) => {
|
|
1754
|
+
const d = document.createElement("div");
|
|
1755
|
+
d.className = "bd";
|
|
1756
|
+
const rgb = (b.color.match(/[\d.]+/g) || [255, 255, 255]).slice(0, 3).join(",");
|
|
1757
|
+
d.setAttribute("style", `left:${b.x}px;top:${b.y}px;width:${b.w}px;height:${b.h}px;background:rgba(${rgb},.82);`);
|
|
1758
|
+
this.over.insertBefore(d, this.over.firstChild);
|
|
1759
|
+
return d;
|
|
1760
|
+
});
|
|
1761
|
+
this.bdStart = this.env.geom().sy;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
makeNode(it) {
|
|
1765
|
+
const n = document.createElement("div");
|
|
1766
|
+
n.style.display = "none";
|
|
1767
|
+
it.node = n;
|
|
1768
|
+
if (it.kind === "rect") {
|
|
1769
|
+
n.className = "it r";
|
|
1770
|
+
const f = document.createElement("div");
|
|
1771
|
+
f.className = "f";
|
|
1772
|
+
n.appendChild(f);
|
|
1773
|
+
} else if (it.kind === "image") {
|
|
1774
|
+
n.className = "it m";
|
|
1775
|
+
n.innerHTML = IMG_ICON;
|
|
1776
|
+
} else if (it.kind === "text") {
|
|
1777
|
+
n.className = "it t";
|
|
1778
|
+
prepText(it);
|
|
1779
|
+
} else if (it.kind === "line") {
|
|
1780
|
+
n.className = "it l";
|
|
1781
|
+
n.innerHTML = '<svg width="1" height="1"><path fill="none"/></svg>';
|
|
1782
|
+
}
|
|
1783
|
+
if (it.op < 0.999) n.style.opacity = it.op;
|
|
1784
|
+
if (it.blend) n.style.mixBlendMode = it.blend;
|
|
1785
|
+
return n;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
renderItem(it) {
|
|
1789
|
+
const tw = it.twinOf;
|
|
1790
|
+
if (tw && it._tv !== tw.v) {
|
|
1791
|
+
it._tv = tw.v;
|
|
1792
|
+
it.vis = tw.vis;
|
|
1793
|
+
it.shown = tw.total ? Math.round((tw.shown / tw.total) * it.total) : it.total;
|
|
1794
|
+
it.dx = tw.dx;
|
|
1795
|
+
it.dy = tw.dy;
|
|
1796
|
+
it.scale = tw.scale;
|
|
1797
|
+
it.done = tw.done;
|
|
1798
|
+
it.v++;
|
|
1799
|
+
}
|
|
1800
|
+
if (it.rv === it.v) return;
|
|
1801
|
+
it.rv = it.v;
|
|
1802
|
+
const n = it.node;
|
|
1803
|
+
if (!it.vis) {
|
|
1804
|
+
if (n.style.display !== "none") n.style.display = "none";
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (n.style.display === "none") n.style.display = "";
|
|
1808
|
+
if (it.settle) {
|
|
1809
|
+
it.settle = false;
|
|
1810
|
+
n.classList.add("settle");
|
|
1811
|
+
clearTimeout(n._st);
|
|
1812
|
+
n._st = setTimeout(() => n.classList.remove("settle"), 340);
|
|
1813
|
+
}
|
|
1814
|
+
if (it.kind === "line") {
|
|
1815
|
+
n.style.transform = `translate(${it.dx}px,${it.dy}px)`;
|
|
1816
|
+
const pts = it.pts.slice(0, it.placed);
|
|
1817
|
+
const d = pts.map((p, i) => `${i ? "L" : "M"}${p.x} ${p.y}`).join(" ") + (it.shut ? " Z" : "");
|
|
1818
|
+
const path = n.firstChild.firstChild;
|
|
1819
|
+
path.setAttribute("d", d);
|
|
1820
|
+
path.setAttribute("stroke", it.stroke);
|
|
1821
|
+
path.setAttribute("stroke-width", it.sw);
|
|
1822
|
+
path.setAttribute("stroke-linecap", "square");
|
|
1823
|
+
if (it.dash) path.setAttribute("stroke-dasharray", it.dash);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const w = it.draw ? it.draw.w : it.w;
|
|
1827
|
+
const h = it.draw ? it.draw.h : it.h;
|
|
1828
|
+
let tf = `translate(${it.x + it.dx}px,${it.y + it.dy}px)`;
|
|
1829
|
+
if (it.scale !== 1) tf += ` scale(${it.scale})`;
|
|
1830
|
+
n.style.transform = tf;
|
|
1831
|
+
n.style.width = w + "px";
|
|
1832
|
+
n.style.height = h + "px";
|
|
1833
|
+
const c = it.clip;
|
|
1834
|
+
n.style.clipPath =
|
|
1835
|
+
it.done && c ? `inset(${Math.max(0, c.t - it.y)}px ${Math.max(0, it.x + it.w - c.r)}px ${Math.max(0, it.y + it.h - c.b)}px ${Math.max(0, c.l - it.x)}px)` : "";
|
|
1836
|
+
|
|
1837
|
+
if (it.kind === "rect") {
|
|
1838
|
+
const css = it.mode === "master" ? it.copyOf.css : it.css;
|
|
1839
|
+
if (n._css !== css) {
|
|
1840
|
+
n.firstChild.setAttribute("style", css);
|
|
1841
|
+
n._css = css;
|
|
1842
|
+
}
|
|
1843
|
+
n.classList.toggle("st", !!it.styled);
|
|
1844
|
+
n.style.borderRadius = it.styled ? (it.mode === "master" ? it.copyOf.radius : it.radius) : "0px";
|
|
1845
|
+
} else if (it.kind === "image") {
|
|
1846
|
+
const src = it.mode === "master" ? it.copyOf : it;
|
|
1847
|
+
if (n._src !== src) {
|
|
1848
|
+
const old = n.querySelector(".md");
|
|
1849
|
+
const m = src.media.cloneNode(true);
|
|
1850
|
+
m.classList.add("md");
|
|
1851
|
+
if (old) {
|
|
1852
|
+
m.classList.add("swap");
|
|
1853
|
+
old.replaceWith(m);
|
|
1854
|
+
} else n.appendChild(m);
|
|
1855
|
+
if (m.localName === "video") {
|
|
1856
|
+
m.muted = true;
|
|
1857
|
+
const t = parseFloat(m.getAttribute("data-t"));
|
|
1858
|
+
if (t > 0) m.addEventListener("loadedmetadata", () => (m.currentTime = t), { once: true });
|
|
1859
|
+
if (m.hasAttribute("data-play")) m.play().catch(() => {});
|
|
1860
|
+
}
|
|
1861
|
+
n._src = src;
|
|
1862
|
+
if (src.frame) {
|
|
1863
|
+
let fr = n.querySelector(".fr");
|
|
1864
|
+
if (!fr) {
|
|
1865
|
+
fr = document.createElement("div");
|
|
1866
|
+
fr.className = "fr";
|
|
1867
|
+
n.appendChild(fr);
|
|
1868
|
+
}
|
|
1869
|
+
fr.setAttribute("style", src.frame + `border-radius:${src.radius};`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
n.classList.toggle("st", !!it.styled);
|
|
1873
|
+
n.style.borderRadius = it.styled ? src.radius : "0px";
|
|
1874
|
+
n.style.boxShadow = it.styled ? src.shadow : "";
|
|
1875
|
+
n.style.backgroundColor = it.styled && src.bg ? src.bg : "";
|
|
1876
|
+
} else if (it.kind === "text") {
|
|
1877
|
+
let content;
|
|
1878
|
+
if (it.mode === "master") {
|
|
1879
|
+
if (!it._mv) it._mv = it.copyOf.full.cloneNode(true);
|
|
1880
|
+
content = it._mv;
|
|
1881
|
+
} else content = it.live;
|
|
1882
|
+
if (n.firstChild !== content) {
|
|
1883
|
+
n.textContent = "";
|
|
1884
|
+
n.appendChild(content);
|
|
1885
|
+
}
|
|
1886
|
+
if (it.mode !== "master" && it.rshown !== it.shown) {
|
|
1887
|
+
it._last = reveal(it, it.shown);
|
|
1888
|
+
it.rshown = it.shown;
|
|
1889
|
+
}
|
|
1890
|
+
content.classList.toggle("sa", !!it.selAll);
|
|
1891
|
+
const wantCaret = it.editing && !it.selAll && it.mode !== "master";
|
|
1892
|
+
if (wantCaret) {
|
|
1893
|
+
if (!it._caret) {
|
|
1894
|
+
it._caret = document.createElement("span");
|
|
1895
|
+
it._caret.className = "caret";
|
|
1896
|
+
}
|
|
1897
|
+
const last = it._last;
|
|
1898
|
+
if (last && last.parentNode) last.parentNode.insertBefore(it._caret, last.nextSibling);
|
|
1899
|
+
else content.insertBefore(it._caret, content.firstChild);
|
|
1900
|
+
} else if (it._caret && it._caret.parentNode) it._caret.remove();
|
|
1901
|
+
if (it.scale !== 1) {
|
|
1902
|
+
const ox = it.tb.x - it.x + (it.align === "center" ? it.tb.w / 2 : it.align === "right" || it.align === "end" ? it.tb.w : 0);
|
|
1903
|
+
n.style.transformOrigin = `${ox}px ${it.tb.y - it.y}px`;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
renderCursors(E) {
|
|
1909
|
+
const vp = E.vp;
|
|
1910
|
+
const seen = new Set();
|
|
1911
|
+
for (const c of E.cursors) {
|
|
1912
|
+
let el = this.cursorEls.get(c.id);
|
|
1913
|
+
if (!el) {
|
|
1914
|
+
el = document.createElement("div");
|
|
1915
|
+
el.className = "cursor";
|
|
1916
|
+
el.innerHTML = '<svg width="28" height="28" viewBox="0 0 28 28"></svg>';
|
|
1917
|
+
this.cursorsEl.appendChild(el);
|
|
1918
|
+
this.cursorEls.set(c.id, el);
|
|
1919
|
+
el._shape = "";
|
|
1920
|
+
}
|
|
1921
|
+
seen.add(c.id);
|
|
1922
|
+
const X = c.sticky ? c.x : c.x - vp.sx + vp.ox;
|
|
1923
|
+
const Y = c.sticky ? c.y : c.y - vp.sy + vp.oy;
|
|
1924
|
+
el.style.transform = `translate(${f1(X)}px,${f1(Y)}px)`;
|
|
1925
|
+
const svg = el.firstChild;
|
|
1926
|
+
if (el._shape !== c.shape) {
|
|
1927
|
+
svg.innerHTML = cursorSVG(c.shape, c.color);
|
|
1928
|
+
el._shape = c.shape;
|
|
1929
|
+
const [hx, hy] = HOT[c.shape];
|
|
1930
|
+
el._hx = hx;
|
|
1931
|
+
el._hy = hy;
|
|
1932
|
+
svg.style.transformOrigin = `${hx}px ${hy}px`;
|
|
1933
|
+
}
|
|
1934
|
+
const s = 1 - c.press * 0.12;
|
|
1935
|
+
svg.style.transform = `translate(${-el._hx}px,${-el._hy}px) rotate(${f1(c.angle)}deg) scale(${s})`;
|
|
1936
|
+
}
|
|
1937
|
+
for (const [id, el] of this.cursorEls)
|
|
1938
|
+
if (!seen.has(id)) {
|
|
1939
|
+
el.remove();
|
|
1940
|
+
this.cursorEls.delete(id);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
renderOverlay(E) {
|
|
1945
|
+
const vp = E.vp;
|
|
1946
|
+
const W = this.W;
|
|
1947
|
+
const H = this.H;
|
|
1948
|
+
const X = (x, st) => f1(st ? x : x - vp.sx + vp.ox);
|
|
1949
|
+
const Y = (y, st) => f1(st ? y : y - vp.sy + vp.oy);
|
|
1950
|
+
let s = "";
|
|
1951
|
+
let b = "";
|
|
1952
|
+
for (const g of E.guides) {
|
|
1953
|
+
if (!g.vis || g.op <= 0) continue;
|
|
1954
|
+
const sw = 1 + g.flash;
|
|
1955
|
+
const flt = g.flash > 0 ? ` style="filter:drop-shadow(0 0 ${f1(2 + g.flash * 3)}px ${RED})"` : "";
|
|
1956
|
+
if (g.axis === "x") {
|
|
1957
|
+
const x = X(g.cur);
|
|
1958
|
+
s += `<line x1="${x}" x2="${x}" y1="0" y2="${H}" stroke="${RED}" stroke-width="${sw}" opacity="${f1(g.op)}"${flt}/>`;
|
|
1959
|
+
} else {
|
|
1960
|
+
const y = Y(g.cur);
|
|
1961
|
+
s += `<line x1="0" x2="${W}" y1="${y}" y2="${y}" stroke="${RED}" stroke-width="${sw}" opacity="${f1(g.op)}"${flt}/>`;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
for (const it of E.editLines) {
|
|
1965
|
+
const st = it.layer !== "world";
|
|
1966
|
+
const pts = it.pts.slice(0, it.placed).map((p) => [X(p.x + it.dx, st), Y(p.y + it.dy, st)]);
|
|
1967
|
+
const o = it.owner;
|
|
1968
|
+
const all = o ? [...pts, [X(o.x, o.sticky), Y(o.y, o.sticky)]] : pts;
|
|
1969
|
+
s += `<polyline points="${all.map((p) => p.join(",")).join(" ")}${it.shut && pts[0] ? " " + pts[0].join(",") : ""}" fill="none" stroke="${BLUE}" stroke-width="1.5"/>`;
|
|
1970
|
+
for (const p of pts) s += `<circle cx="${p[0]}" cy="${p[1]}" r="3.6" fill="#fff" stroke="${BLUE}" stroke-width="1.5"/>`;
|
|
1971
|
+
}
|
|
1972
|
+
const rt = E.rulers.t;
|
|
1973
|
+
for (const c of E.cursors) {
|
|
1974
|
+
if (!c.ov) continue;
|
|
1975
|
+
const st = c.sticky;
|
|
1976
|
+
for (const v of c.ov()) {
|
|
1977
|
+
if (v.t === "box") {
|
|
1978
|
+
const x = X(v.b.x, st);
|
|
1979
|
+
const y = Y(v.b.y, st);
|
|
1980
|
+
const w = f1(v.b.w);
|
|
1981
|
+
const h = f1(v.b.h);
|
|
1982
|
+
const col = v.color === "red" ? RED : BLUE;
|
|
1983
|
+
s += `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="${col}" stroke-width="1"/>`;
|
|
1984
|
+
if (v.underline && v.underline.w > 0) {
|
|
1985
|
+
const u = v.underline;
|
|
1986
|
+
s += `<line x1="${X(u.x, st)}" x2="${X(u.x + u.w, st)}" y1="${Y(u.y, st)}" y2="${Y(u.y, st)}" stroke="${BLUE}" stroke-width="1.5"/>`;
|
|
1987
|
+
}
|
|
1988
|
+
if (v.rulerLines && rt > 0.05)
|
|
1989
|
+
s += `<g stroke="${BLUE}" stroke-width="1" stroke-dasharray="2 2" opacity="${f1(rt)}"><line x1="${RULER}" x2="${x}" y1="${f1(y + h / 2)}" y2="${f1(y + h / 2)}"/><line x1="${f1(x + w / 2)}" x2="${f1(x + w / 2)}" y1="${RULER}" y2="${y}"/></g>`;
|
|
1990
|
+
if (v.handles)
|
|
1991
|
+
for (const [hx, hy] of [[x, y], [x + w, y], [x, y + h], [x + w, y + h]])
|
|
1992
|
+
s += `<rect x="${f1(hx - 3.5)}" y="${f1(hy - 3.5)}" width="7" height="7" fill="#fff" stroke="${col}" stroke-width="1"/>`;
|
|
1993
|
+
if (v.badge && w > 0) b += `<div class="badge" style="left:${f1(x + w / 2)}px;top:${f1(y + h + 8)}px;background:${col}">${v.badge}</div>`;
|
|
1994
|
+
} else if (v.t === "measure") {
|
|
1995
|
+
const a = { x: X(v.a.x, st), y: Y(v.a.y, st) };
|
|
1996
|
+
const z = { x: X(v.b.x, st), y: Y(v.b.y, st) };
|
|
1997
|
+
s += `<line x1="${a.x}" y1="${a.y}" x2="${z.x}" y2="${z.y}" stroke="${RED}" stroke-width="1"/>`;
|
|
1998
|
+
const horiz = a.y === z.y;
|
|
1999
|
+
b += `<div class="badge" style="left:${f1(horiz ? (a.x + z.x) / 2 : a.x + 18)}px;top:${f1(horiz ? a.y + 8 : (a.y + z.y) / 2 - 9)}px;background:${RED}">${v.label}</div>`;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (s !== this._ovS) {
|
|
2004
|
+
this.ov.innerHTML = s;
|
|
2005
|
+
this._ovS = s;
|
|
2006
|
+
}
|
|
2007
|
+
if (b !== this._ovB) {
|
|
2008
|
+
this.badges.innerHTML = b;
|
|
2009
|
+
this._ovB = b;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
renderRulers(E) {
|
|
2014
|
+
const t = E.rulers.t;
|
|
2015
|
+
if (t <= 0.001) {
|
|
2016
|
+
if (this._rulersOn) {
|
|
2017
|
+
this.rulers.innerHTML = "";
|
|
2018
|
+
this._rulersOn = false;
|
|
2019
|
+
}
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
this._rulersOn = true;
|
|
2023
|
+
const vp = E.vp;
|
|
2024
|
+
const W = this.W;
|
|
2025
|
+
const H = this.H;
|
|
2026
|
+
const step = 100;
|
|
2027
|
+
let u = "";
|
|
2028
|
+
let a = "";
|
|
2029
|
+
const x0 = Math.floor((vp.sx - vp.ox) / step) * step;
|
|
2030
|
+
for (let c = x0; c - vp.sx + vp.ox < W; c += step / 2) {
|
|
2031
|
+
const x = f1(c - vp.sx + vp.ox);
|
|
2032
|
+
const major = c % step === 0;
|
|
2033
|
+
u += `<line x1="${x}" x2="${x}" y1="${major ? 13 : 16}" y2="${RULER}" stroke="#5E5E5E"/>`;
|
|
2034
|
+
if (major) u += `<text x="${f1(x + 3)}" y="11" class="rtext">${c}</text>`;
|
|
2035
|
+
}
|
|
2036
|
+
const y0 = Math.floor((vp.sy - vp.oy) / step) * step;
|
|
2037
|
+
for (let c = y0; c - vp.sy + vp.oy < H; c += step / 2) {
|
|
2038
|
+
const y = f1(c - vp.sy + vp.oy);
|
|
2039
|
+
const major = c % step === 0;
|
|
2040
|
+
a += `<line y1="${y}" y2="${y}" x1="${major ? 13 : 16}" x2="${RULER}" stroke="#5E5E5E"/>`;
|
|
2041
|
+
if (major) a += `<text x="11" y="${f1(y - 3)}" class="rtext" transform="rotate(-90 11 ${f1(y - 3)})">${c}</text>`;
|
|
2042
|
+
}
|
|
2043
|
+
for (const c of E.cursors) {
|
|
2044
|
+
if (!c.ov || c.sticky) continue;
|
|
2045
|
+
for (const v of c.ov()) {
|
|
2046
|
+
if (v.t !== "box" || !v.handles) continue;
|
|
2047
|
+
const l = f1(v.b.x - vp.sx + vp.ox);
|
|
2048
|
+
const r = f1(v.b.x + v.b.w - vp.sx + vp.ox);
|
|
2049
|
+
const tp = f1(v.b.y - vp.sy + vp.oy);
|
|
2050
|
+
const bt = f1(v.b.y + v.b.h - vp.sy + vp.oy);
|
|
2051
|
+
u = `<rect x="${l}" y="0" width="${f1(r - l)}" height="${RULER}" fill="rgba(13,153,255,0.22)"/>` + u + `<text x="${f1(l + 3)}" y="11" class="rtext rsel">${Math.round(v.b.x)}</text>`;
|
|
2052
|
+
a = `<rect y="${tp}" x="0" height="${f1(bt - tp)}" width="${RULER}" fill="rgba(13,153,255,0.22)"/>` + a;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
for (const g of E.guides) {
|
|
2056
|
+
if (!g.dragging) continue;
|
|
2057
|
+
if (g.axis === "x") {
|
|
2058
|
+
const x = f1(g.cur - vp.sx + vp.ox);
|
|
2059
|
+
u += `<line x1="${x}" x2="${x}" y1="0" y2="${RULER}" stroke="${RED}"/><text x="${f1(x + 4)}" y="11" class="rtext" fill="${RED}" style="fill:${RED}">${Math.round(g.cur)}</text>`;
|
|
2060
|
+
} else {
|
|
2061
|
+
const y = f1(g.cur - vp.sy + vp.oy);
|
|
2062
|
+
a += `<line y1="${y}" y2="${y}" x1="0" x2="${RULER}" stroke="${RED}"/>`;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
this.rulers.innerHTML =
|
|
2066
|
+
`<div class="ruler ruler-top" style="transform:translateY(${f1((t - 1) * 100)}%)"><svg width="${W}" height="${RULER}">${u}</svg></div>` +
|
|
2067
|
+
`<div class="ruler ruler-left" style="transform:translateX(${f1((t - 1) * 100)}%)"><svg width="${RULER}" height="${H}">${a}</svg></div>` +
|
|
2068
|
+
`<div class="ruler-corner" style="opacity:${f1(t)}"></div>`;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
render(E) {
|
|
2072
|
+
this.syncWorld();
|
|
2073
|
+
if (this.backdrops && this.backdrops.length) {
|
|
2074
|
+
const o = f1(clamp((Math.abs(E.vp.sy - this.bdStart) - 8) / 80, 0, 1));
|
|
2075
|
+
if (o !== this._bdO) {
|
|
2076
|
+
this._bdO = o;
|
|
2077
|
+
for (const d of this.backdrops) d.style.opacity = o;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (E.items) for (const it of E.items) this.renderItem(it);
|
|
2081
|
+
this.renderOverlay(E);
|
|
2082
|
+
this.renderCursors(E);
|
|
2083
|
+
this.renderRulers(E);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// ----------------------------------------------------------------- engine
|
|
2088
|
+
|
|
2089
|
+
class Engine {
|
|
2090
|
+
constructor(view) {
|
|
2091
|
+
this.view = view;
|
|
2092
|
+
this.vp = view.vp;
|
|
2093
|
+
this.items = null;
|
|
2094
|
+
this.tasks = [];
|
|
2095
|
+
this.groups = [];
|
|
2096
|
+
this.guides = [];
|
|
2097
|
+
this.editLines = new Set();
|
|
2098
|
+
this.cursors = [];
|
|
2099
|
+
this.started = false;
|
|
2100
|
+
this.ready = false;
|
|
2101
|
+
this.frontier = 0;
|
|
2102
|
+
this.vel = 0;
|
|
2103
|
+
this.tempo = 1;
|
|
2104
|
+
this.speed = 1;
|
|
2105
|
+
this.time = 0;
|
|
2106
|
+
this.rulers = { want: 0, t: 0, lastNeed: -10 };
|
|
2107
|
+
this.lastScroll = null;
|
|
2108
|
+
this.nextId = 0;
|
|
2109
|
+
this.spawnCooldown = 0;
|
|
2110
|
+
this.flocking = false;
|
|
2111
|
+
this.lastT = null;
|
|
2112
|
+
this.tv = { x: 0, y: 0 };
|
|
2113
|
+
this.mouse = null;
|
|
2114
|
+
this.retired = 0;
|
|
2115
|
+
this.doneCount = 0;
|
|
2116
|
+
this.waitCount = 0;
|
|
2117
|
+
const vp = this.vp;
|
|
2118
|
+
for (let n = 0; n < INTRO_AGENTS; n++) {
|
|
2119
|
+
const side = n % 2 === 0;
|
|
2120
|
+
const p = this.fromViewport(side ? vp.ox - 40 - rand(0, 200) : vp.ox + vp.vw + 40 + rand(0, 200), vp.oy + rand(0.05, 0.95) * vp.vh);
|
|
2121
|
+
const c = this.makeCursor(p.x, p.y, n < MIN_AGENTS);
|
|
2122
|
+
c.follow = true;
|
|
2123
|
+
this.cursors.push(c);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
setModel(m) {
|
|
2128
|
+
this.items = m.items;
|
|
2129
|
+
this.tasks = m.tasks;
|
|
2130
|
+
this.groups = m.groups;
|
|
2131
|
+
this.guides = m.guides;
|
|
2132
|
+
this.guideDeps = new Map(m.guides.map((g) => [g, g.deps || []]));
|
|
2133
|
+
this.waitCount = m.tasks.length;
|
|
2134
|
+
this.ready = true;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
makeCursor(x, y, home = false) {
|
|
2138
|
+
const id = this.nextId++;
|
|
2139
|
+
const fast = Math.random() < 0.25;
|
|
2140
|
+
return {
|
|
2141
|
+
id, x, y, home,
|
|
2142
|
+
name: NAMES[id % NAMES.length],
|
|
2143
|
+
color: COLORS[id % COLORS.length],
|
|
2144
|
+
shape: "arrow", press: 0, task: null, gen: null, ov: null,
|
|
2145
|
+
idleFor: 0, leaving: false, worked: false, sticky: false,
|
|
2146
|
+
vx: 0, vy: 0, angle: 0,
|
|
2147
|
+
speed: fast ? rand(1000, 1500) : rand(300, 750),
|
|
2148
|
+
force: fast ? rand(3500, 6000) : rand(700, 1800),
|
|
2149
|
+
spin: fast ? rand(2.2, 3.4) : rand(0.8, 1.8),
|
|
2150
|
+
curl: rand(-1, 1), orbit: rand(70, 170), follow: false,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
get viewTop() {
|
|
2155
|
+
return this.vp.sy;
|
|
2156
|
+
}
|
|
2157
|
+
get viewBot() {
|
|
2158
|
+
return this.vp.sy + this.vp.vh;
|
|
2159
|
+
}
|
|
2160
|
+
fromViewport(x, y) {
|
|
2161
|
+
const vp = this.vp;
|
|
2162
|
+
return { x: x - vp.ox + vp.sx, y: y - vp.oy + vp.sy };
|
|
2163
|
+
}
|
|
2164
|
+
taskY(t) {
|
|
2165
|
+
return t.layer === "world" ? t.y : this.viewTop + t.y;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// screen position of a cursor / an item (for panning and loudness)
|
|
2169
|
+
scr(c) {
|
|
2170
|
+
const vp = this.vp;
|
|
2171
|
+
return c.sticky ? { x: c.x, y: c.y } : { x: c.x - vp.sx + vp.ox, y: c.y - vp.sy + vp.oy };
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
sfxPlace(kind, it, b) {
|
|
2175
|
+
if (!this.sfx) return;
|
|
2176
|
+
const vp = this.vp;
|
|
2177
|
+
b = b || this.bounds(it);
|
|
2178
|
+
let x = b.x + b.w / 2;
|
|
2179
|
+
let y = b.y + b.h / 2;
|
|
2180
|
+
if (!it || it.layer === "world") {
|
|
2181
|
+
x += vp.ox - vp.sx;
|
|
2182
|
+
y += vp.oy - vp.sy;
|
|
2183
|
+
}
|
|
2184
|
+
this.sfx.place(kind, x, y, b.w, b.h, vp.vw, vp.vh, (it && it.fs) || 0);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
start() {
|
|
2188
|
+
if (this.started || !this.ready) return;
|
|
2189
|
+
this.started = true;
|
|
2190
|
+
this.startTime = this.time;
|
|
2191
|
+
for (const c of this.cursors) {
|
|
2192
|
+
c.follow = false;
|
|
2193
|
+
c.gen = null;
|
|
2194
|
+
c.angle = ((((c.angle + 180) % 360) + 360) % 360) - 180;
|
|
2195
|
+
}
|
|
2196
|
+
if (this.sfx) this.sfx.start();
|
|
2197
|
+
if (this.onStart) this.onStart();
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
bounds(it) {
|
|
2201
|
+
if (it.kind === "text") {
|
|
2202
|
+
let h = it.tb.h;
|
|
2203
|
+
if (it.editing && !it.selAll && it.total) {
|
|
2204
|
+
const lines = Math.max(1, Math.round(it.tb.h / it.lh));
|
|
2205
|
+
h = Math.min(it.tb.h, Math.max(it.lh, Math.ceil((it.shown / it.total) * lines) * it.lh));
|
|
2206
|
+
}
|
|
2207
|
+
const s = it.scale;
|
|
2208
|
+
const ax = it.align === "center" ? it.tb.w * (1 - s) / 2 : it.align === "right" || it.align === "end" ? it.tb.w * (1 - s) : 0;
|
|
2209
|
+
return { x: it.tb.x + it.dx + ax, y: it.tb.y + it.dy, w: it.tb.w * s, h: h * s };
|
|
2210
|
+
}
|
|
2211
|
+
if (it.kind === "line") {
|
|
2212
|
+
return { x: it.x + it.dx, y: it.y + it.dy, w: it.w, h: it.h };
|
|
2213
|
+
}
|
|
2214
|
+
return {
|
|
2215
|
+
x: it.x + it.dx,
|
|
2216
|
+
y: it.y + it.dy,
|
|
2217
|
+
w: it.draw ? it.draw.w : it.w,
|
|
2218
|
+
h: it.draw ? it.draw.h : it.h,
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
unionBounds(list) {
|
|
2223
|
+
const bs = list.map((it) => this.bounds(it));
|
|
2224
|
+
const x = Math.min(...bs.map((b) => b.x));
|
|
2225
|
+
const y = Math.min(...bs.map((b) => b.y));
|
|
2226
|
+
return { x, y, w: Math.max(...bs.map((b) => b.x + b.w)) - x, h: Math.max(...bs.map((b) => b.y + b.h)) - y };
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
underline(it, b) {
|
|
2230
|
+
return { x: b.x, y: b.y + (it.lh * 0.5 + it.fs * 0.42) * it.scale, w: Math.min(b.w, it.tb.w * it.scale), h: 0 };
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
selection(it, extra) {
|
|
2234
|
+
const b = this.bounds(it);
|
|
2235
|
+
return Object.assign(
|
|
2236
|
+
{
|
|
2237
|
+
t: "box",
|
|
2238
|
+
b,
|
|
2239
|
+
handles: true,
|
|
2240
|
+
badge: `${Math.round(b.w)} × ${Math.round(b.h)}`,
|
|
2241
|
+
rulerLines: true,
|
|
2242
|
+
underline: it.kind === "text" ? this.underline(it, b) : undefined,
|
|
2243
|
+
},
|
|
2244
|
+
extra,
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
available() {
|
|
2249
|
+
const out = [];
|
|
2250
|
+
for (const t of this.tasks) {
|
|
2251
|
+
if (t.state !== "wait") continue;
|
|
2252
|
+
if (t.layer === "world" && t.gate > this.frontier) continue;
|
|
2253
|
+
let ok = true;
|
|
2254
|
+
for (const d of t.deps)
|
|
2255
|
+
if (d.state !== "done") {
|
|
2256
|
+
ok = false;
|
|
2257
|
+
break;
|
|
2258
|
+
}
|
|
2259
|
+
if (ok) out.push(t);
|
|
2260
|
+
}
|
|
2261
|
+
return out;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
pick(c, list) {
|
|
2265
|
+
let best = null;
|
|
2266
|
+
let bestS = Infinity;
|
|
2267
|
+
const top = this.viewTop - 40;
|
|
2268
|
+
const bot = this.viewBot;
|
|
2269
|
+
const cy = c.sticky ? c.y + this.viewTop : c.y;
|
|
2270
|
+
for (const t of list) {
|
|
2271
|
+
const y = this.taskY(t);
|
|
2272
|
+
const u = y >= top && y <= bot ? 0 : y < top ? 1 : 2;
|
|
2273
|
+
const s = u * 1e6 + (u === 1 ? top - y : y) * 0.6 + Math.abs(y - cy) * 0.4 - (t.spec.type === "guide" ? 5e5 : 0);
|
|
2274
|
+
if (s < bestS) {
|
|
2275
|
+
bestS = s;
|
|
2276
|
+
best = t;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
return best;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// work that's unlocked but unfinished (things past the frontier need scrolling, not waiting)
|
|
2283
|
+
backlog() {
|
|
2284
|
+
let n = 0;
|
|
2285
|
+
for (const t of this.tasks) if (t.state !== "done" && (t.layer !== "world" || t.gate <= this.frontier)) n++;
|
|
2286
|
+
return n;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
step(dt) {
|
|
2290
|
+
dt = Math.min(dt, 0.05);
|
|
2291
|
+
this.time += dt;
|
|
2292
|
+
const vp = (this.vp = this.view.vp);
|
|
2293
|
+
const sv = Math.abs(vp.sy - (this.lastScroll == null ? vp.sy : this.lastScroll)) / Math.max(dt, 0.001);
|
|
2294
|
+
this.lastScroll = vp.sy;
|
|
2295
|
+
this.vel = Math.max(this.vel * Math.exp(-dt * 1.2), this.vel * 0.7 + sv * 0.3);
|
|
2296
|
+
if (this.started) this.frontier = Math.max(this.frontier, this.viewBot);
|
|
2297
|
+
const avail = this.started ? this.available() : [];
|
|
2298
|
+
this.tempo = clamp(2.1 + this.vel / 900 + (avail.length / Math.max(1, this.cursors.length)) * 0.5, 2.1, 4.5) * this.speed;
|
|
2299
|
+
|
|
2300
|
+
if (this.rulers.want > 0) this.rulers.lastNeed = this.time;
|
|
2301
|
+
const rt = this.time - this.rulers.lastNeed < 0.7 ? 1 : 0;
|
|
2302
|
+
this.rulers.t += (rt - this.rulers.t) * Math.min(1, dt * 10);
|
|
2303
|
+
if (Math.abs(rt - this.rulers.t) < 0.002) this.rulers.t = rt;
|
|
2304
|
+
|
|
2305
|
+
for (const g of this.guides) {
|
|
2306
|
+
if (!g.vis) continue;
|
|
2307
|
+
if (g.flash > 0) g.flash = Math.max(0, g.flash - dt * 1.5);
|
|
2308
|
+
const deps = this.guideDeps.get(g) || [];
|
|
2309
|
+
if (g.shownAt == null) g.shownAt = this.time;
|
|
2310
|
+
if (!g.dragging && g.op > 0 && this.time - g.shownAt > 7 && !deps.some((t) => t.state === "active")) g.op = Math.max(0, g.op - dt * 1.2);
|
|
2311
|
+
else if (!g.dragging && deps.every((t) => t.state === "done")) {
|
|
2312
|
+
const last = Math.max(0, ...deps.map((t) => t.doneAt));
|
|
2313
|
+
if (this.time - last > 1.2 && g.op > 0) g.op = Math.max(0, g.op - dt * 1.2);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
if (!this.flocking && this.started && this.waitCount === 0) {
|
|
2318
|
+
for (const c of this.cursors)
|
|
2319
|
+
if (!c.task && !c.follow) {
|
|
2320
|
+
c.follow = true;
|
|
2321
|
+
c.gen = null;
|
|
2322
|
+
c.leaving = false;
|
|
2323
|
+
c.ov = null;
|
|
2324
|
+
c.shape = "arrow";
|
|
2325
|
+
}
|
|
2326
|
+
if (this.cursors.every((c) => c.follow)) {
|
|
2327
|
+
this.flocking = true;
|
|
2328
|
+
this.endTime = this.time;
|
|
2329
|
+
if (this.onComplete) this.onComplete();
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
if (this.flocking) {
|
|
2333
|
+
this.flock(dt);
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
if (!this.started) {
|
|
2337
|
+
const center = this.fromViewport(vp.ox + vp.vw / 2, vp.oy + vp.vh / 2);
|
|
2338
|
+
if (this.follow(dt, this.cursors, center, 0.7) && this.ready) this.start();
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (this.cursors.some((c) => c.follow))
|
|
2342
|
+
this.follow(
|
|
2343
|
+
dt,
|
|
2344
|
+
this.cursors.filter((c) => c.follow),
|
|
2345
|
+
);
|
|
2346
|
+
|
|
2347
|
+
this.spawnCooldown -= dt;
|
|
2348
|
+
const want = clamp(MIN_AGENTS + Math.floor(this.vel / 40) + Math.floor(avail.length / 2), MIN_AGENTS, MAX_AGENTS);
|
|
2349
|
+
const active = this.cursors.filter((c) => !c.leaving).length;
|
|
2350
|
+
const busy = this.cursors.filter((c) => c.task).length;
|
|
2351
|
+
if (active < want && avail.length > active - busy && this.spawnCooldown <= 0) {
|
|
2352
|
+
const left = Math.random() < 0.5;
|
|
2353
|
+
const p = this.fromViewport(left ? vp.ox - 40 : vp.ox + vp.vw + 40, vp.oy + rand(0.2, 0.8) * vp.vh);
|
|
2354
|
+
this.cursors.push(this.makeCursor(p.x, p.y));
|
|
2355
|
+
this.spawnCooldown = 0.035;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
for (const c of this.cursors) {
|
|
2359
|
+
if (c.task || c.leaving || c.follow) continue;
|
|
2360
|
+
const t = this.pick(c, avail);
|
|
2361
|
+
if (!t) continue;
|
|
2362
|
+
avail.splice(avail.indexOf(t), 1);
|
|
2363
|
+
t.state = "active";
|
|
2364
|
+
this.waitCount--;
|
|
2365
|
+
c.task = t;
|
|
2366
|
+
c.worked = true;
|
|
2367
|
+
c.idleFor = 0;
|
|
2368
|
+
if (t.layer !== "world") {
|
|
2369
|
+
c.sticky = true;
|
|
2370
|
+
c.x += vp.ox - vp.sx;
|
|
2371
|
+
c.y += vp.oy - vp.sy;
|
|
2372
|
+
}
|
|
2373
|
+
c.gen = this.run(c, t);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
for (const c of [...this.cursors]) {
|
|
2377
|
+
if (c.follow) continue;
|
|
2378
|
+
c.press = Math.max(0, c.press - dt * 6);
|
|
2379
|
+
if (c.angle !== 0) c.angle = Math.abs(c.angle) < 0.5 ? 0 : c.angle * (1 - Math.min(1, dt * 10));
|
|
2380
|
+
if (!c.task) {
|
|
2381
|
+
c.idleFor += dt;
|
|
2382
|
+
if (!c.leaving && c.idleFor > 2.5 && this.cursors.filter((m) => !m.leaving).length > MIN_AGENTS && !c.home) {
|
|
2383
|
+
c.leaving = true;
|
|
2384
|
+
c.gen = this.leave(c);
|
|
2385
|
+
}
|
|
2386
|
+
if (!c.gen) c.gen = this.idle(c);
|
|
2387
|
+
}
|
|
2388
|
+
if (c.gen.next(dt).done) {
|
|
2389
|
+
if (c.task) {
|
|
2390
|
+
c.task.state = "done";
|
|
2391
|
+
c.task.doneAt = this.time;
|
|
2392
|
+
this.doneCount++;
|
|
2393
|
+
c.task = null;
|
|
2394
|
+
c.ov = null;
|
|
2395
|
+
if (c.sticky) {
|
|
2396
|
+
c.sticky = false;
|
|
2397
|
+
c.x -= vp.ox - vp.sx;
|
|
2398
|
+
c.y -= vp.oy - vp.sy;
|
|
2399
|
+
}
|
|
2400
|
+
c.shape = "arrow";
|
|
2401
|
+
}
|
|
2402
|
+
c.gen = null;
|
|
2403
|
+
if (c.leaving) {
|
|
2404
|
+
if (c.worked) this.retired++;
|
|
2405
|
+
this.cursors.splice(this.cursors.indexOf(c), 1);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
flockTarget() {
|
|
2412
|
+
if (this.mouse) return this.fromViewport(this.mouse.x, this.mouse.y);
|
|
2413
|
+
const t = this.time;
|
|
2414
|
+
const vp = this.vp;
|
|
2415
|
+
return this.fromViewport(vp.ox + vp.vw * (0.5 + 0.28 * Math.sin(t * 0.37)), vp.oy + vp.vh * (0.5 + 0.25 * Math.sin(t * 0.53 + 1)));
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
flock(dt) {
|
|
2419
|
+
this.spawnCooldown -= dt;
|
|
2420
|
+
if (this.cursors.length < FLOCK_AGENTS && this.spawnCooldown <= 0 && !this.releasing) {
|
|
2421
|
+
const vp = this.vp;
|
|
2422
|
+
const left = Math.random() < 0.5;
|
|
2423
|
+
const p = this.fromViewport(left ? vp.ox - 30 : vp.ox + vp.vw + 30, vp.oy + rand(0.1, 0.9) * vp.vh);
|
|
2424
|
+
const c = this.makeCursor(p.x, p.y);
|
|
2425
|
+
c.follow = true;
|
|
2426
|
+
this.cursors.push(c);
|
|
2427
|
+
this.spawnCooldown = 0.04;
|
|
2428
|
+
}
|
|
2429
|
+
if (this.releasing) {
|
|
2430
|
+
for (const c of [...this.cursors]) {
|
|
2431
|
+
if (!c.gen) c.gen = this.leave(c);
|
|
2432
|
+
if (c.gen.next(dt).done) this.cursors.splice(this.cursors.indexOf(c), 1);
|
|
2433
|
+
}
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
this.follow(dt, this.cursors);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// Boids-ish orbit around a target, ported from the original.
|
|
2440
|
+
follow(dt, list, target, scale = 1) {
|
|
2441
|
+
const m = this.mouse ? this.fromViewport(this.mouse.x, this.mouse.y) : null;
|
|
2442
|
+
const o = target || this.flockTarget();
|
|
2443
|
+
const time = this.time;
|
|
2444
|
+
if (m && this.lastT && dt > 0) {
|
|
2445
|
+
const k = Math.min(1, dt * 18);
|
|
2446
|
+
this.tv.x += ((m.x - this.lastT.x) / dt - this.tv.x) * k;
|
|
2447
|
+
this.tv.y += ((m.y - this.lastT.y) / dt - this.tv.y) * k;
|
|
2448
|
+
}
|
|
2449
|
+
this.lastT = m;
|
|
2450
|
+
const R = 135 * scale * (1 + 0.18 * Math.sin(time * 0.55));
|
|
2451
|
+
const swirl = Math.tanh(Math.cos(time * 0.11) * 3);
|
|
2452
|
+
const want = [];
|
|
2453
|
+
for (const c of list) {
|
|
2454
|
+
const vx = c.x - o.x;
|
|
2455
|
+
const vy = c.y - o.y;
|
|
2456
|
+
const d = Math.hypot(vx, vy) || 0.01;
|
|
2457
|
+
const nx = vx / d;
|
|
2458
|
+
const ny = vy / d;
|
|
2459
|
+
const ang = Math.atan2(vy, vx);
|
|
2460
|
+
const off = (c.orbit - 120) * 0.55 * scale;
|
|
2461
|
+
const rr = R * (1 + 0.3 * Math.sin(3 * ang + time * 0.7)) + off;
|
|
2462
|
+
const tang = 300 * (0.7 + c.spin * 0.2) * swirl;
|
|
2463
|
+
const rad = clamp((rr - d) * 4, -2600, 1400);
|
|
2464
|
+
const far = Math.min(1, Math.max(0, (d - rr) / 600));
|
|
2465
|
+
want.push([-ny * tang + nx * rad - ny * c.curl * 400 * far, nx * tang + ny * rad + nx * c.curl * 400 * far]);
|
|
2466
|
+
}
|
|
2467
|
+
for (let i = 0; i < list.length; i++) {
|
|
2468
|
+
const c = list[i];
|
|
2469
|
+
let ax = 0;
|
|
2470
|
+
let ay = 0;
|
|
2471
|
+
let wsum = 0;
|
|
2472
|
+
for (let j = 0; j < list.length; j++) {
|
|
2473
|
+
if (j === i) continue;
|
|
2474
|
+
const q = list[j];
|
|
2475
|
+
const d = Math.hypot(q.x - c.x, q.y - c.y);
|
|
2476
|
+
if (d > 90) continue;
|
|
2477
|
+
const w = 1 - d / 90;
|
|
2478
|
+
ax += q.vx * w;
|
|
2479
|
+
ay += q.vy * w;
|
|
2480
|
+
wsum += w;
|
|
2481
|
+
}
|
|
2482
|
+
let [fx, fy] = want[i];
|
|
2483
|
+
if (wsum > 0) {
|
|
2484
|
+
fx = fx * 0.7 + (ax / wsum) * 0.3;
|
|
2485
|
+
fy = fy * 0.7 + (ay / wsum) * 0.3;
|
|
2486
|
+
}
|
|
2487
|
+
const dist = Math.max(0, Math.hypot(c.x - o.x, c.y - o.y) - 180 * scale);
|
|
2488
|
+
const maxV = c.speed * (1.3 + Math.min(3.2, dist / 90));
|
|
2489
|
+
const L = Math.hypot(fx, fy);
|
|
2490
|
+
if (L > maxV) {
|
|
2491
|
+
fx *= maxV / L;
|
|
2492
|
+
fy *= maxV / L;
|
|
2493
|
+
}
|
|
2494
|
+
let gx = (fx - c.vx) * 6;
|
|
2495
|
+
let gy = (fy - c.vy) * 6;
|
|
2496
|
+
const maxF = c.force * (1.4 + Math.min(5, dist / 50));
|
|
2497
|
+
const G = Math.hypot(gx, gy);
|
|
2498
|
+
if (G > maxF) {
|
|
2499
|
+
gx *= maxF / G;
|
|
2500
|
+
gy *= maxF / G;
|
|
2501
|
+
}
|
|
2502
|
+
c.vx += gx * dt;
|
|
2503
|
+
c.vy += gy * dt;
|
|
2504
|
+
c.x += c.vx * dt;
|
|
2505
|
+
c.y += c.vy * dt;
|
|
2506
|
+
}
|
|
2507
|
+
const R0 = 48;
|
|
2508
|
+
let touched = false;
|
|
2509
|
+
if (m)
|
|
2510
|
+
for (const c of list) {
|
|
2511
|
+
const vx = c.x - m.x;
|
|
2512
|
+
const vy = c.y - m.y;
|
|
2513
|
+
const d = Math.hypot(vx, vy) || 0.01;
|
|
2514
|
+
if (d >= R0) continue;
|
|
2515
|
+
touched = true;
|
|
2516
|
+
const nx = vx / d;
|
|
2517
|
+
const ny = vy / d;
|
|
2518
|
+
c.x = m.x + nx * R0;
|
|
2519
|
+
c.y = m.y + ny * R0;
|
|
2520
|
+
const rel = (c.vx - this.tv.x) * nx + (c.vy - this.tv.y) * ny;
|
|
2521
|
+
if (rel < 0) {
|
|
2522
|
+
c.vx -= nx * rel * 1.6;
|
|
2523
|
+
c.vy -= ny * rel * 1.6;
|
|
2524
|
+
}
|
|
2525
|
+
c.vx += nx * 260;
|
|
2526
|
+
c.vy += ny * 260;
|
|
2527
|
+
}
|
|
2528
|
+
for (const c of list) {
|
|
2529
|
+
if (Math.hypot(c.vx, c.vy) < 30) continue;
|
|
2530
|
+
const d = ((((Math.atan2(c.vy, c.vx) * 180) / Math.PI + 128 - c.angle + 540) % 360) + 360) % 360 - 180;
|
|
2531
|
+
c.angle += d * Math.min(1, dt * 8);
|
|
2532
|
+
c.press = Math.max(0, c.press - dt * 6);
|
|
2533
|
+
}
|
|
2534
|
+
return touched;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// ------------------------------------------------------- primitives
|
|
2538
|
+
|
|
2539
|
+
*wait(t) {
|
|
2540
|
+
let n = 0;
|
|
2541
|
+
while (n < t) n += (yield) * this.tempo;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
*moveTo(c, target, o = {}) {
|
|
2545
|
+
const get = typeof target === "function" ? target : () => target;
|
|
2546
|
+
const x0 = c.x;
|
|
2547
|
+
const y0 = c.y;
|
|
2548
|
+
const p0 = get();
|
|
2549
|
+
const dist = Math.hypot(p0.x - x0, p0.y - y0);
|
|
2550
|
+
if (dist < 0.5) {
|
|
2551
|
+
c.x = p0.x;
|
|
2552
|
+
c.y = p0.y;
|
|
2553
|
+
if (o.onStep) o.onStep();
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
const dur = clamp(0.12 + Math.sqrt(dist) * 0.021, 0.14, 0.9) / (o.speed || 1);
|
|
2557
|
+
const bend = (o.bend != null ? o.bend : (Math.random() < 0.5 ? -1 : 1) * rand(0.04, 0.14)) * Math.min(dist, 600);
|
|
2558
|
+
let t = 0;
|
|
2559
|
+
while (t < dur) {
|
|
2560
|
+
t += (yield) * this.tempo;
|
|
2561
|
+
const k = Math.min(1, t / dur);
|
|
2562
|
+
const e = o.drag || k < 0.15 ? easeInOut(k) : easeOut(k);
|
|
2563
|
+
const p = get();
|
|
2564
|
+
const dx = p.x - x0;
|
|
2565
|
+
const dy = p.y - y0;
|
|
2566
|
+
const L = Math.hypot(dx, dy) || 1;
|
|
2567
|
+
const off = Math.sin(Math.PI * e) * bend;
|
|
2568
|
+
c.x = x0 + dx * e + (-dy / L) * off;
|
|
2569
|
+
c.y = y0 + dy * e + (dx / L) * off;
|
|
2570
|
+
if (o.onStep) o.onStep();
|
|
2571
|
+
}
|
|
2572
|
+
const p = get();
|
|
2573
|
+
c.x = p.x;
|
|
2574
|
+
c.y = p.y;
|
|
2575
|
+
if (o.onStep) o.onStep();
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
*click(c) {
|
|
2579
|
+
c.press = 1;
|
|
2580
|
+
if (this.sfx) this.sfx.press(this.scr(c).x, this.vp.vw);
|
|
2581
|
+
yield* this.wait(0.07);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
*type(it, c) {
|
|
2585
|
+
const N = it.total;
|
|
2586
|
+
const k = clamp(70 / Math.max(1, N), 0.06, 1);
|
|
2587
|
+
let budget = 0;
|
|
2588
|
+
let i = it.shown;
|
|
2589
|
+
while (i < N) {
|
|
2590
|
+
budget += (yield) * this.tempo;
|
|
2591
|
+
while (i < N && budget > 0) {
|
|
2592
|
+
const ch = it.seq[i];
|
|
2593
|
+
let d = rand(0.012, 0.04);
|
|
2594
|
+
if (ch === " ") d += rand(0, 0.03);
|
|
2595
|
+
if (ch === "\n") d += 0.1;
|
|
2596
|
+
if (Math.random() < 0.025) d += rand(0.08, 0.2);
|
|
2597
|
+
budget -= d * k;
|
|
2598
|
+
i++;
|
|
2599
|
+
}
|
|
2600
|
+
if (c && this.sfx && i > it.shown) this.sfx.key(this.scr(c).x, this.vp.vw, c.id, i - it.shown, /\s/.test(it.seq.slice(it.shown, i)) ? 1 : 0);
|
|
2601
|
+
it.shown = i;
|
|
2602
|
+
bump(it);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
*idle(c) {
|
|
2607
|
+
for (;;) {
|
|
2608
|
+
const vp = this.vp;
|
|
2609
|
+
const top = this.viewTop;
|
|
2610
|
+
const bot = this.viewBot;
|
|
2611
|
+
const p = { x: rand(vp.sx + vp.vw * 0.17, vp.sx + vp.vw * 0.83), y: rand(top + (bot - top) * 0.2, top + (bot - top) * 0.8) };
|
|
2612
|
+
yield* this.moveTo(c, p, { speed: 0.35 });
|
|
2613
|
+
let w = rand(0.4, 1.4);
|
|
2614
|
+
while (w > 0) {
|
|
2615
|
+
w -= yield;
|
|
2616
|
+
if (c.y < this.viewTop || c.y > this.viewBot) break;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
*leave(c) {
|
|
2622
|
+
const right = c.x > this.vp.sx + this.vp.vw / 2;
|
|
2623
|
+
yield* this.moveTo(
|
|
2624
|
+
c,
|
|
2625
|
+
() => {
|
|
2626
|
+
const vp = this.vp;
|
|
2627
|
+
return this.fromViewport(right ? vp.ox + vp.vw + 60 : vp.ox - 60, c.y - this.viewTop + vp.oy);
|
|
2628
|
+
},
|
|
2629
|
+
{ speed: 0.8 },
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
run(c, t) {
|
|
2634
|
+
const s = t.spec;
|
|
2635
|
+
switch (s.type) {
|
|
2636
|
+
case "rect":
|
|
2637
|
+
return this.drawBox(c, s.it, false);
|
|
2638
|
+
case "image":
|
|
2639
|
+
return this.drawBox(c, s.it, true);
|
|
2640
|
+
case "line":
|
|
2641
|
+
return this.drawLine(c, s.it);
|
|
2642
|
+
case "text":
|
|
2643
|
+
return this.drawText(c, s.it, s.snap || null, !!s.grow);
|
|
2644
|
+
case "guide":
|
|
2645
|
+
return this.dragGuide(c, s.g);
|
|
2646
|
+
case "dup":
|
|
2647
|
+
return this.duplicate(c, s.g, s.k);
|
|
2648
|
+
case "edit":
|
|
2649
|
+
return this.editText(c, s.it);
|
|
2650
|
+
case "restyle":
|
|
2651
|
+
case "swap":
|
|
2652
|
+
return this.restyle(c, s.it);
|
|
2653
|
+
}
|
|
2654
|
+
return this.wait(0.1);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// Where a drag should end so a huge box doesn't drag the cursor off-screen.
|
|
2658
|
+
dragEnd(c, it) {
|
|
2659
|
+
const vp = this.vp;
|
|
2660
|
+
const screen = c.sticky;
|
|
2661
|
+
const right = screen ? vp.ox + vp.vw : vp.sx + vp.vw;
|
|
2662
|
+
const bot = screen ? vp.oy + vp.vh : this.viewBot;
|
|
2663
|
+
const x = it.x + it.w > right - 16 ? Math.max(it.x + Math.min(it.w, 40), right - 30) : it.x + it.w;
|
|
2664
|
+
const y = it.y + it.h > bot - 16 ? Math.max(it.y + Math.min(it.h, 40), bot - 40) : it.y + it.h;
|
|
2665
|
+
return { x: Math.min(x, it.x + it.w), y: Math.min(y, it.y + it.h) };
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
*drawBox(c, it, isImg) {
|
|
2669
|
+
c.shape = "cross";
|
|
2670
|
+
yield* this.moveTo(c, { x: it.x, y: it.y });
|
|
2671
|
+
c.press = 1;
|
|
2672
|
+
it.vis = true;
|
|
2673
|
+
if (it.big) it.styled = true;
|
|
2674
|
+
it.draw = { w: 0, h: 0 };
|
|
2675
|
+
bump(it);
|
|
2676
|
+
c.ov = () => [this.selection(it)];
|
|
2677
|
+
const end = this.dragEnd(c, it);
|
|
2678
|
+
yield* this.moveTo(c, end, {
|
|
2679
|
+
speed: 0.75,
|
|
2680
|
+
drag: true,
|
|
2681
|
+
bend: 0,
|
|
2682
|
+
onStep: () => {
|
|
2683
|
+
it.draw = { w: Math.max(0, c.x - it.x), h: Math.max(0, c.y - it.y) };
|
|
2684
|
+
bump(it);
|
|
2685
|
+
},
|
|
2686
|
+
});
|
|
2687
|
+
if (end.x < it.x + it.w - 0.5 || end.y < it.y + it.h - 0.5) {
|
|
2688
|
+
const w0 = it.draw.w;
|
|
2689
|
+
const h0 = it.draw.h;
|
|
2690
|
+
let k = 0;
|
|
2691
|
+
while (k < 1) {
|
|
2692
|
+
k = Math.min(1, k + ((yield) * this.tempo) / 0.5);
|
|
2693
|
+
const e = easeOut(k);
|
|
2694
|
+
it.draw = { w: w0 + (it.w - w0) * e, h: h0 + (it.h - h0) * e };
|
|
2695
|
+
bump(it);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
it.draw = null;
|
|
2699
|
+
bump(it);
|
|
2700
|
+
c.shape = "arrow";
|
|
2701
|
+
yield* this.wait(rand(0.18, 0.3));
|
|
2702
|
+
it.styled = true;
|
|
2703
|
+
bump(it);
|
|
2704
|
+
this.sfxPlace(isImg ? "image" : "rect", it);
|
|
2705
|
+
yield* this.wait(isImg ? 0.5 : 0.35);
|
|
2706
|
+
c.ov = null;
|
|
2707
|
+
it.done = true;
|
|
2708
|
+
bump(it);
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
*drawLine(c, it) {
|
|
2712
|
+
c.shape = "pen-add";
|
|
2713
|
+
yield* this.moveTo(c, it.pts[0]);
|
|
2714
|
+
yield* this.click(c);
|
|
2715
|
+
it.vis = true;
|
|
2716
|
+
it.edit = true;
|
|
2717
|
+
it.placed = 1;
|
|
2718
|
+
it.owner = c;
|
|
2719
|
+
bump(it);
|
|
2720
|
+
this.editLines.add(it);
|
|
2721
|
+
c.shape = "pen";
|
|
2722
|
+
for (let i = 1; i < it.pts.length; i++) {
|
|
2723
|
+
yield* this.moveTo(c, it.pts[i], { bend: rand(-0.03, 0.03) });
|
|
2724
|
+
yield* this.click(c);
|
|
2725
|
+
it.placed = i + 1;
|
|
2726
|
+
bump(it);
|
|
2727
|
+
yield* this.wait(rand(0.04, 0.12));
|
|
2728
|
+
}
|
|
2729
|
+
it.owner = null;
|
|
2730
|
+
bump(it);
|
|
2731
|
+
yield* this.moveTo(c, { x: c.x + rand(14, 30), y: c.y + rand(10, 24) }, { speed: 1.4 });
|
|
2732
|
+
yield* this.wait(0.3);
|
|
2733
|
+
it.edit = false;
|
|
2734
|
+
this.editLines.delete(it);
|
|
2735
|
+
it.done = true;
|
|
2736
|
+
bump(it);
|
|
2737
|
+
this.sfxPlace("line", it);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
*drawText(c, it, guide, grow) {
|
|
2741
|
+
if (guide) {
|
|
2742
|
+
if (guide.axis === "x") {
|
|
2743
|
+
it.dx = rand(28, 64);
|
|
2744
|
+
it.dy = rand(-8, 8);
|
|
2745
|
+
} else {
|
|
2746
|
+
it.dx = rand(-8, 8);
|
|
2747
|
+
it.dy = rand(18, 34) * (Math.random() < 0.5 ? -1 : 1);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
if (grow) it.scale = Math.min(1, 18 / it.fs);
|
|
2751
|
+
const b0 = this.bounds(it);
|
|
2752
|
+
c.shape = "cross";
|
|
2753
|
+
yield* this.moveTo(c, { x: b0.x, y: b0.y });
|
|
2754
|
+
const lineH = Math.min(b0.h, it.lh * it.scale);
|
|
2755
|
+
if (b0.w < 140 && it.tb.h < it.lh * 1.6) {
|
|
2756
|
+
yield* this.click(c);
|
|
2757
|
+
} else {
|
|
2758
|
+
c.press = 1;
|
|
2759
|
+
const ghost = { x: b0.x, y: b0.y, w: 0, h: 0 };
|
|
2760
|
+
c.ov = () => [{ t: "box", b: ghost, handles: true, badge: `${Math.round(ghost.w)} × ${Math.round(ghost.h)}`, rulerLines: true }];
|
|
2761
|
+
yield* this.moveTo(
|
|
2762
|
+
c,
|
|
2763
|
+
{ x: b0.x + b0.w, y: b0.y + lineH },
|
|
2764
|
+
{
|
|
2765
|
+
speed: 0.9,
|
|
2766
|
+
drag: true,
|
|
2767
|
+
bend: 0,
|
|
2768
|
+
onStep: () => {
|
|
2769
|
+
ghost.w = Math.max(0, c.x - b0.x);
|
|
2770
|
+
ghost.h = Math.max(0, c.y - b0.y);
|
|
2771
|
+
},
|
|
2772
|
+
},
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
it.vis = true;
|
|
2776
|
+
it.editing = true;
|
|
2777
|
+
it.shown = 0;
|
|
2778
|
+
bump(it);
|
|
2779
|
+
c.shape = "ibeam";
|
|
2780
|
+
c.ov = () => [{ t: "box", b: this.bounds(it) }];
|
|
2781
|
+
yield* this.moveTo(c, { x: c.x + rand(-30, 10), y: c.y + rand(10, 30) }, { speed: 1.5 });
|
|
2782
|
+
yield* this.type(it, c);
|
|
2783
|
+
yield* this.wait(0.12);
|
|
2784
|
+
it.editing = false;
|
|
2785
|
+
bump(it);
|
|
2786
|
+
this.sfxPlace("text", it);
|
|
2787
|
+
c.shape = "arrow";
|
|
2788
|
+
c.ov = () => [this.selection(it)];
|
|
2789
|
+
if (grow) {
|
|
2790
|
+
const steps = [24, 32, 40, 48, 56, 64, 72, 88, 104].filter((s) => s < it.fs && s > 18);
|
|
2791
|
+
let k = 0;
|
|
2792
|
+
for (const s of [...steps, it.fs]) {
|
|
2793
|
+
yield* this.wait(0.06);
|
|
2794
|
+
it.scale = s / it.fs;
|
|
2795
|
+
bump(it);
|
|
2796
|
+
if (this.sfx) this.sfx.grow(k++, this.scr(c).x, this.vp.vw);
|
|
2797
|
+
}
|
|
2798
|
+
it.scale = 1;
|
|
2799
|
+
bump(it);
|
|
2800
|
+
yield* this.wait(0.15);
|
|
2801
|
+
}
|
|
2802
|
+
if (guide) {
|
|
2803
|
+
// give the guide a moment to land; never block a cursor on it
|
|
2804
|
+
let waited = 0;
|
|
2805
|
+
while (guide.task.state !== "done" && waited < 1.6) waited += (yield) * this.tempo;
|
|
2806
|
+
if (guide.task.state !== "done") guide = null;
|
|
2807
|
+
}
|
|
2808
|
+
if (guide) {
|
|
2809
|
+
const u = this.bounds(it);
|
|
2810
|
+
const a = { x: u.x + Math.min(u.w * 0.4, 60) + rand(-6, 6), y: u.y + Math.min(u.h * 0.5, 22) };
|
|
2811
|
+
yield* this.moveTo(c, a);
|
|
2812
|
+
c.press = 1;
|
|
2813
|
+
const p = { x: a.x - it.dx, y: a.y - it.dy };
|
|
2814
|
+
const m = {
|
|
2815
|
+
x: p.x + (guide.axis === "x" ? -rand(2, 5) : rand(-6, 6)),
|
|
2816
|
+
y: p.y + (guide.axis === "y" ? -rand(2, 5) : rand(-3, 3)),
|
|
2817
|
+
};
|
|
2818
|
+
const drag = () => {
|
|
2819
|
+
let h = c.x - p.x;
|
|
2820
|
+
let v = c.y - p.y;
|
|
2821
|
+
if (guide.axis === "x" && Math.abs(h) < 9) {
|
|
2822
|
+
if (h !== 0 && it.dx !== 0) {
|
|
2823
|
+
if (guide.flash < 0.5 && this.sfx) this.sfx.snap(this.scr(c).x, this.vp.vw);
|
|
2824
|
+
guide.flash = 1;
|
|
2825
|
+
}
|
|
2826
|
+
h = 0;
|
|
2827
|
+
}
|
|
2828
|
+
if (guide.axis === "y" && Math.abs(v) < 9) {
|
|
2829
|
+
if (v !== 0 && it.dy !== 0) {
|
|
2830
|
+
if (guide.flash < 0.5 && this.sfx) this.sfx.snap(this.scr(c).x, this.vp.vw);
|
|
2831
|
+
guide.flash = 1;
|
|
2832
|
+
}
|
|
2833
|
+
v = 0;
|
|
2834
|
+
}
|
|
2835
|
+
it.dx = h;
|
|
2836
|
+
it.dy = v;
|
|
2837
|
+
bump(it);
|
|
2838
|
+
};
|
|
2839
|
+
yield* this.moveTo(c, m, { speed: 0.6, drag: true, onStep: drag });
|
|
2840
|
+
yield* this.wait(0.1);
|
|
2841
|
+
yield* this.moveTo(c, p, { speed: 2, onStep: drag });
|
|
2842
|
+
it.dx = 0;
|
|
2843
|
+
it.dy = 0;
|
|
2844
|
+
bump(it);
|
|
2845
|
+
yield* this.wait(0.25);
|
|
2846
|
+
} else if (it.dx || it.dy) {
|
|
2847
|
+
const dx0 = it.dx;
|
|
2848
|
+
const dy0 = it.dy;
|
|
2849
|
+
let k = 0;
|
|
2850
|
+
while (k < 1) {
|
|
2851
|
+
k = Math.min(1, k + ((yield) * this.tempo) / 0.3);
|
|
2852
|
+
const e = easeInOut(k);
|
|
2853
|
+
it.dx = dx0 * (1 - e);
|
|
2854
|
+
it.dy = dy0 * (1 - e);
|
|
2855
|
+
bump(it);
|
|
2856
|
+
}
|
|
2857
|
+
yield* this.wait(0.15);
|
|
2858
|
+
} else yield* this.wait(0.2);
|
|
2859
|
+
c.ov = null;
|
|
2860
|
+
it.done = true;
|
|
2861
|
+
bump(it);
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
*dragGuide(c, g) {
|
|
2865
|
+
this.rulers.want++;
|
|
2866
|
+
try {
|
|
2867
|
+
const r = rand(-0.15, 0.15);
|
|
2868
|
+
const from = () => {
|
|
2869
|
+
const vp = this.vp;
|
|
2870
|
+
return g.axis === "x"
|
|
2871
|
+
? this.fromViewport(vp.ox + 10, vp.oy + vp.vh * (0.45 + r))
|
|
2872
|
+
: this.fromViewport(vp.ox + vp.vw * 0.3 + r * 200, vp.oy + 10);
|
|
2873
|
+
};
|
|
2874
|
+
c.shape = "arrow";
|
|
2875
|
+
yield* this.moveTo(c, from);
|
|
2876
|
+
while (this.rulers.t < 0.95) yield;
|
|
2877
|
+
yield* this.moveTo(c, from, { speed: 3 });
|
|
2878
|
+
c.press = 1;
|
|
2879
|
+
c.shape = g.axis === "x" ? "ew" : "ns";
|
|
2880
|
+
g.vis = true;
|
|
2881
|
+
g.dragging = true;
|
|
2882
|
+
g.op = 1;
|
|
2883
|
+
const upd = () => {
|
|
2884
|
+
g.cur = g.axis === "x" ? c.x : c.y;
|
|
2885
|
+
};
|
|
2886
|
+
upd();
|
|
2887
|
+
const to = () => {
|
|
2888
|
+
const s = from();
|
|
2889
|
+
return g.axis === "x" ? { x: g.pos, y: s.y + 40 } : { x: s.x + 30, y: g.pos };
|
|
2890
|
+
};
|
|
2891
|
+
yield* this.moveTo(c, to, { speed: 0.55, drag: true, onStep: upd });
|
|
2892
|
+
g.cur = g.pos;
|
|
2893
|
+
g.dragging = false;
|
|
2894
|
+
if (this.sfx) {
|
|
2895
|
+
const p = this.scr(c);
|
|
2896
|
+
this.sfx.place("guide", p.x, p.y, 1, 1, this.vp.vw, this.vp.vh, 0);
|
|
2897
|
+
}
|
|
2898
|
+
c.shape = "arrow";
|
|
2899
|
+
yield* this.wait(0.25);
|
|
2900
|
+
} finally {
|
|
2901
|
+
this.rulers.want--;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
*duplicate(c, g, k) {
|
|
2906
|
+
const dst = g.dst[k];
|
|
2907
|
+
let src = g.src;
|
|
2908
|
+
const cand = [g.src, ...g.dst.filter((d) => d.task.state === "done")];
|
|
2909
|
+
const dd = (a) => Math.hypot(a.box.x - dst.box.x, a.box.y - dst.box.y);
|
|
2910
|
+
for (const s of cand) if (dd(s) < dd(src)) src = s;
|
|
2911
|
+
const sb = this.unionBounds(src.items);
|
|
2912
|
+
c.shape = "arrow";
|
|
2913
|
+
const u = { x: sb.x + Math.min(sb.w * 0.45, 70) + rand(-8, 8), y: sb.y + Math.min(sb.h * 0.45, 30) + rand(-4, 4) };
|
|
2914
|
+
yield* this.moveTo(c, u);
|
|
2915
|
+
yield* this.click(c);
|
|
2916
|
+
c.ov = () => [{ t: "box", b: sb, handles: true, badge: `${Math.round(sb.w)} × ${Math.round(sb.h)}` }];
|
|
2917
|
+
yield* this.wait(rand(0.15, 0.3));
|
|
2918
|
+
c.shape = "dup";
|
|
2919
|
+
yield* this.wait(0.12);
|
|
2920
|
+
c.press = 1;
|
|
2921
|
+
const ox = src.box.x - dst.box.x;
|
|
2922
|
+
const oy = src.box.y - dst.box.y;
|
|
2923
|
+
for (const it of dst.items) {
|
|
2924
|
+
it.vis = true;
|
|
2925
|
+
it.mode = "master";
|
|
2926
|
+
it.dx = ox;
|
|
2927
|
+
it.dy = oy;
|
|
2928
|
+
if (it.kind === "rect" || it.kind === "image") {
|
|
2929
|
+
it.styled = true;
|
|
2930
|
+
it.draw = null;
|
|
2931
|
+
}
|
|
2932
|
+
if (it.kind === "line") {
|
|
2933
|
+
it.placed = it.pts.length;
|
|
2934
|
+
}
|
|
2935
|
+
bump(it);
|
|
2936
|
+
}
|
|
2937
|
+
const m = { x: u.x - ox, y: u.y - oy };
|
|
2938
|
+
c.ov = () => {
|
|
2939
|
+
const cb = this.unionBounds(dst.items);
|
|
2940
|
+
const h = [
|
|
2941
|
+
{ t: "box", b: sb, color: "red" },
|
|
2942
|
+
{ t: "box", b: cb, color: "red" },
|
|
2943
|
+
];
|
|
2944
|
+
const sp = this.spacing(sb, cb);
|
|
2945
|
+
if (sp) h.push(sp);
|
|
2946
|
+
return h;
|
|
2947
|
+
};
|
|
2948
|
+
yield* this.moveTo(c, m, {
|
|
2949
|
+
speed: 0.6,
|
|
2950
|
+
drag: true,
|
|
2951
|
+
onStep: () => {
|
|
2952
|
+
for (const it of dst.items) {
|
|
2953
|
+
it.dx = ox + (c.x - u.x);
|
|
2954
|
+
it.dy = oy + (c.y - u.y);
|
|
2955
|
+
bump(it);
|
|
2956
|
+
}
|
|
2957
|
+
},
|
|
2958
|
+
});
|
|
2959
|
+
this.sfxPlace("dup", dst.items[0], this.unionBounds(dst.items));
|
|
2960
|
+
for (const it of dst.items) {
|
|
2961
|
+
it.dx = 0;
|
|
2962
|
+
it.dy = 0;
|
|
2963
|
+
it.settle = true;
|
|
2964
|
+
if (!it.editTask) it.mode = "own";
|
|
2965
|
+
if (it.kind === "text" && it.mode === "own") it.shown = it.total;
|
|
2966
|
+
bump(it);
|
|
2967
|
+
}
|
|
2968
|
+
yield* this.wait(0.1);
|
|
2969
|
+
c.shape = "arrow";
|
|
2970
|
+
c.ov = () => {
|
|
2971
|
+
const cb = this.unionBounds(dst.items);
|
|
2972
|
+
return [{ t: "box", b: cb, handles: true, badge: `${Math.round(cb.w)} × ${Math.round(cb.h)}` }];
|
|
2973
|
+
};
|
|
2974
|
+
yield* this.wait(0.35);
|
|
2975
|
+
for (const it of dst.items)
|
|
2976
|
+
if (!it.editTask) {
|
|
2977
|
+
it.done = true;
|
|
2978
|
+
bump(it);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
spacing(a, b) {
|
|
2983
|
+
const oy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
|
|
2984
|
+
const ox = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
|
|
2985
|
+
if (oy > 0) {
|
|
2986
|
+
const y = Math.max(a.y, b.y) + oy / 2;
|
|
2987
|
+
const [l, r] = a.x < b.x ? [a, b] : [b, a];
|
|
2988
|
+
const gap = r.x - (l.x + l.w);
|
|
2989
|
+
if (gap > 0) return { t: "measure", a: { x: l.x + l.w, y }, b: { x: r.x, y }, label: String(Math.round(gap)) };
|
|
2990
|
+
} else if (ox > 0) {
|
|
2991
|
+
const x = Math.max(a.x, b.x) + ox / 2;
|
|
2992
|
+
const [t, btm] = a.y < b.y ? [a, b] : [b, a];
|
|
2993
|
+
const gap = btm.y - (t.y + t.h);
|
|
2994
|
+
if (gap > 0) return { t: "measure", a: { x, y: t.y + t.h }, b: { x, y: btm.y }, label: String(Math.round(gap)) };
|
|
2995
|
+
}
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
*editText(c, it) {
|
|
3000
|
+
c.shape = "arrow";
|
|
3001
|
+
const b = this.bounds(it);
|
|
3002
|
+
yield* this.moveTo(c, { x: b.x + Math.min(b.w * 0.5, 50) + rand(-5, 5), y: b.y + Math.min(it.lh, b.h) * 0.55 });
|
|
3003
|
+
yield* this.click(c);
|
|
3004
|
+
yield* this.wait(0.08);
|
|
3005
|
+
yield* this.click(c);
|
|
3006
|
+
it.selAll = true;
|
|
3007
|
+
it.editing = true;
|
|
3008
|
+
bump(it);
|
|
3009
|
+
c.shape = "ibeam";
|
|
3010
|
+
c.ov = () => [{ t: "box", b: this.bounds(it) }];
|
|
3011
|
+
yield* this.wait(rand(0.2, 0.35));
|
|
3012
|
+
it.mode = "own";
|
|
3013
|
+
it.selAll = false;
|
|
3014
|
+
it.shown = 0;
|
|
3015
|
+
bump(it);
|
|
3016
|
+
yield* this.type(it, c);
|
|
3017
|
+
yield* this.wait(0.12);
|
|
3018
|
+
it.editing = false;
|
|
3019
|
+
bump(it);
|
|
3020
|
+
c.shape = "arrow";
|
|
3021
|
+
c.ov = () => [this.selection(it)];
|
|
3022
|
+
this.sfxPlace("edit", it);
|
|
3023
|
+
yield* this.wait(0.3);
|
|
3024
|
+
c.ov = null;
|
|
3025
|
+
it.done = true;
|
|
3026
|
+
bump(it);
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
*restyle(c, it) {
|
|
3030
|
+
c.shape = "arrow";
|
|
3031
|
+
const b = this.bounds(it);
|
|
3032
|
+
yield* this.moveTo(c, { x: b.x + Math.min(b.w * 0.5, 60) + rand(-6, 6), y: b.y + Math.min(b.h * 0.5, 30) + rand(-4, 4) });
|
|
3033
|
+
yield* this.click(c);
|
|
3034
|
+
c.ov = () => [this.selection(it)];
|
|
3035
|
+
yield* this.wait(rand(0.15, 0.3));
|
|
3036
|
+
it.mode = "own";
|
|
3037
|
+
bump(it);
|
|
3038
|
+
this.sfxPlace(it.kind === "image" ? "image" : "restyle", it);
|
|
3039
|
+
yield* this.wait(0.4);
|
|
3040
|
+
c.ov = null;
|
|
3041
|
+
it.done = true;
|
|
3042
|
+
bump(it);
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// ------------------------------------------------------------------ sound
|
|
3047
|
+
|
|
3048
|
+
// A glass piano in E. Every finished piece of the page plays one note of the
|
|
3049
|
+
// E major pentatonic, quantized to a soft 16th-note grid, so a busy swarm comes
|
|
3050
|
+
// out as a generative melody: big sections low, small things high, pitch rising
|
|
3051
|
+
// left to right, panned to where the cursor is. Typing and clicks are quiet
|
|
3052
|
+
// paper ticks underneath. All synthesized; no audio files.
|
|
3053
|
+
const PENTA = [0, 2, 4, 7, 9]; // E F# G# B C#
|
|
3054
|
+
const E2 = 40;
|
|
3055
|
+
const mtof = (m) => 440 * Math.pow(2, (m - 69) / 12);
|
|
3056
|
+
let soundPref = true;
|
|
3057
|
+
const SPK_ICON = '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6v4h2.5l3.5 3V3L5 6z" fill="currentColor"/>';
|
|
3058
|
+
|
|
3059
|
+
class Sound {
|
|
3060
|
+
constructor() {
|
|
3061
|
+
this.on = soundPref;
|
|
3062
|
+
this.ctx = null;
|
|
3063
|
+
this.bpm = 104;
|
|
3064
|
+
this.slots = new Map();
|
|
3065
|
+
this.lastKey = new Map();
|
|
3066
|
+
this.keyTimes = [];
|
|
3067
|
+
this.pressTimes = [];
|
|
3068
|
+
this.ends = []; // when each sounding piano voice dies out
|
|
3069
|
+
this.fakeNow = null; // offline renders drive the clock explicitly
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
init(offline) {
|
|
3073
|
+
if (this.ctx) return true;
|
|
3074
|
+
try {
|
|
3075
|
+
const Ctx = window.AudioContext || window.webkitAudioContext;
|
|
3076
|
+
this.ctx = offline || new Ctx({ latencyHint: "interactive" });
|
|
3077
|
+
} catch (_) {
|
|
3078
|
+
return false;
|
|
3079
|
+
}
|
|
3080
|
+
const ctx = this.ctx;
|
|
3081
|
+
const out = ctx.createGain();
|
|
3082
|
+
out.gain.value = this.on ? 1 : 0;
|
|
3083
|
+
const comp = ctx.createDynamicsCompressor();
|
|
3084
|
+
comp.threshold.value = -18;
|
|
3085
|
+
comp.knee.value = 14;
|
|
3086
|
+
comp.ratio.value = 5;
|
|
3087
|
+
comp.attack.value = 0.003;
|
|
3088
|
+
comp.release.value = 0.3;
|
|
3089
|
+
const master = ctx.createGain();
|
|
3090
|
+
master.gain.value = 0.9;
|
|
3091
|
+
master.connect(comp);
|
|
3092
|
+
comp.connect(out);
|
|
3093
|
+
out.connect(ctx.destination);
|
|
3094
|
+
const verb = ctx.createConvolver();
|
|
3095
|
+
verb.buffer = this.impulse(3.2, 2.4);
|
|
3096
|
+
const wet = ctx.createGain();
|
|
3097
|
+
wet.gain.value = 0.5;
|
|
3098
|
+
verb.connect(wet);
|
|
3099
|
+
wet.connect(master);
|
|
3100
|
+
this.out = out;
|
|
3101
|
+
this.master = master;
|
|
3102
|
+
this.verb = verb;
|
|
3103
|
+
this.noiseBuf = this.noise(1.5);
|
|
3104
|
+
this.t0 = this.now();
|
|
3105
|
+
return true;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
now() {
|
|
3109
|
+
return this.fakeNow != null ? this.fakeNow : this.ctx.currentTime;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// Generated room: decaying stereo noise, darkened as it rings out.
|
|
3113
|
+
impulse(secs, decay) {
|
|
3114
|
+
const sr = this.ctx.sampleRate;
|
|
3115
|
+
const n = Math.floor(sr * secs);
|
|
3116
|
+
const b = this.ctx.createBuffer(2, n, sr);
|
|
3117
|
+
for (let ch = 0; ch < 2; ch++) {
|
|
3118
|
+
const d = b.getChannelData(ch);
|
|
3119
|
+
let lp = 0;
|
|
3120
|
+
for (let i = 0; i < n; i++) {
|
|
3121
|
+
const k = i / n;
|
|
3122
|
+
const a = 0.55 - 0.4 * k; // lowpass closes over time: warm tail
|
|
3123
|
+
lp += a * (Math.random() * 2 - 1 - lp);
|
|
3124
|
+
d[i] = lp * Math.pow(1 - k, decay) * (i < sr * 0.012 ? i / (sr * 0.012) : 1);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
return b;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
noise(secs) {
|
|
3131
|
+
const sr = this.ctx.sampleRate;
|
|
3132
|
+
const b = this.ctx.createBuffer(1, Math.floor(sr * secs), sr);
|
|
3133
|
+
const d = b.getChannelData(0);
|
|
3134
|
+
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
|
3135
|
+
return b;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
setOn(on) {
|
|
3139
|
+
this.on = soundPref = on;
|
|
3140
|
+
if (!this.ctx) return;
|
|
3141
|
+
const t = this.ctx.currentTime;
|
|
3142
|
+
this.out.gain.cancelScheduledValues(t);
|
|
3143
|
+
this.out.gain.setTargetAtTime(on ? 1 : 0, t, 0.05);
|
|
3144
|
+
if (on) this.resume();
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
resume() {
|
|
3148
|
+
if (this.ctx && this.ctx.state === "suspended" && this.ctx.resume) this.ctx.resume().catch(() => {});
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
get blocked() {
|
|
3152
|
+
return !!this.ctx && this.ctx.state !== "running" && this.fakeNow == null;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
log(fn, args) {
|
|
3156
|
+
if (Array.isArray(window.__swarmSfxLog)) window.__swarmSfxLog.push({ t: performance.now() / 1000, fn, args });
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
ready() {
|
|
3160
|
+
return this.ctx && this.on && (this.fakeNow != null || this.ctx.state === "running");
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// next 16th-note on the grid (or 8th for slow, low events)
|
|
3164
|
+
grid(div = 4) {
|
|
3165
|
+
const step = 60 / this.bpm / div;
|
|
3166
|
+
const now = this.now() + 0.015;
|
|
3167
|
+
const k = Math.ceil((now - this.t0) / step);
|
|
3168
|
+
return { when: this.t0 + k * step, slot: `${div}:${k}` };
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
pan(x, vw) {
|
|
3172
|
+
return clamp((x / Math.max(1, vw)) * 1.6 - 0.8, -0.8, 0.8);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
bus(when, pan, send) {
|
|
3176
|
+
const g = this.ctx.createGain();
|
|
3177
|
+
const p = this.ctx.createStereoPanner();
|
|
3178
|
+
p.pan.setValueAtTime(pan, when);
|
|
3179
|
+
g.connect(p);
|
|
3180
|
+
p.connect(this.master);
|
|
3181
|
+
if (send) {
|
|
3182
|
+
const s = this.ctx.createGain();
|
|
3183
|
+
s.gain.value = send;
|
|
3184
|
+
p.connect(s);
|
|
3185
|
+
s.connect(this.verb);
|
|
3186
|
+
}
|
|
3187
|
+
return g;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// Glass piano: a few sine partials (one inharmonic, for the glass) with a
|
|
3191
|
+
// soft felt-hammer tick, long exponential ring.
|
|
3192
|
+
piano(midi, when, vel, pan, ring = 2.4) {
|
|
3193
|
+
const now = this.now();
|
|
3194
|
+
this.ends = this.ends.filter((e) => e > now);
|
|
3195
|
+
if (this.ends.length >= 28) return;
|
|
3196
|
+
const ctx = this.ctx;
|
|
3197
|
+
const f = mtof(midi);
|
|
3198
|
+
const v = this.bus(when, pan, 0.85);
|
|
3199
|
+
v.gain.value = 0.3 * vel;
|
|
3200
|
+
const hi = f > 900;
|
|
3201
|
+
const parts = [
|
|
3202
|
+
[1, 1, 1],
|
|
3203
|
+
[2, 0.38, 0.55],
|
|
3204
|
+
[3, 0.12, 0.32],
|
|
3205
|
+
[2.756, 0.16, 0.42],
|
|
3206
|
+
[5.4, 0.05, 0.16],
|
|
3207
|
+
];
|
|
3208
|
+
let last = null;
|
|
3209
|
+
for (const [r, a, d] of parts) {
|
|
3210
|
+
if (f * r > 15000) continue;
|
|
3211
|
+
const o = ctx.createOscillator();
|
|
3212
|
+
o.type = "sine";
|
|
3213
|
+
o.frequency.value = f * r * (1 + (Math.random() - 0.5) * 0.002);
|
|
3214
|
+
const g = ctx.createGain();
|
|
3215
|
+
const dec = ring * d * (hi ? 0.65 : 1);
|
|
3216
|
+
g.gain.setValueAtTime(0, when);
|
|
3217
|
+
g.gain.linearRampToValueAtTime(a, when + 0.005);
|
|
3218
|
+
g.gain.exponentialRampToValueAtTime(0.0006, when + dec);
|
|
3219
|
+
o.connect(g);
|
|
3220
|
+
g.connect(v);
|
|
3221
|
+
o.start(when);
|
|
3222
|
+
o.stop(when + dec + 0.05);
|
|
3223
|
+
last = o;
|
|
3224
|
+
}
|
|
3225
|
+
this.hammer(when, v, f, 0.5);
|
|
3226
|
+
this.ends.push(when + ring * (hi ? 0.65 : 1));
|
|
3227
|
+
if (last) last.onended = () => v.disconnect();
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
hammer(when, dest, f, amt) {
|
|
3231
|
+
const ctx = this.ctx;
|
|
3232
|
+
const src = ctx.createBufferSource();
|
|
3233
|
+
src.buffer = this.noiseBuf;
|
|
3234
|
+
const bp = ctx.createBiquadFilter();
|
|
3235
|
+
bp.type = "bandpass";
|
|
3236
|
+
bp.frequency.value = Math.min(9000, f * 4 + 1200);
|
|
3237
|
+
bp.Q.value = 1.2;
|
|
3238
|
+
const g = ctx.createGain();
|
|
3239
|
+
g.gain.setValueAtTime(0, when);
|
|
3240
|
+
g.gain.linearRampToValueAtTime(0.25 * amt, when + 0.001);
|
|
3241
|
+
g.gain.exponentialRampToValueAtTime(0.0005, when + 0.03);
|
|
3242
|
+
src.connect(bp);
|
|
3243
|
+
bp.connect(g);
|
|
3244
|
+
g.connect(dest);
|
|
3245
|
+
src.start(when, Math.random());
|
|
3246
|
+
src.stop(when + 0.05);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// short filtered-noise ticks: typing and clicks
|
|
3250
|
+
tick(when, pan, freq, q, vel, len) {
|
|
3251
|
+
const ctx = this.ctx;
|
|
3252
|
+
const src = ctx.createBufferSource();
|
|
3253
|
+
src.buffer = this.noiseBuf;
|
|
3254
|
+
const bp = ctx.createBiquadFilter();
|
|
3255
|
+
bp.type = "bandpass";
|
|
3256
|
+
bp.frequency.value = freq;
|
|
3257
|
+
bp.Q.value = q;
|
|
3258
|
+
const g = this.bus(when, pan, 0.06);
|
|
3259
|
+
g.gain.setValueAtTime(0, when);
|
|
3260
|
+
g.gain.linearRampToValueAtTime(vel, when + 0.0008);
|
|
3261
|
+
g.gain.exponentialRampToValueAtTime(0.0004, when + len);
|
|
3262
|
+
src.connect(bp);
|
|
3263
|
+
bp.connect(g);
|
|
3264
|
+
src.start(when, Math.random() * 1.2);
|
|
3265
|
+
src.stop(when + len + 0.02);
|
|
3266
|
+
src.onended = () => g.disconnect();
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// ---- events (screen coords in; all plain numbers so a log can be replayed)
|
|
3270
|
+
|
|
3271
|
+
// one mechanical key: a sharp click on top of a short "thock" (deeper for space)
|
|
3272
|
+
clack(when, pan, space, vel = 1) {
|
|
3273
|
+
this.tick(when, pan, rand(3600, 5600), 2.2, rand(0.2, 0.3) * vel, rand(0.006, 0.011));
|
|
3274
|
+
this.tick(when + rand(0.001, 0.005), pan, space ? rand(380, 560) : rand(850, 1500), 1.6, (space ? 0.26 : rand(0.13, 0.2)) * vel, space ? 0.065 : rand(0.025, 0.042));
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
key(x, vw, id, n = 1, space = 0) {
|
|
3278
|
+
this.log("key", [x, vw, id, n, space]);
|
|
3279
|
+
if (!this.ready()) return;
|
|
3280
|
+
const t = this.now();
|
|
3281
|
+
if (t - (this.lastKey.get(id) || -1) < 0.024) return;
|
|
3282
|
+
this.keyTimes = this.keyTimes.filter((k) => t - k < 1);
|
|
3283
|
+
if (this.keyTimes.length > 80) return;
|
|
3284
|
+
this.lastKey.set(id, t);
|
|
3285
|
+
const pan = this.pan(x, vw);
|
|
3286
|
+
// a burst of characters in one frame becomes a quick roll of keys
|
|
3287
|
+
let at = t + 0.004;
|
|
3288
|
+
for (let k = 0; k < Math.min(n, 3) && this.keyTimes.length <= 80; k++) {
|
|
3289
|
+
this.keyTimes.push(t);
|
|
3290
|
+
this.clack(at, pan, space && k === 0, rand(0.75, 1));
|
|
3291
|
+
at += rand(0.018, 0.032);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// a mouse button: press click, then a lighter release
|
|
3296
|
+
press(x, vw) {
|
|
3297
|
+
this.log("press", [x, vw]);
|
|
3298
|
+
if (!this.ready()) return;
|
|
3299
|
+
const t = this.now();
|
|
3300
|
+
this.pressTimes = this.pressTimes.filter((k) => t - k < 1);
|
|
3301
|
+
if (this.pressTimes.length > 40) return;
|
|
3302
|
+
this.pressTimes.push(t);
|
|
3303
|
+
const pan = this.pan(x, vw);
|
|
3304
|
+
this.tick(t + 0.004, pan, rand(2100, 2700), 4, 0.32, 0.012);
|
|
3305
|
+
this.tick(t + 0.005, pan, rand(650, 850), 2, 0.16, 0.022);
|
|
3306
|
+
this.tick(t + rand(0.055, 0.075), pan, rand(2900, 3400), 4, 0.15, 0.008);
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// a finished element: rect, image, text, line, dup, guide, edit, restyle
|
|
3310
|
+
place(kind, x, y, w, h, vw, vh, fs) {
|
|
3311
|
+
this.log("place", [kind, x, y, w, h, vw, vh, fs]);
|
|
3312
|
+
if (!this.ready()) return;
|
|
3313
|
+
// only boxes, media, duplicates and guides sing; text and lines stay percussive
|
|
3314
|
+
if (kind === "text" || kind === "edit" || kind === "line") return;
|
|
3315
|
+
const area = (w * h) / Math.max(1, vw * vh);
|
|
3316
|
+
let oct;
|
|
3317
|
+
if (kind === "text") oct = fs >= 40 ? 2 : fs >= 22 ? 3 : 4;
|
|
3318
|
+
else if (kind === "guide") oct = 5;
|
|
3319
|
+
else oct = area > 0.08 ? 1 : area > 0.015 ? 2 : area > 0.002 ? 3 : 4;
|
|
3320
|
+
const deg = Math.floor(clamp(x / Math.max(1, vw), 0, 0.999) * 5);
|
|
3321
|
+
const midi = E2 + oct * 12 + PENTA[deg];
|
|
3322
|
+
const { when, slot } = this.grid(2);
|
|
3323
|
+
const seen = this.slots.get(slot) || [];
|
|
3324
|
+
if (seen.length >= 1) return;
|
|
3325
|
+
seen.push(midi);
|
|
3326
|
+
this.slots.set(slot, seen);
|
|
3327
|
+
if (this.slots.size > 64) this.slots.delete(this.slots.keys().next().value);
|
|
3328
|
+
const off = y < -40 || y > vh + 40 ? 0.45 : 1;
|
|
3329
|
+
const pan = this.pan(x, vw);
|
|
3330
|
+
const vel = ({ rect: 0.55, image: 0.5, dup: 0.5, guide: 0.3, restyle: 0.34 }[kind] || 0.4) * 0.42;
|
|
3331
|
+
const ring = oct <= 1 ? 3.6 : oct >= 4 ? 1.6 : 2.4;
|
|
3332
|
+
this.piano(midi, when, vel * off, pan, ring);
|
|
3333
|
+
if (kind === "image") this.piano(midi + (PENTA[(deg + 2) % 5] - PENTA[deg] + 12) % 12, when + 0.04, vel * 0.6 * off, pan, ring);
|
|
3334
|
+
if (kind === "dup") this.piano(E2 + 12 + (deg % 2 ? 7 : 0), when, 0.15 * off, pan * 0.5, 2.8);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// headings stepping up in size: a little rising run
|
|
3338
|
+
grow(step, x, vw) {
|
|
3339
|
+
this.log("grow", [step, x, vw]);
|
|
3340
|
+
if (!this.ready()) return;
|
|
3341
|
+
const t = this.now() + 0.004;
|
|
3342
|
+
this.tick(t, this.pan(x, vw), 2400 + step * 260, 5, 0.12, 0.01);
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
snap(x, vw) {
|
|
3346
|
+
this.log("snap", [x, vw]);
|
|
3347
|
+
if (!this.ready()) return;
|
|
3348
|
+
const t = this.now() + 0.004;
|
|
3349
|
+
const pan = this.pan(x, vw);
|
|
3350
|
+
this.tick(t, pan, 4800, 3, 0.36, 0.012);
|
|
3351
|
+
this.tick(t + 0.002, pan, 1200, 2, 0.2, 0.03);
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
// the page dissolving: an airy swell over a low open fifth
|
|
3355
|
+
intro() {
|
|
3356
|
+
this.log("intro", []);
|
|
3357
|
+
if (!this.ready()) return;
|
|
3358
|
+
const ctx = this.ctx;
|
|
3359
|
+
const t = this.now() + 0.02;
|
|
3360
|
+
const src = ctx.createBufferSource();
|
|
3361
|
+
src.buffer = this.noiseBuf;
|
|
3362
|
+
src.loop = true;
|
|
3363
|
+
const bp = ctx.createBiquadFilter();
|
|
3364
|
+
bp.type = "bandpass";
|
|
3365
|
+
bp.Q.value = 0.8;
|
|
3366
|
+
bp.frequency.setValueAtTime(260, t);
|
|
3367
|
+
bp.frequency.exponentialRampToValueAtTime(2400, t + 0.9);
|
|
3368
|
+
bp.frequency.exponentialRampToValueAtTime(900, t + 1.8);
|
|
3369
|
+
const g = this.bus(t, 0, 0.6);
|
|
3370
|
+
g.gain.setValueAtTime(0, t);
|
|
3371
|
+
g.gain.linearRampToValueAtTime(0.16, t + 0.7);
|
|
3372
|
+
g.gain.exponentialRampToValueAtTime(0.0005, t + 2.2);
|
|
3373
|
+
src.connect(bp);
|
|
3374
|
+
bp.connect(g);
|
|
3375
|
+
src.start(t);
|
|
3376
|
+
src.stop(t + 2.3);
|
|
3377
|
+
src.onended = () => g.disconnect();
|
|
3378
|
+
for (const [m, d] of [[E2, 0], [E2 + 7, 0.08], [E2 + 12, 0.16]]) this.pad(m, t + 0.3 + d, 0.07, 2.6);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
pad(midi, when, vel, len) {
|
|
3382
|
+
const ctx = this.ctx;
|
|
3383
|
+
const g = this.bus(when, 0, 0.9);
|
|
3384
|
+
g.gain.setValueAtTime(0, when);
|
|
3385
|
+
g.gain.linearRampToValueAtTime(vel, when + len * 0.4);
|
|
3386
|
+
g.gain.exponentialRampToValueAtTime(0.0005, when + len);
|
|
3387
|
+
for (const det of [-4, 4]) {
|
|
3388
|
+
const o = ctx.createOscillator();
|
|
3389
|
+
o.type = "triangle";
|
|
3390
|
+
o.frequency.value = mtof(midi);
|
|
3391
|
+
o.detune.value = det;
|
|
3392
|
+
o.connect(g);
|
|
3393
|
+
o.start(when);
|
|
3394
|
+
o.stop(when + len + 0.05);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// the swarm fans out: E G# B E arpeggio on the grid
|
|
3399
|
+
start() {
|
|
3400
|
+
this.log("start", []);
|
|
3401
|
+
if (!this.ready()) return;
|
|
3402
|
+
const { when } = this.grid(4);
|
|
3403
|
+
const step = 60 / this.bpm / 4;
|
|
3404
|
+
[E2 + 24, E2 + 28, E2 + 31, E2 + 36, E2 + 40].forEach((m, i) => this.piano(m, when + i * step, 0.2, -0.5 + i * 0.25, 2));
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
// the last chord rings: E major add9, strummed
|
|
3408
|
+
finale() {
|
|
3409
|
+
this.log("finale", []);
|
|
3410
|
+
if (!this.ready()) return;
|
|
3411
|
+
const { when } = this.grid(2);
|
|
3412
|
+
const chord = [E2, E2 + 7, E2 + 12, E2 + 16, E2 + 19, E2 + 26, E2 + 28, E2 + 31, E2 + 36, E2 + 43];
|
|
3413
|
+
chord.forEach((m, i) => this.piano(m, when + i * 0.035, 0.34 - i * 0.015, -0.6 + (i / chord.length) * 1.2, 5.5));
|
|
3414
|
+
this.pad(E2 + 12, when, 0.05, 5);
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// back to the real page: a falling sweep
|
|
3418
|
+
release() {
|
|
3419
|
+
this.log("release", []);
|
|
3420
|
+
if (!this.ready()) return;
|
|
3421
|
+
const ctx = this.ctx;
|
|
3422
|
+
const t = this.now() + 0.01;
|
|
3423
|
+
const src = ctx.createBufferSource();
|
|
3424
|
+
src.buffer = this.noiseBuf;
|
|
3425
|
+
src.loop = true;
|
|
3426
|
+
const bp = ctx.createBiquadFilter();
|
|
3427
|
+
bp.type = "bandpass";
|
|
3428
|
+
bp.Q.value = 1;
|
|
3429
|
+
bp.frequency.setValueAtTime(3000, t);
|
|
3430
|
+
bp.frequency.exponentialRampToValueAtTime(220, t + 1);
|
|
3431
|
+
const g = this.bus(t, 0, 0.5);
|
|
3432
|
+
g.gain.setValueAtTime(0.12, t);
|
|
3433
|
+
g.gain.exponentialRampToValueAtTime(0.0005, t + 1.1);
|
|
3434
|
+
src.connect(bp);
|
|
3435
|
+
bp.connect(g);
|
|
3436
|
+
src.start(t);
|
|
3437
|
+
src.stop(t + 1.2);
|
|
3438
|
+
this.piano(E2 + 12, t + 0.05, 0.3, 0, 2.5);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
close(after = 0) {
|
|
3442
|
+
if (!this.ctx || this.fakeNow != null) return;
|
|
3443
|
+
const ctx = this.ctx;
|
|
3444
|
+
const t = ctx.currentTime;
|
|
3445
|
+
this.out.gain.cancelScheduledValues(t);
|
|
3446
|
+
this.out.gain.setValueAtTime(this.out.gain.value, t);
|
|
3447
|
+
this.out.gain.linearRampToValueAtTime(0, t + Math.max(0.05, after));
|
|
3448
|
+
setTimeout(() => ctx.close().catch(() => {}), (after + 0.2) * 1000);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// -------------------------------------------------------------------- app
|
|
3453
|
+
|
|
3454
|
+
class App {
|
|
3455
|
+
constructor() {
|
|
3456
|
+
this.dead = false;
|
|
3457
|
+
this.t0 = performance.now();
|
|
3458
|
+
this.tour = false;
|
|
3459
|
+
this.tourV = 0;
|
|
3460
|
+
this.tourAcc = 0;
|
|
3461
|
+
this.speeds = [1, 2, 3];
|
|
3462
|
+
this.speedIx = 0;
|
|
3463
|
+
this.complete = false;
|
|
3464
|
+
this.timing = {};
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
async loadShot() {
|
|
3468
|
+
let src = window.__swarmShot;
|
|
3469
|
+
window.__swarmShot = null;
|
|
3470
|
+
if (src) lastShot = { src, at: this.env.geom() };
|
|
3471
|
+
else if (lastShot) src = lastShot.src; // Replay reuses the click-time screenshot
|
|
3472
|
+
if (!src) return null;
|
|
3473
|
+
const img = new Image();
|
|
3474
|
+
img.src = src;
|
|
3475
|
+
try {
|
|
3476
|
+
await img.decode();
|
|
3477
|
+
} catch (_) {
|
|
3478
|
+
return null;
|
|
3479
|
+
}
|
|
3480
|
+
const g = lastShot && lastShot.src === src ? lastShot.at : this.env.geom();
|
|
3481
|
+
return { img, sx: g.sx, sy: g.sy, scale: img.naturalWidth / innerWidth, valid: false };
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
async run() {
|
|
3485
|
+
this.env = detectEnv();
|
|
3486
|
+
const shotP = this.loadShot();
|
|
3487
|
+
this.view = new View(this.env);
|
|
3488
|
+
this.engine = new Engine(this.view);
|
|
3489
|
+
this.sfx = new Sound();
|
|
3490
|
+
this.sfx.init();
|
|
3491
|
+
this.engine.sfx = this.sfx;
|
|
3492
|
+
this.engine.onStart = () => this.view.intro.classList.add("gone");
|
|
3493
|
+
this.engine.onComplete = () => this.onComplete();
|
|
3494
|
+
this.bind();
|
|
3495
|
+
this.last = performance.now();
|
|
3496
|
+
this.raf = requestAnimationFrame((t) => this.frame(t));
|
|
3497
|
+
await nextFrame();
|
|
3498
|
+
this.renderSoundBtn();
|
|
3499
|
+
this.sfx.intro();
|
|
3500
|
+
// 白底不做 .5s 淡入(见 scripts/swarm-patch.mjs):第一帧就盖死,别让原页面透出来。
|
|
3501
|
+
this.view.bg.style.setProperty("transition", "none", "important");
|
|
3502
|
+
this.view.bg.classList.add("on");
|
|
3503
|
+
this.view.intro.classList.add("on");
|
|
3504
|
+
this.view.hud.classList.add("on");
|
|
3505
|
+
const mark = (k) => (this.timing[k] = Math.round(performance.now() - this.t0));
|
|
3506
|
+
await sleep(520);
|
|
3507
|
+
await unhidePage();
|
|
3508
|
+
mark("blank");
|
|
3509
|
+
if (this.dead) return;
|
|
3510
|
+
// 等插件放行(它要先拍完快照),并且给开场留够时间:白幕→特效这段是
|
|
3511
|
+
// "空白 + 光标飞进来打转",太短会一闪而过(用户实测豆瓣那种快站看不到)。
|
|
3512
|
+
// 至少 2.8 秒、最多 5 秒,之后无论有没有信号都开始画,不能把页面卡在空白上。
|
|
3513
|
+
{
|
|
3514
|
+
const waitFrom = performance.now();
|
|
3515
|
+
while (
|
|
3516
|
+
(window.__dshSwarmGo !== true || performance.now() - waitFrom < 2800) &&
|
|
3517
|
+
!this.dead &&
|
|
3518
|
+
performance.now() - waitFrom < 5000
|
|
3519
|
+
) {
|
|
3520
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
// 提前注入时 document_start 的 DOM 还没建全,滚动容器可能认错(异步渲染站)。
|
|
3524
|
+
// 放行之后页面已 load,重认一次滚动环境;**不动白底颜色**,避免中途变色闪一下。
|
|
3525
|
+
if (window.__dshSwarmEarly === true) {
|
|
3526
|
+
const oldTarget = this.env.isRoot ? window : this.env.scroller;
|
|
3527
|
+
const fresh = detectEnv();
|
|
3528
|
+
fresh.host = this.view.host;
|
|
3529
|
+
this.env = fresh;
|
|
3530
|
+
this.view.env = fresh;
|
|
3531
|
+
this.view.measure();
|
|
3532
|
+
// bind() 在开场就把 scroll 监听绑死在"当时的" scroller 上。重认后若换了容器,
|
|
3533
|
+
// 补一条到新容器,否则容器滚动时世界图层不跟随(2026-11-09 复核 find A)。
|
|
3534
|
+
// 旧监听留在原目标上无害:syncWorld() 每次读的都是新 env,幂等。
|
|
3535
|
+
const newTarget = fresh.isRoot ? window : fresh.scroller;
|
|
3536
|
+
if (newTarget !== oldTarget) {
|
|
3537
|
+
const sync = () => { if (!this.view.frozen) this.view.syncWorld(); };
|
|
3538
|
+
newTarget.addEventListener("scroll", sync, { passive: true });
|
|
3539
|
+
(this.unbinds || (this.unbinds = [])).push(() => newTarget.removeEventListener("scroll", sync, { passive: true }));
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
await this.scan();
|
|
3543
|
+
mark("scan");
|
|
3544
|
+
if (this.dead) return;
|
|
3545
|
+
try {
|
|
3546
|
+
await Promise.race([document.fonts.ready, sleep(1200)]);
|
|
3547
|
+
} catch (_) {}
|
|
3548
|
+
mark("fonts");
|
|
3549
|
+
this.view.measure();
|
|
3550
|
+
const shot = await shotP;
|
|
3551
|
+
if (shot) {
|
|
3552
|
+
const g = this.env.geom();
|
|
3553
|
+
shot.valid = Math.abs(g.sx - shot.sx) < 1 && Math.abs(g.sy - shot.sy) < 1;
|
|
3554
|
+
}
|
|
3555
|
+
let model;
|
|
3556
|
+
try {
|
|
3557
|
+
model = captureModel(this.env, shot);
|
|
3558
|
+
} catch (err) {
|
|
3559
|
+
console.error("[swarm-rebuild] capture failed", err);
|
|
3560
|
+
model = { items: [], tasks: [], groups: [], guides: [], docW: 0, docH: 0 };
|
|
3561
|
+
}
|
|
3562
|
+
mark("capture");
|
|
3563
|
+
if (this.dead) return;
|
|
3564
|
+
this.view.mount(model);
|
|
3565
|
+
this.engine.setModel(model);
|
|
3566
|
+
this.model = model;
|
|
3567
|
+
mark("mount");
|
|
3568
|
+
const wait = 1500 - (performance.now() - this.t0);
|
|
3569
|
+
if (wait > 0) await sleep(wait);
|
|
3570
|
+
if (this.dead) return;
|
|
3571
|
+
this.engine.start();
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
async scan() {
|
|
3575
|
+
const env = this.env;
|
|
3576
|
+
const g = env.geom();
|
|
3577
|
+
const H = env.scrollHeight();
|
|
3578
|
+
if (H <= g.vh * 1.25) return;
|
|
3579
|
+
this.view.frozen = { ...this.view.vp };
|
|
3580
|
+
const steps = Math.min(28, Math.ceil(H / (g.vh * 0.85)));
|
|
3581
|
+
for (let i = 1; i <= steps && !this.dead; i++) {
|
|
3582
|
+
env.scrollTo(Math.min(H, (H / steps) * i));
|
|
3583
|
+
await nextFrame();
|
|
3584
|
+
await sleep(30);
|
|
3585
|
+
}
|
|
3586
|
+
env.scrollTo(g.sy, g.sx);
|
|
3587
|
+
await nextFrame();
|
|
3588
|
+
await sleep(750);
|
|
3589
|
+
env.scrollTo(g.sy, g.sx);
|
|
3590
|
+
this.view.frozen = null;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
bind() {
|
|
3594
|
+
const on = (t, ev, fn, o) => {
|
|
3595
|
+
t.addEventListener(ev, fn, o);
|
|
3596
|
+
(this.unbinds || (this.unbinds = [])).push(() => t.removeEventListener(ev, fn, o));
|
|
3597
|
+
};
|
|
3598
|
+
const scrollTarget = this.env.isRoot ? window : this.env.scroller;
|
|
3599
|
+
on(scrollTarget, "scroll", () => {
|
|
3600
|
+
if (!this.view.frozen) this.view.syncWorld();
|
|
3601
|
+
}, { passive: true });
|
|
3602
|
+
on(window, "resize", () => this.view.measure());
|
|
3603
|
+
on(window, "wheel", () => {
|
|
3604
|
+
if (this.engine.ready) this.engine.start();
|
|
3605
|
+
}, { passive: true });
|
|
3606
|
+
on(window, "pointermove", (e) => {
|
|
3607
|
+
this.engine.mouse = { x: e.clientX, y: e.clientY };
|
|
3608
|
+
}, { passive: true });
|
|
3609
|
+
on(document.documentElement, "pointerleave", () => {
|
|
3610
|
+
this.engine.mouse = null;
|
|
3611
|
+
});
|
|
3612
|
+
on(window, "keydown", (e) => {
|
|
3613
|
+
if (e.key === "Escape") {
|
|
3614
|
+
e.stopPropagation();
|
|
3615
|
+
stop(false);
|
|
3616
|
+
} else if ((e.key === "m" || e.key === "M") && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
3617
|
+
this.sfx.setOn(!this.sfx.on);
|
|
3618
|
+
this.renderSoundBtn();
|
|
3619
|
+
}
|
|
3620
|
+
this.unlockSound();
|
|
3621
|
+
}, true);
|
|
3622
|
+
// browsers only let audio start after a click/keypress on the page
|
|
3623
|
+
on(window, "pointerdown", () => this.unlockSound(), true);
|
|
3624
|
+
if (!this.env.isRoot) {
|
|
3625
|
+
on(this.view.host, "wheel", (e) => {
|
|
3626
|
+
e.preventDefault();
|
|
3627
|
+
this.env.scroller.scrollBy({ top: e.deltaY, left: e.deltaX, behavior: "instant" });
|
|
3628
|
+
}, { passive: false });
|
|
3629
|
+
}
|
|
3630
|
+
on(this.view.hud, "click", (e) => {
|
|
3631
|
+
const b = e.target.closest("button");
|
|
3632
|
+
if (!b) return;
|
|
3633
|
+
const a = b.dataset.a;
|
|
3634
|
+
if (a === "close") stop(false);
|
|
3635
|
+
else if (a === "live") stop(true);
|
|
3636
|
+
else if (a === "replay") restart();
|
|
3637
|
+
else if (a === "tour") {
|
|
3638
|
+
this.tour = !this.tour;
|
|
3639
|
+
b.classList.toggle("on", this.tour);
|
|
3640
|
+
b.textContent = this.tour ? "Touring" : "Tour";
|
|
3641
|
+
if (this.tour) this.engine.start();
|
|
3642
|
+
} else if (a === "sound") {
|
|
3643
|
+
if (this.sfx.on && this.sfx.blocked) this.sfx.resume();
|
|
3644
|
+
else this.sfx.setOn(!this.sfx.on);
|
|
3645
|
+
setTimeout(() => this.renderSoundBtn(), 60);
|
|
3646
|
+
} else if (a === "speed") {
|
|
3647
|
+
this.speedIx = (this.speedIx + 1) % this.speeds.length;
|
|
3648
|
+
this.engine.speed = this.speeds[this.speedIx];
|
|
3649
|
+
b.textContent = this.speeds[this.speedIx] + "×";
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
unlockSound() {
|
|
3655
|
+
if (this.dead || !this.sfx || !this.sfx.blocked) return;
|
|
3656
|
+
this.sfx.resume();
|
|
3657
|
+
setTimeout(() => this.renderSoundBtn(), 80);
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
renderSoundBtn() {
|
|
3661
|
+
const b = this.view.root.querySelector('[data-a="sound"]');
|
|
3662
|
+
if (!b || !this.sfx) return;
|
|
3663
|
+
const tap = this.sfx.on && this.sfx.blocked;
|
|
3664
|
+
const key = this.sfx.on ? (tap ? "tap" : "on") : "off";
|
|
3665
|
+
if (b._k === key) return;
|
|
3666
|
+
b._k = key;
|
|
3667
|
+
b.classList.toggle("off", key === "off");
|
|
3668
|
+
b.classList.toggle("tap", tap);
|
|
3669
|
+
const waves = '<path d="M11 5.5a3.5 3.5 0 0 1 0 5M12.8 3.6a6 6 0 0 1 0 8.8"/>';
|
|
3670
|
+
const mute = '<path d="M11 6l4 4M15 6l-4 4"/>';
|
|
3671
|
+
b.innerHTML = `${SPK_ICON}${key === "off" ? mute : waves}</svg>${tap ? "<span>Tap for sound</span>" : ""}`;
|
|
3672
|
+
b.title = key === "off" ? "Sound off (M)" : "Sound on (M)";
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
frame(now) {
|
|
3676
|
+
if (this.dead) return;
|
|
3677
|
+
const dt = Math.min(0.05, (now - this.last) / 1000);
|
|
3678
|
+
this.last = now;
|
|
3679
|
+
try {
|
|
3680
|
+
this.engine.step(dt);
|
|
3681
|
+
if (this.tour) this.tourStep(dt);
|
|
3682
|
+
this.view.render(this.engine);
|
|
3683
|
+
if (!this._hudT || now - this._hudT > 200) {
|
|
3684
|
+
this._hudT = now;
|
|
3685
|
+
this.updateHud();
|
|
3686
|
+
this.renderSoundBtn();
|
|
3687
|
+
}
|
|
3688
|
+
} catch (err) {
|
|
3689
|
+
console.error("[swarm-rebuild]", err);
|
|
3690
|
+
}
|
|
3691
|
+
this.raf = requestAnimationFrame((t) => this.frame(t));
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
tourStep(dt) {
|
|
3695
|
+
const E = this.engine;
|
|
3696
|
+
if (!E.started) return;
|
|
3697
|
+
const g = this.env.geom();
|
|
3698
|
+
const maxY = this.env.scrollHeight() - g.vh;
|
|
3699
|
+
if (g.sy >= maxY - 1) {
|
|
3700
|
+
this.tourV = 0;
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
const crew = Math.max(MIN_AGENTS, E.cursors.filter((c) => !c.leaving).length);
|
|
3704
|
+
const backlog = E.backlog();
|
|
3705
|
+
const target = backlog <= Math.max(4, crew * 0.6) ? 230 : backlog <= crew * 1.5 ? 80 : 0;
|
|
3706
|
+
this.tourV += (target - this.tourV) * Math.min(1, dt * 2.5);
|
|
3707
|
+
this.tourAcc += this.tourV * dt;
|
|
3708
|
+
if (this.tourAcc >= 1) {
|
|
3709
|
+
const d = Math.floor(this.tourAcc);
|
|
3710
|
+
this.tourAcc -= d;
|
|
3711
|
+
this.env.scrollBy(d);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
updateHud() {
|
|
3716
|
+
const E = this.engine;
|
|
3717
|
+
const v = this.view;
|
|
3718
|
+
if (!v.hud.isConnected) return;
|
|
3719
|
+
if (!E.ready) return;
|
|
3720
|
+
const workers = E.cursors.filter((c) => c.worked).length + E.retired;
|
|
3721
|
+
const total = E.tasks.length;
|
|
3722
|
+
const done = E.doneCount;
|
|
3723
|
+
if (this.complete) return;
|
|
3724
|
+
if (!E.started) v.status.innerHTML = `<b>${E.cursors.length}</b> agents ready<span class="sep"></span>${total.toLocaleString()} layers`;
|
|
3725
|
+
else {
|
|
3726
|
+
const pend = E.tasks.length - done;
|
|
3727
|
+
const below = E.tasks.some((t) => t.state === "wait" && t.layer === "world" && t.gate > E.frontier);
|
|
3728
|
+
v.status.innerHTML =
|
|
3729
|
+
`<b>${workers}</b> agents<span class="sep"></span><b>${done.toLocaleString()}</b> / ${total.toLocaleString()} layers` +
|
|
3730
|
+
(below && pend && !this.tour && E.backlog() === 0 ? `<span class="sep"></span>scroll to keep building` : "");
|
|
3731
|
+
}
|
|
3732
|
+
v.barFill.style.width = total ? `${(done / total) * 100}%` : "0%";
|
|
3733
|
+
const busy = E.cursors.filter((c) => c.task).slice(-5);
|
|
3734
|
+
const key = busy.map((c) => c.id).join(",");
|
|
3735
|
+
if (key !== this._dots) {
|
|
3736
|
+
this._dots = key;
|
|
3737
|
+
v.dots.innerHTML = busy.map((c) => `<i style="background:${c.color}" title="${c.name}">${c.name[0]}</i>`).join("");
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
onComplete() {
|
|
3742
|
+
this.complete = true;
|
|
3743
|
+
if (this.sfx) this.sfx.finale();
|
|
3744
|
+
const E = this.engine;
|
|
3745
|
+
const v = this.view;
|
|
3746
|
+
const workers = E.cursors.filter((c) => c.worked).length + E.retired;
|
|
3747
|
+
const secs = (E.endTime - E.startTime).toFixed(1);
|
|
3748
|
+
v.status.innerHTML = `Rebuilt by <b>${workers}</b> agents in <b>${secs}s</b>`;
|
|
3749
|
+
v.barFill.style.width = "100%";
|
|
3750
|
+
v.dots.innerHTML = "";
|
|
3751
|
+
// 控制条已移除:逐个找、找到才设,找不到就跳过(原来这里是硬取 .hidden)。
|
|
3752
|
+
for (const [name, hidden] of [["live", false], ["replay", false], ["tour", true], ["speed", true]]) {
|
|
3753
|
+
const button = v.root.querySelector('[data-a="' + name + '"]');
|
|
3754
|
+
if (button) button.hidden = hidden;
|
|
3755
|
+
}
|
|
3756
|
+
this.tour = false;
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
destroy(reveal) {
|
|
3760
|
+
unhidePage();
|
|
3761
|
+
if (this.sfx) {
|
|
3762
|
+
if (reveal) this.sfx.release();
|
|
3763
|
+
this.sfx.close(reveal ? 1.4 : 0.25);
|
|
3764
|
+
}
|
|
3765
|
+
const finish = () => {
|
|
3766
|
+
this.dead = true;
|
|
3767
|
+
cancelAnimationFrame(this.raf);
|
|
3768
|
+
for (const u of this.unbinds || []) u();
|
|
3769
|
+
this.view.host.remove();
|
|
3770
|
+
};
|
|
3771
|
+
if (reveal) {
|
|
3772
|
+
this.engine.releasing = true;
|
|
3773
|
+
this.engine.flocking = true;
|
|
3774
|
+
for (const c of this.engine.cursors) {
|
|
3775
|
+
c.gen = null;
|
|
3776
|
+
c.follow = false;
|
|
3777
|
+
}
|
|
3778
|
+
this.view.hud.classList.remove("on");
|
|
3779
|
+
setTimeout(() => {
|
|
3780
|
+
this.view.host.style.setProperty("opacity", "0", "important");
|
|
3781
|
+
}, 450);
|
|
3782
|
+
setTimeout(finish, 1150);
|
|
3783
|
+
} else {
|
|
3784
|
+
this.view.host.style.setProperty("transition", "opacity .25s ease", "important");
|
|
3785
|
+
this.view.host.style.setProperty("opacity", "0", "important");
|
|
3786
|
+
this.dead = true;
|
|
3787
|
+
cancelAnimationFrame(this.raf);
|
|
3788
|
+
for (const u of this.unbinds || []) u();
|
|
3789
|
+
setTimeout(() => this.view.host.remove(), 260);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
// background.js may have hidden everything but media for its screenshot;
|
|
3795
|
+
// put the page back once our canvas covers it (or if we bail out early).
|
|
3796
|
+
async function unhidePage() {
|
|
3797
|
+
if (!window.__swarmHidden) return;
|
|
3798
|
+
window.__swarmHidden = false;
|
|
3799
|
+
try {
|
|
3800
|
+
if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.sendMessage) await chrome.runtime.sendMessage({ type: "swarm:unhide" });
|
|
3801
|
+
} catch (_) {}
|
|
3802
|
+
if (typeof window.__swarmUnhide === "function") {
|
|
3803
|
+
try {
|
|
3804
|
+
window.__swarmUnhide();
|
|
3805
|
+
} catch (_) {}
|
|
3806
|
+
}
|
|
3807
|
+
await nextFrame();
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
let app = null;
|
|
3811
|
+
let lastShot = null;
|
|
3812
|
+
|
|
3813
|
+
function start() {
|
|
3814
|
+
if (app) return;
|
|
3815
|
+
app = new App();
|
|
3816
|
+
app.run().catch((err) => console.error("[swarm-rebuild]", err));
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
function stop(reveal) {
|
|
3820
|
+
if (!app) return;
|
|
3821
|
+
const a = app;
|
|
3822
|
+
app = null;
|
|
3823
|
+
a.destroy(reveal);
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
function restart() {
|
|
3827
|
+
if (app) {
|
|
3828
|
+
const a = app;
|
|
3829
|
+
app = null;
|
|
3830
|
+
a.dead = true;
|
|
3831
|
+
cancelAnimationFrame(a.raf);
|
|
3832
|
+
for (const u of a.unbinds || []) u();
|
|
3833
|
+
a.view.host.remove();
|
|
3834
|
+
if (a.sfx) a.sfx.close(0.1);
|
|
3835
|
+
// rebuild from where the first run started so screenshot crops line up again
|
|
3836
|
+
if (lastShot) a.env.scrollTo(lastShot.at.sy, lastShot.at.sx);
|
|
3837
|
+
}
|
|
3838
|
+
start();
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
window[KEY] = {
|
|
3842
|
+
toggle() {
|
|
3843
|
+
if (app) stop(false);
|
|
3844
|
+
else start();
|
|
3845
|
+
},
|
|
3846
|
+
start,
|
|
3847
|
+
stop,
|
|
3848
|
+
get app() {
|
|
3849
|
+
return app;
|
|
3850
|
+
},
|
|
3851
|
+
debug: { captureModel, detectEnv, Sound },
|
|
3852
|
+
};
|
|
3853
|
+
// document_start 注入时 <html> 还没建好(实测 scrollingElement 为 null)。等解析器
|
|
3854
|
+
// 把 <html> 挂上来再启动,就能从第一帧用**原版自己的引擎**盖住页面:白底 + 18 个
|
|
3855
|
+
// 光标飞进来打转,全程只有这一套,不再有"自造白幕 → 切换引擎"的断层。
|
|
3856
|
+
const _dshBoot = () => {
|
|
3857
|
+
if (document.documentElement) { start(); return; }
|
|
3858
|
+
const mo = new MutationObserver(() => {
|
|
3859
|
+
if (document.documentElement) { mo.disconnect(); start(); }
|
|
3860
|
+
});
|
|
3861
|
+
mo.observe(document, { childList: true });
|
|
3862
|
+
};
|
|
3863
|
+
_dshBoot();
|
|
3864
|
+
})();
|