Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 591 lines 17 kB view raw
1#!/usr/bin/env node 2 3/** 4 * Rosary Bead Visualizer for KidLisp - Sixel Output 5 * 6 * Parses KidLisp code and renders it as a rosary bead diagram. 7 * Each expression becomes a bead on a string, loops form circles. 8 * 9 * Usage: 10 * ./rosary-sixel.mjs "red, ink blue, line 0 0 100 100" 11 * ./rosary-sixel.mjs "$mtz" # fetch from AC 12 * echo "wipe, ink, box" | ./rosary-sixel.mjs 13 */ 14 15const WIDTH = 400; 16const HEIGHT = 300; 17 18// Bead types with colors (Calder-inspired palette) 19const BEAD_STYLES = { 20 state: { color: [220, 60, 60], symbol: '○' }, // red - wipe, ink, def 21 draw: { color: [60, 100, 220], symbol: '●' }, // blue - line, box, circle, point 22 effect: { color: [220, 180, 40], symbol: '◆' }, // yellow - scroll, blur, zoom 23 timing: { color: [180, 80, 180], symbol: '◇' }, // purple - 1s, 0.5s, later 24 branch: { color: [40, 180, 120], symbol: '◈' }, // green - if, ? 25 loop: { color: [255, 140, 40], symbol: '⬡' }, // orange - repeat 26 atom: { color: [150, 150, 150], symbol: '·' }, // gray - numbers, strings 27}; 28 29// Command classification 30const COMMAND_TYPES = { 31 // State changes 32 'wipe': 'state', 'ink': 'state', 'def': 'state', 'let': 'state', 33 // Drawing 34 'line': 'draw', 'box': 'draw', 'circle': 'draw', 'point': 'draw', 35 'write': 'draw', 'oval': 'draw', 'tri': 'draw', 'draw': 'draw', 36 // Effects 37 'scroll': 'effect', 'blur': 'effect', 'zoom': 'effect', 'pan': 'effect', 38 'coat': 'effect', 'fade': 'effect', 'noise': 'effect', 39 // Timing 40 '1s': 'timing', '0.5s': 'timing', '0.3s': 'timing', '2s': 'timing', 41 'later': 'timing', 'every': 'timing', 42 // Branching 43 'if': 'branch', '?': 'branch', 'cond': 'branch', 44 // Loops 45 'repeat': 'loop', 'loop': 'loop', 'for': 'loop', 46}; 47 48// Color names to RGB 49const COLOR_NAMES = { 50 'red': [255, 80, 80], 'blue': [80, 80, 255], 'green': [80, 200, 80], 51 'yellow': [255, 255, 80], 'purple': [180, 80, 180], 'orange': [255, 160, 80], 52 'cyan': [80, 255, 255], 'magenta': [255, 80, 255], 'white': [255, 255, 255], 53 'black': [40, 40, 40], 'gray': [150, 150, 150], 'navy': [40, 40, 120], 54 'brown': [140, 90, 60], 'pink': [255, 180, 200], 55}; 56 57class SixelCanvas { 58 constructor(width, height) { 59 this.width = width; 60 this.height = height; 61 this.buffer = new Uint8Array(width * height * 3); 62 this.clear(20, 20, 30); // Dark background 63 } 64 65 clear(r, g, b) { 66 for (let i = 0; i < this.buffer.length; i += 3) { 67 this.buffer[i] = r; 68 this.buffer[i + 1] = g; 69 this.buffer[i + 2] = b; 70 } 71 } 72 73 setPixel(x, y, r, g, b) { 74 x = Math.round(x); 75 y = Math.round(y); 76 if (x >= 0 && x < this.width && y >= 0 && y < this.height) { 77 const idx = (y * this.width + x) * 3; 78 this.buffer[idx] = r; 79 this.buffer[idx + 1] = g; 80 this.buffer[idx + 2] = b; 81 } 82 } 83 84 // Draw a filled circle (bead) 85 fillCircle(cx, cy, r, color) { 86 const [cr, cg, cb] = color; 87 for (let y = -r; y <= r; y++) { 88 for (let x = -r; x <= r; x++) { 89 if (x * x + y * y <= r * r) { 90 this.setPixel(cx + x, cy + y, cr, cg, cb); 91 } 92 } 93 } 94 } 95 96 // Draw circle outline 97 strokeCircle(cx, cy, r, color, thickness = 1) { 98 const [cr, cg, cb] = color; 99 for (let y = -r - thickness; y <= r + thickness; y++) { 100 for (let x = -r - thickness; x <= r + thickness; x++) { 101 const dist = Math.sqrt(x * x + y * y); 102 if (dist >= r - thickness / 2 && dist <= r + thickness / 2) { 103 this.setPixel(cx + x, cy + y, cr, cg, cb); 104 } 105 } 106 } 107 } 108 109 // Draw a line (string between beads) 110 line(x0, y0, x1, y1, color, thickness = 2) { 111 const [r, g, b] = color; 112 const dx = Math.abs(x1 - x0); 113 const dy = Math.abs(y1 - y0); 114 const steps = Math.max(dx, dy) * 2; 115 116 for (let i = 0; i <= steps; i++) { 117 const t = i / steps; 118 const x = x0 + (x1 - x0) * t; 119 const y = y0 + (y1 - y0) * t; 120 121 // Thicken the line 122 for (let ox = -thickness / 2; ox <= thickness / 2; ox++) { 123 for (let oy = -thickness / 2; oy <= thickness / 2; oy++) { 124 this.setPixel(x + ox, y + oy, r, g, b); 125 } 126 } 127 } 128 } 129 130 // Draw curved arc (for loops) 131 arc(cx, cy, r, startAngle, endAngle, color, thickness = 2) { 132 const [cr, cg, cb] = color; 133 const steps = Math.abs(endAngle - startAngle) * r; 134 135 for (let i = 0; i <= steps; i++) { 136 const t = i / steps; 137 const angle = startAngle + (endAngle - startAngle) * t; 138 const x = cx + Math.cos(angle) * r; 139 const y = cy + Math.sin(angle) * r; 140 141 for (let ox = -thickness / 2; ox <= thickness / 2; ox++) { 142 for (let oy = -thickness / 2; oy <= thickness / 2; oy++) { 143 this.setPixel(x + ox, y + oy, cr, cg, cb); 144 } 145 } 146 } 147 } 148 149 toSixel() { 150 let output = '\x1bPq'; 151 const colors = new Map(); 152 let colorIndex = 0; 153 154 for (let band = 0; band < Math.ceil(this.height / 6); band++) { 155 const bandArrays = new Map(); 156 157 for (let x = 0; x < this.width; x++) { 158 for (let dy = 0; dy < 6; dy++) { 159 const y = band * 6 + dy; 160 if (y >= this.height) break; 161 162 const idx = (y * this.width + x) * 3; 163 const r = this.buffer[idx]; 164 const g = this.buffer[idx + 1]; 165 const b = this.buffer[idx + 2]; 166 167 const colorKey = (r << 16) | (g << 8) | b; 168 169 if (!colors.has(colorKey)) { 170 colors.set(colorKey, colorIndex++); 171 output += `#${colors.get(colorKey)};2;${Math.round(r * 100 / 255)};${Math.round(g * 100 / 255)};${Math.round(b * 100 / 255)}`; 172 } 173 174 const color = colors.get(colorKey); 175 if (!bandArrays.has(color)) { 176 bandArrays.set(color, new Array(this.width).fill(0)); 177 } 178 bandArrays.get(color)[x] |= (1 << dy); 179 } 180 } 181 182 for (const [color, pixels] of bandArrays) { 183 output += `#${color}`; 184 for (const pixel of pixels) { 185 output += String.fromCharCode(63 + pixel); 186 } 187 output += '$'; 188 } 189 output += '-'; 190 } 191 192 return output + '\x1b\\'; 193 } 194} 195 196// Simple KidLisp tokenizer (handles commas and parens) 197// Splits on commas and newlines, keeps paren expressions together 198function tokenize(code) { 199 const tokens = []; 200 let current = ''; 201 let inString = false; 202 let parenDepth = 0; 203 204 for (let i = 0; i < code.length; i++) { 205 const char = code[i]; 206 207 if (char === '"') { 208 inString = !inString; 209 current += char; 210 } else if (inString) { 211 current += char; 212 } else if (char === '(') { 213 if (parenDepth === 0 && current.trim()) { 214 tokens.push(current.trim()); 215 current = ''; 216 } 217 parenDepth++; 218 current += char; 219 } else if (char === ')') { 220 parenDepth--; 221 current += char; 222 if (parenDepth === 0) { 223 tokens.push(current.trim()); 224 current = ''; 225 } 226 } else if (parenDepth > 0) { 227 current += char; 228 } else if (char === ',' || char === '\n') { 229 if (current.trim()) tokens.push(current.trim()); 230 current = ''; 231 } else { 232 current += char; 233 } 234 } 235 236 if (current.trim()) tokens.push(current.trim()); 237 return tokens; 238} 239 240// Parse a token into a bead 241function parseBead(token) { 242 token = token.trim(); 243 244 // Handle parenthesized expressions 245 if (token.startsWith('(') && token.endsWith(')')) { 246 const inner = token.slice(1, -1).trim(); 247 const parts = inner.split(/\s+/); 248 const head = parts[0]; 249 250 // Check for timing like (1s ...) 251 if (/^\d+(\.\d+)?s$/.test(head)) { 252 return { 253 type: 'timing', 254 name: head, 255 children: parts.slice(1).map(p => parseBead(p)), 256 style: BEAD_STYLES.timing 257 }; 258 } 259 260 // Check for known commands 261 const type = COMMAND_TYPES[head] || 'state'; 262 return { 263 type, 264 name: head, 265 args: parts.slice(1), 266 children: type === 'loop' ? parts.slice(2).map(p => parseBead(p)) : [], 267 style: BEAD_STYLES[type] 268 }; 269 } 270 271 // Handle simple commands like "red" or "ink blue" 272 const parts = token.split(/\s+/); 273 const head = parts[0]; 274 275 // Check if it's a color name (acts as wipe) 276 if (COLOR_NAMES[head] && parts.length === 1) { 277 return { 278 type: 'state', 279 name: head, 280 args: [], 281 style: { ...BEAD_STYLES.state, color: COLOR_NAMES[head] } 282 }; 283 } 284 285 const type = COMMAND_TYPES[head] || 'state'; 286 return { 287 type, 288 name: head, 289 args: parts.slice(1), 290 style: BEAD_STYLES[type] 291 }; 292} 293 294// Layout beads in a rosary pattern 295function layoutRosary(beads, width, height) { 296 const positions = []; 297 const n = beads.length; 298 299 if (n === 0) return positions; 300 301 const centerX = width / 2; 302 const centerY = height / 2; 303 const radius = Math.min(width, height) * 0.35; 304 const beadRadius = Math.max(8, Math.min(20, 200 / n)); 305 306 // Arrange in a circle 307 for (let i = 0; i < n; i++) { 308 const angle = (i / n) * Math.PI * 2 - Math.PI / 2; // Start from top 309 positions.push({ 310 x: centerX + Math.cos(angle) * radius, 311 y: centerY + Math.sin(angle) * radius, 312 radius: beadRadius, 313 bead: beads[i], 314 angle 315 }); 316 } 317 318 return positions; 319} 320 321// Draw the rosary 322function drawRosary(canvas, positions) { 323 const stringColor = [100, 100, 120]; 324 325 // Draw the string (connect all beads in a loop) 326 for (let i = 0; i < positions.length; i++) { 327 const curr = positions[i]; 328 const next = positions[(i + 1) % positions.length]; 329 canvas.line(curr.x, curr.y, next.x, next.y, stringColor, 2); 330 } 331 332 // Draw beads 333 for (const pos of positions) { 334 const { x, y, radius, bead } = pos; 335 const color = bead.style.color; 336 337 // Draw bead shadow 338 canvas.fillCircle(x + 2, y + 2, radius, [20, 20, 30]); 339 340 // Draw bead 341 canvas.fillCircle(x, y, radius, color); 342 343 // Highlight 344 canvas.fillCircle(x - radius * 0.3, y - radius * 0.3, radius * 0.3, 345 [Math.min(255, color[0] + 60), Math.min(255, color[1] + 60), Math.min(255, color[2] + 60)]); 346 } 347 348 // Draw legend 349 drawLegend(canvas, positions); 350} 351 352function drawLegend(canvas, positions) { 353 const startY = 20; 354 const startX = 10; 355 let y = startY; 356 357 // Draw title 358 // (Would need font rendering - skip for now) 359 360 // Draw bead labels with lines pointing to beads 361 const labelColor = [180, 180, 200]; 362 363 // Just draw type indicators in corner 364 const types = ['state', 'draw', 'effect', 'timing', 'loop']; 365 types.forEach((type, i) => { 366 const style = BEAD_STYLES[type]; 367 canvas.fillCircle(startX + 8, y + i * 18, 6, style.color); 368 }); 369} 370 371// Fetch code from AC 372async function fetchCode(slug) { 373 if (!slug.startsWith('$')) slug = '$' + slug; 374 375 try { 376 const response = await fetch(`https://aesthetic.computer/api/code/${slug.slice(1)}`); 377 if (response.ok) { 378 const data = await response.json(); 379 return data.code || data.text || ''; 380 } 381 } catch (e) { 382 // Ignore 383 } 384 return null; 385} 386 387async function main() { 388 let code = process.argv[2]; 389 const asciiMode = process.argv.includes('--ascii'); 390 391 // Check if it's a slug 392 if (code && (code.startsWith('$') || /^[a-z0-9]{3}$/i.test(code))) { 393 const fetched = await fetchCode(code); 394 if (fetched) { 395 code = fetched; 396 console.error(`Fetched: ${code.substring(0, 50)}...`); 397 } 398 } 399 400 // Or read from stdin 401 if (!code) { 402 const chunks = []; 403 for await (const chunk of process.stdin) { 404 chunks.push(chunk); 405 } 406 code = Buffer.concat(chunks).toString().trim(); 407 } 408 409 if (!code) { 410 console.error('Usage: rosary-sixel.mjs "code" or rosary-sixel.mjs $slug'); 411 console.error(' rosary-sixel.mjs "code" --ascii (text mode)'); 412 console.error('Example: rosary-sixel.mjs "red, ink blue, line 0 0 100 100"'); 413 process.exit(1); 414 } 415 416 // Parse and render 417 const tokens = tokenize(code); 418 const beads = tokens.map(parseBead); 419 420 console.error(`Parsed ${beads.length} beads: ${beads.map(b => b.name).join(' → ')}`); 421 422 if (asciiMode) { 423 // ASCII rosary 424 renderAsciiRosary(beads); 425 } else { 426 // Sixel rosary 427 const canvas = new SixelCanvas(WIDTH, HEIGHT); 428 const positions = layoutRosary(beads, WIDTH, HEIGHT); 429 drawRosary(canvas, positions); 430 431 process.stdout.write('\x1b[H\x1b[2J'); // Clear screen 432 process.stdout.write(canvas.toSixel()); 433 process.stdout.write('\n'); 434 } 435} 436 437// ASCII rosary renderer 438function renderAsciiRosary(beads) { 439 const n = beads.length; 440 if (n === 0) { 441 console.log('(empty rosary)'); 442 return; 443 } 444 445 // Type symbols 446 const symbols = { 447 state: '○', draw: '●', effect: '◆', timing: '◇', branch: '◈', loop: '⬡', atom: '·' 448 }; 449 450 // ANSI colors 451 const colors = { 452 state: '\x1b[91m', // red 453 draw: '\x1b[94m', // blue 454 effect: '\x1b[93m', // yellow 455 timing: '\x1b[95m', // magenta 456 branch: '\x1b[92m', // green 457 loop: '\x1b[33m', // orange-ish 458 atom: '\x1b[90m', // gray 459 }; 460 const reset = '\x1b[0m'; 461 const dim = '\x1b[2m'; 462 463 // Circular layout for larger programs 464 if (n > 4) { 465 renderCircularRosary(beads, symbols, colors, reset, dim); 466 } else { 467 // Linear for small programs 468 console.log(); 469 console.log(' ┌' + '─'.repeat(n * 4 + 2) + '┐'); 470 console.log(' │ ' + dim + 'LOOP' + reset + ' '.repeat(n * 4 - 2) + '│'); 471 console.log(' └─┬' + '─'.repeat(n * 4) + '┬┘'); 472 console.log(' │' + ' '.repeat(n * 4) + '│'); 473 474 let beadLine = ' '; 475 let nameLine = ' '; 476 477 for (let i = 0; i < n; i++) { 478 const b = beads[i]; 479 const sym = symbols[b.type] || '○'; 480 const col = colors[b.type] || ''; 481 482 beadLine += col + sym + reset + ' ── '; 483 const name = b.name.substring(0, 5).padEnd(5); 484 nameLine += name; 485 } 486 487 beadLine = beadLine.slice(0, -4) + dim + ' ↩' + reset; 488 489 console.log(beadLine); 490 console.log(dim + nameLine + reset); 491 } 492 493 console.log(); 494 console.log(dim + ' Legend: ' + reset + 495 colors.state + '○' + reset + ' state ' + 496 colors.draw + '●' + reset + ' draw ' + 497 colors.effect + '◆' + reset + ' effect ' + 498 colors.timing + '◇' + reset + ' timing ' + 499 colors.loop + '⬡' + reset + ' loop'); 500 console.log(); 501} 502 503// Render circular rosary for larger programs 504function renderCircularRosary(beads, symbols, colors, reset, dim) { 505 const n = beads.length; 506 const width = 50; 507 const height = 20; 508 const cx = width / 2; 509 const cy = height / 2; 510 const rx = 18; // x radius 511 const ry = 7; // y radius (compressed for terminal) 512 513 // Create grid 514 const grid = Array(height).fill(null).map(() => Array(width).fill(' ')); 515 const colorGrid = Array(height).fill(null).map(() => Array(width).fill('')); 516 517 // Place beads around ellipse 518 const positions = []; 519 for (let i = 0; i < n; i++) { 520 const angle = (i / n) * Math.PI * 2 - Math.PI / 2; 521 const x = Math.round(cx + Math.cos(angle) * rx); 522 const y = Math.round(cy + Math.sin(angle) * ry); 523 positions.push({ x, y, bead: beads[i], index: i }); 524 } 525 526 // Draw connecting arcs (simplified as lines) 527 for (let i = 0; i < n; i++) { 528 const curr = positions[i]; 529 const next = positions[(i + 1) % n]; 530 drawLine(grid, curr.x, curr.y, next.x, next.y, dim + '·' + reset); 531 } 532 533 // Place beads (overwrite lines) 534 for (const pos of positions) { 535 const b = pos.bead; 536 const sym = symbols[b.type] || '○'; 537 const col = colors[b.type] || ''; 538 539 if (pos.y >= 0 && pos.y < height && pos.x >= 0 && pos.x < width) { 540 grid[pos.y][pos.x] = sym; 541 colorGrid[pos.y][pos.x] = col; 542 } 543 } 544 545 // Render 546 console.log(); 547 for (let y = 0; y < height; y++) { 548 let line = ' '; 549 for (let x = 0; x < width; x++) { 550 const col = colorGrid[y][x]; 551 const char = grid[y][x]; 552 if (col) { 553 line += col + char + reset; 554 } else { 555 line += char; 556 } 557 } 558 console.log(line); 559 } 560 561 // Labels below 562 console.log(); 563 console.log(dim + ' Beads: ' + reset + beads.map((b, i) => { 564 const col = colors[b.type] || ''; 565 return col + (i + 1) + '.' + b.name + reset; 566 }).join(' → ')); 567} 568 569// Bresenham line for ASCII 570function drawLine(grid, x0, y0, x1, y1, char) { 571 const dx = Math.abs(x1 - x0); 572 const dy = Math.abs(y1 - y0); 573 const sx = x0 < x1 ? 1 : -1; 574 const sy = y0 < y1 ? 1 : -1; 575 let err = dx - dy; 576 577 let x = x0, y = y0; 578 while (true) { 579 if (y >= 0 && y < grid.length && x >= 0 && x < grid[0].length) { 580 if (grid[y][x] === ' ') grid[y][x] = char; 581 } 582 583 if (x === x1 && y === y1) break; 584 585 const e2 = 2 * err; 586 if (e2 > -dy) { err -= dy; x += sx; } 587 if (e2 < dx) { err += dx; y += sy; } 588 } 589} 590 591main().catch(console.error);