const MAGIC = [0x42, 0x4e, 0x17, 0xee]; enum DrawEntryType { Image = 1, Animation = 2, HScroll = 3, VScroll = 4, Line = 5, } enum ScrollWrap { Restarting = 0, Continuous = 1, } enum HScrollDirection { Left = 0, Right = 1, } enum VScrollDirection { Up = 0, Down = 1, } interface DrawHeader { posX: number; posY: number; } interface DrawImage { type: DrawEntryType.Image; header: DrawHeader; width: number; height: number; data: Uint8Array; } interface DrawAnimation { type: DrawEntryType.Animation; header: DrawHeader; width: number; height: number; frameCount: number; updateInterval: number; data: Uint8Array; } interface DrawHScroll { type: DrawEntryType.HScroll; header: DrawHeader; width: number; height: number; contentWidth: number; scrollSpeed: number; wrap: ScrollWrap; direction: HScrollDirection; data: Uint8Array; } interface DrawVScroll { type: DrawEntryType.VScroll; header: DrawHeader; width: number; height: number; contentHeight: number; scrollSpeed: number; wrap: ScrollWrap; direction: VScrollDirection; data: Uint8Array; } interface DrawLine { type: DrawEntryType.Line; header: DrawHeader; endX: number; endY: number; } type DrawEntry = DrawImage | DrawAnimation | DrawHScroll | DrawVScroll | DrawLine; function bitmapSizeBytes(width: number, height: number): number { return Math.floor((width * height + 7) / 8); } function setPixelInBitmap( x: number, y: number, memory: Uint8Array, width: number, height: number, value: boolean ): void { 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); } } function isPixelSetInBitmap( x: number, y: number, memory: Uint8Array, width: number, height: number ): boolean { const index = y * width + x; const byteIndex = Math.floor(index / 8); const bitIndex = index % 8; return (memory[byteIndex] & (1 << bitIndex)) !== 0; } class BinaryFramebufferEncoder { private entries: DrawEntry[] = []; addImage( posX: number, posY: number, width: number, height: number, pixelData: boolean[][] ): void { 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: number, posY: number, width: number, height: number, frameCount: number, updateInterval: number, frames: boolean[][][] ): void { 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, }); } addHScroll( posX: number, posY: number, width: number, height: number, contentWidth: number, scrollSpeed: number, wrap: ScrollWrap, direction: HScrollDirection, pixelData: boolean[][] ): void { const data = new Uint8Array(bitmapSizeBytes(contentWidth, height)); for (let y = 0; y < height; y++) { for (let x = 0; x < contentWidth; x++) { setPixelInBitmap(x, y, data, contentWidth, height, pixelData[y][x]); } } this.entries.push({ type: DrawEntryType.HScroll, header: { posX, posY }, width, height, contentWidth, scrollSpeed, wrap, direction, data, }); } addVScroll( posX: number, posY: number, width: number, height: number, contentHeight: number, scrollSpeed: number, wrap: ScrollWrap, direction: VScrollDirection, pixelData: boolean[][] ): void { const data = new Uint8Array(bitmapSizeBytes(width, contentHeight)); for (let y = 0; y < contentHeight; y++) { for (let x = 0; x < width; x++) { setPixelInBitmap(x, y, data, width, contentHeight, pixelData[y][x]); } } this.entries.push({ type: DrawEntryType.VScroll, header: { posX, posY }, width, height, contentHeight, scrollSpeed, wrap, direction, data, }); } addLine(posX: number, posY: number, endX: number, endY: number): void { this.entries.push({ type: DrawEntryType.Line, header: { posX, posY }, endX, endY, }); } encode(): Uint8Array { 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; case DrawEntryType.HScroll: case DrawEntryType.VScroll: totalSize += 5 + entry.data.length; break; case DrawEntryType.Line: totalSize += 2; 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; case DrawEntryType.HScroll: buffer[pos++] = entry.width; buffer[pos++] = entry.height; buffer[pos++] = entry.contentWidth & 0xff; buffer[pos++] = (entry.contentWidth >> 8) & 0xff; buffer[pos++] = (entry.scrollSpeed & 0x3f) | (entry.direction === HScrollDirection.Right ? 0x40 : 0) | (entry.wrap === ScrollWrap.Continuous ? 0x80 : 0); buffer.set(entry.data, pos); pos += entry.data.length; break; case DrawEntryType.VScroll: buffer[pos++] = entry.width; buffer[pos++] = entry.height; buffer[pos++] = entry.contentHeight & 0xff; buffer[pos++] = (entry.contentHeight >> 8) & 0xff; buffer[pos++] = (entry.scrollSpeed & 0x3f) | (entry.direction === VScrollDirection.Down ? 0x40 : 0) | (entry.wrap === ScrollWrap.Continuous ? 0x80 : 0); buffer.set(entry.data, pos); pos += entry.data.length; break; case DrawEntryType.Line: buffer[pos++] = entry.endX; buffer[pos++] = entry.endY; break; } } return buffer; } } // Image processing utilities async function loadBMP(data: Uint8Array): Promise { const view = new DataView(data.buffer, data.byteOffset); if (data[0] !== 0x42 || data[1] !== 0x4d) { throw new Error('Not a BMP file'); } const offset = view.getUint32(10, true); const width = view.getInt32(18, true); const height = Math.abs(view.getInt32(22, true)); const bpp = view.getUint16(28, true); const pixels: boolean[][] = Array(height).fill(null).map(() => Array(width).fill(false)); if (bpp === 1) { const rowSize = Math.floor((width + 31) / 32) * 4; for (let y = 0; y < height; y++) { const rowStart = offset + (height - 1 - y) * rowSize; for (let x = 0; x < width; x++) { const byteIdx = Math.floor(x / 8); const bitIdx = 7 - (x % 8); pixels[y][x] = (data[rowStart + byteIdx] & (1 << bitIdx)) === 0; } } } else { const rowSize = Math.floor((width * bpp / 8 + 3) / 4) * 4; for (let y = 0; y < height; y++) { const rowStart = offset + (height - 1 - y) * rowSize; for (let x = 0; x < width; x++) { const pixelStart = rowStart + x * (bpp / 8); const b = data[pixelStart]; const g = data[pixelStart + 1]; const r = data[pixelStart + 2]; const luminance = 0.299 * r + 0.587 * g + 0.114 * b; pixels[y][x] = luminance < 128; } } } return pixels; } async function loadGIF(data: Uint8Array): Promise { if (data[0] !== 0x47 || data[1] !== 0x49 || data[2] !== 0x46) { throw new Error('Not a GIF file'); } // Simple GIF parser - supports basic single frame const view = new DataView(data.buffer, data.byteOffset); const width = view.getUint16(6, true); const height = view.getUint16(8, true); // For now, just return a single frame placeholder const frame: boolean[][] = Array(height).fill(null).map(() => Array(width).fill(false)); return [frame]; } async function loadJSON(text: string): Promise { const json = JSON.parse(text); if (!Array.isArray(json)) { throw new Error('JSON must be an array of frames'); } const frames: boolean[][][] = []; for (const frame of json) { if (!Array.isArray(frame)) { throw new Error('Each frame must be an array of rows'); } const pixels: boolean[][] = "0".repeat(60).split("").map(row => "0".repeat(120).split("").map(pixel_in_x => false) as boolean[]); for (const change of frame) { const [x, y] = change; if (y < pixels.length) { if (x < pixels[y].length) { pixels[y][x] = true; } } } if (frames.length > 50) continue const usage = pixels.reduce((prev, row, y, fullarray) => prev + row.reduce((prev, pixel, x) => pixel ? prev + 1 : prev, 0), 0) / (120 * 60); console.log(`frame ${frames.length} (usage: ${(usage * 100).toFixed(2)}%)`); frames.push(pixels); } return frames; } async function loadImage(path: string): Promise { const data = new Uint8Array(await Bun.file(path).arrayBuffer()); if (path.toLowerCase().endsWith('.bmp')) { return [await loadBMP(data)]; } else if (path.toLowerCase().endsWith('.gif')) { return await loadGIF(data); } else if (path.toLowerCase().endsWith('.json')) { const text = await Bun.file(path).text(); return await loadJSON(text); } throw new Error(`Unsupported format: ${path} `); } type LoadImageFunction = (params: string) => Promise; async function createAnimationBinary( images: string[], output: string, startX: number, startY: number, updateInterval: number, mode: 'frame' | 'differential', loadImage: LoadImageFunction, ): Promise { const encoder = new BinaryFramebufferEncoder(); if (mode === 'differential') { if (images.length !== 2) { throw new Error('Differential mode requires exactly 2 images'); } const frames: boolean[][][] = []; for (const img of images) { const loaded = await loadImage(img); const frame = Array.isArray(loaded[0]) ? loaded[0] : loaded; frames.push(frame as boolean[][]); } const width = frames[0][0].length; const height = frames[0].length; encoder.addAnimation(startX, startY, width, height, 2, updateInterval, frames); } else { const allFrames: boolean[][][] = []; for (const img of images) { const loaded = await loadImage(img); if (Array.isArray(loaded[0][0])) { allFrames.push(...(loaded as boolean[][][])); } else { allFrames.push(loaded as boolean[][]); } } if (allFrames.length === 1) { const width = allFrames[0][0].length; const height = allFrames[0].length; encoder.addImage(startX, startY, width, height, allFrames[0]); } else { const width = allFrames[0][0].length; const height = allFrames[0].length; encoder.addAnimation(startX, startY, width, height, allFrames.length, updateInterval, allFrames); } } const binary = encoder.encode(); await Bun.write(output, binary); console.log(`Created ${output} (${binary.length} bytes)`); } function printHelp() { console.log(` Usage: bun run encoder.ts[options] < images...> Convert BMP / GIF images to binary animation format Arguments: images Image file(s)(BMP, GIF, or JSON).GIF frames are extracted automatically.JSON format: array of frames, each frame is array of rows with [x, y, on / off] pixels.Single frame creates static object, multiple frames create animation. Options: -o, --output < file > Output binary file path(default: animation.bin) - x, --start - x < n > X coordinate for start position(default: 0) - y, --start - y < n > Y coordinate for start position(default: 0) - i, --interval < n > Update interval(0 = every tick, 1 = every second tick, etc.)(default: 3) - m, --mode < mode > Animation mode: "frame" = each image / frame is a frame, "differential" = alternate between 2 images(requires exactly 2 images) (default: frame) - h, --help Show this help message `); } function parseArgs(args: string[]): { images: string[]; output: string; startX: number; startY: number; updateInterval: number; mode: 'frame' | 'differential'; } | null { const result = { images: [] as string[], output: 'animation.bin', startX: 0, startY: 0, updateInterval: 3, mode: 'frame' as 'frame' | 'differential', }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '-h' || arg === '--help') { return null; } else if (arg === '-o' || arg === '--output') { result.output = args[++i]; } else if (arg === '-x' || arg === '--start-x') { result.startX = parseInt(args[++i]); } else if (arg === '-y' || arg === '--start-y') { result.startY = parseInt(args[++i]); } else if (arg === '-i' || arg === '--interval') { result.updateInterval = parseInt(args[++i]); } else if (arg === '-m' || arg === '--mode') { const mode = args[++i]; if (mode !== 'frame' && mode !== 'differential') { throw new Error('Mode must be "frame" or "differential"'); } result.mode = mode; } else if (!arg.startsWith('-')) { result.images.push(arg); } } if (result.images.length === 0) { throw new Error('No images specified'); } return result; } async function main() { const args = process.argv.slice(2); if (args.length === 0 || args.includes('-h') || args.includes('--help')) { printHelp(); return; } try { const params = parseArgs(args); if (!params) { printHelp(); return; } await createAnimationBinary( params.images, params.output, params.startX, params.startY, params.updateInterval, params.mode, loadImage, ); } catch (error) { console.error('Error:', error instanceof Error ? error.message : error); process.exit(1); } } // Export for library use export { BinaryFramebufferEncoder, DrawEntryType, ScrollWrap, HScrollDirection, VScrollDirection, bitmapSizeBytes, setPixelInBitmap, isPixelSetInBitmap, createAnimationBinary, LoadImageFunction, }; // Run if executed directly if (import.meta.main) { main(); }