Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2// Render animated KidLisp pieces to WebP via self-contained WASM.
3// Usage: node animate.mjs <piece.lisp> [frames] [fps] [size]
4
5import { readFileSync, mkdirSync } from "fs";
6import { basename } from "path";
7import sharp from "sharp";
8import { Compiler } from "./compiler.mjs";
9
10const OUT_DIR = new URL("./output/", import.meta.url).pathname;
11mkdirSync(OUT_DIR, { recursive: true });
12
13const input = process.argv[2] || "anim.lisp";
14const FRAMES = parseInt(process.argv[3]) || 120;
15const FPS = parseInt(process.argv[4]) || 30;
16const SIZE = parseInt(process.argv[5]) || 256;
17
18const path = new URL(input, import.meta.url).pathname;
19const source = readFileSync(path, "utf-8");
20const name = basename(input, ".lisp");
21
22console.log(`Compiling ${input}...`);
23const compiler = new Compiler();
24const wasmBytes = compiler.compile(source);
25const mathImports = {
26 math: {
27 sin: (x) => Math.fround(Math.sin(x)),
28 cos: (x) => Math.fround(Math.cos(x)),
29 random: () => Math.fround(Math.random()),
30 },
31};
32const { instance } = await WebAssembly.instantiate(wasmBytes, mathImports);
33console.log(`WASM: ${wasmBytes.length} bytes | ${FRAMES} frames @ ${FPS}fps | ${SIZE}x${SIZE}`);
34
35// Render all frames
36const delay = Math.round(1000 / FPS);
37const framePngs = [];
38
39for (let f = 0; f < FRAMES; f++) {
40 instance.exports.paint(SIZE, SIZE, f);
41
42 const mem = new Uint8Array(instance.exports.memory.buffer);
43 const pixels = Buffer.from(mem.slice(0, SIZE * SIZE * 4));
44
45 const png = await sharp(pixels, {
46 raw: { width: SIZE, height: SIZE, channels: 4 },
47 }).png().toBuffer();
48
49 framePngs.push(png);
50
51 if ((f + 1) % 30 === 0 || f === FRAMES - 1) {
52 process.stdout.write(`\r Rendered ${f + 1}/${FRAMES} frames`);
53 }
54}
55console.log();
56
57// Encode animated WebP
58console.log("Encoding animated WebP...");
59const outPath = `${OUT_DIR}${name}.webp`;
60
61await sharp(framePngs[0], { animated: true })
62 .webp({ quality: 80 })
63 .toFile(outPath + ".tmp");
64
65// sharp doesn't do animated WebP natively from frames,
66// so use ffmpeg which is available
67import { execSync } from "child_process";
68
69// Write frames to temp dir
70const tmpDir = `${OUT_DIR}.frames-${name}`;
71mkdirSync(tmpDir, { recursive: true });
72
73for (let f = 0; f < framePngs.length; f++) {
74 const framePath = `${tmpDir}/frame-${String(f).padStart(5, "0")}.png`;
75 await sharp(framePngs[f]).toFile(framePath);
76}
77
78execSync(
79 `ffmpeg -y -framerate ${FPS} -i "${tmpDir}/frame-%05d.png" -loop 0 -lossless 1 "${outPath}" 2>/dev/null`,
80);
81
82// Clean up
83execSync(`rm -rf "${tmpDir}" "${outPath}.tmp"`);
84
85const { statSync } = await import("fs");
86const size = statSync(outPath).size;
87console.log(`${name}.webp (${SIZE}x${SIZE}, ${FRAMES} frames, ${(size / 1024).toFixed(1)}KB)`);