Well, so this is my implementation. First I made a CLI command to read all .bmp files in a directory and create one binary file containing all frames chained, where each byte contains 8 pixels.
Since JUCE doesn’t read BMP files, I used this one-header library.
#include <JuceHeader.h>
using namespace juce;
#define LOADBMP_IMPLEMENTATION
#include "./loadbmp.h"
int main (int argc, char* argv[])
{
String Dir = "./"; // default current directory
if (argc > 1) // get directory from first optinoal argument
Dir = String(argv[1]);
File fDir(Dir);
if (!fDir.isDirectory())
{
printf("Please specify a directory\n");
return 0;
}
int fileCount = 0;
auto fileList = fDir.findChildFiles(File::TypesOfFileToFind::findFiles | File::TypesOfFileToFind::ignoreHiddenFiles, false);
if (fileList.size() == 0)
{
printf("The directory is empty\n");
return 0;
}
Array<uint8> animData;
for (auto f : fileList)
{
// Skip non BMP files
if (!f.getFileExtension().endsWithIgnoreCase("bmp"))
continue;
auto cFile = (Dir + f.getFileName()).toStdString();
// Read bitmap file
unsigned char* BMP = nullptr;
unsigned int width, height;
unsigned int err = loadbmp_decode_file(cFile.c_str(), &BMP, &width, &height, LOADBMP_RGB);
if (err) { printf("Skipping File: %s - LoadBMP Load Error: %u\n", cFile.c_str(), err); continue; }
printf("Loading file # %d: %s\n", fileCount, f.getFileName().toStdString().c_str());
// Get ready to encode it
uint8 byte = 0;
int bit = 0;
for (unsigned int i = 0; i < (width * height); ++i)
{
auto R = *BMP++;
auto G = *BMP++;
auto B = *BMP++;
auto c = Colour::fromRGB(R, G, B);
byte ^= ((c.getBrightness() > 0.5f ? 1 : 0) << bit++);
// Byte complete, store it into the array and prepare next byte
if (bit > 7)
{
animData.add(byte);
bit = 0;
byte = 0;
}
}
fileCount++;
}
if (fileCount == 0)
printf("No BMP files have been found\n");
else
{
File animFile(Dir + "/Animation.bin");
animFile.deleteFile();
if (animFile.create().ok())
animFile.replaceWithData(animData.data(), animData.size());
}
return 0;
}
The resulting Animation.bin file can be zipped and added to the resources of the destination project.
And here’s the reader/player:
// Class members:
int FilmFrame = 0;
int FilmFrames = 0;
MemoryBlock animData;
std::unique_ptr<Image> FilmBMP;
// On load
// Get zipped animation file from the resources
int size(0);
auto data = BinaryData::getNamedResource(String("Animation.zip").replace(".", "_").replace("-", "").toRawUTF8(), size);
if (size != 0)
{
// Unzip into a MemoryBlock
ZipFile zip(MemoryInputStream(data, size, false));
std::unique_ptr<InputStream> is(zip.createStreamForEntry(0));
is->readIntoMemoryBlock(animData, is->getTotalLength());
// Prepare the frame Image
FilmBMP.reset(new Image(Image::ARGB, 1024, 512, true));
// Get the number of frames
FilmFrames = is->getTotalLength() / (1024 * 512 / 8);
}
// repaint() is called by a Timer every 40 milliseconds (25 FPS)
void paint(Graphics& g) override
{
g.fillAll(Colours::black);
// Create some fancy background...
// ...
// ...
// Extract the next frame from the animation file
static constexpr int dataLen = 1024 * 512 / 8;
int dataStart = FilmFrame * dataLen;
int dataEnd = dataStart + dataLen;
int x = 0, y = 0;
// Frames are 2-colors only, where each bit in a byte represents black or white
for (int b = dataStart; b < dataEnd; b++)
{
// Copy the pixels to the Image object
auto byte = ((int8*)animData.getData())[b];
for (int bit = 0; bit < 8; bit++)
{
// Make white transparant and paint only the black color
auto col = ((byte >> bit) & 1) ? Colours::transparentWhite : Colours::black;
FilmBMP->setPixelAt(x, y, col);
if (++x >= 1024) { x = 0; ++y; }
}
}
// Print the image
g.drawImageAt(*FilmBMP.get(), 0, 0);
// Prepare next frame and loop at end
if (++FilmFrame >= FilmFrames)
FilmFrame = 0;
}
This way I can have some 500 frames of 1024x512 in size in a 3 MB zipped file and take about 32 MB of RAM during playback.