Files
music-video-gen/metabolism/src/render/png.js
T
Dejvino 13b0939221 Metabolism: song as substrate, ecology grown from the track
A new top-level section building the metabolism concept: the track
supplies the food field, event impulses and per-section laws; the seed
picks the species (a rule set, not a parameter vector). Same song + seed
=> bit-identical frames; different songs => different worlds.

- analysis: single FFT pass -> per-frame feature table; score (onsets
  folded into world geometry via voice ring + pitch-class sector), wells
  (food placed by the harmony), graph (band correlations -> interaction
  matrix), laws (per-section regimes)
- sim: typed-array world, 3 castes (builder/orbit, forager/commute,
  spark/orbit-wide) with causality rings, persistence trails,
  consequence field; fully deterministic
- lens: software framebuffer renderer (PNG in node, ImageData in
  browser) + event-reactive camera (cuts on activity change, alternating
  shot sizes, drift-and-push)
- metrics: coupling x residual fitness (not per-frame descriptors) plus
  a grown-structure fingerprint; report + strip tools; 15 tests green
- DESIGN.md captures the concept, invariants and build order

3 reference test signals (drone/runner/dusk) grow measurably distinct
worlds while seeds of one song stay in the same biome.
2026-10-24 08:23:33 +02:00

88 lines
2.7 KiB
JavaScript

// Minimal PNG writer (8-bit RGBA, no interlace). Node only: the browser demo
// blits the same framebuffer through ImageData and never touches this file.
import zlib from 'node:zlib';
const CRC_TABLE = (() => {
const t = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
t[n] = c >>> 0;
}
return t;
})();
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(body), 0);
return Buffer.concat([len, body, crc]);
}
/**
* @param {number} width
* @param {number} height
* @param {Uint8ClampedArray|Uint8Array} rgba
* @returns {Buffer}
*/
export function encodePNG(width, height, rgba) {
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let y = 0; y < height; y++) {
raw[y * (stride + 1)] = 0;
raw.set(rgba.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1);
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 6;
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr),
chunk('IDAT', zlib.deflateSync(raw, { level: 6 })),
chunk('IEND', Buffer.alloc(0)),
]);
}
/** Tile several bitmaps into one grid. Bitmaps are {w, h, data}. */
export function tile(bitmaps, cols, rows, gap = 0) {
if (!bitmaps.length) throw new Error('tile: nothing to tile');
const w = bitmaps[0].w;
const h = bitmaps[0].h;
const W = cols * w + (cols - 1) * gap;
const H = rows * h + (rows - 1) * gap;
const out = new Uint8ClampedArray(W * H * 4);
for (let i = 0; i < out.length; i += 4) {
out[i] = 8;
out[i + 1] = 8;
out[i + 2] = 10;
out[i + 3] = 255;
}
for (let i = 0; i < bitmaps.length; i++) {
const b = bitmaps[i];
const cx = i % cols;
const cy = (i / cols) | 0;
const ox = cx * (w + gap);
const oy = cy * (h + gap);
for (let y = 0; y < h; y++) {
const src = y * b.w * 4;
const dst = ((oy + y) * W + ox) * 4;
out.set(b.data.subarray(src, src + b.w * 4), dst);
}
}
return { w: W, h: H, data: out };
}