//@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 { number } x * @param { number } y * @param { string } filepath * //!\\ THE CACHE DOESN'T CLEAN ITSELF UP //!\\ * once you finished displaying a image use removeFromCache to free the associated memory */ async function drawImgFile(x, y, filepath) { let bitmap //not caching would imply reading the image each time we want to write it to the screen even for small movement if(!imageCache[filepath]) { const img = await jimp.read(filepath) bitmap = img.bitmap bitmap.data = rgbaTObgra(bitmap.data) imageCache[filepath] = bitmap } else { bitmap = imageCache[filepath] } 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) buffer.set(Array.from(line), dx * screen.byteDepth + screen.width * (dy + y) * screen.byteDepth) } } /** * @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, parseHexColor, setPixel }