#!/usr/bin/env node /** * Converts scraped JSON animation data to firmware/src/splash_animations.h. * * Per-animation palette (up to 10 entries) is converted to RGB565. Cells in * each frame are palette indices (0..9). Splash module looks up colors via * palette[cell]. * * Usage: node convert_to_c.js [--in DIR] [--out FILE] */ const fs = require('fs'); const path = require('path'); const args = process.argv.slice(2); const opt = (k, def) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : def; }; const IN_DIR = path.resolve(opt('--in', path.join(__dirname, 'claudepix_data'))); const OUT_FILE = path.resolve(opt('--out', path.join(__dirname, '..', 'firmware', 'src', 'splash_animations.h'))); const PALETTE_SIZE = 10; function safeIdent(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); } function hexToRgb565(hex) { if (!hex || hex === 'transparent') return 0x0000; // dark bg let h = hex.replace('#', ''); if (h.length === 3) h = h.split('').map(c => c + c).join(''); const r = parseInt(h.substr(0, 2), 16); const g = parseInt(h.substr(2, 2), 16); const b = parseInt(h.substr(4, 2), 16); return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); } function paletteToRgb565(palette) { const out = new Array(PALETTE_SIZE).fill(0x0000); for (let i = 0; i < palette.length && i < PALETTE_SIZE; i++) { out[i] = hexToRgb565(palette[i]); } return out; } function main() { if (!fs.existsSync(IN_DIR)) { console.error(`No scraped data at ${IN_DIR}. Run scrape_claudepix.js first.`); process.exit(1); } const indexPath = path.join(IN_DIR, '_index.json'); if (!fs.existsSync(indexPath)) { console.error(`Missing ${indexPath}.`); process.exit(1); } const index = JSON.parse(fs.readFileSync(indexPath, 'utf8')); console.log(`Converting ${index.length} animations`); let out = ''; out += '// ============================================================\n'; out += '// Splash animations — generated by tools/convert_to_c.js.\n'; out += '// Source: https://claudepix.vercel.app (20x20 pixel-art creature\n'; out += '// animation library). Frames extracted by tools/scrape_claudepix.js\n'; out += '// from per-animation HTML files served by the source site.\n'; out += '// Do not edit by hand — re-run the scraper + converter to refresh.\n'; out += '// ============================================================\n'; out += '// Each animation carries a 10-entry RGB565 palette.\n'; out += '// Cell values 0..9 index into palette.\n'; out += '#pragma once\n#include \n\n'; out += `#define SPLASH_PALETTE_SIZE ${PALETTE_SIZE}\n\n`; out += 'typedef struct {\n'; out += ' const char *name;\n'; out += ' const char *category;\n'; out += ' uint16_t frame_count;\n'; out += ' const uint16_t *palette;\n'; out += ' const uint8_t (*frames)[400];\n'; out += ' const uint16_t *holds;\n'; out += '} splash_anim_def_t;\n\n'; const entries = []; for (const meta of index) { const ident = safeIdent(meta.filename.replace(/\.html?$/, '')); const dataPath = path.join(IN_DIR, meta.filename.replace(/\.html?$/, '.json')); const data = JSON.parse(fs.readFileSync(dataPath, 'utf8')); const pal565 = paletteToRgb565(data.palette); out += `static const uint16_t splash_${ident}_palette[${PALETTE_SIZE}] = {`; out += pal565.map(c => `0x${c.toString(16).toUpperCase().padStart(4, '0')}`).join(','); out += '};\n'; out += `static const uint8_t splash_${ident}_frames[${data.frames.length}][400] = {\n`; for (const f of data.frames) { const flat = []; for (let r = 0; r < 20; r++) for (let c = 0; c < 20; c++) flat.push(f.grid[r][c]); out += ' {' + flat.join(',') + '},\n'; } out += '};\n'; out += `static const uint16_t splash_${ident}_holds[${data.frames.length}] = {`; out += data.frames.map(f => f.hold).join(','); out += '};\n\n'; entries.push({ ident, name: data.name, category: data.category, count: data.frames.length }); } out += `#define SPLASH_ANIM_COUNT ${entries.length}\n`; out += 'static const splash_anim_def_t splash_anims[SPLASH_ANIM_COUNT] = {\n'; for (const e of entries) { out += ` {"${e.name}", "${e.category}", ${e.count}, splash_${e.ident}_palette, splash_${e.ident}_frames, splash_${e.ident}_holds},\n`; } out += '};\n'; fs.writeFileSync(OUT_FILE, out); console.log(`Wrote ${OUT_FILE} (${entries.length} animations, ${(out.length / 1024).toFixed(1)} KB)`); } main();