Skip to content

Commit

Permalink
Create RGB images feature
Browse files Browse the repository at this point in the history
  • Loading branch information
HerrZatacke authored Jan 28, 2025
1 parent 60d05c4 commit 6186aa2
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ Webserver will be available at http://192.168.7.1/
Frontend code development requires node.js (>=20)
* Navigate to the `frontend` folder.
* run `npm install` to install all dependencies
* run `npm run dev` to start a local dev server on [127.0.0.1:3000](http://127.0.0.1:3000/). The server also does proxy the `/list.json`, `/status.json` and `/download` endpoints from a Pico which must be connected to the same machine.
* run `npm run dev` to start a local dev server on [127.0.0.1:3000](http://127.0.0.1:3000/). The server also does proxy the `/status.json` and `/download` endpoints from a Pico which must be connected to the same machine.
* run `npm run build` to build the static files (html/css/js). Files will be built to `./fs`
* When building the rom file locally, also run `./regen-fsdata.sh`
1 change: 1 addition & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h1>Pico Game Boy Printer</h1>
<button id="delete_selected_btn" disabled><span>Delete</span></button>
<button id="average_selected_btn" disabled><span>Average</span></button>
<button id="gif_selected_btn" disabled><span>Animate</span></button>
<button id="rgb_selected_btn" disabled><span>RGB</span></button>
<label class="select">
<select id="download_size">
<option value="1">Scale: 1x</option>
Expand Down
28 changes: 24 additions & 4 deletions frontend/src/functions/canvas/imageDatasToBlob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const imageDatasToBlob = (frames: ImageData[], fps: number): Blob => {
const buf: number[] = [];
const gifWriter: GifWriter = new GifWriter(buf, frames[0].width, frames[0].height, { loop: 0xffff });

let frameCount = 0;

for (const frame of frames) {
const {
palette,
Expand All @@ -23,15 +25,33 @@ export const imageDatasToBlob = (frames: ImageData[], fps: number): Blob => {
acc.palette.push(color);
}

if (colorIndex > 256) {
colorIndex = 0;
}

acc.pixels.push(colorIndex);

return acc;
}, { pixels: [], palette: [] });

gifWriter.addFrame(0, 0, frame.width, frame.height, pixels, {
delay: Math.round(100 / fps),
palette,
});
const p = [
...palette,
...(new Array(256).fill(0)),
].slice(0, 256);

if (palette.length <= 256) {
frameCount += 1;
gifWriter.addFrame(0, 0, frame.width, frame.height, pixels, {
delay: Math.round(100 / fps),
palette: p,
});
}
}

if (frameCount !== frames.length) {
const msg = 'Some frames in your image contain more than 256 colors, which makes creating a GIF impossible';
alert(msg);
throw new Error(msg);
}

const bufferSize = gifWriter.end();
Expand Down
58 changes: 58 additions & 0 deletions frontend/src/functions/gallery/buttons.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import chunk from 'chunk';
import { LOCALSTORAGE_FPS_KEY, LOCALSTORAGE_SCALE_KEY } from '../../consts.ts';
import { imageDatasToBlob } from '../canvas/imageDatasToBlob.ts';
import { DataType, DbAccess } from '../storage/database.ts';
Expand All @@ -8,6 +9,7 @@ const deleteSelectedBtn = document.getElementById("delete_selected_btn") as HTML
const selectAllBtn = document.getElementById("select_all_btn") as HTMLButtonElement;
const averageSelectedBtn = document.getElementById("average_selected_btn") as HTMLButtonElement;
const gifSelectedBtn = document.getElementById("gif_selected_btn") as HTMLButtonElement;
const rgbSelectedBtn = document.getElementById("rgb_selected_btn") as HTMLButtonElement;
const scaleSelect = document.getElementById("download_size") as HTMLSelectElement;
const fpsSelect = document.getElementById("download_fps") as HTMLSelectElement;

Expand All @@ -18,6 +20,7 @@ export const updateButtons = () => {
deleteSelectedBtn.disabled = numSelectedItems < 1;
averageSelectedBtn.disabled = numSelectedItems < 2 || numSelectedItemsFinal !== 0;
gifSelectedBtn.disabled = numSelectedItems < 2 || numSelectedItemsFinal !== 0;
rgbSelectedBtn.disabled = numSelectedItems !== 3 || numSelectedItemsFinal !== 0;

scaleSelect.value = localStorage.getItem(LOCALSTORAGE_SCALE_KEY) || '1';
fpsSelect.value = localStorage.getItem(LOCALSTORAGE_FPS_KEY) || '12';
Expand Down Expand Up @@ -191,6 +194,61 @@ export const initButtons = (store: DbAccess) => {
});
});

rgbSelectedBtn.addEventListener('click', async () => {
const items = [...gallery.querySelectorAll('.marked-for-action')] as HTMLDivElement[];

if (items.length !== 3) {
return;
}

items.sort(sortBySelectionOrder((gallery.children.length + 1).toString(10)));

const images = items.map((item) => item.querySelector('img')) as HTMLImageElement[];

const dimensions = getCommonSize(images);

if (!dimensions) {
alert("Image dimensions must be the same to create a RGB image");
return;
}

const [pixelsR, pixelsG, pixelsB]: number[][][] = images.map((imageSource): number[][] => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
canvas.width = dimensions.width;
canvas.height = dimensions.height;
ctx.drawImage(imageSource, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return chunk(imageData.data, 4);
});

const pixels = new Array(dimensions.width * dimensions.height)
.fill(null)
.map((_, pixelIndex) => {
const [rr, rg, rb] = pixelsR[pixelIndex];
const [gr, gg, gb] = pixelsG[pixelIndex];
const [br, bg, bb] = pixelsB[pixelIndex];

const r = Math.floor((rr + rg + rb) / 3);
const g = Math.floor((gr + gg + gb) / 3);
const b = Math.floor((br + bg + bb) / 3);

return [r, g, b, 255];
})
.flat(1);

const rgbImageData = new ImageData(new Uint8ClampedArray(pixels), dimensions.width, dimensions.height);

store.add({
type: DataType.IMAGE_DATA,
timestamp: Date.now(),
data: rgbImageData,
});

unselectAll();
});


scaleSelect.addEventListener('change', () => {
const scale = parseInt(scaleSelect.value || '0', 10) || 1;
localStorage.setItem(LOCALSTORAGE_SCALE_KEY, scale.toString(10));
Expand Down
17 changes: 17 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import * as fs from 'fs';
import * as path from 'path';
import { defineConfig } from 'vite';
import { $fetch } from 'ofetch';

const input = fs.readdirSync(__dirname)
.reduce((acc: string[], file) => {
const filePath = path.join(__dirname, file);
const stats = fs.statSync(filePath);

if (!stats.isDirectory() && path.extname(file) === ".html") {
return [...acc, path.resolve(filePath)];
}

return acc;
}, []);

export default defineConfig({
build: {
outDir: '../fs/',
emptyOutDir: true,
rollupOptions: {
input,
}
},
plugins: [
{
Expand Down

0 comments on commit 6186aa2

Please sign in to comment.