let Applied, Design, Gpu, failure;
try {
const engine = await import("/__devpage/design-engine.js");
await engine.default({ module_or_path: "/__devpage/design-engine.wasm" });
({ Applied, Design, Gpu } = engine);
} catch (error) {
failure = error;
}
const print = !!document.querySelector("script[data-print]");
let gpu = null;
if (!print && !failure) {
try {
gpu = await Gpu.open();
} catch (error) {
console.info("devpage: no WebGPU, drawing designs on the CPU", error);
}
}
const MAX_BITMAP = 8192;
const hosts = new Set();
const started = new WeakSet();
function address(link, base) {
return decodeURIComponent(new URL(link, base).pathname).replace(/^\//, "");
}
function varint(n) {
const bytes = [];
for (; ; n >>>= 7) {
if (n < 0x80) { bytes.push(n); return bytes; }
bytes.push((n & 0x7f) | 0x80);
}
}
function watchRequest(paths) {
const fields = [];
for (const path of paths) {
const name = new TextEncoder().encode(path);
fields.push(0x0a, ...varint(name.length), ...name);
}
const body = new Uint8Array(5 + fields.length);
new DataView(body.buffer).setUint32(1, fields.length);
body.set(fields, 5);
return body;
}
function addressed(message) {
let at = 0;
const next = () => {
let n = 0;
for (let shift = 0; ; shift += 7) {
const byte = message[at++];
n += (byte & 0x7f) * 2 ** shift;
if (byte < 0x80) return n;
}
};
let path = "";
while (at < message.length) {
const key = next();
const field = Math.floor(key / 8);
const type = key & 7;
if (type === 0) next();
else if (type === 2) {
const length = next();
if (field === 4) path = new TextDecoder().decode(message.subarray(at, at + length));
at += length;
} else if (type === 1) at += 8;
else if (type === 5) at += 4;
else break; }
return path;
}
function room(backdrop) {
const slide = backdrop.closest(".slide");
if (!slide) return innerHeight;
let from = backdrop;
for (let at = backdrop.parentElement; at && at !== slide; at = at.parentElement) {
if (/^(inline-)?(grid|flex)$/.test(getComputedStyle(at).display)) {
from = at;
break;
}
}
const box = slide.getBoundingClientRect();
const scale = box.height / slide.offsetHeight || 1;
const top = (from.getBoundingClientRect().top - box.top) / scale;
let padding = 0;
for (let at = backdrop.parentElement; at && !padding; at = at === slide ? null : at.parentElement) {
padding = parseFloat(getComputedStyle(at).paddingBottom) || 0;
}
return Math.max(0, slide.offsetHeight - top - padding);
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function pageTheme() {
const theme = document.documentElement.dataset.theme;
if (theme === "light" || theme === "dark") return theme;
return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function background(element) {
const layers = [];
for (let at = element; at; at = at.parentElement) {
const [r, g, b, a = 1] = getComputedStyle(at).backgroundColor.match(/[\d.]+/g).map(Number);
if (a > 0) layers.push([r, g, b, a]);
if (a >= 1) break;
}
let [r, g, b] = [255, 255, 255];
for (const [lr, lg, lb, la] of layers.reverse()) {
[r, g, b] = [lr * la + r * (1 - la), lg * la + g * (1 - la), lb * la + b * (1 - la)];
}
return (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b);
}
class Host {
constructor(canvas, path, { mode, parent = null, zoom = 1 }) {
this.canvas = canvas;
this.mode = mode; this.parent = parent;
this.path = path;
this.history = []; this.view = { zoom, x: 0, y: 0, fitted: true };
this.cpu = null; this.overlay = null;
this.frame = 0;
this.own = null;
if (mode === "embed") {
const backdrop = canvas.parentElement;
for (const stale of backdrop.querySelectorAll(":scope > .design-corner, :scope > .design-problems")) {
stale.remove();
}
delete backdrop.dataset.theme;
canvas.removeAttribute("style");
const corner = document.createElement("div");
corner.className = "design-corner";
this.toggle = backdrop.querySelector(":scope > button.design-theme");
if (this.toggle) {
corner.append(this.toggle);
this.toggle.addEventListener("click", () => this.switchTheme());
}
this.link = document.createElement("a");
this.link.className = "design-open";
this.link.textContent = "Open";
this.link.title = "Open this design on its own page";
corner.append(this.link);
backdrop.append(corner);
this.resized = new ResizeObserver(() => this.paint());
this.resized.observe(canvas.parentElement);
}
this.attach();
this.listen();
hosts.add(this);
this.open(path);
}
attach() {
this.target = null;
if (gpu) {
try {
this.target = gpu.target(this.canvas);
} catch (error) {
console.info("devpage: this canvas can't use WebGPU, drawing on the CPU", error);
const fresh = this.canvas.cloneNode();
this.canvas.replaceWith(fresh);
this.canvas = fresh;
}
}
this.context = this.target ? null : this.canvas.getContext("2d");
this.canvas.dataset.backend = this.target ? "gpu" : "cpu";
}
open(path) {
this.design?.free();
this.design = new Design();
this.path = path;
this.cpu = null;
this.view.fitted = true;
this.showLink();
watch.key = null;
rewatch();
}
receive(message) {
const applied = this.design.apply(message);
if (applied === Applied.Changed) {
if (this.toggle) this.toggle.hidden = !this.design.themed();
this.showProblems([]);
this.cpu = null;
this.paint();
} else if (applied === Applied.Problems) {
this.showProblems(this.design.problems());
this.settle();
}
}
close() {
this.closeOverlay();
this.resized?.disconnect();
cancelAnimationFrame(this.frame);
hosts.delete(this);
this.design?.free();
this.design = null;
rewatch();
}
showProblems(problems) {
const holder = this.mode === "page" ? document.body : this.canvas.parentElement;
let bar = holder.querySelector(":scope > .design-problems");
if (!problems.length) {
bar?.remove();
return;
}
if (!bar) {
bar = document.createElement("ul");
bar.className = "design-problems";
holder.prepend(bar);
}
bar.replaceChildren(
...problems.map((problem) => {
const item = document.createElement("li");
item.textContent = problem;
return item;
}),
);
}
theme() {
if (this.mode === "overlay") return this.parent.theme();
return this.own ?? pageTheme();
}
switchTheme() {
this.own = this.theme() === "dark" ? "light" : "dark";
this.canvas.parentElement.dataset.theme = this.own;
const label = this.own === "dark" ? "Switch this design to light" : "Switch this design to dark";
this.toggle.title = label;
this.toggle.setAttribute("aria-label", label);
this.paint();
this.overlay?.paint();
}
showLink() {
if (this.link) this.link.href = `/${this.path}?theme=${this.theme()}`;
}
fit() {
const [w, h] = this.design.size();
if (!w) return false;
const box = this.canvas.getBoundingClientRect();
if (this.mode === "embed") {
const backdrop = this.canvas.parentElement;
const style = getComputedStyle(backdrop);
const across = backdrop.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
const down = room(backdrop) - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
if (across <= 0 || down <= 0) return false;
const tall = print && !backdrop.closest(".slide") ? Infinity : down / h;
const zoom = Math.min(1, across / w, tall);
this.canvas.style.width = `${w * zoom}px`;
this.canvas.style.height = `${h * zoom}px`;
Object.assign(this.view, { zoom, x: 0, y: 0 });
} else if (this.mode === "overlay") {
const zoom = this.parent.view.zoom;
this.canvas.style.width = `${w * zoom}px`;
this.canvas.style.height = `${h * zoom}px`;
Object.assign(this.view, { zoom, x: 0, y: 0 });
} else if (this.view.fitted) {
const pad = 32;
const zoom = Math.min(1, (box.width - 2 * pad) / w, (box.height - 2 * pad) / h);
Object.assign(this.view, { zoom, x: Math.round((box.width - w * zoom) / 2), y: Math.round((box.height - h * zoom) / 2) });
}
return true;
}
paint() {
delete this.canvas.dataset.painted;
if (this.frame) return;
this.frame = requestAnimationFrame(() => {
this.frame = 0;
this.draw();
});
}
scaled() {
return this.canvas.getBoundingClientRect().width / this.canvas.offsetWidth || 1;
}
draw() {
if (!this.design || !this.canvas.isConnected) return;
if (this.design.set_theme(this.theme() === "dark")) this.cpu = null;
this.showLink();
if (!this.fit()) return this.settle();
const box = this.canvas.getBoundingClientRect();
const width = Math.max(1, Math.round(box.width * (window.devicePixelRatio || 1)));
const height = Math.max(1, Math.round(box.height * (window.devicePixelRatio || 1)));
const ratio = (window.devicePixelRatio || 1) * this.scaled();
if (this.canvas.width !== width) this.canvas.width = width;
if (this.canvas.height !== height) this.canvas.height = height;
const { zoom, x, y } = this.view;
this.canvas.dataset.view = `${zoom},${x},${y}`;
this.canvas.dataset.size = this.design.size().join(",");
if (this.target) {
const behind = background(this.canvas.parentElement);
if (gpu.draw(this.target, this.design, width, height, zoom * ratio, x * ratio, y * ratio, behind)) this.painted();
return;
}
this.drawCpu(ratio, width, height);
}
drawCpu(ratio, width, height) {
const [w, h] = this.design.size();
const { zoom, x, y } = this.view;
const scale = Math.min(zoom * ratio, MAX_BITMAP / w, MAX_BITMAP / h);
const show = (bitmap) => {
this.context.clearRect(0, 0, width, height);
this.context.drawImage(bitmap, x * ratio, y * ratio, w * zoom * ratio, h * zoom * ratio);
};
if (this.cpu) show(this.cpu.bitmap);
if (this.cpu?.scale === scale) return this.painted();
clearTimeout(this.repaint);
this.repaint = setTimeout(
async () => {
const pixels = this.design.paint(scale);
if (!pixels) return;
const image = new ImageData(new Uint8ClampedArray(pixels.take()), pixels.width, pixels.height);
pixels.free();
this.cpu = { scale, bitmap: await createImageBitmap(image) };
show(this.cpu.bitmap);
this.painted();
},
this.cpu ? 120 : 0,
);
}
painted() {
this.canvas.dataset.painted = "true";
}
settle() {
if (this.frame || this.canvas.dataset.painted || !this.design?.problems().length) return;
this.canvas.dataset.painted = "problems";
}
point(event) {
const box = this.canvas.getBoundingClientRect();
const scale = this.scaled();
const { zoom, x, y } = this.view;
return [((event.clientX - box.left) / scale - x) / zoom, ((event.clientY - box.top) / scale - y) / zoom];
}
listen() {
const canvas = this.canvas;
const pointers = new Map();
let moved = false;
let pinch = null;
canvas.addEventListener("pointerdown", (event) => {
pointers.set(event.pointerId, [event.clientX, event.clientY]);
moved = false;
if (this.mode === "page") canvas.setPointerCapture(event.pointerId);
});
canvas.addEventListener("pointermove", (event) => {
const last = pointers.get(event.pointerId);
if (this.mode === "page" && last) {
if (pointers.size === 2) {
pointers.set(event.pointerId, [event.clientX, event.clientY]);
const [a, b] = [...pointers.values()];
const distance = Math.hypot(a[0] - b[0], a[1] - b[1]);
if (pinch) this.zoomBy(distance / pinch, (a[0] + b[0]) / 2, (a[1] + b[1]) / 2);
pinch = distance || null;
moved = true;
return;
}
const [dx, dy] = [event.clientX - last[0], event.clientY - last[1]];
if (moved || Math.hypot(dx, dy) > 3) {
moved = true;
pointers.set(event.pointerId, [event.clientX, event.clientY]);
this.panBy(dx, dy);
canvas.style.cursor = "grabbing";
return;
}
}
canvas.style.cursor = this.design.action_at(...this.point(event)) ? "pointer" : "";
});
const up = (event) => {
pointers.delete(event.pointerId);
if (pointers.size < 2) pinch = null;
canvas.style.cursor = "";
};
canvas.addEventListener("pointerup", up);
canvas.addEventListener("pointercancel", up);
canvas.addEventListener("click", (event) => {
if (moved) return;
const action = this.design.action_at(...this.point(event));
if (action) {
event.preventDefault();
this.act(action);
}
});
if (this.mode !== "page") return;
canvas.addEventListener(
"wheel",
(event) => {
event.preventDefault();
if (event.ctrlKey || event.metaKey) this.zoomBy(Math.exp(-event.deltaY * 0.01), event.clientX, event.clientY);
else this.panBy(-event.deltaX, -event.deltaY);
},
{ passive: false },
);
}
panBy(dx, dy) {
this.view.x += dx;
this.view.y += dy;
this.view.fitted = false;
this.paint();
}
zoomBy(factor, clientX, clientY) {
const box = this.canvas.getBoundingClientRect();
const [px, py] = [clientX - box.left, clientY - box.top];
const zoom = Math.min(64, Math.max(0.05, this.view.zoom * factor));
const k = zoom / this.view.zoom;
this.view.x = px - (px - this.view.x) * k;
this.view.y = py - (py - this.view.y) * k;
this.view.zoom = zoom;
this.view.fitted = false;
this.paint();
}
act(action) {
const [verb, ...rest] = action.trim().split(/\s+/);
const target = rest.length ? rest.join(" ") : null;
if (this.mode === "overlay") {
const parent = this.parent;
parent.closeOverlay();
if (verb !== "close") parent.act(target ? `${verb} ${target}` : verb);
return;
}
if (verb === "navigate" && target) {
if (this.mode === "page") history.pushState(null, "", `/${target}${asked ? `?theme=${asked}` : ""}`);
else this.history.push(this.path);
this.closeOverlay();
this.open(target);
} else if (verb === "back") {
this.closeOverlay();
if (this.mode === "page") history.back();
else if (this.history.length) this.open(this.history.pop());
} else if (verb === "overlay" && target) {
this.showOverlay(target);
}
}
showOverlay(path) {
this.closeOverlay();
const layer = document.createElement("div");
layer.className = "design-overlay";
if (this.mode === "page") layer.classList.add("design-overlay-page");
const canvas = document.createElement("canvas");
canvas.className = "design";
layer.append(canvas);
(this.mode === "page" ? document.body : this.canvas.parentElement).append(layer);
this.overlay = new Host(canvas, path, { mode: "overlay", parent: this, zoom: this.view.zoom });
this.overlay.layer = layer;
}
closeOverlay() {
if (!this.overlay) return;
this.overlay.close();
this.overlay.layer.remove();
this.overlay = null;
}
}
function fail(canvas, message) {
canvas.dataset.failed = "true";
const note = document.createElement("p");
note.className = "design-failed";
note.textContent = message;
canvas.before(note);
}
export function scan(root = document) {
const carried = [];
for (const host of [...hosts]) {
if (host.canvas.isConnected || host.mode === "overlay") continue;
if (host.mode === "embed") {
carried.push({ src: host.canvas.getAttribute("src"), path: host.path, history: host.history });
}
host.close();
}
const page = document.body.classList.contains("design-page");
for (const canvas of root.querySelectorAll("canvas.design[src]")) {
if (started.has(canvas)) continue;
started.add(canvas);
canvas.dataset.started = "true";
if (failure) {
console.error("devpage: the design engine didn't load", failure);
fail(canvas, `This design can't be drawn: the design engine didn't load (${failure.message ?? failure}).`);
continue;
}
const base = canvas.closest("[data-source]")?.dataset.source;
const path = address(canvas.getAttribute("src"), new URL(base ?? location.pathname, location.origin));
const at = page ? -1 : carried.findIndex((state) => state.src === canvas.getAttribute("src"));
const state = at >= 0 ? carried.splice(at, 1)[0] : null;
const host = new Host(canvas, state?.path ?? path, { mode: page ? "page" : "embed" });
if (state) host.history = state.history;
if (page) {
addEventListener("popstate", () => {
host.closeOverlay();
host.open(address(location.pathname, location.origin));
});
}
}
}
addEventListener("resize", () => {
for (const host of hosts) host.paint();
});
new MutationObserver(() => {
for (const host of hosts) host.paint();
}).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
const asked = (() => {
const theme = new URLSearchParams(location.search).get("theme");
return theme === "light" || theme === "dark" ? theme : null;
})();
if (document.body.classList.contains("design-page")) {
const system = matchMedia("(prefers-color-scheme: dark)");
const saved = () => {
try {
const theme = localStorage.getItem("devpage-theme");
return theme === "light" || theme === "dark" ? theme : null;
} catch {
return null;
}
};
let chosen = asked;
const apply = (theme = chosen ?? saved()) => {
document.documentElement.dataset.theme = theme || (system.matches ? "dark" : "light");
const label = document.documentElement.dataset.theme === "dark" ? "Switch to light" : "Switch to dark";
for (const button of document.querySelectorAll(".theme-toggle")) {
button.title = label;
button.setAttribute("aria-label", label);
}
};
apply();
system.addEventListener("change", () => apply());
document.addEventListener("click", (event) => {
if (!event.target.closest?.(".theme-toggle")) return;
const theme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
try {
localStorage.setItem("devpage-theme", theme);
} catch {
}
chosen = theme;
apply(theme);
});
}
const watch = { key: null, controller: null, pending: false };
function watched() {
const paths = new Set();
for (const host of hosts) if (host.canvas.isConnected) paths.add(host.path);
return [...paths].sort();
}
function rewatch() {
if (watch.pending) return;
watch.pending = true;
queueMicrotask(() => {
watch.pending = false;
connect();
});
}
function connect() {
const paths = watched();
const key = paths.join("\n");
if (key === watch.key && watch.controller) return;
watch.controller?.abort();
watch.controller = null;
watch.key = key;
if (!paths.length || document.hidden) return;
const controller = new AbortController();
watch.controller = controller;
stream(paths, controller);
}
async function stream(paths, controller) {
while (!controller.signal.aborted) {
try {
if (await read(paths, controller.signal)) break;
} catch {
}
if (controller.signal.aborted || print) return;
await sleep(1000);
}
if (watch.controller === controller) watch.controller = null;
}
async function read(paths, signal) {
const response = await fetch("/devpage.design.v1.Designs/Watch", {
method: "POST",
headers: { "content-type": "application/grpc-web+proto", "x-grpc-web": "1" },
body: watchRequest(paths),
signal,
});
const reader = response.body.getReader();
let buffer = new Uint8Array(0);
let ended = false;
for (;;) {
const { done, value } = await reader.read();
if (done) return ended;
const joined = new Uint8Array(buffer.length + value.length);
joined.set(buffer);
joined.set(value, buffer.length);
buffer = joined;
while (buffer.length >= 5) {
const n = new DataView(buffer.buffer, buffer.byteOffset).getUint32(1);
if (buffer.length < 5 + n) break;
const flag = buffer[0];
const message = buffer.slice(5, 5 + n);
buffer = buffer.slice(5 + n);
if (flag & 0x80) {
ended = true;
continue;
}
if (signal.aborted) return false;
const path = addressed(message);
for (const host of hosts) {
if (host.path === path && host.canvas.isConnected) host.receive(message);
}
}
}
}
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
watch.controller?.abort();
watch.controller = null;
} else {
connect();
}
});
scan();