const fs = require('fs'); const { PNG } = require('pngjs'); const path = require('path'); const TRANSPARENT = { r: 0, g: 0, b: 0, a: 0 }; const WHITE = { r: 255, g: 255, b : 255, a: 255 }; const RED = { r: 255, g: 0, b: 0, a: 255 }; const TILE_WIDTH = 8; const TILE_HEIGHT = 8; // Helpers const pixelIsSame = (left, right, alpha) => { if(left.r !== right.r) return false; if(left.g !== right.g) return false; if(left.b !== right.b) return false; if(!alpha) return true; return left.a === right.a; } const imageOut = (pixels, width, fileName) => { const png = new PNG({ width, height: pixels.length / width }); pixels.forEach((pixel, i) => { const x = i % width; const y = (i - x) / width; const idx = (width * y + x) << 2; png.data[idx] = pixel.r; png.data[idx+1] = pixel.g; png.data[idx+2] = pixel.b; png.data[idx+3] = pixel.a; }); if(!fs.existsSync('out')) fs.mkdirSync('out'); const out = PNG.sync.write(png); fs.writeFileSync(path.join('out', fileName), out); } const imageIn = fileName => { const data = fs.readFileSync(fileName); const png = PNG.sync.read(data); const pixels = []; for(let y = 0; y < png.height; y++) { for (let x = 0; x < png.width; x++) { let idx = (png.width * y + x) << 2; const r = png.data[idx]; const g = png.data[idx+1]; const b = png.data[idx+2]; const a = png.data[idx+3]; let pixel = { r, g, b, a }; if(a === 0) { pixel = { ...WHITE }; } else { pixel.a = 255; } pixels.push(pixel); } } return { pixels, width: png.width, height: png.height }; } const tileFromPixel = (x, y, original) => { const byEightX = Math.floor(x / 8); const byEightY = Math.floor(y / 8); const byEightWidth = Math.floor(original.width / 8); const byEightId = byEightX + (byEightY * byEightWidth); return { x: byEightX, y: byEightY, columns: byEightWidth, id: byEightId }; } // Read Input File const original = imageIn('bruh.png'); const columns = (original.width / TILE_WIDTH); const rows = (original.height / TILE_HEIGHT); // Foreach pixel const palette = []; const paletteByEight = []; const withPaletteOverflows = []; for(let y = 0; y < original.height; y++) { for(let x = 0; x < original.width; x++) { const id = x + (y * original.width); const pixel = original.pixels[id]; let errorPixel = { ...pixel }; const tile = tileFromPixel(x, y, original); // Handle palettes if(pixel.a != 0) { const pb8 = (paletteByEight[tile.id] = paletteByEight[tile.id] || []); if(!pb8.some(p => pixelIsSame(pixel, p))) { pb8.push(pixel); } // Handle palette overflow if(pb8.length > 4) errorPixel = { ...RED }; // Append to palette if(!palette.some(p => pixelIsSame(pixel, p))) palette.push(pixel); } withPaletteOverflows.push(errorPixel); } } // Generate the palette set image const outPaletteByEight = []; let outByEightWidth = 1; paletteByEight.forEach((pal,y) => { pal.forEach((p,x) => { outByEightWidth = Math.max(outByEightWidth, x+1); }) }); paletteByEight.forEach((pal,y) => { for(let x = 0; x < outByEightWidth; x++) { outPaletteByEight.push(x >= pal.length ? TRANSPARENT : pal[x]); } }); // Now determine for each TILE what palette to use. const paletteGroups = []; const gbVersion = []; const paletteImage = []; for(let y = 0; y < original.height; y++) { for(let x = 0; x < original.width; x++) { const id = x + (y * original.width); const pixel = original.pixels[id]; const tile = tileFromPixel(x, y, original); // Get the palette const paletteSet = paletteByEight[tile.id]; // Check for matching let palId = paletteGroups.findIndex(pg => { // Check for cases where one of the pallet group palettes may have // less pixels than the current set we're checking, e.g. we do a tile that // has only two colors, then we iterate over a tile with 4 colors that has // two colors shared with that other tile. In that case we just add our // two extra colors. if(paletteSet.length > pg.length) { return pg.every(p => paletteSet.some(pss => pixelIsSame(pss, p))); } else { return paletteSet.every(p => pg.some(pgs => pixelIsSame(pgs, p))); } }); if(palId === -1) { palId = paletteGroups.length; paletteGroups.push(paletteSet); } const paletteGroupSet = paletteGroups[palId]; // This is where we correct the missing pixels if we share that tileset from // earlier paletteSet.forEach(ps => { const existing = paletteGroupSet.some(pgs => pixelIsSame(pgs, ps)); if(existing) return; paletteGroupSet.push(ps); }); // Sort the paletteGroupSet... const pgsGetWeight = thing => { return thing.r + thing.g + thing.b; } paletteGroupSet.sort((l,r) => { return pgsGetWeight(l) - pgsGetWeight(r); }); const examples = [ /* 0 */{ r: 0, g: 0, b: 0, a: 255 }, /* 1 */{ r: 255, g: 0, b: 0, a: 255 }, /* 2 */{ r: 0, g: 255, b: 0, a: 255 }, /* 3 */{ r: 0, g: 0, b: 255, a: 255 }, /* 4 */{ r: 255, g: 255, b: 0, a: 255 }, /* 5 */{ r: 255, g: 0, b: 255, a: 255 }, /* 6 */{ r: 0, g: 255, b: 255, a: 255 }, /* 7 */{ r: 255, g: 255, b: 255, a: 255 }, /* S */{ r: 100, g: 0, b: 0, a: 255 }, /* S */{ r: 0, g: 100, b: 0, a: 255 }, /* S */{ r: 0, g: 0, b: 100, a: 255 }, /* S */{ r: 100, g: 100, b: 0, a: 255 }, /* S */{ r: 0, g: 100, b: 100, a: 255 }, /* S */{ r: 100, g: 0, b: 100, a: 255 }, /* S */{ r: 100, g: 100, b: 100, a: 255 }, ]; paletteImage.push(examples[palId]); const pixelIndex = paletteGroupSet.findIndex(ps => pixelIsSame(ps, pixel)); const nonColor = [ { r: 8, g: 24, b: 32, a: 255 }, { r: 52, g: 104, b: 86, a: 255 }, { r: 136, g: 192, b: 112, a: 255 }, { r: 224, g: 248, b: 208, a: 255 } ]; gbVersion.push(nonColor[pixelIndex % nonColor.length]); } } console.log('Found', paletteGroups.length, 'palettes'); imageOut(original.pixels, original.width, 'original.png'); imageOut(withPaletteOverflows, original.width, 'errors.png'); imageOut(palette, palette.length, 'palette.png'); imageOut(outPaletteByEight, outByEightWidth, 'paletteByEight.png'); imageOut(paletteImage, original.width, 'palettes.png'); imageOut(gbVersion, original.width, 'gameboy.png'); // Now generate the GB files // let rearranged = []; // let n = 0; // for(let i = 0; i < columns * rows; i++) { // const tileX = i % columns; // const tileY = Math.floor(i / columns) % rows; // for(let y = 0; y < TILE_HEIGHT; y++) { // for(let x = 0; x < TILE_WIDTH; x++) { // const px = (tileX * TILE_WIDTH) + x; // const py = (tileY * TILE_HEIGHT) + y; // const pi = (py * png.width) + px; // rearranged[n++] = original.pixels[pi]; // } // } // } // // Now turn into a tileset // const bits = []; // for(let i = 0; i < rearranged.length; i += TILE_WIDTH) { // let lowBits = 0x00; // let highBits = 0x00; // for(let j = 0; j < TILE_WIDTH; j++) { // const pixel = rearranged[i + j]; // lowBits = lowBits | ((pixel & 0x01) << (7-j)); // highBits = highBits | ((pixel & 0x02) >> 1 << (7-j)); // } // bits.push(lowBits, highBits); // }