<style>
.rizzma-live-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
align-items: start;
}
.rizzma-live-grid .rizzma-live {
margin: 0;
}
.rizzma-live canvas {
border-radius: 3px;
}
.rizzma-live-expand {
display: inline-block;
margin: 2px 0 0;
padding: 0;
background: none;
border: none;
font: inherit;
font-size: 12px;
color: inherit;
opacity: 0.65;
cursor: pointer;
}
.rizzma-live-expand:hover {
opacity: 1;
text-decoration: underline;
}
.rizzma-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.rizzma-modal-panel {
position: relative;
background: #fff;
border-radius: 8px;
padding: 26px 14px 12px;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.45);
max-width: 96vw;
max-height: 94vh;
overflow: auto;
}
.rizzma-modal-close {
position: absolute;
top: 2px;
right: 8px;
padding: 2px 6px;
background: none;
border: none;
font: inherit;
font-size: 18px;
line-height: 1;
color: #444;
cursor: pointer;
}
.rizzma-modal-close:hover {
color: #000;
}
</style>
<script type="module">
(() => {
const PKG =
window.RIZZMA_DEMO_PKG ||
"https://orbitalcommons.github.io/rizzma/demo/pkg/rizzma.js";
let animators = [];
const visible = new WeakSet();
const animIo = new IntersectionObserver(
(entries) => {
for (const en of entries) {
if (en.isIntersecting) visible.add(en.target);
else visible.delete(en.target);
}
},
{ rootMargin: "100px" },
);
const t0 = performance.now();
setInterval(() => {
const t = (performance.now() - t0) / 1000;
for (const a of animators) {
if (visible.has(a.host)) a.tick(t);
}
}, 50);
const animate = (host, tick) => {
animIo.observe(host);
animators.push({ host, tick });
};
const unanimate = (host) => {
animators = animators.filter((a) => a.host !== host);
animIo.unobserve(host);
};
const F64 = Float64Array;
const linspace = (a, b, n) => {
const out = new F64(n);
for (let i = 0; i < n; i++) out[i] = a + ((b - a) * i) / (n - 1);
return out;
};
const demos = {
beats(mod, canvas, host, k = 1) {
const n = 700;
const xs = linspace(0, 16 * Math.PI, n);
const ys = new F64(n);
const fill = (phase) => {
for (let i = 0; i < n; i++) {
ys[i] = Math.sin(3 * xs[i] - phase) + Math.sin(3.35 * xs[i] - phase);
}
};
fill(0);
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const ax = fig.add_subplot(1, 1, 1);
fig.plot(ax, xs, ys);
fig.set_title(ax, "plot: two strings beating");
fig.set_ylim(ax, -2.2, 2.2);
const session = fig.bind(canvas.id);
animate(host, (t) => {
fill(t * 4);
session.set_line_data(ax, 0, xs, ys);
});
return session;
},
interference(mod, canvas, host, k = 1) {
const n = 500;
const xs = linspace(0, 4 * Math.PI, n);
const y1 = new F64(n);
const y2 = new F64(n);
const sum = new F64(n);
const fill = (t) => {
for (let i = 0; i < n; i++) {
y1[i] = Math.sin(xs[i] - 2 * t) * 0.9;
y2[i] = Math.sin(1.5 * xs[i] + 2 * t) * 0.7;
sum[i] = y1[i] + y2[i];
}
};
fill(0);
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const ax = fig.add_subplot(1, 1, 1);
fig.plot_styled(ax, xs, y1, { color: "tab:gray", lw: 1.0 });
fig.plot_styled(ax, xs, y2, { color: "tab:gray", lw: 1.0 });
fig.plot_styled(ax, xs, sum, { color: "tab:blue", lw: 2.0 });
fig.set_title(ax, "counter-propagating waves");
fig.set_ylim(ax, -2.0, 2.0);
const session = fig.bind(canvas.id);
animate(host, (t) => {
fill(t);
session.set_line_data(ax, 0, xs, y1);
session.set_line_data(ax, 1, xs, y2);
session.set_line_data(ax, 2, xs, sum);
});
return session;
},
rose(mod, canvas, host, k = 1) {
const n = 720;
const xs = new F64(n);
const ys = new F64(n);
const fill = (kk) => {
for (let i = 0; i < n; i++) {
const th = (2 * Math.PI * i) / (n - 1);
const r = Math.cos(kk * th);
xs[i] = r * Math.cos(th);
ys[i] = r * Math.sin(th);
}
};
fill(3);
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const ax = fig.add_subplot(1, 1, 1);
fig.plot_styled(ax, xs, ys, { color: "tab:purple", lw: 1.5 });
fig.set_title(ax, "rose, petals in motion");
fig.set_xlim(ax, -1.15, 1.15);
fig.set_ylim(ax, -1.15, 1.15);
const session = fig.bind(canvas.id);
animate(host, (t) => {
fill(4.5 + 2.5 * Math.sin(0.25 * t));
session.set_line_data(ax, 0, xs, ys);
});
return session;
},
spectro(mod, canvas, host, k = 1) {
const fs = 80; const win = 64; const bins = 24; const cols = 120; const dt = 0.05; const span = cols * dt; let seed = 0xd5b;
const rand = () => {
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return ((seed >>> 0) % 10000) / 10000;
};
let f1 = 8;
let f2 = 22;
let ph1 = 0;
let ph2 = 0;
const samples = [];
const times = [];
const grid = new F64(bins * cols);
const hann = new F64(win);
for (let j = 0; j < win; j++) {
hann[j] = 0.5 - 0.5 * Math.cos((2 * Math.PI * j) / (win - 1));
}
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const top = fig.add_subplot(2, 1, 1);
const bottom = fig.add_subplot(2, 1, 2);
fig.plot(top, new F64(0), new F64(0));
fig.set_title(top, "signal + live spectrogram");
fig.set_xlim(top, -span, 0);
fig.set_ylim(top, -2.6, 2.6);
fig.imshow(bottom, grid, bins, cols, [-span, 0, 0, 30], "", 0, 1.1);
fig.set_ylabel(bottom, "Hz");
fig.set_xlabel(bottom, "t − now (s)");
fig.sharex(bottom, top);
const session = fig.bind(canvas.id);
let lastT = 0;
animate(host, (t) => {
f1 = Math.min(14, Math.max(3, f1 + (rand() - 0.5) * 0.6));
f2 = Math.min(28, Math.max(16, f2 + (rand() - 0.5) * 0.9));
const steps = Math.max(1, Math.min(8, Math.round((t - lastT) * fs)));
for (let j = 0; j < steps; j++) {
ph1 += (2 * Math.PI * f1) / fs;
ph2 += (2 * Math.PI * f2) / fs;
samples.push(
Math.sin(ph1) + 0.8 * Math.sin(ph2) + (rand() - 0.5) * 0.7,
);
times.push(t - (steps - 1 - j) / fs);
}
while (times.length > 1 && t - times[0] > span) {
times.shift();
samples.shift();
}
const rel = new F64(times.length);
for (let i = 0; i < times.length; i++) rel[i] = times[i] - t;
session.set_line_data(top, 0, rel, F64.from(samples));
grid.copyWithin(0, 1);
for (let r = 0; r < bins; r++) grid[(r + 1) * cols - 1] = 0;
if (samples.length >= win) {
const o = samples.length - win;
for (let kk = 1; kk <= bins; kk++) {
let re = 0;
let im = 0;
for (let j = 0; j < win; j++) {
const a = (2 * Math.PI * kk * j) / win;
const s = samples[o + j] * hann[j];
re += s * Math.cos(a);
im -= s * Math.sin(a);
}
const row = bins - kk;
grid[(row + 1) * cols - 1] = Math.hypot(re, im) / (win / 4);
}
}
session.set_image_data(bottom, 0, grid, bins, cols, [-span, 0, 0, 30]);
lastT = t;
});
return session;
},
galaxy(mod, canvas, host, k = 1) {
const stars = 900;
const gx = new F64(stars);
const gy = new F64(stars);
const gz = new F64(stars);
const rx = new F64(stars);
const ry = new F64(stars);
let seed = 0x5eed;
const rand = () => {
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return ((seed >>> 0) % 10000) / 10000;
};
for (let i = 0; i < stars; i++) {
const arm = i % 2 === 0 ? 0 : Math.PI;
const a = 0.4 + 3.4 * (i / stars);
const r = 0.22 * a;
gx[i] = r * Math.cos(2.1 * a + arm) + (rand() - 0.5) * 0.09;
gy[i] = r * Math.sin(2.1 * a + arm) + (rand() - 0.5) * 0.09;
gz[i] = ((rand() - 0.5) * 0.08) / (1 + 2.5 * r);
}
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const ax = fig.add_subplot(1, 1, 1);
fig.scatter(ax, gx, gy);
fig.set_title(ax, "scatter: zoom into the galaxy");
fig.set_xlim(ax, -0.95, 0.95);
fig.set_ylim(ax, -0.95, 0.95);
const session = fig.bind(canvas.id);
const ca = Math.cos(0.96); const sa = Math.sin(0.96);
animate(host, (t) => {
const th = 0.4 * t; const ph = 0.06 * t; const c1 = Math.cos(th);
const s1 = Math.sin(th);
const c2 = Math.cos(ph);
const s2 = Math.sin(ph);
const m00 = c2 * c1 - s2 * ca * s1;
const m01 = -c2 * s1 - s2 * ca * c1;
const m02 = s2 * sa;
const m10 = s2 * c1 + c2 * ca * s1;
const m11 = -s2 * s1 + c2 * ca * c1;
const m12 = -c2 * sa;
for (let i = 0; i < stars; i++) {
rx[i] = m00 * gx[i] + m01 * gy[i] + m02 * gz[i];
ry[i] = m10 * gx[i] + m11 * gy[i] + m12 * gz[i];
}
session.set_scatter_offsets(ax, 0, rx, ry);
});
return session;
},
surface3d(mod, canvas, host, k = 1) {
const n = 32;
const gx = linspace(-8, 8, n);
const gz = new F64(n * n);
for (let j = 0; j < n; j++) {
for (let i = 0; i < n; i++) {
const r = Math.hypot(gx[i], gx[j]);
gz[j * n + i] = r < 1e-9 ? 1 : Math.sin(r) / r;
}
}
const ax = new mod.WasmAxes3D(
Math.round(360 * k),
Math.round(270 * k),
100,
);
ax.plot_surface(gx, gx, gz);
ax.set_title("plot_surface: sombrero, spinning");
ax.set_view(28, -60);
ax.render(canvas.id);
animate(host, (t) => {
ax.set_view(28, -60 + 12 * t);
ax.render(canvas.id);
});
return ax;
},
ripples(mod, canvas, host, k = 1) {
const nr = 40;
const nc = 54;
const grid = new F64(nr * nc);
const fill = (t) => {
for (let r = 0; r < nr; r++) {
for (let c = 0; c < nc; c++) {
const x = (c + 0.5) / nc;
const y = (r + 0.5) / nr;
const r1 = Math.hypot(x - 0.32, y - 0.5);
const r2 = Math.hypot(x - 0.68, y - 0.5);
grid[r * nc + c] =
Math.sin(38 * r1 - 5 * t) / (1 + 4 * r1) +
Math.sin(38 * r2 - 5 * t) / (1 + 4 * r2);
}
}
};
fill(0);
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const ax = fig.add_subplot(1, 1, 1);
fig.imshow(ax, grid, nr, nc, [0, 1.35, 0, 1], "coolwarm", -1.4, 1.4);
fig.set_title(ax, "imshow: a ripple tank");
const session = fig.bind(canvas.id);
animate(host, (t) => {
fill(t);
session.set_image_data(ax, 0, grid, nr, nc, [0, 1.35, 0, 1]);
});
return session;
},
lorenz(mod, canvas, host, k = 1) {
const keep = 2200;
let x = 0.9;
let y = 1.4;
let z = 19;
const xs = [];
const zs = [];
const step = () => {
const dt = 0.005;
const dx = 10 * (y - x);
const dy = x * (28 - z) - y;
const dz = x * y - (8 / 3) * z;
x += dx * dt;
y += dy * dt;
z += dz * dt;
xs.push(x);
zs.push(z);
if (xs.length > keep) {
xs.shift();
zs.shift();
}
};
for (let i = 0; i < 400; i++) step();
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
const ax = fig.add_subplot(1, 1, 1);
fig.plot_styled(ax, F64.from(xs), F64.from(zs), {
color: "tab:orange",
lw: 1.0,
});
fig.set_title(ax, "the Lorenz attractor, live");
fig.set_xlim(ax, -23, 23);
fig.set_ylim(ax, 0, 52);
const session = fig.bind(canvas.id);
animate(host, () => {
for (let i = 0; i < 12; i++) step();
session.set_line_data(ax, 0, F64.from(xs), F64.from(zs));
});
return session;
},
scope(mod, canvas, host, k = 1) {
const fig = new mod.WasmFigure(3.6 * k, 2.7 * k);
fig.set_facecolor("#0a0d0b");
const strips = [
fig.add_axes(0.0, 0.68, 1.0, 0.32),
fig.add_axes(0.0, 0.34, 1.0, 0.32),
fig.add_axes(0.0, 0.0, 1.0, 0.32),
];
const empty = new F64(0);
for (const s of strips) {
fig.oscilloscope(s);
fig.plot(s, empty, empty);
}
fig.sharex(strips[1], strips[0]);
fig.sharex(strips[2], strips[0]);
const session = fig.bind(canvas.id);
let seed = 0xacab;
const rand = () => {
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return ((seed >>> 0) % 10000) / 10000;
};
const T = [];
const C = [[], [], []];
let drift = 0;
animate(host, (t) => {
drift += (rand() - 0.5) * 0.05;
T.push(t);
C[0].push(Math.sin(t * 2.1) * 0.4 + (rand() - 0.5) * 0.12);
C[1].push(Math.cos(t * 1.3) * 0.3 + drift + (rand() - 0.5) * 0.12);
C[2].push(18 + Math.sin(t * 0.7) * 3 + (rand() - 0.5) * 2);
while (T.length > 1 && t - T[0] > 10) {
T.shift();
for (const c of C) c.shift();
}
const ts = F64.from(T);
strips.forEach((s, i) => {
session.set_line_data(s, 0, ts, F64.from(C[i]));
});
});
return session;
},
};
const sessions = [];
let loading = null;
const boot = () =>
(loading ??= import(PKG).then(async (mod) => {
await mod.default();
return mod;
}));
const newCanvas = () => {
const canvas = document.createElement("canvas");
canvas.id = "rizzma-live-" + Math.random().toString(36).slice(2);
canvas.style.touchAction = "none";
canvas.style.display = "block";
return canvas;
};
let openOverlay = null;
const closeModal = () => {
if (!openOverlay) return;
const { overlay, panel, session, onKey } = openOverlay;
openOverlay = null;
unanimate(panel);
try {
session.free();
} catch (_) {
}
document.removeEventListener("keydown", onKey);
overlay.remove();
document.body.style.overflow = "";
};
const openModal = (mod, name) => {
closeModal();
const build = demos[name];
if (!build) return;
const k = Math.max(
1,
Math.min(
2.4,
(Math.min(window.innerWidth, 1200) - 90) / 360,
(window.innerHeight - 130) / 270,
),
);
const overlay = document.createElement("div");
overlay.className = "rizzma-modal-overlay";
const panel = document.createElement("div");
panel.className = "rizzma-modal-panel";
const close = document.createElement("button");
close.className = "rizzma-modal-close";
close.textContent = "×";
close.setAttribute("aria-label", "close");
close.addEventListener("click", closeModal);
const canvas = newCanvas();
panel.append(close, canvas);
overlay.append(panel);
overlay.addEventListener("pointerdown", (e) => {
if (e.target === overlay) closeModal();
});
const onKey = (e) => {
if (e.key === "Escape") closeModal();
};
document.addEventListener("keydown", onKey);
document.body.append(overlay);
document.body.style.overflow = "hidden";
try {
const session = build(mod, canvas, panel, k);
openOverlay = { overlay, panel, session, onKey };
} catch (e) {
overlay.remove();
document.body.style.overflow = "";
document.removeEventListener("keydown", onKey);
console.warn("rizzma full-size demo unavailable:", e);
}
};
const upgrade = async (el) => {
if (el.dataset.rizzmaMounted) return;
el.dataset.rizzmaMounted = "1";
try {
const mod = await boot();
const build = demos[el.dataset.demo];
if (!build) return;
const canvas = newCanvas();
el.prepend(canvas);
sessions.push(build(mod, canvas, el));
const expand = document.createElement("button");
expand.className = "rizzma-live-expand";
expand.textContent = "⤢ full size";
expand.addEventListener("click", () => openModal(mod, el.dataset.demo));
canvas.after(expand);
for (const img of el.querySelectorAll("img")) {
img.style.display = "none";
}
} catch (e) {
console.warn("rizzma live demo unavailable:", e);
}
};
const start = () => {
const hosts = document.querySelectorAll(".rizzma-live[data-demo]");
if (!hosts.length) return;
const io = new IntersectionObserver(
(entries) => {
for (const en of entries) {
if (en.isIntersecting) {
io.unobserve(en.target);
upgrade(en.target);
}
}
},
{ rootMargin: "200px" },
);
hosts.forEach((h) => io.observe(h));
const m = /^#rizzma-live=([a-z0-9_]+)$/.exec(location.hash);
if (m && demos[m[1]]) {
boot()
.then((mod) => openModal(mod, m[1]))
.catch(() => {});
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
})();
</script>