Possibly a simple question: How do I stream a picture from SD to the screen buffer?
Mmmm ⌠not sure why I couldnât remember this. For anyone else who wants to know, here is how I did it. I am using Mode 15 (220 x 176 , 16 colour). You will have to change the dimensions and the divisor for other modes. If using 256 colours you do not need to divide by 2 at all. If you are using 4 colour then divide by 4.
File file;
if (file.openRO("music/title.img")) {
file.read(PD::getBuffer(), 2 + (220 * 176 / 2));
}
How do you create that image?
I havenât tested this, it may have worked some time in the past, I cut/pasted it out of an old projectâŚ
Itâs using mode 13 (8bit 220x88) and loads a standard windows bitmap also saved as 8bit. There is no error checking or any sort of verification, so if the format is not correct you will get a mess.
#include "Pokitto.h"
#include "SDFileSystem.h"
unsigned short pal[256]; // assign a 256 entry array to hold the palette
unsigned char texture[110*88] __attribute__ ((aligned (4))); // texture for covers
void readimage(const char * filename){
uint32_t myFile = fileOpen("bitmap.bmp", FILE_MODE_READONLY);
if( !myFile ){
char temp=0;
for(int t=0; t<54; t++){
fileReadBytes(&temp, 1);
}
unsigned char col[3];
for(int temp=0; temp<256; temp++){
fileReadBytes(&col[0], 3);
pal[temp] = (col[0]>>3) | ((col[1] >> 2) << 5) | ((col[2] >> 3) << 11);
}
Pokitto::Display::load565Palette(&pal[0]); // load a palette the same way as any other palette in any other screen mode
Pokitto::Display::print("Thing\n");
Pokitto::Display::print("Read Pic\n");
fread(texture, 1, 110*88, file);
Pokitto::Display::print("Draw Pic\n");
Pokitto::Display::drawBitmap(0, 0, texture);
fileClose();
} // if myFile
}
int main(){
using PC=Pokitto::Core;
using PD=Pokitto::Display;
PC::begin();
PD::persistence = true;
PD::invisiblecolor = 0;
readimage("bitmap.bmp");
while( PC::isRunning() ){
if( !PC::update() )
continue;
}
return 0;
}
I create them using a script that I think you provided! If it wasnât you, then it was @carbonacat ⌠I have actually forgotten who gave it to me.
I will publish the code that I am using with apologies to the author (I am sure if they gave it to me they are happy for others to use it as well ) In addition to Node.js, you will need to install canvas
using the command $ npm install canvas
/* Usage: node gfx.js file.png w h output */
const fs = require("fs"); const Canvas = require('canvas'); const Image = Canvas.Image;
/* Pico-8 */
const palette = [
0, 0, 0,
29, 43, 83,
126, 37, 83,
0, 135, 81,
171, 82, 54,
95, 87, 79,
194, 195, 199,
255, 241, 232,
255, 0, 77,
255, 163, 0,
255, 236, 39,
0, 228, 54,
41, 173, 255,
131, 118, 156,
255, 119, 168,
255, 204, 170
];
let file = process.argv[2]; w = parseInt(process.argv[3]); h = parseInt(process.argv[4]);
if( !file ) {
console.log("Usage: node gfx.js file.png w h output");
}
else {
if (process.argv[5] == "") {
fs.writeFileSync( `${file.replace(/\..*$/, '')}.img`, Buffer.from( img4bpp( file, w, h ) ) );
}
else {
fs.writeFileSync(process.argv[5], Buffer.from( img4bpp( file, w, h ) ) );
}
console.log( `${file.replace(/\..*$/, '')}.img`);
}
function img4bpp( path, w, h ){
const canvas = Canvas.createCanvas(w, h);
const ctx = canvas.getContext('2d');
const image = new Image(); image.src = fs.readFileSync( path );
ctx.drawImage( image, 0, 0, w, h );
const buf = canvas.toBuffer('raw');
const pixel = [0,0,0];
const infl = [];
infl.push( w / 16 ); infl.push( w % 16 ); infl.push( h / 16 ); infl.push( h % 16 );
let line = '';
for( let i=0; i<buf.length; ++i ){
let j;
pixel[2] = buf[i++];
pixel[1] = buf[i++];
pixel[0] = buf[i++];
let minind = 999;
for( j=0; j<palette.length / 3; ++j ){
if (Math.abs(pixel[0] - palette[j * 3]) < 4 && Math.abs(pixel[1] - palette[(j * 3) + 1]) < 4 && Math.abs(pixel[2] - palette[(j * 3) + 2]) < 4) {
minind = j;
break;
}
}
if (minind == 999) {
console.log("%i %i %i", w, h, buf.length); console.log("I: %i, X: %i, Y: %i - %i,%i,%i", i, ((i/4)%w), ((i/4)/w), pixel[0], pixel[1], pixel[2] );
}
infl.push( minind );
line += minind.toString(16); if (line.length == w ){ if (w % 2 == 1) {
infl.push( 0 ); }
// console.log(line);
line = ''; }
}
const out = [];
for( let i=0; i<infl.length; ) {
let hi = infl[i++];
let lo = infl[i++];
out.push( (hi<<4) | lo );
}
return out;
}
As you can see at the top, I have hard coded the Pico-8 palette but you can alter this to your own needs.
If a colour cannot be matched, it logs out the coordinates of the pixel and continues on. This allows you to go back into the picture in FemtoIDE and recolour the area with a colour from the palette.
I vaguely recall the original using a âclose enoughâ algorithm when correlating colours from the image to indexes in the palette. I recall that I was getting incorrect results so I removed that logic.
Finally, this version of the code puts the width and height as the first two bytes of the output file which makes sense if you are reading the file into an array and then are using the PD:drawBitmap()
command. If you are rendering it directly to the screen buffer, remove the width and height from the output stream.
Alternatively, you can just âburnâ the first two bytes like this:
if (file.openRO("music/Title.img")) {
file.read(PD::getBuffer(), 2);
file.read(PD::getBuffer(), (220 * 176 / 2));
}
Definitely not from me!
(Also, technically you donât need that canvas nodejs dependency, you can just create a canvas element for such an operation - @FManga does it in various scripts including the one converting all .png to .h files in femtoide)
Yup, probably was me, just I forgot. But the original author was @FManga I suppose, it comes from Abbayes porting effort.
I must check if I add this kind of output also to my img2pok toolâŚ
Ahh ⌠I thought it came from you but when you asked the question, I figured I had it wrong.
Some people could ⌠but not me. I know so little about NodeHS!
The next step could be find a way to load a picture from sd to flash memory, and so have variable resources loaded on demand.
I think @FManga exposed this idea a long time ago.
Its easy to load a picture into flash memory.
File file;
uint8_t imgBuffer[2 + (220 * 176 / 2)];
if (file.openRO("music/title.img")) {
file.read(buffer, 2 + (220 * 176 / 2));
}
I assume this is what you mean? As for loading them âon demandâ you could wrap the loading into a function and pass a value representing the image you want.
enum class Images : uint8_t {
Splash,
Title,
...
};
Images bufferedImage
function drawBitmap(Images imageToRender, int16_t x, int16_t y, uint8_t w, uint8_t h) {
File file;
uint8_t imgBuffer[2 + (220 * 176 / 2)];
char fileName[19] = { 'm', 'u', 's', 'i', 'c', '/', 'G', 'A', 'M', 'E', '_', 0', '0', '0', '.', 'i', 'm', 'g', ' ' };
// Only refresh the image buffer if it is not already loaded ..
if (bufferedImage != imageIndex) {
uint8_t imageIndex = static_cast<uint8_t>(imageToRender);
fileName[11] = 48 + (imageIndex / 100);
fileName[12] = 48 + ((imageIndex - ((imageIndex / 100) * 100)) / 10);
fileName[13] = 48 + (imageIndex % 10);
fileName[18] = 0;
if (file.openRO(filename)) {
file.read(buffer, 2 + (w * h/ 2));
}
bufferedImage = imageToRender;
}
PD::drawBitmap(x, y, buffer);
}
Totally untested - it may not even compile!
For that to work youâd have to make the buffer statically allocated (i.e. global or a static
local), otherwise the array will just be overwritten with nonsense from the stack. (Not to mention an array that size could potentially upset the stack.)
While Iâm at it, you could also save a bit of effort by initialising the file name with a string literal (which will be copied into the array, and the size of the array will be inferred) and slightly improve flexibility by using std::size
with negative offsets to index the array, so anything prior to the numeric suffix can be edited without breaking the code.
E.g.
// Need a header of some kind for std::size
#include <array>
enum class Image : std::uint8_t
{
None,
Splash,
Title,
// ...
};
// Need a 'none' value to indicate 'nothing loaded yet'
Image bufferedImage = Image::None;
std::uint8_t buffer[2 + (220 * 176 / 2)] {};
void drawBitmap(Image imageToRender, std::int16_t x, std::int16_t y, std::uint8_t width, std::uint8_t height)
{
using Pokitto::Display;
// Only refresh the image buffer if it is not already loaded ...
if (bufferedImage != imageIndex)
{
uint8_t imageIndex = static_cast<std::uint8_t>(imageToRender);
char fileName[] = "music/GAME_000.img";
fileName[std::size(fileName) - 7] = ('0' + ((imageIndex / 100) % 10));
fileName[std::size(fileName) - 8] = ('0' + ((imageIndex / 10) % 10));
fileName[std::size(fileName) - 9] = ('0' + ((imageIndex / 1) % 10));
File file;
if (file.openRO(filename))
file.read(buffer, 2 + (width * height / 2));
bufferedImage = imageToRender;
}
Display::drawBitmap(x, y, buffer);
}
Also untested of course, but I canât immediately see a reason why it wouldnât work.
(If youâre worried about speed with the BCD conversion, try a âdouble-dabbleâ.)
I would have it as a global along with the bufferedImage
var.
The solution might change further based on the naming convention so indexing the filename from the front or negatively would be appropriate in certain cases.
Pretty sure
fileName[std::size(fileName) - 7] = ('0' + ((imageIndex / 100) % 10));
fileName[std::size(fileName) - 8] = ('0' + ((imageIndex / 10) % 10));
fileName[std::size(fileName) - 9] = ('0' + ((imageIndex / 1) % 10));
needs to actually be:
fileName[std::size(fileName) - 9] = ('0' + ((imageIndex / 100) % 10));
fileName[std::size(fileName) - 8] = ('0' + ((imageIndex / 10) % 10));
fileName[std::size(fileName) - 7] = ('0' + ((imageIndex / 1) % 10));
Well sure, if you want to do it the boring old âmost significant digit firstâ way. :P
Yes, I guess its a silly convention isnât it?