You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
165 lines
4.6 KiB
165 lines
4.6 KiB
// 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);
|
|
}
|
|
|