// Binary Framebuffer Encoder // Converts pixel data to binary format for bus display const MAGIC = [66, 78, 23, 238]; const DrawEntryType = { Image: 1, Animation: 2, HScroll: 3, VScroll: 4, Line: 5 }; function bitmapSizeBytes(width, height) { return Math.floor((width * height + 7) / 8); } function setPixelInBitmap(x, y, memory, width, height, value) { const index = y * width + x; const byteIndex = Math.floor(index / 8); const bitIndex = index % 8; if (value) { memory[byteIndex] |= 1 << bitIndex; } else { memory[byteIndex] &= ~(1 << bitIndex); } } class BinaryFramebufferEncoder { entries = []; addImage(posX, posY, width, height, pixelData) { const data = new Uint8Array(bitmapSizeBytes(width, height)); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { setPixelInBitmap(x, y, data, width, height, pixelData[y][x]); } } this.entries.push({ type: DrawEntryType.Image, header: { posX, posY }, width, height, data }); } addAnimation(posX, posY, width, height, frameCount, updateInterval, frames) { const frameSize = bitmapSizeBytes(width, height); const data = new Uint8Array(frameSize * frameCount); for (let f = 0; f < frameCount; f++) { const frameData = data.subarray(f * frameSize, (f + 1) * frameSize); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { setPixelInBitmap(x, y, frameData, width, height, frames[f][y][x]); } } } this.entries.push({ type: DrawEntryType.Animation, header: { posX, posY }, width, height, frameCount, updateInterval, data }); } encode() { let totalSize = 5; for (const entry of this.entries) { totalSize += 3; switch (entry.type) { case DrawEntryType.Image: totalSize += 2 + entry.data.length; break; case DrawEntryType.Animation: totalSize += 4 + entry.data.length; break; } } const buffer = new Uint8Array(totalSize); let pos = 0; buffer.set(MAGIC, pos); pos += 4; buffer[pos++] = this.entries.length; for (const entry of this.entries) { buffer[pos++] = entry.type; buffer[pos++] = entry.header.posX; buffer[pos++] = entry.header.posY; switch (entry.type) { case DrawEntryType.Image: buffer[pos++] = entry.width; buffer[pos++] = entry.height; buffer.set(entry.data, pos); pos += entry.data.length; break; case DrawEntryType.Animation: buffer[pos++] = entry.width; buffer[pos++] = entry.height; buffer[pos++] = entry.frameCount; buffer[pos++] = entry.updateInterval; buffer.set(entry.data, pos); pos += entry.data.length; break; } } return buffer; } } function parseJSONFrames(text, width = 120, height = 60, maxFrames = 50) { const json = JSON.parse(text); if (!Array.isArray(json)) { throw new Error("JSON must be an array of frames"); } const frames = []; for (const frame of json) { if (!Array.isArray(frame)) { throw new Error("Each frame must be an array of coordinates"); } const pixels = Array(height).fill(null).map(() => Array(width).fill(false)); for (const change of frame) { const [x, y] = change; if (y < pixels.length && x < pixels[y].length) { pixels[y][x] = true; } } if (frames.length >= maxFrames) continue; frames.push(pixels); } return frames; } function jsonToBinary(jsonString, options = {}) { const { startX = 0, startY = 0, updateInterval = 3, width = 120, height = 60, maxFrames = 50 } = options; const encoder = new BinaryFramebufferEncoder(); const frames = parseJSONFrames(jsonString, width, height, maxFrames); if (frames.length === 1) { encoder.addImage(startX, startY, width, height, frames[0]); } else { encoder.addAnimation(startX, startY, width, height, frames.length, updateInterval, frames); } return encoder.encode(); } // Helper function to drop every nth frame from a frame array function dropEveryNthFrame(frames, n) { if (n <= 1 || frames.length <= 1) return frames; return frames.filter((frame, index) => (index + 1) % n !== 0); } // Helper function to keep 1 frame, drop 2 frames in repeating pattern // Pattern: keep, drop, drop, keep, drop, drop, ... function keepOneDropTwo(frames) { if (frames.length <= 1) return frames; return frames.filter((frame, index) => index % 3 === 0); }