rather
// than a custom tag (a page could define that tag and reach a closed root via ElementInternals).
const host = document.createElement("div");
host.setAttribute("data-swarm-rebuild", "");
host.style.cssText =
"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;";
env.host = host;
this.host = host;
const root = host.attachShadow({ mode: "closed" });
this.root = root;
root.innerHTML = `
Scanning page…
`;
const $ = (s) => root.querySelector(s);
this.bg = $(".bg");
this.under = $(".under");
this.over = $(".over");
this.port = $(".port");
this.world = $(".world");
this.intro = $(".intro");
this.ov = $(".ov");
this.badges = $(".badges");
this.cursorsEl = $(".cursors");
this.rulers = $(".rulers");
this.hud = $(".hud");
this.unhideBtn = $(".hud-unhide");
this.status = $(".status");
this.barFill = $(".bar i");
this.dots = $(".dots");
this.cursorEls = new Map();
this._ovS = "";
this._ovB = "";
this._rulersOn = false;
const pb = env.pageBg;
this.bg.style.backgroundColor = pb.color;
const rgb = (pb.color.match(/[\d.]+/g) || [255, 255, 255]).map(Number);
const lum = (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
if (lum < 0.4) this.host.style.setProperty("--ph", "#3B3D42");
if (pb.image && pb.image !== "none") {
this.bg.style.backgroundImage = pb.image;
this.bg.style.backgroundSize = pb.size;
this.bg.style.backgroundPosition = pb.position;
this.bg.style.backgroundRepeat = pb.repeat;
}
this.makeIntroLogo();
document.documentElement.appendChild(host);
this.measure();
}
makeIntroLogo() {
const links = [...document.querySelectorAll('link[rel~="icon"], link[rel="apple-touch-icon"], link[rel="shortcut icon"]')];
const sized = links
.map((l) => ({ href: l.href, s: parseInt((l.getAttribute("sizes") || "0").split("x")[0], 10) || (l.rel.includes("apple") ? 180 : 32) }))
.filter((l) => l.href)
.sort((a, b) => b.s - a.s);
const label = (document.title || location.hostname || "?").replace(/^www\./, "").trim();
const mono = () => {
this.intro.innerHTML = "";
const d = document.createElement("div");
d.className = "mono";
d.textContent = (label[0] || "?").toUpperCase();
this.intro.appendChild(d);
};
const href = sized.length ? sized[0].href : location.origin + "/favicon.ico";
const img = document.createElement("img");
img.onerror = mono;
img.src = href;
this.intro.appendChild(img);
}
measure() {
this.W = innerWidth;
this.H = innerHeight;
this.g = this.env.geom();
const g = this.g;
this.port.style.left = g.ox + "px";
this.port.style.top = g.oy + "px";
this.port.style.width = g.vw + "px";
this.port.style.height = g.vh + "px";
}
get vp() {
if (this.frozen) return this.frozen;
const g = this.env.geom();
return { ...g, ox: this.g.ox, oy: this.g.oy };
}
syncWorld() {
const vp = this.vp;
this.world.style.transform = `translate(${-vp.sx}px,${-vp.sy}px)`;
}
mount(model) {
this.world.style.width = model.docW + "px";
this.world.style.height = model.docH + "px";
const by = { world: [], under: [], over: [] };
for (const it of model.items) by[it.layer].push(it);
if (model.portKey != null) {
// inner scroller: its scrolling content sits at the scroller's own place in
// the stacking order (above its dialog/panel background, below later UI)
const screen = [...by.under, ...by.over].sort((a, b) => a.key - b.key);
const frag = document.createDocumentFragment();
let placed = false;
for (const it of screen) {
if (!placed && it.key > model.portKey) {
frag.appendChild(this.port);
placed = true;
}
frag.appendChild(this.makeNode(it));
}
if (!placed) frag.appendChild(this.port);
this.over.appendChild(frag);
const wl = by.world.sort((a, b) => a.key - b.key);
const wf = document.createDocumentFragment();
for (const it of wl) wf.appendChild(this.makeNode(it));
this.world.appendChild(wf);
} else
for (const layer of ["under", "world", "over"]) {
const list = by[layer].sort((a, b) => a.key - b.key);
const frag = document.createDocumentFragment();
for (const it of list) frag.appendChild(this.makeNode(it));
(layer === "world" ? this.world : this[layer]).appendChild(frag);
}
this.backdrops = (model.backdrops || []).map((b) => {
const d = document.createElement("div");
d.className = "bd";
const rgb = (b.color.match(/[\d.]+/g) || [255, 255, 255]).slice(0, 3).join(",");
d.setAttribute("style", `left:${b.x}px;top:${b.y}px;width:${b.w}px;height:${b.h}px;background:rgba(${rgb},.82);`);
this.over.insertBefore(d, this.over.firstChild);
return d;
});
this.bdStart = this.env.geom().sy;
}
makeNode(it) {
const n = document.createElement("div");
n.style.display = "none";
it.node = n;
if (it.kind === "rect") {
n.className = "it r";
const f = document.createElement("div");
f.className = "f";
n.appendChild(f);
} else if (it.kind === "image") {
n.className = "it m";
n.innerHTML = IMG_ICON;
} else if (it.kind === "text") {
n.className = "it t";
prepText(it);
} else if (it.kind === "line") {
n.className = "it l";
n.innerHTML = '
';
}
if (it.op < 0.999) n.style.opacity = it.op;
if (it.blend) n.style.mixBlendMode = it.blend;
return n;
}
renderItem(it) {
const tw = it.twinOf;
if (tw && it._tv !== tw.v) {
it._tv = tw.v;
it.vis = tw.vis;
it.shown = tw.total ? Math.round((tw.shown / tw.total) * it.total) : it.total;
it.dx = tw.dx;
it.dy = tw.dy;
it.scale = tw.scale;
it.done = tw.done;
it.v++;
}
if (it.rv === it.v) return;
it.rv = it.v;
const n = it.node;
if (!it.vis) {
if (n.style.display !== "none") n.style.display = "none";
return;
}
if (n.style.display === "none") n.style.display = "";
if (it.settle) {
it.settle = false;
n.classList.add("settle");
clearTimeout(n._st);
n._st = setTimeout(() => n.classList.remove("settle"), 340);
}
if (it.kind === "line") {
n.style.transform = `translate(${it.dx}px,${it.dy}px)`;
const pts = it.pts.slice(0, it.placed);
const d = pts.map((p, i) => `${i ? "L" : "M"}${p.x} ${p.y}`).join(" ") + (it.shut ? " Z" : "");
const path = n.firstChild.firstChild;
path.setAttribute("d", d);
path.setAttribute("stroke", it.stroke);
path.setAttribute("stroke-width", it.sw);
path.setAttribute("stroke-linecap", "square");
if (it.dash) path.setAttribute("stroke-dasharray", it.dash);
return;
}
const w = it.draw ? it.draw.w : it.w;
const h = it.draw ? it.draw.h : it.h;
let tf = `translate(${it.x + it.dx}px,${it.y + it.dy}px)`;
if (it.scale !== 1) tf += ` scale(${it.scale})`;
n.style.transform = tf;
n.style.width = w + "px";
n.style.height = h + "px";
const c = it.clip;
n.style.clipPath =
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)` : "";
if (it.kind === "rect") {
const css = it.mode === "master" ? it.copyOf.css : it.css;
if (n._css !== css) {
n.firstChild.setAttribute("style", css);
n._css = css;
}
n.classList.toggle("st", !!it.styled);
n.style.borderRadius = it.styled ? (it.mode === "master" ? it.copyOf.radius : it.radius) : "0px";
} else if (it.kind === "image") {
const src = it.mode === "master" ? it.copyOf : it;
if (n._src !== src) {
const old = n.querySelector(".md");
const m = src.media.cloneNode(true);
m.classList.add("md");
if (old) {
m.classList.add("swap");
old.replaceWith(m);
} else n.appendChild(m);
if (m.localName === "video") {
m.muted = true;
const t = parseFloat(m.getAttribute("data-t"));
if (t > 0) m.addEventListener("loadedmetadata", () => (m.currentTime = t), { once: true });
if (m.hasAttribute("data-play")) m.play().catch(() => {});
}
n._src = src;
if (src.frame) {
let fr = n.querySelector(".fr");
if (!fr) {
fr = document.createElement("div");
fr.className = "fr";
n.appendChild(fr);
}
fr.setAttribute("style", src.frame + `border-radius:${src.radius};`);
}
}
n.classList.toggle("st", !!it.styled);
n.style.borderRadius = it.styled ? src.radius : "0px";
n.style.boxShadow = it.styled ? src.shadow : "";
n.style.backgroundColor = it.styled && src.bg ? src.bg : "";
} else if (it.kind === "text") {
let content;
if (it.mode === "master") {
if (!it._mv) it._mv = it.copyOf.full.cloneNode(true);
content = it._mv;
} else content = it.live;
if (n.firstChild !== content) {
n.textContent = "";
n.appendChild(content);
}
if (it.mode !== "master" && it.rshown !== it.shown) {
it._last = reveal(it, it.shown);
it.rshown = it.shown;
}
content.classList.toggle("sa", !!it.selAll);
const wantCaret = it.editing && !it.selAll && it.mode !== "master";
if (wantCaret) {
if (!it._caret) {
it._caret = document.createElement("span");
it._caret.className = "caret";
}
const last = it._last;
if (last && last.parentNode) last.parentNode.insertBefore(it._caret, last.nextSibling);
else content.insertBefore(it._caret, content.firstChild);
} else if (it._caret && it._caret.parentNode) it._caret.remove();
if (it.scale !== 1) {
const ox = it.tb.x - it.x + (it.align === "center" ? it.tb.w / 2 : it.align === "right" || it.align === "end" ? it.tb.w : 0);
n.style.transformOrigin = `${ox}px ${it.tb.y - it.y}px`;
}
}
}
renderCursors(E) {
const vp = E.vp;
const seen = new Set();
for (const c of E.cursors) {
let el = this.cursorEls.get(c.id);
if (!el) {
el = document.createElement("div");
el.className = "cursor";
el.innerHTML = '
';
this.cursorsEl.appendChild(el);
this.cursorEls.set(c.id, el);
el._shape = "";
}
seen.add(c.id);
const X = c.sticky ? c.x : c.x - vp.sx + vp.ox;
const Y = c.sticky ? c.y : c.y - vp.sy + vp.oy;
el.style.transform = `translate(${f1(X)}px,${f1(Y)}px)`;
const svg = el.firstChild;
if (el._shape !== c.shape) {
svg.innerHTML = cursorSVG(c.shape, c.color);
el._shape = c.shape;
const [hx, hy] = HOT[c.shape];
el._hx = hx;
el._hy = hy;
svg.style.transformOrigin = `${hx}px ${hy}px`;
}
const s = 1 - c.press * 0.12;
svg.style.transform = `translate(${-el._hx}px,${-el._hy}px) rotate(${f1(c.angle)}deg) scale(${s})`;
}
for (const [id, el] of this.cursorEls)
if (!seen.has(id)) {
el.remove();
this.cursorEls.delete(id);
}
}
renderOverlay(E) {
const vp = E.vp;
const W = this.W;
const H = this.H;
const X = (x, st) => f1(st ? x : x - vp.sx + vp.ox);
const Y = (y, st) => f1(st ? y : y - vp.sy + vp.oy);
let s = "";
let b = "";
for (const g of E.guides) {
if (!g.vis || g.op <= 0) continue;
const sw = 1 + g.flash;
const flt = g.flash > 0 ? ` style="filter:drop-shadow(0 0 ${f1(2 + g.flash * 3)}px ${RED})"` : "";
if (g.axis === "x") {
const x = X(g.cur);
s += `
`;
} else {
const y = Y(g.cur);
s += `
`;
}
}
for (const it of E.editLines) {
const st = it.layer !== "world";
const pts = it.pts.slice(0, it.placed).map((p) => [X(p.x + it.dx, st), Y(p.y + it.dy, st)]);
const o = it.owner;
const all = o ? [...pts, [X(o.x, o.sticky), Y(o.y, o.sticky)]] : pts;
s += `
`;
for (const p of pts) s += `
`;
}
const rt = E.rulers.t;
for (const c of E.cursors) {
if (!c.ov) continue;
const st = c.sticky;
for (const v of c.ov()) {
if (v.t === "box") {
const x = X(v.b.x, st);
const y = Y(v.b.y, st);
const w = f1(v.b.w);
const h = f1(v.b.h);
const col = v.color === "red" ? RED : BLUE;
s += `
`;
if (v.underline && v.underline.w > 0) {
const u = v.underline;
s += `
`;
}
if (v.rulerLines && rt > 0.05)
s += `
`;
if (v.handles)
for (const [hx, hy] of [[x, y], [x + w, y], [x, y + h], [x + w, y + h]])
s += `
`;
if (v.badge && w > 0) b += `
${v.badge}
`;
} else if (v.t === "measure") {
const a = { x: X(v.a.x, st), y: Y(v.a.y, st) };
const z = { x: X(v.b.x, st), y: Y(v.b.y, st) };
s += `
`;
const horiz = a.y === z.y;
b += `
${v.label}
`;
}
}
}
if (s !== this._ovS) {
this.ov.innerHTML = s;
this._ovS = s;
}
if (b !== this._ovB) {
this.badges.innerHTML = b;
this._ovB = b;
}
}
renderRulers(E) {
const t = E.rulers.t;
if (t <= 0.001) {
if (this._rulersOn) {
this.rulers.innerHTML = "";
this._rulersOn = false;
}
return;
}
this._rulersOn = true;
const vp = E.vp;
const W = this.W;
const H = this.H;
const step = 100;
let u = "";
let a = "";
const x0 = Math.floor((vp.sx - vp.ox) / step) * step;
for (let c = x0; c - vp.sx + vp.ox < W; c += step / 2) {
const x = f1(c - vp.sx + vp.ox);
const major = c % step === 0;
u += `
`;
if (major) u += `
${c}`;
}
const y0 = Math.floor((vp.sy - vp.oy) / step) * step;
for (let c = y0; c - vp.sy + vp.oy < H; c += step / 2) {
const y = f1(c - vp.sy + vp.oy);
const major = c % step === 0;
a += `
`;
if (major) a += `
${c}`;
}
for (const c of E.cursors) {
if (!c.ov || c.sticky) continue;
for (const v of c.ov()) {
if (v.t !== "box" || !v.handles) continue;
const l = f1(v.b.x - vp.sx + vp.ox);
const r = f1(v.b.x + v.b.w - vp.sx + vp.ox);
const tp = f1(v.b.y - vp.sy + vp.oy);
const bt = f1(v.b.y + v.b.h - vp.sy + vp.oy);
u = `
` + u + `
${Math.round(v.b.x)}`;
a = `
` + a;
}
}
for (const g of E.guides) {
if (!g.dragging) continue;
if (g.axis === "x") {
const x = f1(g.cur - vp.sx + vp.ox);
u += `
${Math.round(g.cur)}`;
} else {
const y = f1(g.cur - vp.sy + vp.oy);
a += `
`;
}
}
this.rulers.innerHTML =
`
` +
`
` +
`
`;
}
render(E) {
this.syncWorld();
if (this.backdrops && this.backdrops.length) {
const o = f1(clamp((Math.abs(E.vp.sy - this.bdStart) - 8) / 80, 0, 1));
if (o !== this._bdO) {
this._bdO = o;
for (const d of this.backdrops) d.style.opacity = o;
}
}
if (E.items) for (const it of E.items) this.renderItem(it);
this.renderOverlay(E);
this.renderCursors(E);
this.renderRulers(E);
}
}
// ----------------------------------------------------------------- engine
class Engine {
constructor(view) {
this.view = view;
this.vp = view.vp;
this.items = null;
this.tasks = [];
this.groups = [];
this.guides = [];
this.editLines = new Set();
this.cursors = [];
this.started = false;
this.ready = false;
this.frontier = 0;
this.vel = 0;
this.tempo = 1;
this.speed = 1;
this.time = 0;
this.rulers = { want: 0, t: 0, lastNeed: -10 };
this.lastScroll = null;
this.nextId = 0;
this.spawnCooldown = 0;
this.flocking = false;
this.lastT = null;
this.tv = { x: 0, y: 0 };
this.mouse = null;
this.retired = 0;
this.doneCount = 0;
this.waitCount = 0;
const vp = this.vp;
for (let n = 0; n < INTRO_AGENTS; n++) {
const side = n % 2 === 0;
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);
const c = this.makeCursor(p.x, p.y, n < MIN_AGENTS);
c.follow = true;
this.cursors.push(c);
}
}
setModel(m) {
this.items = m.items;
this.tasks = m.tasks;
this.groups = m.groups;
this.guides = m.guides;
this.guideDeps = new Map(m.guides.map((g) => [g, g.deps || []]));
this.waitCount = m.tasks.length;
this.ready = true;
}
makeCursor(x, y, home = false) {
const id = this.nextId++;
const fast = Math.random() < 0.25;
return {
id, x, y, home,
name: NAMES[id % NAMES.length],
color: COLORS[id % COLORS.length],
shape: "arrow", press: 0, task: null, gen: null, ov: null,
idleFor: 0, leaving: false, worked: false, sticky: false,
vx: 0, vy: 0, angle: 0,
speed: fast ? rand(1000, 1500) : rand(300, 750),
force: fast ? rand(3500, 6000) : rand(700, 1800),
spin: fast ? rand(2.2, 3.4) : rand(0.8, 1.8),
curl: rand(-1, 1), orbit: rand(70, 170), follow: false,
};
}
get viewTop() {
return this.vp.sy;
}
get viewBot() {
return this.vp.sy + this.vp.vh;
}
fromViewport(x, y) {
const vp = this.vp;
return { x: x - vp.ox + vp.sx, y: y - vp.oy + vp.sy };
}
taskY(t) {
return t.layer === "world" ? t.y : this.viewTop + t.y;
}
// screen position of a cursor / an item (for panning and loudness)
scr(c) {
const vp = this.vp;
return c.sticky ? { x: c.x, y: c.y } : { x: c.x - vp.sx + vp.ox, y: c.y - vp.sy + vp.oy };
}
sfxPlace(kind, it, b) {
if (!this.sfx) return;
const vp = this.vp;
b = b || this.bounds(it);
let x = b.x + b.w / 2;
let y = b.y + b.h / 2;
if (!it || it.layer === "world") {
x += vp.ox - vp.sx;
y += vp.oy - vp.sy;
}
this.sfx.place(kind, x, y, b.w, b.h, vp.vw, vp.vh, (it && it.fs) || 0);
}
start() {
if (this.started || !this.ready) return;
this.started = true;
this.startTime = this.time;
for (const c of this.cursors) {
c.follow = false;
c.gen = null;
c.angle = ((((c.angle + 180) % 360) + 360) % 360) - 180;
}
if (this.sfx) this.sfx.start();
if (this.onStart) this.onStart();
}
bounds(it) {
if (it.kind === "text") {
let h = it.tb.h;
if (it.editing && !it.selAll && it.total) {
const lines = Math.max(1, Math.round(it.tb.h / it.lh));
h = Math.min(it.tb.h, Math.max(it.lh, Math.ceil((it.shown / it.total) * lines) * it.lh));
}
const s = it.scale;
const ax = it.align === "center" ? it.tb.w * (1 - s) / 2 : it.align === "right" || it.align === "end" ? it.tb.w * (1 - s) : 0;
return { x: it.tb.x + it.dx + ax, y: it.tb.y + it.dy, w: it.tb.w * s, h: h * s };
}
if (it.kind === "line") {
return { x: it.x + it.dx, y: it.y + it.dy, w: it.w, h: it.h };
}
return {
x: it.x + it.dx,
y: it.y + it.dy,
w: it.draw ? it.draw.w : it.w,
h: it.draw ? it.draw.h : it.h,
};
}
unionBounds(list) {
const bs = list.map((it) => this.bounds(it));
const x = Math.min(...bs.map((b) => b.x));
const y = Math.min(...bs.map((b) => b.y));
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 };
}
underline(it, b) {
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 };
}
selection(it, extra) {
const b = this.bounds(it);
return Object.assign(
{
t: "box",
b,
handles: true,
badge: `${Math.round(b.w)} × ${Math.round(b.h)}`,
rulerLines: true,
underline: it.kind === "text" ? this.underline(it, b) : undefined,
},
extra,
);
}
available() {
const out = [];
for (const t of this.tasks) {
if (t.state !== "wait") continue;
if (t.layer === "world" && t.gate > this.frontier) continue;
let ok = true;
for (const d of t.deps)
if (d.state !== "done") {
ok = false;
break;
}
if (ok) out.push(t);
}
return out;
}
pick(c, list) {
let best = null;
let bestS = Infinity;
const top = this.viewTop - 40;
const bot = this.viewBot;
const cy = c.sticky ? c.y + this.viewTop : c.y;
for (const t of list) {
const y = this.taskY(t);
const u = y >= top && y <= bot ? 0 : y < top ? 1 : 2;
const s = u * 1e6 + (u === 1 ? top - y : y) * 0.6 + Math.abs(y - cy) * 0.4 - (t.spec.type === "guide" ? 5e5 : 0);
if (s < bestS) {
bestS = s;
best = t;
}
}
return best;
}
// work that's unlocked but unfinished (things past the frontier need scrolling, not waiting)
backlog() {
let n = 0;
for (const t of this.tasks) if (t.state !== "done" && (t.layer !== "world" || t.gate <= this.frontier)) n++;
return n;
}
step(dt) {
dt = Math.min(dt, 0.05);
this.time += dt;
const vp = (this.vp = this.view.vp);
const sv = Math.abs(vp.sy - (this.lastScroll == null ? vp.sy : this.lastScroll)) / Math.max(dt, 0.001);
this.lastScroll = vp.sy;
this.vel = Math.max(this.vel * Math.exp(-dt * 1.2), this.vel * 0.7 + sv * 0.3);
if (this.started) this.frontier = Math.max(this.frontier, this.viewBot);
const avail = this.started ? this.available() : [];
this.tempo = clamp(2.1 + this.vel / 900 + (avail.length / Math.max(1, this.cursors.length)) * 0.5, 2.1, 4.5) * this.speed;
if (this.rulers.want > 0) this.rulers.lastNeed = this.time;
const rt = this.time - this.rulers.lastNeed < 0.7 ? 1 : 0;
this.rulers.t += (rt - this.rulers.t) * Math.min(1, dt * 10);
if (Math.abs(rt - this.rulers.t) < 0.002) this.rulers.t = rt;
for (const g of this.guides) {
if (!g.vis) continue;
if (g.flash > 0) g.flash = Math.max(0, g.flash - dt * 1.5);
const deps = this.guideDeps.get(g) || [];
if (g.shownAt == null) g.shownAt = this.time;
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);
else if (!g.dragging && deps.every((t) => t.state === "done")) {
const last = Math.max(0, ...deps.map((t) => t.doneAt));
if (this.time - last > 1.2 && g.op > 0) g.op = Math.max(0, g.op - dt * 1.2);
}
}
if (!this.flocking && this.started && this.waitCount === 0) {
for (const c of this.cursors)
if (!c.task && !c.follow) {
c.follow = true;
c.gen = null;
c.leaving = false;
c.ov = null;
c.shape = "arrow";
}
if (this.cursors.every((c) => c.follow)) {
this.flocking = true;
this.endTime = this.time;
if (this.onComplete) this.onComplete();
}
}
if (this.flocking) {
this.flock(dt);
return;
}
if (!this.started) {
const center = this.fromViewport(vp.ox + vp.vw / 2, vp.oy + vp.vh / 2);
if (this.follow(dt, this.cursors, center, 0.7) && this.ready) this.start();
return;
}
if (this.cursors.some((c) => c.follow))
this.follow(
dt,
this.cursors.filter((c) => c.follow),
);
this.spawnCooldown -= dt;
const want = clamp(MIN_AGENTS + Math.floor(this.vel / 40) + Math.floor(avail.length / 2), MIN_AGENTS, MAX_AGENTS);
const active = this.cursors.filter((c) => !c.leaving).length;
const busy = this.cursors.filter((c) => c.task).length;
if (active < want && avail.length > active - busy && this.spawnCooldown <= 0) {
const left = Math.random() < 0.5;
const p = this.fromViewport(left ? vp.ox - 40 : vp.ox + vp.vw + 40, vp.oy + rand(0.2, 0.8) * vp.vh);
this.cursors.push(this.makeCursor(p.x, p.y));
this.spawnCooldown = 0.035;
}
for (const c of this.cursors) {
if (c.task || c.leaving || c.follow) continue;
const t = this.pick(c, avail);
if (!t) continue;
avail.splice(avail.indexOf(t), 1);
t.state = "active";
this.waitCount--;
c.task = t;
c.worked = true;
c.idleFor = 0;
if (t.layer !== "world") {
c.sticky = true;
c.x += vp.ox - vp.sx;
c.y += vp.oy - vp.sy;
}
c.gen = this.run(c, t);
}
for (const c of [...this.cursors]) {
if (c.follow) continue;
c.press = Math.max(0, c.press - dt * 6);
if (c.angle !== 0) c.angle = Math.abs(c.angle) < 0.5 ? 0 : c.angle * (1 - Math.min(1, dt * 10));
if (!c.task) {
c.idleFor += dt;
if (!c.leaving && c.idleFor > 2.5 && this.cursors.filter((m) => !m.leaving).length > MIN_AGENTS && !c.home) {
c.leaving = true;
c.gen = this.leave(c);
}
if (!c.gen) c.gen = this.idle(c);
}
if (c.gen.next(dt).done) {
if (c.task) {
c.task.state = "done";
c.task.doneAt = this.time;
this.doneCount++;
c.task = null;
c.ov = null;
if (c.sticky) {
c.sticky = false;
c.x -= vp.ox - vp.sx;
c.y -= vp.oy - vp.sy;
}
c.shape = "arrow";
}
c.gen = null;
if (c.leaving) {
if (c.worked) this.retired++;
this.cursors.splice(this.cursors.indexOf(c), 1);
}
}
}
}
flockTarget() {
if (this.mouse) return this.fromViewport(this.mouse.x, this.mouse.y);
const t = this.time;
const vp = this.vp;
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)));
}
flock(dt) {
this.spawnCooldown -= dt;
if (this.cursors.length < FLOCK_AGENTS && this.spawnCooldown <= 0 && !this.releasing) {
const vp = this.vp;
const left = Math.random() < 0.5;
const p = this.fromViewport(left ? vp.ox - 30 : vp.ox + vp.vw + 30, vp.oy + rand(0.1, 0.9) * vp.vh);
const c = this.makeCursor(p.x, p.y);
c.follow = true;
this.cursors.push(c);
this.spawnCooldown = 0.04;
}
if (this.releasing) {
for (const c of [...this.cursors]) {
if (!c.gen) c.gen = this.leave(c);
if (c.gen.next(dt).done) this.cursors.splice(this.cursors.indexOf(c), 1);
}
return;
}
this.follow(dt, this.cursors);
}
// Boids-ish orbit around a target, ported from the original.
follow(dt, list, target, scale = 1) {
const m = this.mouse ? this.fromViewport(this.mouse.x, this.mouse.y) : null;
const o = target || this.flockTarget();
const time = this.time;
if (m && this.lastT && dt > 0) {
const k = Math.min(1, dt * 18);
this.tv.x += ((m.x - this.lastT.x) / dt - this.tv.x) * k;
this.tv.y += ((m.y - this.lastT.y) / dt - this.tv.y) * k;
}
this.lastT = m;
const R = 135 * scale * (1 + 0.18 * Math.sin(time * 0.55));
const swirl = Math.tanh(Math.cos(time * 0.11) * 3);
const want = [];
for (const c of list) {
const vx = c.x - o.x;
const vy = c.y - o.y;
const d = Math.hypot(vx, vy) || 0.01;
const nx = vx / d;
const ny = vy / d;
const ang = Math.atan2(vy, vx);
const off = (c.orbit - 120) * 0.55 * scale;
const rr = R * (1 + 0.3 * Math.sin(3 * ang + time * 0.7)) + off;
const tang = 300 * (0.7 + c.spin * 0.2) * swirl;
const rad = clamp((rr - d) * 4, -2600, 1400);
const far = Math.min(1, Math.max(0, (d - rr) / 600));
want.push([-ny * tang + nx * rad - ny * c.curl * 400 * far, nx * tang + ny * rad + nx * c.curl * 400 * far]);
}
for (let i = 0; i < list.length; i++) {
const c = list[i];
let ax = 0;
let ay = 0;
let wsum = 0;
for (let j = 0; j < list.length; j++) {
if (j === i) continue;
const q = list[j];
const d = Math.hypot(q.x - c.x, q.y - c.y);
if (d > 90) continue;
const w = 1 - d / 90;
ax += q.vx * w;
ay += q.vy * w;
wsum += w;
}
let [fx, fy] = want[i];
if (wsum > 0) {
fx = fx * 0.7 + (ax / wsum) * 0.3;
fy = fy * 0.7 + (ay / wsum) * 0.3;
}
const dist = Math.max(0, Math.hypot(c.x - o.x, c.y - o.y) - 180 * scale);
const maxV = c.speed * (1.3 + Math.min(3.2, dist / 90));
const L = Math.hypot(fx, fy);
if (L > maxV) {
fx *= maxV / L;
fy *= maxV / L;
}
let gx = (fx - c.vx) * 6;
let gy = (fy - c.vy) * 6;
const maxF = c.force * (1.4 + Math.min(5, dist / 50));
const G = Math.hypot(gx, gy);
if (G > maxF) {
gx *= maxF / G;
gy *= maxF / G;
}
c.vx += gx * dt;
c.vy += gy * dt;
c.x += c.vx * dt;
c.y += c.vy * dt;
}
const R0 = 48;
let touched = false;
if (m)
for (const c of list) {
const vx = c.x - m.x;
const vy = c.y - m.y;
const d = Math.hypot(vx, vy) || 0.01;
if (d >= R0) continue;
touched = true;
const nx = vx / d;
const ny = vy / d;
c.x = m.x + nx * R0;
c.y = m.y + ny * R0;
const rel = (c.vx - this.tv.x) * nx + (c.vy - this.tv.y) * ny;
if (rel < 0) {
c.vx -= nx * rel * 1.6;
c.vy -= ny * rel * 1.6;
}
c.vx += nx * 260;
c.vy += ny * 260;
}
for (const c of list) {
if (Math.hypot(c.vx, c.vy) < 30) continue;
const d = ((((Math.atan2(c.vy, c.vx) * 180) / Math.PI + 128 - c.angle + 540) % 360) + 360) % 360 - 180;
c.angle += d * Math.min(1, dt * 8);
c.press = Math.max(0, c.press - dt * 6);
}
return touched;
}
// ------------------------------------------------------- primitives
*wait(t) {
let n = 0;
while (n < t) n += (yield) * this.tempo;
}
*moveTo(c, target, o = {}) {
const get = typeof target === "function" ? target : () => target;
const x0 = c.x;
const y0 = c.y;
const p0 = get();
const dist = Math.hypot(p0.x - x0, p0.y - y0);
if (dist < 0.5) {
c.x = p0.x;
c.y = p0.y;
if (o.onStep) o.onStep();
return;
}
const dur = clamp(0.12 + Math.sqrt(dist) * 0.021, 0.14, 0.9) / (o.speed || 1);
const bend = (o.bend != null ? o.bend : (Math.random() < 0.5 ? -1 : 1) * rand(0.04, 0.14)) * Math.min(dist, 600);
let t = 0;
while (t < dur) {
t += (yield) * this.tempo;
const k = Math.min(1, t / dur);
const e = o.drag || k < 0.15 ? easeInOut(k) : easeOut(k);
const p = get();
const dx = p.x - x0;
const dy = p.y - y0;
const L = Math.hypot(dx, dy) || 1;
const off = Math.sin(Math.PI * e) * bend;
c.x = x0 + dx * e + (-dy / L) * off;
c.y = y0 + dy * e + (dx / L) * off;
if (o.onStep) o.onStep();
}
const p = get();
c.x = p.x;
c.y = p.y;
if (o.onStep) o.onStep();
}
*click(c) {
c.press = 1;
if (this.sfx) this.sfx.press(this.scr(c).x, this.vp.vw);
yield* this.wait(0.07);
}
*type(it, c) {
const N = it.total;
const k = clamp(70 / Math.max(1, N), 0.06, 1);
let budget = 0;
let i = it.shown;
while (i < N) {
budget += (yield) * this.tempo;
while (i < N && budget > 0) {
const ch = it.seq[i];
let d = rand(0.012, 0.04);
if (ch === " ") d += rand(0, 0.03);
if (ch === "\n") d += 0.1;
if (Math.random() < 0.025) d += rand(0.08, 0.2);
budget -= d * k;
i++;
}
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);
it.shown = i;
bump(it);
}
}
*idle(c) {
for (;;) {
const vp = this.vp;
const top = this.viewTop;
const bot = this.viewBot;
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) };
yield* this.moveTo(c, p, { speed: 0.35 });
let w = rand(0.4, 1.4);
while (w > 0) {
w -= yield;
if (c.y < this.viewTop || c.y > this.viewBot) break;
}
}
}
*leave(c) {
const right = c.x > this.vp.sx + this.vp.vw / 2;
yield* this.moveTo(
c,
() => {
const vp = this.vp;
return this.fromViewport(right ? vp.ox + vp.vw + 60 : vp.ox - 60, c.y - this.viewTop + vp.oy);
},
{ speed: 0.8 },
);
}
run(c, t) {
const s = t.spec;
switch (s.type) {
case "rect":
return this.drawBox(c, s.it, false);
case "image":
return this.drawBox(c, s.it, true);
case "line":
return this.drawLine(c, s.it);
case "text":
return this.drawText(c, s.it, s.snap || null, !!s.grow);
case "guide":
return this.dragGuide(c, s.g);
case "dup":
return this.duplicate(c, s.g, s.k);
case "edit":
return this.editText(c, s.it);
case "restyle":
case "swap":
return this.restyle(c, s.it);
}
return this.wait(0.1);
}
// Where a drag should end so a huge box doesn't drag the cursor off-screen.
dragEnd(c, it) {
const vp = this.vp;
const screen = c.sticky;
const right = screen ? vp.ox + vp.vw : vp.sx + vp.vw;
const bot = screen ? vp.oy + vp.vh : this.viewBot;
const x = it.x + it.w > right - 16 ? Math.max(it.x + Math.min(it.w, 40), right - 30) : it.x + it.w;
const y = it.y + it.h > bot - 16 ? Math.max(it.y + Math.min(it.h, 40), bot - 40) : it.y + it.h;
return { x: Math.min(x, it.x + it.w), y: Math.min(y, it.y + it.h) };
}
*drawBox(c, it, isImg) {
c.shape = "cross";
yield* this.moveTo(c, { x: it.x, y: it.y });
c.press = 1;
it.vis = true;
if (it.big) it.styled = true;
it.draw = { w: 0, h: 0 };
bump(it);
c.ov = () => [this.selection(it)];
const end = this.dragEnd(c, it);
yield* this.moveTo(c, end, {
speed: 0.75,
drag: true,
bend: 0,
onStep: () => {
it.draw = { w: Math.max(0, c.x - it.x), h: Math.max(0, c.y - it.y) };
bump(it);
},
});
if (end.x < it.x + it.w - 0.5 || end.y < it.y + it.h - 0.5) {
const w0 = it.draw.w;
const h0 = it.draw.h;
let k = 0;
while (k < 1) {
k = Math.min(1, k + ((yield) * this.tempo) / 0.5);
const e = easeOut(k);
it.draw = { w: w0 + (it.w - w0) * e, h: h0 + (it.h - h0) * e };
bump(it);
}
}
it.draw = null;
bump(it);
c.shape = "arrow";
yield* this.wait(rand(0.18, 0.3));
it.styled = true;
bump(it);
this.sfxPlace(isImg ? "image" : "rect", it);
yield* this.wait(isImg ? 0.5 : 0.35);
c.ov = null;
it.done = true;
bump(it);
}
*drawLine(c, it) {
c.shape = "pen-add";
yield* this.moveTo(c, it.pts[0]);
yield* this.click(c);
it.vis = true;
it.edit = true;
it.placed = 1;
it.owner = c;
bump(it);
this.editLines.add(it);
c.shape = "pen";
for (let i = 1; i < it.pts.length; i++) {
yield* this.moveTo(c, it.pts[i], { bend: rand(-0.03, 0.03) });
yield* this.click(c);
it.placed = i + 1;
bump(it);
yield* this.wait(rand(0.04, 0.12));
}
it.owner = null;
bump(it);
yield* this.moveTo(c, { x: c.x + rand(14, 30), y: c.y + rand(10, 24) }, { speed: 1.4 });
yield* this.wait(0.3);
it.edit = false;
this.editLines.delete(it);
it.done = true;
bump(it);
this.sfxPlace("line", it);
}
*drawText(c, it, guide, grow) {
if (guide) {
if (guide.axis === "x") {
it.dx = rand(28, 64);
it.dy = rand(-8, 8);
} else {
it.dx = rand(-8, 8);
it.dy = rand(18, 34) * (Math.random() < 0.5 ? -1 : 1);
}
}
if (grow) it.scale = Math.min(1, 18 / it.fs);
const b0 = this.bounds(it);
c.shape = "cross";
yield* this.moveTo(c, { x: b0.x, y: b0.y });
const lineH = Math.min(b0.h, it.lh * it.scale);
if (b0.w < 140 && it.tb.h < it.lh * 1.6) {
yield* this.click(c);
} else {
c.press = 1;
const ghost = { x: b0.x, y: b0.y, w: 0, h: 0 };
c.ov = () => [{ t: "box", b: ghost, handles: true, badge: `${Math.round(ghost.w)} × ${Math.round(ghost.h)}`, rulerLines: true }];
yield* this.moveTo(
c,
{ x: b0.x + b0.w, y: b0.y + lineH },
{
speed: 0.9,
drag: true,
bend: 0,
onStep: () => {
ghost.w = Math.max(0, c.x - b0.x);
ghost.h = Math.max(0, c.y - b0.y);
},
},
);
}
it.vis = true;
it.editing = true;
it.shown = 0;
bump(it);
c.shape = "ibeam";
c.ov = () => [{ t: "box", b: this.bounds(it) }];
yield* this.moveTo(c, { x: c.x + rand(-30, 10), y: c.y + rand(10, 30) }, { speed: 1.5 });
yield* this.type(it, c);
yield* this.wait(0.12);
it.editing = false;
bump(it);
this.sfxPlace("text", it);
c.shape = "arrow";
c.ov = () => [this.selection(it)];
if (grow) {
const steps = [24, 32, 40, 48, 56, 64, 72, 88, 104].filter((s) => s < it.fs && s > 18);
let k = 0;
for (const s of [...steps, it.fs]) {
yield* this.wait(0.06);
it.scale = s / it.fs;
bump(it);
if (this.sfx) this.sfx.grow(k++, this.scr(c).x, this.vp.vw);
}
it.scale = 1;
bump(it);
yield* this.wait(0.15);
}
if (guide) {
// give the guide a moment to land; never block a cursor on it
let waited = 0;
while (guide.task.state !== "done" && waited < 1.6) waited += (yield) * this.tempo;
if (guide.task.state !== "done") guide = null;
}
if (guide) {
const u = this.bounds(it);
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) };
yield* this.moveTo(c, a);
c.press = 1;
const p = { x: a.x - it.dx, y: a.y - it.dy };
const m = {
x: p.x + (guide.axis === "x" ? -rand(2, 5) : rand(-6, 6)),
y: p.y + (guide.axis === "y" ? -rand(2, 5) : rand(-3, 3)),
};
const drag = () => {
let h = c.x - p.x;
let v = c.y - p.y;
if (guide.axis === "x" && Math.abs(h) < 9) {
if (h !== 0 && it.dx !== 0) {
if (guide.flash < 0.5 && this.sfx) this.sfx.snap(this.scr(c).x, this.vp.vw);
guide.flash = 1;
}
h = 0;
}
if (guide.axis === "y" && Math.abs(v) < 9) {
if (v !== 0 && it.dy !== 0) {
if (guide.flash < 0.5 && this.sfx) this.sfx.snap(this.scr(c).x, this.vp.vw);
guide.flash = 1;
}
v = 0;
}
it.dx = h;
it.dy = v;
bump(it);
};
yield* this.moveTo(c, m, { speed: 0.6, drag: true, onStep: drag });
yield* this.wait(0.1);
yield* this.moveTo(c, p, { speed: 2, onStep: drag });
it.dx = 0;
it.dy = 0;
bump(it);
yield* this.wait(0.25);
} else if (it.dx || it.dy) {
const dx0 = it.dx;
const dy0 = it.dy;
let k = 0;
while (k < 1) {
k = Math.min(1, k + ((yield) * this.tempo) / 0.3);
const e = easeInOut(k);
it.dx = dx0 * (1 - e);
it.dy = dy0 * (1 - e);
bump(it);
}
yield* this.wait(0.15);
} else yield* this.wait(0.2);
c.ov = null;
it.done = true;
bump(it);
}
*dragGuide(c, g) {
this.rulers.want++;
try {
const r = rand(-0.15, 0.15);
const from = () => {
const vp = this.vp;
return g.axis === "x"
? this.fromViewport(vp.ox + 10, vp.oy + vp.vh * (0.45 + r))
: this.fromViewport(vp.ox + vp.vw * 0.3 + r * 200, vp.oy + 10);
};
c.shape = "arrow";
yield* this.moveTo(c, from);
while (this.rulers.t < 0.95) yield;
yield* this.moveTo(c, from, { speed: 3 });
c.press = 1;
c.shape = g.axis === "x" ? "ew" : "ns";
g.vis = true;
g.dragging = true;
g.op = 1;
const upd = () => {
g.cur = g.axis === "x" ? c.x : c.y;
};
upd();
const to = () => {
const s = from();
return g.axis === "x" ? { x: g.pos, y: s.y + 40 } : { x: s.x + 30, y: g.pos };
};
yield* this.moveTo(c, to, { speed: 0.55, drag: true, onStep: upd });
g.cur = g.pos;
g.dragging = false;
if (this.sfx) {
const p = this.scr(c);
this.sfx.place("guide", p.x, p.y, 1, 1, this.vp.vw, this.vp.vh, 0);
}
c.shape = "arrow";
yield* this.wait(0.25);
} finally {
this.rulers.want--;
}
}
*duplicate(c, g, k) {
const dst = g.dst[k];
let src = g.src;
const cand = [g.src, ...g.dst.filter((d) => d.task.state === "done")];
const dd = (a) => Math.hypot(a.box.x - dst.box.x, a.box.y - dst.box.y);
for (const s of cand) if (dd(s) < dd(src)) src = s;
const sb = this.unionBounds(src.items);
c.shape = "arrow";
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) };
yield* this.moveTo(c, u);
yield* this.click(c);
c.ov = () => [{ t: "box", b: sb, handles: true, badge: `${Math.round(sb.w)} × ${Math.round(sb.h)}` }];
yield* this.wait(rand(0.15, 0.3));
c.shape = "dup";
yield* this.wait(0.12);
c.press = 1;
const ox = src.box.x - dst.box.x;
const oy = src.box.y - dst.box.y;
for (const it of dst.items) {
it.vis = true;
it.mode = "master";
it.dx = ox;
it.dy = oy;
if (it.kind === "rect" || it.kind === "image") {
it.styled = true;
it.draw = null;
}
if (it.kind === "line") {
it.placed = it.pts.length;
}
bump(it);
}
const m = { x: u.x - ox, y: u.y - oy };
c.ov = () => {
const cb = this.unionBounds(dst.items);
const h = [
{ t: "box", b: sb, color: "red" },
{ t: "box", b: cb, color: "red" },
];
const sp = this.spacing(sb, cb);
if (sp) h.push(sp);
return h;
};
yield* this.moveTo(c, m, {
speed: 0.6,
drag: true,
onStep: () => {
for (const it of dst.items) {
it.dx = ox + (c.x - u.x);
it.dy = oy + (c.y - u.y);
bump(it);
}
},
});
this.sfxPlace("dup", dst.items[0], this.unionBounds(dst.items));
for (const it of dst.items) {
it.dx = 0;
it.dy = 0;
it.settle = true;
if (!it.editTask) it.mode = "own";
if (it.kind === "text" && it.mode === "own") it.shown = it.total;
bump(it);
}
yield* this.wait(0.1);
c.shape = "arrow";
c.ov = () => {
const cb = this.unionBounds(dst.items);
return [{ t: "box", b: cb, handles: true, badge: `${Math.round(cb.w)} × ${Math.round(cb.h)}` }];
};
yield* this.wait(0.35);
for (const it of dst.items)
if (!it.editTask) {
it.done = true;
bump(it);
}
}
spacing(a, b) {
const oy = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
const ox = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
if (oy > 0) {
const y = Math.max(a.y, b.y) + oy / 2;
const [l, r] = a.x < b.x ? [a, b] : [b, a];
const gap = r.x - (l.x + l.w);
if (gap > 0) return { t: "measure", a: { x: l.x + l.w, y }, b: { x: r.x, y }, label: String(Math.round(gap)) };
} else if (ox > 0) {
const x = Math.max(a.x, b.x) + ox / 2;
const [t, btm] = a.y < b.y ? [a, b] : [b, a];
const gap = btm.y - (t.y + t.h);
if (gap > 0) return { t: "measure", a: { x, y: t.y + t.h }, b: { x, y: btm.y }, label: String(Math.round(gap)) };
}
return null;
}
*editText(c, it) {
c.shape = "arrow";
const b = this.bounds(it);
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 });
yield* this.click(c);
yield* this.wait(0.08);
yield* this.click(c);
it.selAll = true;
it.editing = true;
bump(it);
c.shape = "ibeam";
c.ov = () => [{ t: "box", b: this.bounds(it) }];
yield* this.wait(rand(0.2, 0.35));
it.mode = "own";
it.selAll = false;
it.shown = 0;
bump(it);
yield* this.type(it, c);
yield* this.wait(0.12);
it.editing = false;
bump(it);
c.shape = "arrow";
c.ov = () => [this.selection(it)];
this.sfxPlace("edit", it);
yield* this.wait(0.3);
c.ov = null;
it.done = true;
bump(it);
}
*restyle(c, it) {
c.shape = "arrow";
const b = this.bounds(it);
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) });
yield* this.click(c);
c.ov = () => [this.selection(it)];
yield* this.wait(rand(0.15, 0.3));
it.mode = "own";
bump(it);
this.sfxPlace(it.kind === "image" ? "image" : "restyle", it);
yield* this.wait(0.4);
c.ov = null;
it.done = true;
bump(it);
}
}
// ------------------------------------------------------------------ sound
// A glass piano in E. Every finished piece of the page plays one note of the
// E major pentatonic, quantized to a soft 16th-note grid, so a busy swarm comes
// out as a generative melody: big sections low, small things high, pitch rising
// left to right, panned to where the cursor is. Typing and clicks are quiet
// paper ticks underneath. All synthesized; no audio files.
const PENTA = [0, 2, 4, 7, 9]; // E F# G# B C#
const E2 = 40;
const mtof = (m) => 440 * Math.pow(2, (m - 69) / 12);
let soundPref = true;
const SPK_ICON = '
${tap ? "
Tap for sound" : ""}`;
b.title = key === "off" ? "Sound off (M)" : "Sound on (M)";
}
frame(now) {
if (this.dead) return;
const dt = Math.min(0.05, (now - this.last) / 1000);
this.last = now;
try {
this.engine.step(dt);
if (this.tour) this.tourStep(dt);
this.view.render(this.engine);
if (!this._hudT || now - this._hudT > 200) {
this._hudT = now;
this.updateHud();
this.renderSoundBtn();
}
} catch (err) {
console.error("[swarm-rebuild]", err);
}
this.raf = requestAnimationFrame((t) => this.frame(t));
}
tourStep(dt) {
const E = this.engine;
if (!E.started) return;
const g = this.env.geom();
const maxY = this.env.scrollHeight() - g.vh;
if (g.sy >= maxY - 1) {
this.tourV = 0;
return;
}
const crew = Math.max(MIN_AGENTS, E.cursors.filter((c) => !c.leaving).length);
const backlog = E.backlog();
const target = backlog <= Math.max(4, crew * 0.6) ? 230 : backlog <= crew * 1.5 ? 80 : 0;
this.tourV += (target - this.tourV) * Math.min(1, dt * 2.5);
this.tourAcc += this.tourV * dt;
if (this.tourAcc >= 1) {
const d = Math.floor(this.tourAcc);
this.tourAcc -= d;
this.env.scrollBy(d);
}
}
updateHud() {
const E = this.engine;
const v = this.view;
if (!E.ready) return;
const workers = E.cursors.filter((c) => c.worked).length + E.retired;
const total = E.tasks.length;
const done = E.doneCount;
if (this.complete) return;
if (!E.started) v.status.innerHTML = `
${E.cursors.length} agents ready
${total.toLocaleString()} layers`;
else {
const pend = E.tasks.length - done;
const below = E.tasks.some((t) => t.state === "wait" && t.layer === "world" && t.gate > E.frontier);
v.status.innerHTML =
`
${workers} agents
${done.toLocaleString()} / ${total.toLocaleString()} layers` +
(below && pend && !this.tour && E.backlog() === 0 ? `
scroll to keep building` : "");
}
v.barFill.style.width = total ? `${(done / total) * 100}%` : "0%";
const busy = E.cursors.filter((c) => c.task).slice(-5);
const key = busy.map((c) => c.id).join(",");
if (key !== this._dots) {
this._dots = key;
v.dots.innerHTML = busy.map((c) => `
${c.name[0]}`).join("");
}
}
onComplete() {
this.complete = true;
// a hidden toolbar stays hidden so the end of a recording stays clean (H or the bottom edge brings it back)
if (this.sfx) this.sfx.finale();
const E = this.engine;
const v = this.view;
const workers = E.cursors.filter((c) => c.worked).length + E.retired;
const secs = (E.endTime - E.startTime).toFixed(1);
v.status.innerHTML = `Rebuilt by
${workers} agents in
${secs}s`;
v.barFill.style.width = "100%";
v.dots.innerHTML = "";
v.root.querySelector('[data-a="live"]').hidden = false;
v.root.querySelector('[data-a="replay"]').hidden = false;
v.root.querySelector('[data-a="tour"]').hidden = true;
v.root.querySelector('[data-a="speed"]').hidden = true;
this.tour = false;
}
destroy(reveal) {
unhidePage();
if (this.sfx) {
if (reveal) this.sfx.release();
this.sfx.close(reveal ? 1.4 : 0.25);
}
if (!this.view) {
// startup failed before the overlay existed
this.dead = true;
cancelAnimationFrame(this.raf);
return;
}
const finish = () => {
this.dead = true;
cancelAnimationFrame(this.raf);
for (const u of this.unbinds || []) u();
this.view.host.remove();
};
if (reveal) {
this.engine.releasing = true;
this.engine.flocking = true;
for (const c of this.engine.cursors) {
c.gen = null;
c.follow = false;
}
this.view.hud.classList.remove("on");
setTimeout(() => {
this.view.host.style.setProperty("opacity", "0", "important");
}, 450);
setTimeout(finish, 1150);
} else {
this.view.host.style.setProperty("transition", "opacity .25s ease", "important");
this.view.host.style.setProperty("opacity", "0", "important");
this.dead = true;
cancelAnimationFrame(this.raf);
for (const u of this.unbinds || []) u();
setTimeout(() => this.view.host.remove(), 260);
}
}
}
// background.js may have hidden everything but media for its screenshot;
// put the page back once our canvas covers it (or if we bail out early).
async function unhidePage() {
if (!window.__swarmHidden) return;
window.__swarmHidden = false;
try {
if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.sendMessage) await chrome.runtime.sendMessage({ type: "swarm:unhide" });
} catch (_) {}
if (typeof window.__swarmUnhide === "function") {
try {
window.__swarmUnhide();
} catch (_) {}
}
await nextFrame();
}
let app = null;
let lastShot = null;
function start() {
if (app) return;
const a = (app = new App());
a.run().catch((err) => {
console.error("[swarm-rebuild]", err);
// A failed start must not leave the page hidden, an invisible overlay over it, or a
// half-started app that makes the next click "toggle off" instead of starting again.
unhidePage();
if (app === a) stop(false);
});
}
function stop(reveal) {
if (!app) return;
const a = app;
app = null;
// Session over: the next click must take a fresh screenshot, never reuse this one
// (or one a failed start never consumed). Replay goes through restart(), which keeps it.
lastShot = null;
window.__swarmShot = null;
a.destroy(reveal);
}
function restart() {
if (app) {
const a = app;
app = null;
a.dead = true;
cancelAnimationFrame(a.raf);
for (const u of a.unbinds || []) u();
a.view.host.remove();
if (a.sfx) a.sfx.close(0.1);
// rebuild from where the first run started so screenshot crops line up again
if (lastShot) a.env.scrollTo(lastShot.at.sy, lastShot.at.sx);
}
start();
}
window[KEY] = {
toggle() {
if (app) stop(false);
else start();
},
start,
stop,
get app() {
return app;
},
debug: { captureModel, detectEnv, Sound },
};
start();
})();