Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 110 additions & 110 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion src/tweaks/controls/colour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ const CHECKER = "repeating-conic-gradient(#6b6b6b 0% 25%, #9a9a9a 0% 50%) 0 0 /
// colours back in their own notation, and an rgb-shaped parse of that echo mangled them
// (the "3" in "display-p3" read as a channel + alpha 0; lab() channels read as 0–255;
// a `deg` unit or negative hue fell off the old regex onto the same path).
const COLOR_FN_SPACES = { srgb: "srgb", "display-p3": "p3", rec2020: "rec2020", "prophoto-rgb": "prophoto-rgb" };
// Null-prototype: the space name comes straight out of the parsed string, so an inherited
// key had to miss. `color(constructor …)` / `color(__proto__ …)` resolved to a member of
// Object.prototype — truthy, so it sailed past the `if (!space)` guard below and rode into
// convert(), whose space switch matched nothing and handed `undefined` to the XYZ maths:
// a TypeError thrown straight out of panel.set() / fromJSON() / a persisted restore.
const COLOR_FN_SPACES = Object.assign(Object.create(null), { srgb: "srgb", "display-p3": "p3", rec2020: "rec2020", "prophoto-rgb": "prophoto-rgb" });
const parseAngle = (t) => { const m = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)(deg|grad|rad|turn)$/i.exec(t); if (!m) return num(parseFloat(t)); const n = parseFloat(m[1]); return m[2].toLowerCase() === "turn" ? n * 360 : m[2].toLowerCase() === "grad" ? n * 0.9 : m[2].toLowerCase() === "rad" ? (n * 180) / Math.PI : n; };
function parseColor(str) {
str = String(str == null ? "" : str).trim();
Expand Down
25 changes: 16 additions & 9 deletions src/tweaks/controls/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ function createFps(meta) {
wrap.append(txt("span", "tw-fps-label", meta.label || "FPS"), val, canvas);
const ctx = canvas.getContext("2d");
const N = 80, samples = new Array(N).fill(0), MAX = 120;
let i = 0, last = 0, raf = 0, w = 0, h = 0, wasConnected = false;
let i = 0, last = 0, raf = 0, w = 0, h = 0, wasConnected = false, stopped = false;
const resize = () => { [w, h] = fitCanvas(canvas, ctx, 2); };
// Release everything: the rAF loop and its two listeners. Called on a real unmount
// (below) AND handed to the panel as the blade's `destroy`, so a panel torn down
// before it ever connected — which the "never mounted yet" branch below deliberately
// idles through, so it can never self-stop — doesn't leave the loop spinning forever.
const stop = () => { stopped = true; if (raf) cancelAnimationFrame(raf); raf = 0; window.removeEventListener("resize", resize); window.removeEventListener("tw-reflow", resize); };
const draw = () => {
if (!w) return;
ctx.clearRect(0, 0, w, h);
Expand All @@ -27,18 +32,18 @@ function createFps(meta) {
if (!canvas.isConnected) {
// "Never mounted yet" (a host builds the panel eagerly, appends panel.el later) is
// not "removed": idle cheaply until the first connected tick; only a real unmount
// stops the loop + its listeners for good.
if (wasConnected) { window.removeEventListener("resize", resize); window.removeEventListener("tw-reflow", resize); raf = 0; return; }
// (or panel.destroy(), via the blade's `destroy`) stops the loop + its listeners.
if (wasConnected) { stop(); return; }
last = 0; raf = requestAnimationFrame(tick); return;
}
if (!wasConnected) { wasConnected = true; resize(); } // first connected tick → fit the canvas (it measured 0 detached)
if (last) { const fps = 1000 / (now - last); samples[i] = fps; i = (i + 1) % N; val.textContent = Math.round(fps); draw(); }
last = now; raf = requestAnimationFrame(tick);
};
requestAnimationFrame(() => { resize(); raf = requestAnimationFrame(tick); });
raf = requestAnimationFrame(() => { if (stopped) return; resize(); raf = requestAnimationFrame(tick); }); // held in `raf` (and re-checked) so a destroy() before the first frame can't start the loop behind it
window.addEventListener("resize", resize);
window.addEventListener("tw-reflow", resize); // a tab page revealing this control re-fits the canvas (it measured 0 while hidden)
return blade(wrap);
return blade(wrap, stop);
}

// ── Monitor — poll any getter on an interval and show it: a number as a sparkline
Expand All @@ -58,9 +63,11 @@ function createMonitor(meta) {

let timer = 0, onResize = () => {}, wasConnected = false;
const fmt = (v) => (typeof v === "number" ? (Number.isInteger(v) ? String(v) : v.toFixed(meta.decimals ?? 2)) : String(v));
// Also handed to the panel as the blade's `destroy` — a panel destroyed before it ever
// connected idles below forever, so it could never clear its own interval on unmount.
const stop = () => { if (timer) clearInterval(timer); timer = 0; window.removeEventListener("resize", onResize); window.removeEventListener("tw-reflow", onResize); };
// "Never mounted yet" (a host appends panel.el after building) idles the tick; only a
// panel that was mounted and then removed stops the poll for good.
// panel that was mounted and then removed — or a panel.destroy() — stops the poll.
const poll = (fn) => { timer = setInterval(() => { if (!wrap.isConnected) { if (wasConnected) stop(); return; } wasConnected = true; let v; try { v = get(); } catch { return; } fn(v); }, interval); };

// String buffer (multiline) — the last `rows` values, newest at the bottom.
Expand All @@ -73,10 +80,10 @@ function createMonitor(meta) {
wrap.append(buf);
const lines = [];
poll((v) => { lines.push(fmt(v)); while (lines.length > rows) lines.shift(); buf.textContent = lines.join("\n"); });
return blade(wrap);
return blade(wrap, stop);
}
// Plain readout — just the latest value, refreshed on the interval.
if (!graph) { poll((v) => { val.textContent = fmt(v); }); return blade(wrap); }
if (!graph) { poll((v) => { val.textContent = fmt(v); }); return blade(wrap, stop); }

// Sparkline (numbers).
const canvas = document.createElement("canvas"); canvas.className = "tw-fps-canvas";
Expand Down Expand Up @@ -113,7 +120,7 @@ function createMonitor(meta) {
requestAnimationFrame(() => { onResize(); draw(); });
window.addEventListener("resize", onResize);
window.addEventListener("tw-reflow", onResize); // a tab page revealing this control re-fits the canvas (it measured 0 while hidden)
return blade(wrap);
return blade(wrap, stop);
}

registerControl("fpsgraph", createFps);
Expand Down
2 changes: 1 addition & 1 deletion src/tweaks/controls/plot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function createPlot(meta, onChange) {
const field = el("div", "tw-plot-field");
input = el("input", "tw-plot-input"); input.type = "text"; input.value = expr; input.spellcheck = false;
input.autocapitalize = "off"; input.autocomplete = "off"; input.setAttribute("aria-label", `${meta.label || "Plot"} — expression in x`);
field.append(el("span", "tw-plot-fx", "y ="), input); root.append(field);
field.append(txt("span", "tw-plot-fx", "y ="), input); root.append(field); // txt, not el(…, html): el's third arg is innerHTML, and no label in the kit goes through that door
}

const PAD = 6;
Expand Down
Loading
Loading