//@ts-check const fs = require('fs') const jimp = require('jimp') const screen = getScreenDimensions() const buffer = Buffer.alloc(screen.width * screen.height * screen.byteDepth) //TODO: implement automatic cache cleanup /** * @type {{ [key: string]: import('@jimp/core').Bitmap }} */ const imageCache = {} function getScreenDimensions() { const vsize = fs.readFileSync('/sys/class/graphics/fb0/virtual_size', { encoding: 'utf-8' }).split(',') const byteDepth = parseInt(fs.readFileSync('/sys/class/graphics/fb0/bits_per_pixel').toString()) / 8 return { width: parseInt(vsize[0]), height: parseInt(vsize[1]), byteDepth } } /** * @param { Buffer } data */ function flip(data = buffer) { try { fs.writeFileSync('/dev/fb0', data, { encoding: 'binary' }) } catch(e) { console.log("could not write to /dev/fb0") console.log(e) } } function clear() { buffer.fill(0) } /** * @param {string} filepath */ function removeFromCache(filepath) { delete imageCache[filepath] } /** * @param {string} filepath * @returns { Promise } BGRA buffer */ async function readImgFile(filepath) { const img = await jimp.read(filepath) const bitmap = img.bitmap bitmap.data = rgbaTObgra(bitmap.data) return bitmap } /** * @param { number } x * @param { number } y * @param { import('@jimp/core').Bitmap | Pencil } bitmap */ function drawBitmap(x, y, bitmap, handleTransparency = true) { for(let dy = 0; dy < bitmap.height; dy++) { let bitmapLineEnd = bitmap.width * screen.byteDepth * (dy + 1) let bitmapLineStart = bitmap.width * screen.byteDepth * dy let dx = x const offscreenRight = dx + bitmap.width - screen.width if(offscreenRight > 0) { bitmapLineEnd -= offscreenRight*4 } const offscreenLeft = -dx if(offscreenLeft > 0) { bitmapLineStart += offscreenLeft*4 dx = 0 } const offscreenTop = -y if(offscreenTop >= dy) { dy = offscreenTop continue } const offscreenBottom = bitmap.height + y - screen.height if(offscreenBottom > 0 && bitmap.height - offscreenBottom <= dy) { break } if(bitmapLineEnd < bitmapLineStart) { continue } const line = bitmap.data.subarray(bitmapLineStart, bitmapLineEnd) const bufferPlacement = dx * screen.byteDepth + screen.width * (dy + y) * screen.byteDepth if(handleTransparency) { const currentLine = buffer.subarray(bufferPlacement, bufferPlacement + line.length) for(let i=0; i < line.length; i += 4) { const transparency = line.readUInt8(i+3) / 255 if(transparency < 1) { /** @type { Uint8Array } */ const newLine = new Uint8Array(3) for(let j=0; j < 3; j++) { newLine[j] = (currentLine.readUInt8(j+i)*1-transparency + line.readUInt8(j+i)*transparency) } line.set(newLine, i) } } } buffer.set(Array.from(line), bufferPlacement) } } /** * @param { number } x * @param { number } y * @param { string } filepath * @description this function work fine but will use caching. to clear the cache you can use removeFromCache(filepath) or you can keep the cache on the * client side and use readImgFile and drawBitmap methods instead */ async function drawImgFile(x, y, filepath) { //not caching would imply reading the image each time we want to write it to the screen even for small movement let bitmap; if(!imageCache[filepath]) { bitmap = await readImgFile(filepath) imageCache[filepath] = bitmap } else { bitmap = imageCache[filepath] } drawBitmap(x, y, bitmap) } /** * @param { Buffer } buff */ function rgbaTObgra(buff) { for(let i=0; i < buff.length; i += 4) { const r = buff.readUInt8(i) const b = buff.readUInt8(i+2) buff.set([b], i) buff.set([r], i+2) } return buff } /** * @param { string } color * @returns { number[] } */ function parseHexColor(color) { if(!color.startsWith('#') || color.length !== 7) { throw new Error('invalid hex color code') } const r = parseInt(color.substring(1,2), 16) const v = parseInt(color.substring(3,4), 16) const b = parseInt(color.substring(5,6), 16) return [b, v, r, 0] } /** * * @param { number } x * @param { number } y * @param { string | number[] } colorCode */ function setPixel(x, y, colorCode) { if (screen.byteDepth !== 4) { throw new Error('Depth not supported yet') } if(typeof colorCode === 'string') { colorCode = parseHexColor(colorCode) } buffer.set(colorCode, (x + y * screen.width) * screen.byteDepth) } module.exports = { flip, clear, getScreenDimensions, drawImgFile, readImgFile, drawBitmap, parseHexColor, setPixel, removeFromCache }