fouriers-fire / render.js
Unt1l1f1nd's picture
the model writes two bytes of the assembler; flicker strips; controls; one renderer
ce39eaa verified
Raw History Blame Contribute Delete
3.32 kB
// Shared drawing: the palette, painting a temperature field, colouring a word by its heat.
// Used by index.html (the page) and record.html (the video), so they cannot drift apart.
const PAL = new Uint8ClampedArray(256 * 4);
{
const stops = [[0, [0, 0, 0]], [.18, [70, 4, 0]], [.36, [190, 30, 0]], [.55, [245, 110, 10]], [.75, [255, 200, 60]], [.9, [255, 240, 180]], [1, [255, 255, 255]]];
for (let i = 0; i < 256; i++) {
const t = i / 255; let a = stops[0], b = stops[1];
for (let s = 1; s < stops.length; s++) { if (t <= stops[s][0]) { a = stops[s - 1]; b = stops[s]; break; } }
const f = (t - a[0]) / (b[0] - a[0] || 1);
for (let c = 0; c < 3; c++) PAL[4 * i + c] = a[1][c] + (b[1][c] - a[1][c]) * f;
PAL[4 * i + 3] = 255;
}
}
function paintField(img, T, n) {
const p = img.data;
for (let i = 0; i < n; i++) { const c = 4 * Math.min(255, T[i] * 255 | 0); p[4 * i] = PAL[c]; p[4 * i + 1] = PAL[c + 1]; p[4 * i + 2] = PAL[c + 2]; p[4 * i + 3] = 255; }
}
// a word's heat on the bottom row, from the sentence score s and the word's share
function wordHeat(s, share) { return Math.max(.15, Math.min(1, .6 + .25 * s + 1.8 * share)); }
function wordColor(h, share) { return share < -.05 ? '#4E8FCB' : `rgb(${Math.max(125, Math.round(255 * h))},${Math.round(110 + 120 * h * h)},${Math.round(92 + 40 * h)})`; }
// the per-column ember heat for a fluid of width W
function emberHeat(W, words, shares, s) {
const heat = new Float32Array(W), x0 = W * .12, x1 = W * .88, span = (x1 - x0) / words.length;
words.forEach((w, i) => { const h = wordHeat(s, shares[i]); for (let x = Math.round(x0 + i * span); x < Math.round(x0 + (i + 1) * span) - 1; x++) heat[x] = h; });
return heat;
}
// the 1990s trick, as on the long page: Fourier with a constant updraft. Used here only for its brightness.
function makeTrick(W, H) {
const h = new Float32Array(W * (H + 4)), ember = new Float32Array(W), K = { D: .3, k: .012, p: .5 }; let mean = 0;
function step() {
for (let x = 0; x < W; x++) { if (Math.random() < .08) ember[x] = Math.random() < K.p ? 1 : 0; for (let y = H; y < H + 4; y++) h[y * W + x] = ember[x]; }
let sum = 0;
for (let y = 0; y < H; y++) { const r = (y + 1) * W; for (let x = 0; x < W; x++) {
const xc = Math.min(W - 1, Math.max(0, x + (Math.random() * 3 | 0) - 1)), xl = xc ? xc - 1 : 0, xr = xc < W - 1 ? xc + 1 : xc;
const s = (1 - K.D) * h[r + xc] + K.D * .5 * (h[r + xl] + h[r + xr]) - K.k * (.5 + Math.random()); const t = s > 0 ? s : 0; h[y * W + x] = t; sum += t; } }
mean = sum / (W * H);
}
return { step, K, mean: () => mean };
}
// a sparkline of the last N samples, drawn into a small canvas
function sparkline(canvas, buf, n, i, color) {
const g = canvas.getContext('2d'), W = canvas.width, H = canvas.height;
g.clearRect(0, 0, W, H);
if (n < 2) return;
let lo = Infinity, hi = -Infinity; for (let j = 0; j < n; j++) { const v = buf[(i - n + j + buf.length) % buf.length]; if (v < lo) lo = v; if (v > hi) hi = v; }
const span = hi - lo || 1e-6;
g.strokeStyle = color; g.lineWidth = 1.5; g.beginPath();
for (let j = 0; j < n; j++) { const v = buf[(i - n + j + buf.length) % buf.length]; const x = j / (buf.length - 1) * W, y = H - 2 - (v - lo) / span * (H - 4); j ? g.lineTo(x, y) : g.moveTo(x, y); }
g.stroke();
}