Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 87 lines 2.7 kB view raw
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)`);