manimo-playground / widgets /funcplot.js
jacooob521's picture
fix 14 medium review findings (correctness/robustness/a11y)
9dde5d4 verified
Raw
History Blame Contribute Delete
4.67 kB
/* Function plotter — pick an activation/curve, drag the input x, and watch z = w·x + b feed through
* f(z); the curve draws itself and a tracer marks (x, f(x)). Mirrors the Manim `plot` animation. */
(function () {
const k = MANIMO.kit;
const FNS = {
sigmoid: { f: (z) => 1 / (1 + Math.exp(-z)), tex: "σ(z) = 1 / (1 + e⁻ᶻ)", yr: [0, 1] },
tanh: { f: (z) => Math.tanh(z), tex: "tanh(z)", yr: [-1, 1] },
relu: { f: (z) => Math.max(0, z), tex: "ReLU(z) = max(0, z)", yr: [0, 4] },
decay: { f: (z) => Math.exp(-Math.max(0, z)), tex: "e⁻ᶻ (loss decay)", yr: [0, 1] },
linear: { f: (z) => z, tex: "z", yr: [-4, 4] },
};
function mount(root, concept, theme) {
const p = (concept && concept.params) || {};
let name = FNS[p.shape] ? p.shape : (p.shape === "parabola" ? "relu" : "sigmoid");
let w = p.w != null ? p.w : 1, b = p.b != null ? p.b : 0, x = 0;
const cv = document.createElement("canvas"); cv.width = 460; cv.height = 240; cv.className = "plot";
cv.setAttribute("role", "img"); cv.setAttribute("aria-label", "Activation function plot with a draggable input tracer");
const readout = k.el("div", { class: "readout" });
const sel = k.el("select", { class: "sel", onchange: (e) => { name = e.target.value; draw(); } },
Object.keys(FNS).map((nm) => k.el("option", { value: nm }, [nm])));
sel.value = name;
const sx = slider("x", -4, 4, x, (v) => { x = v; draw(); });
const sw = slider("w", -3, 3, w, (v) => { w = v; draw(); });
const sb = slider("b", -3, 3, b, (v) => { b = v; draw(); });
function draw() {
const fn = FNS[name], W = cv.width, H = cv.height, ctx = cv.getContext("2d");
const xr = [-4, 4], yr = fn.yr;
const X = (xv) => (xv - xr[0]) / (xr[1] - xr[0]) * (W - 30) + 20;
const Y = (yv) => H - 24 - (yv - yr[0]) / (yr[1] - yr[0]) * (H - 40);
ctx.fillStyle = theme.bg; ctx.fillRect(0, 0, W, H);
// axes
ctx.strokeStyle = theme.muted; ctx.lineWidth = 1; ctx.beginPath();
ctx.moveTo(20, Y(Math.max(yr[0], 0))); ctx.lineTo(W - 8, Y(Math.max(yr[0], 0)));
ctx.moveTo(X(0), 16); ctx.lineTo(X(0), H - 18); ctx.stroke();
// curve
ctx.strokeStyle = theme.accent; ctx.lineWidth = 3; ctx.beginPath();
for (let i = 0; i <= 200; i++) { const xv = xr[0] + (xr[1] - xr[0]) * i / 200; const z = w * xv + b; const yv = fn.f(z); (i ? ctx.lineTo : ctx.moveTo).call(ctx, X(xv), Y(yv)); }
ctx.stroke();
// tracer at x
const z = w * x + b, y = fn.f(z);
ctx.fillStyle = theme.accent2; ctx.beginPath(); ctx.arc(X(x), Y(y), 6, 0, 7); ctx.fill();
ctx.strokeStyle = theme.accent2; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(X(x), Y(yr[0])); ctx.lineTo(X(x), Y(y)); ctx.lineTo(X(0), Y(y)); ctx.stroke(); ctx.setLineDash([]);
readout.innerHTML = "z = w·x + b = " + k.fmt(w) + "·" + k.fmt(x) + " + " + k.fmt(b) + " = <b>" + k.fmt(z) + "</b> → " + fn.tex + " = <b>" + k.fmt(y) + "</b>";
}
let raf = null;
function stop() { if (raf) { cancelAnimationFrame(raf); raf = null; } }
function play() { stop(); const t0 = performance.now(); (function tick(t) { x = -4 + 8 * (((t - t0) / 3400) % 1); sx.set(x); draw(); raf = requestAnimationFrame(tick); })(t0); }
// one-shot sweep for "Play all": glide x left→right once (slowly, so the curve reads), then signal done.
function playOnce(done) {
stop(); const dur = 3400, t0 = performance.now();
(function tick(t) {
const u = (t - t0) / dur;
if (u >= 1) { x = 4; sx.set(x); draw(); raf = null; if (done) done(); return; }
x = -4 + 8 * u; sx.set(x); draw(); raf = requestAnimationFrame(tick);
})(t0);
}
root.appendChild(k.row([k.el("label", { class: "fld" }, ["function ", sel]), readout], "ctrls"));
root.appendChild(cv);
root.appendChild(k.row([sx.node, sw.node, sb.node], "sliders"));
root.appendChild(k.row([k.button("▶ Sweep x", play), k.button("⏹ Stop", stop, "ghost")], "ctrls"));
draw();
return { play: playOnce, stop, reset: () => { stop(); x = 0; sx.set(0); draw(); } };
}
function slider(label, lo, hi, val, on) {
const out = MANIMO.kit.el("span", { class: "sval" }, [MANIMO.kit.fmt(val)]);
const inp = MANIMO.kit.el("input", { type: "range", min: lo, max: hi, step: 0.05, value: val, oninput: (e) => { out.textContent = MANIMO.kit.fmt(+e.target.value); on(+e.target.value); } });
return { node: MANIMO.kit.el("label", { class: "fld" }, [label + " ", inp, out]), set: (v) => { inp.value = v; out.textContent = MANIMO.kit.fmt(v); } };
}
MANIMO.registry["plot"] = { title: "Function Plot", mount };
})();