Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 356 lines 13 kB view raw
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}