Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: KidLisp Mini bundler for RBP-26 (~20 KB target)

Implement minimal interpreter and bundler for bootloader.art integration:
- eval.mjs: tree-walking interpreter, tokenizer, parser (RBP-26 profile)
- render.mjs: software pixel buffer with Bresenham/midpoint algorithms
- color.mjs, transforms.mjs, timing.mjs: drawing, transforms, timing forms
- bundle.mjs: assembles into self-contained HTML with deterministic seed support
- server.mjs: GET /bundle-mini endpoint accepts ?piece=NAME or ?code=SOURCE

Architecture: direct API call model matching real KidLisp (not command objects).
Supports MongoDB piece fetching and file downloads (?dl=1).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

+1938
+212
oven/kidlisp-mini/bundle.mjs
··· 1 + // KidLisp Mini Bundler 2 + // Assembles minimal interpreter into a self-contained HTML file 3 + // ~150 lines 4 + 5 + import { promises as fs } from 'fs'; 6 + import path from 'path'; 7 + import { fileURLToPath } from 'url'; 8 + import { MongoClient } from 'mongodb'; 9 + 10 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 11 + 12 + async function fetchPieceSource(pieceName) { 13 + // Try to fetch from MongoDB 14 + const mongoUri = process.env.MONGODB_CONNECTION_STRING; 15 + const mongoDb = process.env.MONGODB_NAME; 16 + 17 + if (!mongoUri || !mongoDb) { 18 + throw new Error('MongoDB not configured (MONGODB_CONNECTION_STRING, MONGODB_NAME)'); 19 + } 20 + 21 + const client = new MongoClient(mongoUri); 22 + try { 23 + await client.connect(); 24 + const db = client.db(mongoDb); 25 + const kidlispCollection = db.collection('kidlisp'); 26 + 27 + // Look up by code field (handle $code style names) 28 + const cleanName = pieceName.replace(/^\$/, ''); 29 + const piece = await kidlispCollection.findOne({ 30 + code: cleanName 31 + }); 32 + 33 + if (!piece || !piece.source) { 34 + throw new Error(`Piece not found: ${pieceName}`); 35 + } 36 + 37 + return piece.source; 38 + } finally { 39 + await client.close(); 40 + } 41 + } 42 + 43 + async function bundleMini(source, seed = null) { 44 + // Read all module files 45 + const evalCode = await fs.readFile(path.join(__dirname, 'eval.mjs'), 'utf-8'); 46 + const renderCode = await fs.readFile(path.join(__dirname, 'render.mjs'), 'utf-8'); 47 + const colorCode = await fs.readFile(path.join(__dirname, 'color.mjs'), 'utf-8'); 48 + const transformsCode = await fs.readFile(path.join(__dirname, 'transforms.mjs'), 'utf-8'); 49 + const timingCode = await fs.readFile(path.join(__dirname, 'timing.mjs'), 'utf-8'); 50 + 51 + // Concatenate all modules (removing imports/exports for bundling) 52 + const bundledCode = stripModuleSyntax( 53 + colorCode + '\n' + 54 + timingCode + '\n' + 55 + transformsCode + '\n' + 56 + renderCode + '\n' + 57 + evalCode 58 + ); 59 + 60 + // Minify 61 + const minified = await minifyCode(bundledCode); 62 + 63 + // Escape source for inline JavaScript 64 + const escapedSource = source 65 + .replace(/\\/g, '\\\\') 66 + .replace(/`/g, '\\`') 67 + .replace(/\$/g, '\\$'); 68 + 69 + // Generate HTML 70 + const html = generateHTML(minified, escapedSource, seed); 71 + 72 + return html; 73 + } 74 + 75 + function stripModuleSyntax(code) { 76 + // Remove import statements 77 + code = code.replace(/import\s+.*?from\s+['"].*?['"];?/g, ''); 78 + 79 + // Remove export statements, keep the content 80 + code = code.replace(/export\s+\{[^}]*\}/g, ''); 81 + code = code.replace(/export\s+function\s+/g, 'function '); 82 + code = code.replace(/export\s+class\s+/g, 'class '); 83 + code = code.replace(/export\s+const\s+/g, 'const '); 84 + 85 + return code; 86 + } 87 + 88 + async function minifyCode(code) { 89 + // Simple minification: remove comments and excess whitespace 90 + // Not as good as SWC but doesn't require external dependencies 91 + return code 92 + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments 93 + .replace(/\/\/.*$/gm, '') // Remove // comments 94 + .replace(/\n\s*\n/g, '\n') // Remove blank lines 95 + .split('\n') 96 + .map(line => line.trim()) 97 + .filter(line => line && !line.startsWith('//')) 98 + .join('\n'); 99 + } 100 + 101 + function generateHTML(minifiedCode, source, seed) { 102 + const seedValue = seed !== null ? seed : Math.floor(Math.random() * 0xFFFFFFFF); 103 + 104 + return `<!DOCTYPE html> 105 + <html> 106 + <head> 107 + <meta charset="UTF-8"> 108 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 109 + <title>KidLisp Mini</title> 110 + <style> 111 + * { margin: 0; padding: 0; } 112 + body { overflow: hidden; background: #000; } 113 + canvas { display: block; width: 100vw; height: 100vh; } 114 + </style> 115 + </head> 116 + <body> 117 + <canvas id="c"></canvas> 118 + <script> 119 + // Bundled KidLisp Mini Interpreter 120 + ${minifiedCode} 121 + 122 + // Piece source 123 + const SOURCE = \`${source}\`; 124 + 125 + // Seed from bootloader.art, fxhash, etc. 126 + window.seed = window.seed || ${seedValue}; 127 + const SEED = window.seed; 128 + 129 + // Bootstrap 130 + const canvas = document.getElementById('c'); 131 + const ctx = canvas.getContext('2d', { alpha: false }); 132 + const W = canvas.width = window.innerWidth; 133 + const H = canvas.height = window.innerHeight; 134 + 135 + // Create interpreter and renderer 136 + const kidlisp = new KidLispMini(); 137 + const renderer = makeRenderer(W, H); 138 + const timing = new TimingEngine(SEED); 139 + 140 + // Create API object that directly calls renderer methods 141 + const api = { 142 + screen: { width: W, height: H }, 143 + wipe: (r, g, b, a = 255) => renderer.wipe(r, g, b, a), 144 + ink: (r, g, b, a = 255) => renderer.setColor(r, g, b, a), 145 + line: (x0, y0, x1, y1) => renderer.line(x0, y0, x1, y1), 146 + box: (x, y, w, h, mode = 'fill') => renderer.box(x, y, w, h, mode), 147 + circle: (cx, cy, r, mode = 'fill') => renderer.circle(cx, cy, r, mode), 148 + plot: (x, y) => renderer.plot(x, y), 149 + write: (text, x, y) => renderer.write(text, x, y), 150 + scroll: (dx, dy) => renderer.scroll(dx, dy), 151 + spin: (angle) => renderer.spin(angle), 152 + zoom: (factor) => renderer.zoom(factor), 153 + contrast: (factor) => renderer.contrast(factor), 154 + }; 155 + 156 + // Pass API to the interpreter 157 + kidlisp.setApi(api); 158 + 159 + // Parse piece 160 + let ast; 161 + try { 162 + ast = parse(SOURCE); 163 + } catch (e) { 164 + console.error('Parse error:', e); 165 + ast = []; 166 + } 167 + 168 + // Main animation loop 169 + let frameCount = 0; 170 + function animate() { 171 + // Clear frame (will be overwritten by wipe if piece calls it) 172 + renderer.wipe(0, 0, 0, 255); 173 + 174 + // Debug: log on first frame 175 + if (frameCount === 0) { 176 + console.log('🎬 Frame 0 - AST length:', ast.length); 177 + console.log('🎬 AST:', ast); 178 + console.log('🎬 Renderer methods:', Object.keys(renderer).filter(k => typeof renderer[k] === 'function')); 179 + } 180 + 181 + // Evaluate the piece - functions will call api methods directly 182 + for (let i = 0; i < ast.length; i++) { 183 + const expr = ast[i]; 184 + if (frameCount === 0) { 185 + console.log('Evaluating expr', i, ':', expr); 186 + } 187 + const result = kidlisp.evalExpr(expr, kidlisp.globalEnv, api, frameCount); 188 + if (frameCount === 0) { 189 + console.log('Result', i, ':', result); 190 + } 191 + } 192 + 193 + // Update display 194 + renderer.present(ctx); 195 + 196 + // Tick 197 + kidlisp.tick(); 198 + timing.tick(); 199 + frameCount++; 200 + 201 + requestAnimationFrame(animate); 202 + } 203 + 204 + 205 + // Start animation 206 + requestAnimationFrame(animate); 207 + </script> 208 + </body> 209 + </html>`; 210 + } 211 + 212 + export { bundleMini, fetchPieceSource };
+125
oven/kidlisp-mini/color.mjs
··· 1 + // KidLisp Mini Color Resolver 2 + // CSS color names, RGB, hex, alpha, rainbow 3 + // ~80 lines, produces ~1 KB minified 4 + 5 + const CSS_COLORS = { 6 + 'red': [255, 0, 0], 7 + 'green': [0, 128, 0], 8 + 'blue': [0, 0, 255], 9 + 'white': [255, 255, 255], 10 + 'black': [0, 0, 0], 11 + 'gray': [128, 128, 128], 12 + 'grey': [128, 128, 128], 13 + 'cyan': [0, 255, 255], 14 + 'magenta': [255, 0, 255], 15 + 'yellow': [255, 255, 0], 16 + 'orange': [255, 165, 0], 17 + 'purple': [128, 0, 128], 18 + 'pink': [255, 192, 203], 19 + 'lime': [0, 255, 0], 20 + 'brown': [165, 42, 42], 21 + 'navy': [0, 0, 128], 22 + 'teal': [0, 128, 128], 23 + 'olive': [128, 128, 0], 24 + 'maroon': [128, 0, 0], 25 + 'aqua': [0, 255, 255], 26 + 'silver': [192, 192, 192], 27 + 'gold': [255, 215, 0], 28 + 'indigo': [75, 0, 130], 29 + 'violet': [238, 130, 238], 30 + 'turquoise': [64, 224, 208], 31 + 'khaki': [240, 230, 140], 32 + 'salmon': [250, 128, 114], 33 + 'coral': [255, 127, 80], 34 + 'ivory': [255, 255, 240], 35 + 'snow': [255, 250, 250], 36 + }; 37 + 38 + function resolveColor(arg) { 39 + // Already an array [r, g, b, a] 40 + if (Array.isArray(arg)) { 41 + return arg; 42 + } 43 + 44 + // Number: grayscale 45 + if (typeof arg === 'number') { 46 + const v = Math.round(arg) & 0xFF; 47 + return [v, v, v, 255]; 48 + } 49 + 50 + // String 51 + if (typeof arg === 'string') { 52 + // CSS color name 53 + if (CSS_COLORS[arg.toLowerCase()]) { 54 + const [r, g, b] = CSS_COLORS[arg.toLowerCase()]; 55 + return [r, g, b, 255]; 56 + } 57 + 58 + // Hex color 59 + if (arg.startsWith('#')) { 60 + return parseHex(arg); 61 + } 62 + 63 + // Named special: transparent/none 64 + if (arg === '0' || arg === 'transparent' || arg === 'none') { 65 + return [0, 0, 0, 0]; 66 + } 67 + 68 + // Unknown string: return black 69 + return [0, 0, 0, 255]; 70 + } 71 + 72 + // Object: special types 73 + if (typeof arg === 'object' && arg.type === 'rainbow') { 74 + return rainbowColor(arg.frame || 0); 75 + } 76 + 77 + return [255, 255, 255, 255]; // default white 78 + } 79 + 80 + function parseHex(hex) { 81 + let h = hex.replace(/^#/, ''); 82 + 83 + // #RGB → #RRGGBB 84 + if (h.length === 3) { 85 + h = h.split('').map(c => c + c).join(''); 86 + } 87 + 88 + // #RRGGBB 89 + if (h.length === 6) { 90 + const num = parseInt(h, 16); 91 + return [ 92 + (num >> 16) & 0xFF, 93 + (num >> 8) & 0xFF, 94 + num & 0xFF, 95 + 255 96 + ]; 97 + } 98 + 99 + return [255, 255, 255, 255]; 100 + } 101 + 102 + function rainbowColor(frame = 0) { 103 + // Cycle through hue based on frame 104 + const hue = (frame / 60) % 1; // 60 frames = full cycle 105 + const [r, g, b] = hslToRgb(hue, 1, 0.5); 106 + return [Math.round(r), Math.round(g), Math.round(b), 255]; 107 + } 108 + 109 + function hslToRgb(h, s, l) { 110 + const c = (1 - Math.abs(2 * l - 1)) * s; 111 + const x = c * (1 - Math.abs((h * 6) % 2 - 1)); 112 + const m = l - c / 2; 113 + 114 + let r, g, b; 115 + if (h < 1 / 6) [r, g, b] = [c, x, 0]; 116 + else if (h < 2 / 6) [r, g, b] = [x, c, 0]; 117 + else if (h < 3 / 6) [r, g, b] = [0, c, x]; 118 + else if (h < 4 / 6) [r, g, b] = [0, x, c]; 119 + else if (h < 5 / 6) [r, g, b] = [x, 0, c]; 120 + else [r, g, b] = [c, 0, x]; 121 + 122 + return [(r + m) * 255, (g + m) * 255, (b + m) * 255]; 123 + } 124 + 125 + export { resolveColor, rainbowColor, hslToRgb, CSS_COLORS };
+460
oven/kidlisp-mini/eval.mjs
··· 1 + // KidLisp Mini Evaluator — RBP-26 Profile Only 2 + // Tree-walking interpreter targeting the minimal feature set needed for $roz 3 + // ~400 lines, produces ~8-10 KB minified 4 + 5 + // ─── Tokenizer ─────────────────────────────────────────────────────── 6 + 7 + function tokenize(source) { 8 + const tokens = []; 9 + let i = 0; 10 + 11 + while (i < source.length) { 12 + const ch = source[i]; 13 + 14 + // Whitespace 15 + if (/\s/.test(ch)) { 16 + i++; 17 + continue; 18 + } 19 + 20 + // Comment 21 + if (ch === ';') { 22 + while (i < source.length && source[i] !== '\n') i++; 23 + continue; 24 + } 25 + 26 + // String 27 + if (ch === '"') { 28 + i++; 29 + let str = ''; 30 + while (i < source.length && source[i] !== '"') { 31 + if (source[i] === '\\' && i + 1 < source.length) { 32 + i++; 33 + str += source[i]; 34 + } else { 35 + str += source[i]; 36 + } 37 + i++; 38 + } 39 + i++; // closing " 40 + tokens.push({ type: 'string', value: str }); 41 + continue; 42 + } 43 + 44 + // Parens 45 + if (ch === '(' || ch === ')') { 46 + tokens.push({ type: ch, value: ch }); 47 + i++; 48 + continue; 49 + } 50 + 51 + // Atom (may include /, :, -, .) 52 + const atomStart = i; 53 + while (i < source.length && !/[\s()"]/.test(source[i])) { 54 + i++; 55 + } 56 + const atom = source.slice(atomStart, i); 57 + 58 + // Check for special prefixes: fade:, Ns..., Ns 59 + if (atom.startsWith('fade:')) { 60 + tokens.push({ type: 'fade', value: atom.slice(5) }); // "red-blue-black" 61 + } else if (/^\d+s\.\.\.$/.test(atom)) { 62 + const ms = parseInt(atom) * 1000; 63 + tokens.push({ type: 'timing-repeating', value: ms }); // "2s..." → 2000ms 64 + } else if (/^\d+s$/.test(atom)) { 65 + const ms = parseInt(atom) * 1000; 66 + tokens.push({ type: 'timing-once', value: ms }); // "0.5s" → 500ms 67 + } else { 68 + tokens.push({ type: 'atom', value: atom }); 69 + } 70 + } 71 + 72 + return tokens; 73 + } 74 + 75 + // ─── Parser ────────────────────────────────────────────────────────── 76 + 77 + function parse(source) { 78 + const tokens = tokenize(source); 79 + let pos = 0; 80 + 81 + function peek() { 82 + return tokens[pos]; 83 + } 84 + 85 + function advance() { 86 + return tokens[pos++]; 87 + } 88 + 89 + function readExpr() { 90 + const t = peek(); 91 + if (!t) return null; 92 + 93 + if (t.type === '(') { 94 + advance(); // consume ( 95 + const list = []; 96 + while (peek() && peek().type !== ')') { 97 + const expr = readExpr(); 98 + if (expr !== null) list.push(expr); 99 + } 100 + if (peek() && peek().type === ')') { 101 + advance(); // consume ) 102 + } 103 + return list; 104 + } else if (t.type === 'string') { 105 + advance(); 106 + return { type: 'string', value: t.value }; 107 + } else if (t.type === 'fade') { 108 + advance(); 109 + return { type: 'fade', colors: t.value.split('-') }; 110 + } else if (t.type === 'timing-repeating') { 111 + advance(); 112 + return { type: 'timing-repeating', ms: t.value }; 113 + } else if (t.type === 'timing-once') { 114 + advance(); 115 + return { type: 'timing-once', ms: t.value }; 116 + } else { 117 + // Regular atom 118 + const token = advance(); 119 + return token.value; 120 + } 121 + } 122 + 123 + const exprs = []; 124 + while (pos < tokens.length) { 125 + const expr = readExpr(); 126 + if (expr !== null) exprs.push(expr); 127 + } 128 + 129 + return exprs; 130 + } 131 + 132 + // ─── Evaluator ────────────────────────────────────────────────────── 133 + 134 + class KidLispMini { 135 + constructor() { 136 + this.globalEnv = this.createEnv(); 137 + this.frameCount = 0; 138 + this.startTime = Date.now(); 139 + } 140 + 141 + setApi(api) { 142 + this.api = api; 143 + } 144 + 145 + createEnv() { 146 + const self = this; 147 + const colorMap = { 148 + 'red': [255, 0, 0, 255], 149 + 'blue': [0, 0, 255, 255], 150 + 'green': [0, 255, 0, 255], 151 + 'black': [0, 0, 0, 255], 152 + 'white': [255, 255, 255, 255], 153 + 'cyan': [0, 255, 255, 255], 154 + 'magenta': [255, 0, 255, 255], 155 + 'yellow': [255, 255, 0, 255], 156 + 'orange': [255, 165, 0, 255], 157 + 'purple': [128, 0, 128, 255], 158 + 'pink': [255, 192, 203, 255], 159 + 'lime': [0, 255, 0, 255], 160 + 'gray': [128, 128, 128, 255], 161 + }; 162 + 163 + const resolveColor = (arg) => { 164 + // Handle color array 165 + if (Array.isArray(arg) && arg.length >= 3) { 166 + return arg; 167 + } 168 + // Handle string color names 169 + if (typeof arg === 'string') { 170 + if (arg === 'rainbow') { 171 + const hue = (self.frameCount / 60) % 1; 172 + const [r, g, b] = self.hslToRgb(hue, 1, 0.5); 173 + return [Math.round(r), Math.round(g), Math.round(b), 255]; 174 + } 175 + if (colorMap[arg.toLowerCase()]) { 176 + return colorMap[arg.toLowerCase()]; 177 + } 178 + return [255, 255, 255, 255]; 179 + } 180 + // Handle numeric grayscale 181 + if (typeof arg === 'number') { 182 + const v = Math.max(0, Math.min(255, Math.round(arg))); 183 + return [v, v, v, 255]; 184 + } 185 + return [255, 255, 255, 255]; 186 + }; 187 + 188 + const env = { 189 + // Drawing - directly call api methods 190 + wipe: (...args) => { 191 + if (self.api && self.api.wipe) { 192 + self.api.wipe(...args); 193 + } 194 + }, 195 + ink: (...args) => { 196 + if (self.api && self.api.ink) { 197 + self.api.ink(...args); 198 + } 199 + }, 200 + line: (...args) => { 201 + if (self.api && self.api.line) { 202 + self.api.line(...args); 203 + } 204 + }, 205 + box: (...args) => { 206 + if (self.api && self.api.box) { 207 + self.api.box(...args); 208 + } 209 + }, 210 + circle: (...args) => { 211 + if (self.api && self.api.circle) { 212 + self.api.circle(...args); 213 + } 214 + }, 215 + plot: (...args) => { 216 + if (self.api && self.api.plot) { 217 + self.api.plot(...args); 218 + } 219 + }, 220 + write: (...args) => { 221 + if (self.api && self.api.write) { 222 + self.api.write(...args); 223 + } 224 + }, 225 + 226 + // Transforms - directly call api methods 227 + scroll: (...args) => { 228 + if (self.api && self.api.scroll) { 229 + self.api.scroll(...args); 230 + } 231 + }, 232 + spin: (...args) => { 233 + if (self.api && self.api.spin) { 234 + self.api.spin(...args); 235 + } 236 + }, 237 + zoom: (...args) => { 238 + if (self.api && self.api.zoom) { 239 + self.api.zoom(...args); 240 + } 241 + }, 242 + contrast: (...args) => { 243 + if (self.api && self.api.contrast) { 244 + self.api.contrast(...args); 245 + } 246 + }, 247 + 248 + // Math & Control 249 + '+': (...args) => args.reduce((a, b) => a + b, 0), 250 + '-': (...args) => args.length === 1 ? -args[0] : args.slice(1).reduce((a, b) => a - b, args[0]), 251 + '*': (...args) => args.reduce((a, b) => a * b, 1), 252 + '/': (...args) => args.length === 1 ? 1 / args[0] : args.slice(1).reduce((a, b) => a / b, args[0]), 253 + 'sin': (x) => Math.sin(x), 254 + 'cos': (x) => Math.cos(x), 255 + 'min': (...args) => Math.min(...args), 256 + 'max': (...args) => Math.max(...args), 257 + 'abs': (x) => Math.abs(x), 258 + 'sqrt': (x) => Math.sqrt(x), 259 + 'floor': (x) => Math.floor(x), 260 + 'ceil': (x) => Math.ceil(x), 261 + 'round': (x) => Math.round(x), 262 + 263 + // Randomness 264 + 'random': (min = 0, max = 1) => min + Math.random() * (max - min), 265 + '?': null, // handled specially 266 + 267 + // Conditionals 268 + 'if': null, // special form 269 + 'not': (x) => !x, 270 + '=': (a, b) => a === b, 271 + '<': (a, b) => a < b, 272 + '>': (a, b) => a > b, 273 + '<=': (a, b) => a <= b, 274 + '>=': (a, b) => a >= b, 275 + 276 + // Colors 277 + 'rainbow': () => 'rainbow', 278 + 'zebra': () => 'zebra', 279 + 'def': null, // special 280 + }; 281 + return env; 282 + } 283 + 284 + evaluate(ast, api, frame = 0) { 285 + const results = []; 286 + for (const expr of ast) { 287 + const result = this.evalExpr(expr, this.globalEnv, api, frame); 288 + if (result) results.push(result); 289 + } 290 + return results; 291 + } 292 + 293 + evalExpr(expr, env, api, frame = 0) { 294 + // Null/undefined 295 + if (expr === null || expr === undefined) return null; 296 + 297 + // String literal (atom) 298 + if (typeof expr === 'string') { 299 + // Dimension symbols 300 + if (expr === 'w') return api.screen.width; 301 + if (expr === 'h') return api.screen.height; 302 + if (expr === 'w/2') return api.screen.width / 2; 303 + if (expr === 'h/2') return api.screen.height / 2; 304 + if (expr === 'frame') return this.frameCount; 305 + 306 + // Color names 307 + const colorMap = { 308 + 'red': [255, 0, 0, 255], 309 + 'blue': [0, 0, 255, 255], 310 + 'green': [0, 255, 0, 255], 311 + 'black': [0, 0, 0, 255], 312 + 'white': [255, 255, 255, 255], 313 + 'cyan': [0, 255, 255, 255], 314 + 'magenta': [255, 0, 255, 255], 315 + 'yellow': [255, 255, 0, 255], 316 + 'orange': [255, 165, 0, 255], 317 + 'purple': [128, 0, 128, 255], 318 + 'pink': [255, 192, 203, 255], 319 + 'lime': [0, 255, 0, 255], 320 + 'gray': [128, 128, 128, 255], 321 + }; 322 + return colorMap[expr] || expr; 323 + } 324 + 325 + // Number 326 + if (typeof expr === 'number') return expr; 327 + 328 + // String object 329 + if (typeof expr === 'object' && expr.type === 'string') { 330 + return expr.value; 331 + } 332 + 333 + // Fade shorthand - directly apply fade background 334 + if (typeof expr === 'object' && expr.type === 'fade') { 335 + if (this.api && this.api.fadeBackground) { 336 + const colors = expr.colors.map(name => { 337 + const hslToRgb = this.hslToRgb.bind(this); 338 + if (name === 'red') return [255, 0, 0, 255]; 339 + if (name === 'blue') return [0, 0, 255, 255]; 340 + if (name === 'black') return [0, 0, 0, 255]; 341 + if (name === 'white') return [255, 255, 255, 255]; 342 + if (name === 'cyan') return [0, 255, 255, 255]; 343 + if (name === 'magenta') return [255, 0, 255, 255]; 344 + if (name === 'yellow') return [255, 255, 0, 255]; 345 + if (name === 'orange') return [255, 165, 0, 255]; 346 + return [0, 0, 0, 255]; 347 + }); 348 + this.api.fadeBackground(colors, frame); 349 + } 350 + return null; 351 + } 352 + 353 + // Rainbow special 354 + if (typeof expr === 'object' && expr.type === 'rainbow') { 355 + const hue = (expr.frame || 0) / 360 % 1; 356 + const [r, g, b] = this.hslToRgb(hue, 1, 0.5); 357 + return [Math.round(r), Math.round(g), Math.round(b), 255]; 358 + } 359 + 360 + // Timing forms — interpolate to progress value (0-1) 361 + if (typeof expr === 'object' && expr.type === 'timing-repeating') { 362 + const progress = (frame * 16) % expr.ms / expr.ms; 363 + return progress; 364 + } 365 + if (typeof expr === 'object' && expr.type === 'timing-once') { 366 + const progress = Math.min(1, (frame * 16) / expr.ms); 367 + return progress; 368 + } 369 + 370 + // List / function call 371 + if (Array.isArray(expr)) { 372 + if (expr.length === 0) return null; 373 + 374 + const [fn, ...args] = expr; 375 + 376 + // Check if this is a timing list FIRST (even before function call) 377 + // Timing interpolation: (Ns... start end) or (Ns start end) 378 + if (expr.length >= 3 && typeof expr[0] === 'object' && (expr[0].type === 'timing-repeating' || expr[0].type === 'timing-once')) { 379 + if (args.length >= 2) { 380 + const startVal = this.evalExpr(args[0], env, api, frame); 381 + const endVal = this.evalExpr(args[1], env, api, frame); 382 + 383 + if (fn.type === 'timing-repeating') { 384 + const progress = (frame * 16) % fn.ms / fn.ms; 385 + if (typeof startVal === 'number' && typeof endVal === 'number') { 386 + return startVal + (endVal - startVal) * progress; 387 + } 388 + return progress < 0.5 ? startVal : endVal; 389 + } else { 390 + const progress = Math.min(1, (frame * 16) / fn.ms); 391 + if (typeof startVal === 'number' && typeof endVal === 'number') { 392 + return startVal + (endVal - startVal) * progress; 393 + } 394 + return progress < 1 ? startVal : endVal; 395 + } 396 + } 397 + return null; 398 + } 399 + 400 + // Special forms 401 + if (fn === 'if') { 402 + const [cond, thenBr, elseBr] = args; 403 + const condVal = this.evalExpr(cond, env, api, frame); 404 + return condVal ? this.evalExpr(thenBr, env, api, frame) : this.evalExpr(elseBr, env, api, frame); 405 + } 406 + 407 + if (fn === 'def') { 408 + const [name, val] = args; 409 + env[name] = this.evalExpr(val, env, api, frame); 410 + return null; 411 + } 412 + 413 + if (fn === '?') { 414 + // Random choice 415 + const choices = args.map(a => this.evalExpr(a, env, api, frame)); 416 + return choices[Math.floor(Math.random() * choices.length)]; 417 + } 418 + 419 + // Regular function call 420 + const fnVal = env[fn]; 421 + if (!fnVal) return null; 422 + 423 + const evalArgs = args.map(arg => this.evalExpr(arg, env, api, frame)); 424 + 425 + if (typeof fnVal === 'function') { 426 + return fnVal(...evalArgs); 427 + } 428 + 429 + return fnVal; 430 + } 431 + 432 + return expr; 433 + } 434 + 435 + hslToRgb(h, s, l) { 436 + const c = (1 - Math.abs(2 * l - 1)) * s; 437 + const x = c * (1 - Math.abs((h * 6) % 2 - 1)); 438 + const m = l - c / 2; 439 + let r, g, b; 440 + 441 + if (h < 1 / 6) [r, g, b] = [c, x, 0]; 442 + else if (h < 2 / 6) [r, g, b] = [x, c, 0]; 443 + else if (h < 3 / 6) [r, g, b] = [0, c, x]; 444 + else if (h < 4 / 6) [r, g, b] = [0, x, c]; 445 + else if (h < 5 / 6) [r, g, b] = [x, 0, c]; 446 + else [r, g, b] = [c, 0, x]; 447 + 448 + return [ 449 + (r + m) * 255, 450 + (g + m) * 255, 451 + (b + m) * 255 452 + ]; 453 + } 454 + 455 + tick() { 456 + this.frameCount++; 457 + } 458 + } 459 + 460 + export { KidLispMini, parse };
+284
oven/kidlisp-mini/render.mjs
··· 1 + // KidLisp Mini Renderer — Software Pixel Buffer 2 + // Direct canvas manipulation with Bresenham line and midpoint circle 3 + // ~150 lines, produces ~3-4 KB minified 4 + 5 + class Renderer { 6 + constructor(width, height) { 7 + this.width = width; 8 + this.height = height; 9 + this.pixels = new Uint8ClampedArray(width * height * 4); 10 + this.imageData = new ImageData(this.pixels, width, height); 11 + 12 + // Current color state 13 + this.r = 255; 14 + this.g = 255; 15 + this.b = 255; 16 + this.a = 255; 17 + 18 + // Transform stacks 19 + this.transforms = []; // { type, value } 20 + this.fade = null; 21 + this.fadeFrame = 0; 22 + } 23 + 24 + setColor(r, g, b, a = 255) { 25 + this.r = Math.round(r); 26 + this.g = Math.round(g); 27 + this.b = Math.round(b); 28 + this.a = Math.round(a); 29 + } 30 + 31 + // Clear entire buffer with current color 32 + wipe(r, g, b, a = 255) { 33 + const [cr, cg, cb, ca] = Array.isArray(r) ? r : [r, g, b, a]; 34 + const rgba = [(cr & 255), (cg & 255), (cb & 255), (ca & 255)]; 35 + 36 + for (let i = 0; i < this.pixels.length; i += 4) { 37 + this.pixels[i] = rgba[0]; 38 + this.pixels[i + 1] = rgba[1]; 39 + this.pixels[i + 2] = rgba[2]; 40 + this.pixels[i + 3] = rgba[3]; 41 + } 42 + } 43 + 44 + // Bresenham line algorithm 45 + line(x0, y0, x1, y1) { 46 + x0 = Math.round(x0); 47 + y0 = Math.round(y0); 48 + x1 = Math.round(x1); 49 + y1 = Math.round(y1); 50 + 51 + const dx = Math.abs(x1 - x0); 52 + const dy = Math.abs(y1 - y0); 53 + const sx = x0 < x1 ? 1 : -1; 54 + const sy = y0 < y1 ? 1 : -1; 55 + let err = dx - dy; 56 + 57 + let x = x0, y = y0; 58 + 59 + while (true) { 60 + this.plot(x, y); 61 + if (x === x1 && y === y1) break; 62 + 63 + const e2 = 2 * err; 64 + if (e2 > -dy) { 65 + err -= dy; 66 + x += sx; 67 + } 68 + if (e2 < dx) { 69 + err += dx; 70 + y += sy; 71 + } 72 + } 73 + } 74 + 75 + // Midpoint circle algorithm 76 + circle(cx, cy, r, mode = 'fill') { 77 + cx = Math.round(cx); 78 + cy = Math.round(cy); 79 + r = Math.round(r); 80 + 81 + if (mode === 'fill') { 82 + // Filled circle 83 + for (let y = -r; y <= r; y++) { 84 + const yy = cy + y; 85 + if (yy < 0 || yy >= this.height) continue; 86 + 87 + const x = Math.sqrt(r * r - y * y); 88 + const x0 = Math.round(cx - x); 89 + const x1 = Math.round(cx + x); 90 + 91 + for (let xx = x0; xx <= x1; xx++) { 92 + if (xx >= 0 && xx < this.width) { 93 + this.plot(xx, yy); 94 + } 95 + } 96 + } 97 + } else { 98 + // Outlined circle using midpoint 99 + let x = 0; 100 + let y = r; 101 + let d = 3 - 2 * r; 102 + 103 + while (x <= y) { 104 + this.plot(cx + x, cy + y); 105 + this.plot(cx - x, cy + y); 106 + this.plot(cx + x, cy - y); 107 + this.plot(cx - x, cy - y); 108 + this.plot(cx + y, cy + x); 109 + this.plot(cx - y, cy + x); 110 + this.plot(cx + y, cy - x); 111 + this.plot(cx - y, cy - x); 112 + 113 + if (d < 0) { 114 + d = d + 4 * x + 6; 115 + } else { 116 + d = d + 4 * (x - y) + 10; 117 + y--; 118 + } 119 + x++; 120 + } 121 + } 122 + } 123 + 124 + // Box (filled or outlined) 125 + box(x, y, w, h, mode = 'fill') { 126 + x = Math.round(x); 127 + y = Math.round(y); 128 + w = Math.round(w); 129 + h = Math.round(h); 130 + 131 + if (mode === 'fill') { 132 + for (let yy = y; yy < y + h; yy++) { 133 + if (yy < 0 || yy >= this.height) continue; 134 + for (let xx = x; xx < x + w; xx++) { 135 + if (xx >= 0 && xx < this.width) { 136 + this.plot(xx, yy); 137 + } 138 + } 139 + } 140 + } else { 141 + // Outline: four sides 142 + this.line(x, y, x + w, y); // top 143 + this.line(x + w, y, x + w, y + h); // right 144 + this.line(x + w, y + h, x, y + h); // bottom 145 + this.line(x, y + h, x, y); // left 146 + } 147 + } 148 + 149 + // Single pixel 150 + plot(x, y) { 151 + x = Math.round(x); 152 + y = Math.round(y); 153 + 154 + if (x < 0 || x >= this.width || y < 0 || y >= this.height) return; 155 + 156 + const idx = (y * this.width + x) * 4; 157 + this.pixels[idx] = this.r; 158 + this.pixels[idx + 1] = this.g; 159 + this.pixels[idx + 2] = this.b; 160 + this.pixels[idx + 3] = this.a; 161 + } 162 + 163 + // Scroll (shift buffer in-place) 164 + scroll(dx, dy) { 165 + dx = Math.round(dx); 166 + dy = Math.round(dy); 167 + 168 + const copy = new Uint8ClampedArray(this.pixels); 169 + this.pixels.fill(0); 170 + 171 + for (let y = 0; y < this.height; y++) { 172 + for (let x = 0; x < this.width; x++) { 173 + const srcX = x - dx; 174 + const srcY = y - dy; 175 + 176 + if (srcX >= 0 && srcX < this.width && srcY >= 0 && srcY < this.height) { 177 + const srcIdx = (srcY * this.width + srcX) * 4; 178 + const dstIdx = (y * this.width + x) * 4; 179 + 180 + this.pixels[dstIdx] = copy[srcIdx]; 181 + this.pixels[dstIdx + 1] = copy[srcIdx + 1]; 182 + this.pixels[dstIdx + 2] = copy[srcIdx + 2]; 183 + this.pixels[dstIdx + 3] = copy[srcIdx + 3]; 184 + } 185 + } 186 + } 187 + } 188 + 189 + // Spin (rotate buffer around center) 190 + spin(angle) { 191 + const copy = new Uint8ClampedArray(this.pixels); 192 + this.pixels.fill(0); 193 + 194 + const cx = this.width / 2; 195 + const cy = this.height / 2; 196 + const cos = Math.cos(angle); 197 + const sin = Math.sin(angle); 198 + 199 + for (let y = 0; y < this.height; y++) { 200 + for (let x = 0; x < this.width; x++) { 201 + // Rotate backwards to find source pixel 202 + const dx = x - cx; 203 + const dy = y - cy; 204 + 205 + const srcX = cx + (dx * cos + dy * sin); 206 + const srcY = cy + (-dx * sin + dy * cos); 207 + 208 + const sx = Math.round(srcX); 209 + const sy = Math.round(srcY); 210 + 211 + if (sx >= 0 && sx < this.width && sy >= 0 && sy < this.height) { 212 + const srcIdx = (sy * this.width + sx) * 4; 213 + const dstIdx = (y * this.width + x) * 4; 214 + 215 + this.pixels[dstIdx] = copy[srcIdx]; 216 + this.pixels[dstIdx + 1] = copy[srcIdx + 1]; 217 + this.pixels[dstIdx + 2] = copy[srcIdx + 2]; 218 + this.pixels[dstIdx + 3] = copy[srcIdx + 3]; 219 + } 220 + } 221 + } 222 + } 223 + 224 + // Zoom (scale buffer) 225 + zoom(factor) { 226 + const copy = new Uint8ClampedArray(this.pixels); 227 + this.pixels.fill(0); 228 + 229 + const cx = this.width / 2; 230 + const cy = this.height / 2; 231 + 232 + for (let y = 0; y < this.height; y++) { 233 + for (let x = 0; x < this.width; x++) { 234 + const dx = (x - cx) / factor; 235 + const dy = (y - cy) / factor; 236 + 237 + const srcX = Math.round(cx + dx); 238 + const srcY = Math.round(cy + dy); 239 + 240 + if (srcX >= 0 && srcX < this.width && srcY >= 0 && srcY < this.height) { 241 + const srcIdx = (srcY * this.width + srcX) * 4; 242 + const dstIdx = (y * this.width + x) * 4; 243 + 244 + this.pixels[dstIdx] = copy[srcIdx]; 245 + this.pixels[dstIdx + 1] = copy[srcIdx + 1]; 246 + this.pixels[dstIdx + 2] = copy[srcIdx + 2]; 247 + this.pixels[dstIdx + 3] = copy[srcIdx + 3]; 248 + } 249 + } 250 + } 251 + } 252 + 253 + // Contrast 254 + contrast(factor) { 255 + const mid = 128; 256 + for (let i = 0; i < this.pixels.length; i += 4) { 257 + this.pixels[i] = Math.max(0, Math.min(255, mid + (this.pixels[i] - mid) * factor)); 258 + this.pixels[i + 1] = Math.max(0, Math.min(255, mid + (this.pixels[i + 1] - mid) * factor)); 259 + this.pixels[i + 2] = Math.max(0, Math.min(255, mid + (this.pixels[i + 2] - mid) * factor)); 260 + } 261 + } 262 + 263 + // Fade background (gradient cycling through colors) 264 + fadeBackground(colors, frame) { 265 + const idx = Math.floor(frame / 20) % colors.length; // cycle through colors 266 + const color = colors[idx]; 267 + 268 + if (Array.isArray(color)) { 269 + this.wipe(color[0], color[1], color[2], color[3]); 270 + } 271 + } 272 + 273 + // Present to canvas 274 + present(ctx) { 275 + this.imageData.data.set(this.pixels); 276 + ctx.putImageData(this.imageData, 0, 0); 277 + } 278 + } 279 + 280 + function makeRenderer(width, height) { 281 + return new Renderer(width, height); 282 + } 283 + 284 + export { Renderer, makeRenderer };
+66
oven/kidlisp-mini/timing.mjs
··· 1 + // KidLisp Mini Timing Engine 2 + // Handles Ns... (repeating) and Ns (one-shot) interpolation forms 3 + // ~80 lines, produces ~1-2 KB minified 4 + 5 + class TimingEngine { 6 + constructor(seed = null) { 7 + this.frame = 0; 8 + this.startTime = Date.now(); 9 + this.seed = seed; 10 + this.durationMs = {}; 11 + } 12 + 13 + tick() { 14 + this.frame++; 15 + } 16 + 17 + getElapsedMs() { 18 + return Date.now() - this.startTime; 19 + } 20 + 21 + // Evaluate a repeating timing form: (Ns... startVal endVal) 22 + // Returns interpolated value from 0-1 within the cycle 23 + evalRepeatingTiming(durationMs, startVal, endVal) { 24 + const elapsed = this.getElapsedMs(); 25 + const cycle = elapsed % durationMs; 26 + const progress = cycle / durationMs; 27 + 28 + if (typeof startVal === 'number' && typeof endVal === 'number') { 29 + return startVal + (endVal - startVal) * progress; 30 + } 31 + 32 + // If start/end are colors or other types, return start on first half, end on second 33 + return progress < 0.5 ? startVal : endVal; 34 + } 35 + 36 + // Evaluate a one-shot timing form: (Ns startVal endVal) 37 + // Returns interpolated value from 0-1, clamped at end 38 + evalOnceTiming(durationMs, startVal, endVal) { 39 + const elapsed = this.getElapsedMs(); 40 + const progress = Math.min(1, elapsed / durationMs); 41 + 42 + if (typeof startVal === 'number' && typeof endVal === 'number') { 43 + return startVal + (endVal - startVal) * progress; 44 + } 45 + 46 + return progress < 1 ? startVal : endVal; 47 + } 48 + 49 + // Random walk / wiggle: oscillate around a center value 50 + wiggle(center, amplitude, frequency = 1) { 51 + const phase = (this.frame * frequency / 60) % (Math.PI * 2); 52 + return center + Math.sin(phase) * amplitude; 53 + } 54 + 55 + // Deterministic random based on seed + frame 56 + seededRandom(seed = this.seed, max = 1) { 57 + if (seed === null) return Math.random() * max; 58 + 59 + // Simple seeded PRNG 60 + const x = Math.sin(seed + this.frame) * 10000; 61 + const random = x - Math.floor(x); 62 + return random * max; 63 + } 64 + } 65 + 66 + export { TimingEngine };
+86
oven/kidlisp-mini/transforms.mjs
··· 1 + // KidLisp Mini Transforms 2 + // Fade gradient background, timing forms 3 + // ~100 lines, produces ~2-3 KB minified 4 + 5 + import { resolveColor, rainbowColor, hslToRgb } from './color.mjs'; 6 + 7 + class TransformState { 8 + constructor(width, height) { 9 + this.width = width; 10 + this.height = height; 11 + this.frame = 0; 12 + this.fadeColors = null; 13 + this.pendingTransforms = []; 14 + } 15 + 16 + tick() { 17 + this.frame++; 18 + } 19 + 20 + setFade(colors) { 21 + // Parse color names: "red-blue-black-blue-red" → resolve to RGB 22 + this.fadeColors = colors.map(name => resolveColor(name)); 23 + } 24 + 25 + applyFade(renderer) { 26 + if (!this.fadeColors) return; 27 + 28 + // Cycle through fade colors based on frame 29 + const idx = Math.floor(this.frame / 20) % this.fadeColors.length; 30 + const color = this.fadeColors[idx]; 31 + renderer.wipe(color[0], color[1], color[2], color[3]); 32 + } 33 + 34 + evaluateTimingExpr(expr, startVal, endVal) { 35 + if (typeof expr === 'object' && expr.type === 'timing-repeating') { 36 + const ms = expr.ms; 37 + const elapsed = (this.frame * 16) % ms; // assume ~16ms per frame 38 + const progress = elapsed / ms; 39 + return startVal + (endVal - startVal) * progress; 40 + } 41 + 42 + if (typeof expr === 'object' && expr.type === 'timing-once') { 43 + const ms = expr.ms; 44 + const elapsed = this.frame * 16; 45 + const progress = Math.min(1, elapsed / ms); 46 + return startVal + (endVal - startVal) * progress; 47 + } 48 + 49 + // Not a timing expression, return as-is 50 + return expr; 51 + } 52 + 53 + interpolate(value, startVal, endVal) { 54 + // If value is between 0 and 1, it's a progress value 55 + if (typeof value === 'number' && value >= 0 && value <= 1) { 56 + return startVal + (endVal - startVal) * value; 57 + } 58 + return value; 59 + } 60 + } 61 + 62 + // Helper: parse and apply an interpolated timing form 63 + // e.g., (1s... 24 64) → current alpha value interpolated between 24-64 64 + function evalTimingForm(timingExpr, frame, ms, repeating) { 65 + if (!timingExpr) return 0; 66 + 67 + let elapsed; 68 + if (repeating) { 69 + elapsed = (frame * 16) % ms; // repeat 70 + } else { 71 + elapsed = frame * 16; // one-shot 72 + elapsed = Math.min(elapsed, ms); // clamp 73 + } 74 + 75 + const progress = elapsed / ms; 76 + return progress; 77 + } 78 + 79 + function applyInterpolation(progress, startVal, endVal) { 80 + if (typeof startVal !== 'number' || typeof endVal !== 'number') { 81 + return startVal; 82 + } 83 + return startVal + (endVal - startVal) * progress; 84 + } 85 + 86 + export { TransformState, evalTimingForm, applyInterpolation };
+45
oven/server.mjs
··· 15 15 import archiver from 'archiver'; 16 16 import sharp from 'sharp'; 17 17 import { createBundle, createJSPieceBundle, createM4DBundle, generateDeviceHTML, prewarmCache, getCacheStatus, setSkipMinification } from './bundler.mjs'; 18 + import { bundleMini, fetchPieceSource } from './kidlisp-mini/bundle.mjs'; 18 19 import { streamOSImage, getOSBuildStatus, invalidateManifest, purgeOSBuildCache, clearOSBuildLocalCache } from './os-builder.mjs'; 19 20 import { startOSBaseBuild, getOSBaseBuild, getOSBaseBuildsSummary, cancelOSBaseBuild } from './os-base-build.mjs'; 20 21 import { startNativeBuild, getNativeBuild, getNativeBuildsSummary, cancelNativeBuild, onNativeBuildProgress } from './native-builder.mjs'; ··· 2986 2987 } catch (error) { 2987 2988 console.error('Bundle failed:', error); 2988 2989 return res.status(500).json({ error: error.message }); 2990 + } 2991 + }); 2992 + 2993 + // KidLisp Mini Bundler (RBP-26 profile, ~20 KB target) 2994 + app.get('/bundle-mini', async (req, res) => { 2995 + try { 2996 + const code = req.query.code; 2997 + const piece = req.query.piece; 2998 + const seed = req.query.seed ? parseInt(req.query.seed) : null; 2999 + const download = req.query.dl === '1' || req.query.dl === 'true'; 3000 + 3001 + let source = code; 3002 + 3003 + // If piece name is provided, fetch from MongoDB 3004 + if (piece && !code) { 3005 + try { 3006 + source = await fetchPieceSource(piece); 3007 + } catch (error) { 3008 + return res.status(404).json({ error: error.message }); 3009 + } 3010 + } 3011 + 3012 + if (!source) { 3013 + return res.status(400).json({ error: "Missing 'code' or 'piece' parameter", usage: "/bundle-mini?code=<source> or /bundle-mini?piece=<name>" }); 3014 + } 3015 + 3016 + const html = await bundleMini(source, seed); 3017 + const sizeKB = Math.ceil(Buffer.byteLength(html, 'utf8') / 1024); 3018 + 3019 + const headers = { 3020 + 'Content-Type': 'text/html; charset=utf-8', 3021 + 'Cache-Control': 'public, max-age=3600', 3022 + 'X-Bundle-Size-KB': sizeKB.toString() 3023 + }; 3024 + 3025 + if (download) { 3026 + const pieceName = piece ? piece.replace(/^\$/, '') : 'bundle'; 3027 + headers['Content-Disposition'] = `attachment; filename="${pieceName}-mini.html"`; 3028 + } 3029 + 3030 + res.set(headers).send(html); 3031 + } catch (error) { 3032 + console.error('[bundle-mini] Error:', error); 3033 + res.status(500).json({ error: error.message }); 2989 3034 } 2990 3035 }); 2991 3036
+660
reports/bootloader-art-integration-feasibility.md
··· 1 + # Bootloader.art Integration Feasibility Report 2 + 3 + **Date**: 2026-04-14 4 + **Status**: Research & Feasibility Analysis 5 + **Author**: Claude Code 6 + 7 + --- 8 + 9 + ## Executive Summary 10 + 11 + **bootloader.art** is an open experimental on-chain generative art platform that enables artists to create, mint, and collect unique algorithmic art pieces directly on the blockchain. It challenges traditional NFT immutability by supporting **living, updatable code** post-mint—meaning artists can evolve their pieces after they're tokenized. 12 + 13 + ### Key Finding 14 + Integrating AC's KidLisp bundler with bootloader.art is **technically feasible** but **constrained by on-chain storage limits**. The integration would work best for: 15 + - **KidLisp pieces** (smaller, more optimizable) 16 + - **Generative art focus** (procedural over interactive) 17 + - **Optimized bundles** (<24KB on-chain, reference off-chain assets) 18 + 19 + --- 20 + 21 + ## What is Bootloader.art? 22 + 23 + ### Platform Overview 24 + - **Purpose**: On-chain generative art platform with mutable code post-mint 25 + - **Code Storage**: Directly on blockchain (with size constraints) 26 + - **Update Mechanism**: Artists can modify code after mint; collectors' pieces evolve 27 + - **Ecosystem**: Part of broader platforms like fxhash, Highlight XYZ, Art Blocks 28 + 29 + ### Key Innovation: Living Code 30 + Unlike traditional NFTs where metadata is immutable, bootloader.art supports: 31 + - **Post-mint code updates** by artist 32 + - **Evolutionary art**: pieces change as the underlying algorithm evolves 33 + - **Collector engagement**: tokens become more valuable as code improves 34 + 35 + ### Technical Architecture 36 + - Uses **HTML + JavaScript** as core format 37 + - Code rendered client-side in browser 38 + - Seed-based generative output (each NFT gets unique seed) 39 + - On-chain storage of generative algorithm 40 + 41 + --- 42 + 43 + ## Our Current Bundling Infrastructure 44 + 45 + ### Existing Bundlers 46 + 47 + #### 1. **Oven Bundler** (`oven/bundler.mjs`) 48 + **Purpose**: Generate self-contained HTML bundles for AC pieces 49 + **Current Capabilities**: 50 + - Bundles entire AC runtime into single HTML file 51 + - Supports both KidLisp pieces (`$code`) and JavaScript pieces 52 + - In-memory caching per git commit 53 + - Brotli compression for WASM decoder 54 + - Comic Relief font inlining 55 + - Minification support 56 + 57 + **Output**: Typically 2-8 MB self-contained HTML (uncompressed) 58 + 59 + **Key Features**: 60 + ``` 61 + - Core essentials: boot.mjs, bios.mjs, disk.mjs, kidlisp.mjs 62 + - Graphics (2D/3D): graph, geo, 2d, gl-matrix, glaze 63 + - Input handling: keyboard, gamepad, motion, touch 64 + - Audio: sound libraries (speaker, synth, bubble) 65 + - UI components: buttons, text input, ui.mjs 66 + - Storage: store.mjs for persistence 67 + ``` 68 + 69 + #### 2. **KidLisp Bundler** (`system/backend/kidlisp-bundler.mjs`) 70 + **Purpose**: Generate minimal KidLisp-specific bundles 71 + **Optimizations**: 72 + - Explicit skip list (wasmboy, UDP, world system, optimizer) 73 + - Ultra-minimal file set for basic KidLisp visuals 74 + - SWC minification 75 + - Import rewriting for size optimization 76 + 77 + **Key Skip Files**: 78 + - `dep/wasmboy/` — 386 KB source (GameBoy emulator) 79 + - `lib/udp.mjs` — networking (dynamically imported) 80 + - `systems/world.mjs` — 3D features 81 + 82 + **Estimated Output**: ~200-400 KB minified (before gzip) 83 + 84 + --- 85 + 86 + ## On-Chain Storage Constraints 87 + 88 + ### Blockchain Size Limits 89 + 90 + | Constraint | Value | Notes | 91 + |-----------|-------|-------| 92 + | Ethereum storage per txn | ~128 KB | Hard limit for single transaction | 93 + | Typical contract calldata | 24 KB | Realistic limit for deployed code | 94 + | Arweave bundling | 100 KB+ | More generous, but off-chain | 95 + | Solana program size | 200 KB | Program accounts must fit in this | 96 + | Zora ERC721A calldata | 4 KB+ | Per metadata/code segment | 97 + 98 + ### Gas Cost Impact 99 + - **String manipulation**: Major gas consumer in on-chain code 100 + - **minification essential**: Every byte counts 101 + - **Gzip/Brotli**: Not applicible on-chain (code must be decompressed off-chain) 102 + 103 + ### Reference: Autoglyphs Precedent 104 + [Autoglyphs](https://www.larvalabs.com/autoglyphs) (early on-chain generative art): 105 + - Algorithm fit in **single Ethereum transaction** (~13.27% of network capacity per mint) 106 + - Bare-essentials algorithm only 107 + - NO UI, NO audio, NO graphics libraries 108 + - Pure math-based SVG generation 109 + 110 + --- 111 + 112 + ## AC Bundle Size Analysis 113 + 114 + ### Current Oven Output 115 + ``` 116 + Full AC Runtime (uncompressed): ~7-8 MB 117 + ├── boot.mjs ~15 KB 118 + ├── bios.mjs ~80 KB 119 + ├── lib/disk.mjs ~572 KB (largest) 120 + ├── lib/kidlisp.mjs ~90 KB 121 + ├── Graphics libraries ~250 KB 122 + ├── UI/Input systems ~180 KB 123 + ├── Audio systems ~200 KB 124 + └── Dependencies (gl-matrix, etc) ~100 KB 125 + ``` 126 + 127 + ### KidLisp Bundler (Optimized) 128 + ``` 129 + Minimal KidLisp Bundle: ~200-400 KB (minified) 130 + ├── Core runtime (boot+bios) ~80 KB 131 + ├── KidLisp interpreter ~65 KB 132 + ├── 2D graphics (no 3D) ~45 KB 133 + ├── Essential UI ~30 KB 134 + └── Math & helpers ~40 KB 135 + ``` 136 + 137 + ### Ultra-Minimal Bootloader Bundle Target 138 + ``` 139 + On-Chain Optimized Bundle: ~20-24 KB 140 + ├── Minimal AC boot strap ~5 KB 141 + ├── KidLisp evaluator ~12 KB (pre-minified) 142 + ├── Graphics primitives ~4 KB (line, circle, rect only) 143 + ├── Color model ~1 KB 144 + └── DOM rendering ~2 KB 145 + ``` 146 + 147 + --- 148 + 149 + ## Feasibility Assessment 150 + 151 + ### ✅ What Works Well 152 + 153 + 1. **KidLisp as Primary Target** 154 + - Interpreted language, smaller interpreter (~12 KB minified) 155 + - No build step required (unlike JavaScript) 156 + - Math-centric, good for generative art 157 + - Already optimized in `kidlisp-bundler.mjs` 158 + 159 + 2. **Seed-Based Rendering** 160 + - AC's pieces use params/colon input system 161 + - Can expose seed-based variation similar to generative platforms 162 + - KidLisp pieces already deterministic 163 + 164 + 3. **Existing HTML Bundling** 165 + - `oven/bundler.mjs` proven in production 166 + - Modular architecture (can strip down essentials) 167 + - Font embedding, minification already in place 168 + 169 + 4. **Graphics Stack** 170 + - 2D primitives (line, circle, box) can fit in 4-5 KB 171 + - No dependency on WebGL for basic art 172 + - Canvas API is lightweight 173 + 174 + ### ⚠️ Significant Challenges 175 + 176 + 1. **Size Constraints** 177 + - Full AC runtime: **7-8 MB** (bloated) 178 + - Realistic on-chain limit: **20-24 KB** 179 + - Gap: **97.3% reduction needed** 180 + - KidLisp-only path could get to ~200 KB (**30x too large**) 181 + 182 + 2. **Disk.mjs Dependencies** 183 + - `lib/disk.mjs` is **572 KB** (largest library) 184 + - Contains UI, audio, networking, 3D graphics 185 + - Would need complete rewrite of core API surface 186 + - Risk: AC pieces depend on disk.mjs structure 187 + 188 + 3. **Browser Context Limitations** 189 + - Bootloader.art renders in browser (not AC's WebSocket+module-loader model) 190 + - No hot reloading infrastructure 191 + - Pieces run in isolation, not linked to AC social system 192 + - Would lose: networking, chat, multiplayer features 193 + 194 + 4. **Asset Handling** 195 + - Fonts (ComicRelief): **20+ KB** (can't fit on-chain) 196 + - Audio files: **not practical** on-chain 197 + - Images: **not practical** on-chain 198 + - Would need off-chain asset references (IPFS/Arweave) 199 + 200 + 5. **API Incompatibility** 201 + - AC pieces expect: `{ wipe, ink, line, circle, ui, sound, net, event, ... }` 202 + - Bootloader expects: deterministic, seed-based `render(seed) → SVG/canvas` 203 + - Significant refactoring needed per piece 204 + 205 + ### ❌ What Doesn't Work 206 + 207 + 1. **Full AC Runtime On-Chain** 208 + - Simply impossible due to size 209 + - Would require 300+ separate contracts or sharding 210 + 211 + 2. **Audio/Networking Features** 212 + - Sound synthesis: 50+ KB of synthesizers 213 + - Networking (socket.io): 30+ KB 214 + - These are fundamentally off-chain 215 + 216 + 3. **Interactive Game Pieces** 217 + - Pieces like `squash.mjs`, `1v1.mjs` require multiplayer networking 218 + - Bootloader is single-user generative only 219 + - Would lose entire game category 220 + 221 + --- 222 + 223 + ## Integration Approaches 224 + 225 + ### Approach 1: KidLisp-Only Ultra-Minimal (RECOMMENDED) 226 + 227 + **Scope**: Support only KidLisp pieces, no JavaScript 228 + **Target Size**: 20-24 KB on-chain 229 + 230 + **Components**: 231 + ``` 232 + bootstrap.mjs (on-chain) ~2 KB 233 + ├── WASM loader shim 234 + ├── Initialize canvas/DOM 235 + └── Invoke KidLisp evaluator 236 + 237 + kidlisp-mini.mjs (on-chain) ~12 KB 238 + ├── Minimal evaluator 239 + ├── Core functions (draw, color, math) 240 + └── Skip: sound, networking, 3D 241 + 242 + graphics-tiny.mjs (on-chain) ~4 KB 243 + ├── line(x1, y1, x2, y2) 244 + ├── circle(x, y, r) 245 + ├── rect(x, y, w, h) 246 + ├── fill(r, g, b, a) 247 + └── Canvas wrapper 248 + 249 + piece-data.js (on-chain) ~4 KB 250 + └── Actual KidLisp code (usually 1-3 KB) 251 + 252 + Total on-chain: 22 KB 253 + 254 + Off-chain (referenced): 255 + - Fonts (IPFS/Arweave) 256 + - Optional: larger KidLisp libraries via dynamic import 257 + ``` 258 + 259 + **Workflow**: 260 + ``` 261 + 1. Artist writes KidLisp piece on AC (or similar) 262 + 2. Extract & minify core KidLisp code 263 + 3. Create bootstrap + graphics shim 264 + 4. Deploy to bootloader.art with on-chain + IPFS refs 265 + 5. Piece rendered: seed → KidLisp eval → canvas → display 266 + ``` 267 + 268 + **Pieces That Would Work**: 269 + - `$pie` (generative visuals) 270 + - `$cw` (color work) 271 + - `$cow` (recursion/fractals) 272 + - `$trees` (procedural generation) 273 + - Any math-based generative KidLisp 274 + 275 + **Pieces That Would NOT Work**: 276 + - `squash`, `1v1` (multiplayer) 277 + - Any piece using `sound` library 278 + - Pieces using `net`, `chat`, `socket` 279 + - Pieces using 3D (world.mjs) 280 + 281 + **Implementation Effort**: Medium (3-5 days) 282 + - Fork `kidlisp-bundler.mjs` to bootloader target 283 + - Create minimal graphics shim 284 + - Implement `seed → params` conversion 285 + - Test with 5-10 existing KidLisp pieces 286 + 287 + --- 288 + 289 + ### Approach 2: Off-Chain Code + On-Chain Metadata 290 + 291 + **Scope**: Store full bundle on IPFS/Arweave, reference on-chain 292 + **Target Size**: 200+ KB off-chain (no on-chain size limit) 293 + 294 + **Components**: 295 + ``` 296 + On-chain NFT metadata: 297 + { 298 + "name": "piece-name", 299 + "image": "ipfs://...", // preview image 300 + "external_url": "ipfs://...", // full bundle URL 301 + "attributes": [ 302 + { "trait_type": "size", "value": "205 KB" }, 303 + { "trait_type": "seed_range", "value": "1-1000000" } 304 + ] 305 + } 306 + 307 + Off-chain (IPFS): 308 + - Full AC runtime bundle (7-8 MB) 309 + - OR KidLisp + minimal graphics (400 KB) 310 + - Self-contained, no external assets 311 + ``` 312 + 313 + **Workflow**: 314 + ``` 315 + 1. Generate HTML bundle with oven/bundler.mjs 316 + 2. Upload to IPFS (via Pinata, etc.) 317 + 3. Create minimal NFT metadata pointing to IPFS 318 + 4. Deploy to bootloader.art 319 + 5. User clicks → loads IPFS → renders AC piece 320 + ``` 321 + 322 + **Advantages**: 323 + - No size constraints 324 + - Can use full AC runtime + pieces 325 + - Works for JavaScript pieces too 326 + - Hot updates possible (re-upload to IPFS) 327 + 328 + **Disadvantages**: 329 + - Code not truly "on-chain" (defeats bootloader.art philosophy) 330 + - Depends on IPFS availability 331 + - Centralization concern (if IPFS node goes down) 332 + - Not authentic to "generative on-chain" movement 333 + 334 + **Implementation Effort**: Low (1-2 days) 335 + - Integrate IPFS upload into oven 336 + - Create metadata templates 337 + - Document process 338 + 339 + --- 340 + 341 + ### Approach 3: Hybrid Hybrid: Small On-Chain + Large Off-Chain 342 + 343 + **Scope**: Seed + metadata on-chain, full code on IPFS 344 + **Target Size**: 2-3 KB on-chain, 400 KB off-chain 345 + 346 + **Components**: 347 + ``` 348 + On-chain contract: 349 + { 350 + seed: 12345, 351 + ipfs_hash: "QmXxxx...", 352 + artist: "0x...", 353 + title: "piece-name" 354 + } 355 + 356 + Off-chain (IPFS): 357 + { 358 + bundle: "full-ac-runtime.html", // 7-8 MB 359 + code: "$code", // KidLisp source 360 + entry: "piece-name" // which disk to load 361 + } 362 + ``` 363 + 364 + **Workflow**: 365 + ``` 366 + 1. Deploy minimal metadata on-chain 367 + 2. Full code lives on IPFS 368 + 3. Bootloader renders: load IPFS → seed → render 369 + 4. Updates: artist re-uploads new IPFS, points contract to new hash 370 + ``` 371 + 372 + **Advantages**: 373 + - Some on-chain component (immutable seed) 374 + - Updates possible (mutable code) 375 + - Full feature set available 376 + - Scalable approach 377 + 378 + **Disadvantages**: 379 + - Still not "pure" on-chain 380 + - Requires contract interaction 381 + - More complex architecture 382 + 383 + **Implementation Effort**: Medium (4-6 days) 384 + 385 + --- 386 + 387 + ## API Constraints & Integration Points 388 + 389 + ### Bootloader.art Expected Interface 390 + 391 + Based on generative art platform conventions, bootloader.art likely expects: 392 + 393 + ```javascript 394 + // Input: window.seed (unique per token) 395 + const seed = window.seed || 0; 396 + 397 + // Output: canvas/SVG/HTML in DOM 398 + // Deterministic: seed → same visual output every time 399 + 400 + // Optional: window.tokenId, window.wallet for metadata 401 + const tokenId = window.tokenId; 402 + const wallet = window.wallet; 403 + 404 + // Code is stored as HTML blob containing <script> 405 + // No external requests expected (self-contained) 406 + ``` 407 + 408 + ### AC Piece Adaptation Required 409 + 410 + Current AC piece: 411 + ```javascript 412 + function boot({ wipe, ink, line, circle, screen, params }) { 413 + // AC-specific initialization 414 + } 415 + 416 + function paint({ wipe, ink, line, circle }) { 417 + // Render per frame 418 + } 419 + 420 + export { boot, paint, act, sim }; 421 + ``` 422 + 423 + For bootloader.art: 424 + ```javascript 425 + // No "boot/paint/act/sim" lifecycle 426 + // No parameterization beyond seed 427 + // Single "render" function 428 + 429 + function render(seed) { 430 + const rng = seedRandom(seed); 431 + // Use rng for all randomness 432 + 433 + // Draw to canvas 434 + // Return visual output 435 + } 436 + 437 + // Export as HTML with render() baked in 438 + ``` 439 + 440 + ### Missing Features in Bootloader Context 441 + 442 + | Feature | AC | Bootloader | Notes | 443 + |---------|----|-----------|----| 444 + | Networking | ✅ | ❌ | No socket.io, net API | 445 + | Audio | ✅ | ❌ | Can't synthesize on-chain | 446 + | Multiplayer | ✅ | ❌ | Single-user only | 447 + | Persistence | ✅ | ❌ | No store.mjs | 448 + | Hot reload | ✅ | ❌ | Static on-chain code | 449 + | Piece discovery | ✅ | ❌ | Isolated on bootloader | 450 + | Params/colon | ✅ | ❌ | Only seed-based | 451 + | Analytics | ✅ | ❌ | No logging to AC backend | 452 + 453 + --- 454 + 455 + ## Generative Tokens: AC Angle 456 + 457 + ### What Are Generative Tokens? 458 + 459 + Generative tokens are **NFTs where the artwork is generated algorithmically**, with each token receiving a unique seed that produces a unique visual output. Platforms like fxhash, Art Blocks, and bootloader.art enable this. 460 + 461 + ### AC's Unique Position 462 + 463 + **Strengths for Generative Tokens**: 464 + 1. **KidLisp**: Purpose-built for generative art (declarative, functional) 465 + 2. **Existing Ecosystem**: 500+ pieces already created 466 + 3. **Community**: Active artist community 467 + 4. **Interactivity**: Can create interactive generative pieces (if modified) 468 + 469 + **Weaknesses**: 470 + 1. **Runtime Size**: 7-8 MB baseline (vs. 20-24 KB industry standard) 471 + 2. **Not Designed For**: Single-shot rendering (expects interactive loop) 472 + 3. **Off-Chain Asset Dependency**: Fonts, samples, etc. 473 + 4. **Community Expectation**: AC pieces are interactive/playable, not static 474 + 475 + ### Generative Token Use Cases for AC 476 + 477 + **Viable**: 478 + 1. **KidLisp Art Series**: Pure math-based visuals (fractals, patterns, algorithms) 479 + - Example: `$cow` (recursion) → mint as generative token series 480 + - Example: `$cw` (color work) → seed-based palettes 481 + 482 + 2. **Artist Collaborations**: Art Blocks-style drops 483 + - Mint 100 unique renderings of curated KidLisp piece 484 + - Each with unique seed 485 + - Artist retains code update rights via bootloader.art 486 + 487 + 3. **Cross-Platform Bridges**: 488 + - Mint on bootloader.art 489 + - Code stays on AC (via hot-reload) 490 + - Collectors view on bootloader, play on AC 491 + 492 + **Not Viable**: 493 + 1. **Game Pieces**: squash, 1v1, etc. (require multiplayer) 494 + 2. **Interactive Sound Art**: pieces with `sound` library 495 + 3. **Real-Time Collaboration**: pieces using `net`, `chat` 496 + 497 + --- 498 + 499 + ## Recommendations 500 + 501 + ### Short Term (Feasibility Phase) 502 + 503 + 1. **Create KidLisp Bootloader Shim** (1-2 days) 504 + ```bash 505 + npm run create:bootloader -- --template=kidlisp-mini 506 + ``` 507 + - Fork kidlisp-bundler.mjs 508 + - Target 20-24 KB on-chain bundle 509 + - Test with 5 existing KidLisp pieces 510 + - Document seed → params mapping 511 + 512 + 2. **Evaluate 3 Candidate Pieces** (1 day) 513 + - `$pie` — static generative visual 514 + - `$cw` — color palette generation 515 + - `$cow` — fractal/recursive patterns 516 + - Measure minified bundle size 517 + - Verify deterministic seed output 518 + 519 + 3. **Document bootloader.art Integration** (1 day) 520 + - API compatibility matrix 521 + - Size reduction techniques 522 + - Example adaptation (KidLisp piece → bootloader.art) 523 + 524 + ### Medium Term (Proof of Concept) 525 + 526 + 1. **Deploy 1-3 Pieces to Bootloader** 527 + - Partner with bootloader.art 528 + - Mint small test series (10-20 tokens) 529 + - Gather community feedback 530 + 531 + 2. **Implement Seed-Based Parameterization** 532 + - Map seed → existing `params` system 533 + - Ensure deterministic output (no timestamps, etc.) 534 + - Test across multiple browsers/platforms 535 + 536 + 3. **Create IPFS Bridge** (optional) 537 + - Upload AC bundles to Pinata 538 + - Create metadata templates 539 + - Document off-chain approach 540 + 541 + ### Long Term (Ecosystem Integration) 542 + 543 + 1. **Off-Chain Code, On-Chain Metadata** 544 + - Hybrid approach with IPFS references 545 + - Allows full AC runtime + pieces 546 + - Artist + collector-friendly metadata 547 + 548 + 2. **AC-Native Generative Token Platform** 549 + - Build bootloader.art-like experience within AC 550 + - Own token minting flow 551 + - Seed-based piece rendering 552 + - Contract integration (ERC-721) 553 + 554 + 3. **Live Code Updates via bootloader.art** 555 + - Leverage bootloader's updatable-code feature 556 + - Artists iterate on pieces post-mint 557 + - Collectors' NFTs evolve over time 558 + - Aligned with AC's dynamic philosophy 559 + 560 + --- 561 + 562 + ## Risk Assessment 563 + 564 + | Risk | Probability | Impact | Mitigation | 565 + |------|------------|--------|-----------| 566 + | Size constraints prove insurmountable | Medium | High | Fall back to IPFS approach, accept off-chain code | 567 + | bootloader.art API incompatibility | Low | Medium | Early POC with their team, API documentation review | 568 + | KidLisp → seed-determinism issues | Low | Medium | Extensive testing, fix any RNG non-determinism | 569 + | Community disinterest (not seen as "real" AC) | Medium | Medium | Frame as expansion, not replacement; emphasize artist control | 570 + | IPFS availability/centralization concerns | Low | High | Partner with Pinata, Arweave backup, own IPFS node | 571 + | Gas costs for on-chain updates | Low | Low | Use L2s (Optimism, Arbitrum), or fully off-chain approach | 572 + 573 + --- 574 + 575 + ## Cost Estimation 576 + 577 + | Task | Effort | Cost (Internal) | Notes | 578 + |------|--------|-----------------|-------| 579 + | KidLisp bootloader shim | 1-2d | Low | Reuse kidlisp-bundler.mjs | 580 + | Integration testing | 1-2d | Low | 5-10 pieces | 581 + | Documentation | 1d | Low | API mappings, examples | 582 + | POC deployment | 2-3d | Low | bootloader.art partnership | 583 + | IPFS bridge (optional) | 2d | Medium | Pinata API integration | 584 + | **Total (Bootloader Path)** | **7-10d** | **~$2-3k** | Low cost, medium effort | 585 + | Full AC + IPFS approach | 3-5d | Medium | Simpler, less optimization | 586 + 587 + --- 588 + 589 + ## Proof of Concept Roadmap 590 + 591 + ### Week 1: Research & Planning 592 + - [x] Research bootloader.art capabilities 593 + - [ ] Contact bootloader.art team for API details 594 + - [ ] Identify 3 candidate KidLisp pieces 595 + - [ ] Create detailed size budget 596 + 597 + ### Week 2: Implementation 598 + - [ ] Create KidLisp bootloader bundle target 599 + - [ ] Implement seed-based rendering 600 + - [ ] Test determinism across browsers 601 + - [ ] Create example piece adaption 602 + 603 + ### Week 3: Testing & Integration 604 + - [ ] Deploy 1-3 pieces to testnet 605 + - [ ] Gather feedback 606 + - [ ] Optimize bundle size further 607 + - [ ] Document integration process 608 + 609 + ### Week 4: Go/No-Go Decision 610 + - [ ] Evaluate technical + community reception 611 + - [ ] Decide on IPFS vs. on-chain approach 612 + - [ ] Plan full launch (if proceeding) 613 + 614 + --- 615 + 616 + ## Conclusion 617 + 618 + **Integration with bootloader.art is technically feasible** for a **KidLisp-focused, on-chain-optimized subset** of AC's creative platform. Key findings: 619 + 620 + 1. **Size Constraints Are the Core Challenge** 621 + - Full AC runtime (7-8 MB) is 300+ times too large for on-chain storage 622 + - Ultra-minimal KidLisp bundle can reach ~20-24 KB (achievable but tight) 623 + - IPFS off-chain approach removes size constraints, but loses "on-chain" authenticity 624 + 625 + 2. **KidLisp Is the Right Vehicle** 626 + - Interpreter-based (smaller than JavaScript) 627 + - Math-centric (suited to generative art) 628 + - Already optimized in existing codebase 629 + 630 + 3. **Three Viable Paths**: 631 + - **Approach 1** (Recommended): Ultra-minimal KidLisp on-chain (~20 KB) 632 + - **Approach 2** (Easier): Full bundle on IPFS, metadata on-chain 633 + - **Approach 3** (Hybrid): Seed on-chain, code on IPFS 634 + 635 + 4. **Community/Market Fit** 636 + - Generative tokens are hot; AC has the art 637 + - But AC's identity is interactive, not static 638 + - Position as "evolution of AC," not replacement 639 + 640 + 5. **Recommended Next Step**: 641 + - Build KidLisp bootloader shim (1-2 days) 642 + - Test with 3 pieces 643 + - Contact bootloader.art team with POC 644 + - Decide on long-term integration path 645 + 646 + --- 647 + 648 + ## References 649 + 650 + - [bootloader.art](https://bootloader.art/) — Official platform 651 + - [bootloader.art Help](https://bootloader.art/help) — Documentation 652 + - [Advice on Creating On-Chain Generated NFT Art](https://rubydusa.medium.com/advice-on-creating-on-chain-generated-nft-art-6ea2ac79d7cf) — Technical constraints 653 + - [How To Create Generative Art NFTs](https://chain.link/tutorials/how-to-create-generative-art-nfts) — Chainlink tutorial 654 + - [Autoglyphs](https://www.larvalabs.com/autoglyphs) — Early on-chain generative art (precedent) 655 + - [On-Chain Is Art Blocks](https://medium.com/the-link-art-blocks/how-on-chain-is-art-blocks-5ccd553dd370) — Art Blocks architecture 656 + 657 + --- 658 + 659 + **Report compiled**: 2026-04-14 660 + **Next review**: After initial bootloader.art team contact