Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2
3// Optimized 64x64 Sixel Renderer with Black Border
4// High performance version with bordered display
5
6const BUFFER_WIDTH = 128;
7const BUFFER_HEIGHT = 128;
8const SCALE = 3;
9const BORDER_BLOCKS = 2; // Black border thickness in buffer blocks (will be scaled)
10
11class BorderedSixelRenderer {
12 constructor(width, height, scale = 3, borderBlocks = 2) {
13 this.width = width;
14 this.height = height;
15 this.scale = scale;
16 this.borderBlocks = borderBlocks;
17
18 // Calculate output dimensions including border (border is also scaled)
19 this.contentWidth = width * scale;
20 this.contentHeight = height * scale;
21 this.borderSize = borderBlocks * scale; // Border is scaled like pixels
22 this.outputWidth = this.contentWidth + (this.borderSize * 2);
23 this.outputHeight = this.contentHeight + (this.borderSize * 2);
24
25 this.buffer = new Uint8Array(width * height * 3); // RGB buffer
26
27 console.log(`Initialized ${width}x${height} → ${this.outputWidth}x${this.outputHeight} (${scale}x scale + ${borderBlocks}×${scale}=${this.borderSize}px chunky border)`);
28 }
29
30 setPixel(x, y, r, g, b) {
31 if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
32 const idx = (y * this.width + x) * 3;
33 this.buffer[idx] = r;
34 this.buffer[idx + 1] = g;
35 this.buffer[idx + 2] = b;
36 }
37 }
38
39 fillRect(x, y, w, h, r, g, b) {
40 for (let dy = 0; dy < h; dy++) {
41 for (let dx = 0; dx < w; dx++) {
42 this.setPixel(x + dx, y + dy, r, g, b);
43 }
44 }
45 }
46
47 clear(r = 0, g = 0, b = 0) {
48 for (let i = 0; i < this.buffer.length; i += 3) {
49 this.buffer[i] = r;
50 this.buffer[i + 1] = g;
51 this.buffer[i + 2] = b;
52 }
53 }
54
55 // Scale buffer and add chunky black border
56 scaleBufferWithBorder() {
57 const outputBuffer = new Uint8Array(this.outputWidth * this.outputHeight * 3);
58
59 // Fill entire output with black (border color)
60 outputBuffer.fill(0);
61
62 // Scale and place content in the center, offset by scaled border
63 for (let y = 0; y < this.contentHeight; y++) {
64 for (let x = 0; x < this.contentWidth; x++) {
65 const srcX = Math.floor(x / this.scale);
66 const srcY = Math.floor(y / this.scale);
67 const srcIdx = (srcY * this.width + srcX) * 3;
68
69 // Output position including scaled border offset
70 const outX = x + this.borderSize;
71 const outY = y + this.borderSize;
72 const outIdx = (outY * this.outputWidth + outX) * 3;
73
74 outputBuffer[outIdx] = this.buffer[srcIdx];
75 outputBuffer[outIdx + 1] = this.buffer[srcIdx + 1];
76 outputBuffer[outIdx + 2] = this.buffer[srcIdx + 2];
77 }
78 }
79
80 return {
81 buffer: outputBuffer,
82 width: this.outputWidth,
83 height: this.outputHeight
84 };
85 }
86
87 // Optimized cursor save/restore rendering with border
88 render() {
89 const scaled = this.scaleBufferWithBorder();
90
91 // Save cursor position + start sixel
92 let output = '\x1b[s\x1bPq';
93
94 const colors = new Map();
95 let colorIndex = 0;
96
97 // Process pixels in sixel bands (6 pixels high)
98 for (let band = 0; band < Math.ceil(scaled.height / 6); band++) {
99 const bandData = new Map();
100
101 for (let x = 0; x < scaled.width; x++) {
102 for (let dy = 0; dy < 6; dy++) {
103 const y = band * 6 + dy;
104 if (y >= scaled.height) break;
105
106 const idx = (y * scaled.width + x) * 3;
107 const r = scaled.buffer[idx];
108 const g = scaled.buffer[idx + 1];
109 const b = scaled.buffer[idx + 2];
110 const colorKey = `${r},${g},${b}`;
111
112 if (!colors.has(colorKey)) {
113 colors.set(colorKey, colorIndex++);
114 output += `#${colors.get(colorKey)};2;${Math.round(r*100/255)};${Math.round(g*100/255)};${Math.round(b*100/255)}`;
115 }
116
117 const color = colors.get(colorKey);
118 if (!bandData.has(color)) {
119 bandData.set(color, new Array(scaled.width).fill(0));
120 }
121 bandData.get(color)[x] |= (1 << dy);
122 }
123 }
124
125 // Output this band
126 for (const [color, pixels] of bandData) {
127 output += `#${color}`;
128 for (const pixel of pixels) {
129 output += String.fromCharCode(63 + pixel);
130 }
131 output += '$'; // New line
132 }
133 output += '-'; // Next band
134 }
135
136 // End sixel + restore cursor
137 output += '\x1b\\\x1b[u';
138 return output;
139 }
140}
141
142// Demo animation patterns optimized for 128x128
143const DemoPatterns = {
144 // Animated color blocks
145 colorBlocks: (renderer, frame) => {
146 const colors = [
147 [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0],
148 [255, 0, 255], [0, 255, 255], [255, 128, 0], [128, 0, 255]
149 ];
150
151 const blockSize = 16; // Larger blocks for 128x128
152 const timeOffset = Math.floor(frame / 5);
153
154 for (let y = 0; y < renderer.height; y++) {
155 for (let x = 0; x < renderer.width; x++) {
156 const blockX = Math.floor(x / blockSize);
157 const blockY = Math.floor(y / blockSize);
158 const colorIndex = (blockX + blockY + timeOffset) % colors.length;
159 const color = colors[colorIndex];
160 renderer.setPixel(x, y, color[0], color[1], color[2]);
161 }
162 }
163 },
164
165 // Plasma effect
166 plasma: (renderer, frame) => {
167 const colors = [
168 [255, 0, 0], [255, 128, 0], [255, 255, 0], [0, 255, 0],
169 [0, 255, 255], [0, 0, 255], [128, 0, 255], [255, 0, 255]
170 ];
171
172 for (let y = 0; y < renderer.height; y++) {
173 for (let x = 0; x < renderer.width; x++) {
174 const plasma = Math.sin(x * 0.1 + frame * 0.1) +
175 Math.cos(y * 0.1 + frame * 0.05) +
176 Math.sin((x + y) * 0.05 + frame * 0.02);
177 const colorIndex = Math.floor((plasma + 3) * colors.length / 6) % colors.length;
178 const color = colors[colorIndex];
179 renderer.setPixel(x, y, color[0], color[1], color[2]);
180 }
181 }
182 },
183
184 // Moving stripes
185 stripes: (renderer, frame) => {
186 const colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]];
187
188 for (let y = 0; y < renderer.height; y++) {
189 for (let x = 0; x < renderer.width; x++) {
190 const stripe = Math.floor((x + frame) / 8) % colors.length; // Medium stripes
191 const color = colors[stripe];
192 renderer.setPixel(x, y, color[0], color[1], color[2]);
193 }
194 }
195 },
196
197 // Bouncing ball
198 bouncingBall: (renderer, frame) => {
199 renderer.clear(0, 32, 64); // Dark blue background
200
201 const ballSize = 8; // Larger ball for 128x128
202 const ballX = Math.floor(Math.abs(Math.sin(frame * 0.1)) * (renderer.width - ballSize));
203 const ballY = Math.floor(Math.abs(Math.cos(frame * 0.07)) * (renderer.height - ballSize));
204
205 // Draw ball
206 renderer.fillRect(ballX, ballY, ballSize, ballSize, 255, 255, 0);
207
208 // Trail effect
209 for (let i = 1; i < 8; i++) {
210 const trailFrame = frame - i * 2;
211 const trailX = Math.floor(Math.abs(Math.sin(trailFrame * 0.1)) * (renderer.width - ballSize));
212 const trailY = Math.floor(Math.abs(Math.cos(trailFrame * 0.07)) * (renderer.height - ballSize));
213 const intensity = 255 - (i * 30);
214
215 if (intensity > 0) {
216 renderer.fillRect(trailX, trailY, ballSize, ballSize, intensity, intensity, 0);
217 }
218 }
219 }
220};
221
222// Performance benchmark
223async function benchmark(patternName = 'colorBlocks', numFrames = 100) {
224 const renderer = new BorderedSixelRenderer(BUFFER_WIDTH, BUFFER_HEIGHT, SCALE, BORDER_BLOCKS);
225 const pattern = DemoPatterns[patternName];
226 const frameTimes = [];
227
228 console.log(`\nBenchmarking ${patternName} pattern (${numFrames} frames)...`);
229
230 for (let frame = 0; frame < numFrames; frame++) {
231 const frameStart = process.hrtime.bigint();
232
233 // Generate frame
234 const genStart = process.hrtime.bigint();
235 pattern(renderer, frame);
236 const genEnd = process.hrtime.bigint();
237
238 // Render frame
239 const renderStart = process.hrtime.bigint();
240 const sixelData = renderer.render();
241 process.stdout.write(sixelData);
242 const renderEnd = process.hrtime.bigint();
243
244 const frameEnd = process.hrtime.bigint();
245
246 const totalTime = Number(frameEnd - frameStart) / 1000000;
247 const genTime = Number(genEnd - genStart) / 1000000;
248 const renderTime = Number(renderEnd - renderStart) / 1000000;
249
250 frameTimes.push({
251 total: totalTime,
252 generation: genTime,
253 render: renderTime,
254 fps: 1000 / totalTime
255 });
256
257 // Progress indicator
258 if ((frame + 1) % 25 === 0) {
259 const avgFps = frameTimes.slice(-25).reduce((sum, f) => sum + f.fps, 0) / 25;
260 process.stderr.write(`\rFrame ${frame + 1}/${numFrames} - Avg FPS: ${avgFps.toFixed(1)}`);
261 }
262 }
263
264 // Calculate statistics
265 const avgTotal = frameTimes.reduce((sum, f) => sum + f.total, 0) / numFrames;
266 const avgGeneration = frameTimes.reduce((sum, f) => sum + f.generation, 0) / numFrames;
267 const avgRender = frameTimes.reduce((sum, f) => sum + f.render, 0) / numFrames;
268 const avgFps = 1000 / avgTotal;
269
270 const minFps = Math.min(...frameTimes.map(f => f.fps));
271 const maxFps = Math.max(...frameTimes.map(f => f.fps));
272
273 const pixelsPerFrame = renderer.outputWidth * renderer.outputHeight;
274 const contentPixels = renderer.contentWidth * renderer.contentHeight;
275 const pixelsPerSecond = pixelsPerFrame * avgFps;
276
277 // Clear and show results
278 process.stdout.write('\x1b[2J\x1b[H');
279
280 console.log('\n=== 128x128 BORDERED SIXEL RENDERER RESULTS ===');
281 console.log(`Pattern: ${patternName}`);
282 console.log(`Content: ${renderer.contentWidth}x${renderer.contentHeight} (${SCALE}x scaled from ${BUFFER_WIDTH}x${BUFFER_HEIGHT})`);
283 console.log(`Output: ${renderer.outputWidth}x${renderer.outputHeight} (with ${BORDER_BLOCKS}×${SCALE}=${renderer.borderSize}px chunky border)`);
284 console.log(`Frames: ${numFrames}`);
285 console.log('');
286 console.log('Performance:');
287 console.log(` Average FPS: ${avgFps.toFixed(1)}`);
288 console.log(` Min FPS: ${minFps.toFixed(1)}`);
289 console.log(` Max FPS: ${maxFps.toFixed(1)}`);
290 console.log('');
291 console.log('Timing Breakdown:');
292 console.log(` Generation: ${avgGeneration.toFixed(2)}ms (${(avgGeneration/avgTotal*100).toFixed(1)}%)`);
293 console.log(` Render: ${avgRender.toFixed(2)}ms (${(avgRender/avgTotal*100).toFixed(1)}%)`);
294 console.log(` Total: ${avgTotal.toFixed(2)}ms`);
295 console.log('');
296 console.log('Throughput:');
297 console.log(` Content pixels per frame: ${contentPixels.toLocaleString()}`);
298 console.log(` Total pixels per frame: ${pixelsPerFrame.toLocaleString()}`);
299 console.log(` Pixels per second: ${(pixelsPerSecond/1000000).toFixed(2)} megapixels/second`);
300 console.log('');
301 console.log('Method: Cursor save/restore with chunky black border');
302
303 return { avgFps, pixelsPerSecond };
304}
305
306// Demo mode
307async function demo(patternName = 'bouncingBall', duration = 10000) {
308 const renderer = new BorderedSixelRenderer(BUFFER_WIDTH, BUFFER_HEIGHT, SCALE, BORDER_BLOCKS);
309 const pattern = DemoPatterns[patternName];
310
311 console.log(`\nRunning ${patternName} demo for ${duration}ms...`);
312 console.log('Press Ctrl+C to stop\n');
313
314 let frame = 0;
315 const startTime = Date.now();
316
317 const interval = setInterval(() => {
318 pattern(renderer, frame++);
319 const sixelData = renderer.render();
320 process.stdout.write(sixelData);
321
322 if (Date.now() - startTime > duration) {
323 clearInterval(interval);
324 process.stdout.write('\x1b[2J\x1b[H');
325 console.log('Demo complete!');
326 }
327 }, 1000/30); // Target 30 FPS for demo
328}
329
330// CLI interface
331if (process.argv.length > 2) {
332 const command = process.argv[2];
333 const pattern = process.argv[3] || 'bouncingBall';
334
335 if (command === 'demo') {
336 const duration = parseInt(process.argv[4]) || 10000;
337 demo(pattern, duration);
338 } else if (command === 'benchmark') {
339 const frames = parseInt(process.argv[4]) || 100;
340 benchmark(pattern, frames);
341 } else {
342 console.log('Usage:');
343 console.log(' node bordered-64x64.mjs demo [pattern] [duration_ms]');
344 console.log(' node bordered-64x64.mjs benchmark [pattern] [frames]');
345 console.log('');
346 console.log('Available patterns: colorBlocks, plasma, stripes, bouncingBall');
347 }
348} else {
349 // Default benchmark
350 benchmark().catch(console.error);
351}
352
353// Export for use as module
354if (typeof module !== 'undefined' && module.exports) {
355 module.exports = { BorderedSixelRenderer, DemoPatterns, benchmark, demo };
356}