Monorepo for Aesthetic.Computer
aesthetic.computer
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);