Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

gb-image-decoder

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gb-image-decoder - npm Package Compare versions

Comparing version
1.3.1
to
2.0.0
+366
-480
dist/index.cjs
'use strict';
const ohash = require('ohash');
var BlendMode = /* @__PURE__ */ ((BlendMode2) => {

@@ -62,6 +64,3 @@ BlendMode2["NORMAL"] = "normal";

const TILES_PER_COLUMN = 18;
const FRAME_SIZE = 2;
const FRAME_TILES = FRAME_SIZE * 2;
const DEFAULT_FULL_PIXEL_HEIGHT = TILES_PER_COLUMN * TILE_PIXEL_HEIGHT;
const DEFAULT_FULL_PIXEL_WIDTH = TILES_PER_LINE * TILE_PIXEL_WIDTH;
const FRAME_WIDTH = 2;
const BW_PALETTE = [16777215, 11184810, 5592405, 0];

@@ -71,7 +70,8 @@ const BW_PALETTE_HEX = ["#ffffff", "#aaaaaa", "#555555", "#000000"];

const WHITE_LINE = Array(TILES_PER_LINE).fill(WHITE);
const defaultPalette = {
r: [0, 84, 172, 255],
g: [0, 84, 172, 255],
b: [0, 84, 172, 255],
n: [0, 85, 170, 255],
const RGBN_SHADES = [0, 85, 170, 255];
const defaultRGBNPalette = {
r: RGBN_SHADES,
g: RGBN_SHADES,
b: RGBN_SHADES,
n: RGBN_SHADES,
blend: BlendMode.MULTIPLY

@@ -113,14 +113,13 @@ };

const tileIndexIsPartOfFrame = (tileIndex, imageStartLine, handleExportFrame = ExportFrameMode.FRAMEMODE_KEEP) => {
const tileIndexIsPartOfFrame = (tileIndex, imageStartLine, handleExportFrame) => {
if (handleExportFrame === ExportFrameMode.FRAMEMODE_CROP) {
return false;
}
const checkIndex = tileIndex - (handleExportFrame === ExportFrameMode.FRAMEMODE_KEEP ? 0 : 20);
if (checkIndex < imageStartLine * 20) {
if (tileIndex < imageStartLine * 20) {
return true;
}
if (checkIndex >= imageStartLine * 20 + 280) {
if (tileIndex >= imageStartLine * 20 + 280) {
return true;
}
switch (checkIndex % 20) {
switch (tileIndex % 20) {
case 0:

@@ -136,14 +135,2 @@ case 1:

const calculateImageStartLine = (handleExportFrame, imageStartLine) => {
switch (handleExportFrame) {
case ExportFrameMode.FRAMEMODE_CROP:
return 0;
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE:
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
return 2;
case ExportFrameMode.FRAMEMODE_KEEP:
default:
return imageStartLine;
}
};
const getRGBValue = ({

@@ -153,9 +140,10 @@ pixels,

tileIndex,
imageStartLine,
handleExportFrame,
colorData,
frameColorData
imageContext: {
imageStartLine,
handleExportFrame,
imagePalette,
framePalette
}
}) => {
const calculatedImageStartLine = calculateImageStartLine(handleExportFrame, imageStartLine);
const palette = tileIndexIsPartOfFrame(tileIndex, calculatedImageStartLine, handleExportFrame) ? frameColorData : colorData;
const palette = tileIndexIsPartOfFrame(tileIndex, imageStartLine, handleExportFrame) ? framePalette : imagePalette;
const value = palette[pixels[index]];

@@ -172,270 +160,139 @@ return {

const createCanvasElement = () => {
try {
return document.createElement("canvas");
} catch (error) {
throw new Error("cannot create canvas element");
}
};
const createImageData = (rawImageData, width, height) => new ImageData(rawImageData, width, height);
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class Decoder {
constructor(options) {
__publicField$1(this, "canvas");
__publicField$1(this, "tiles");
__publicField$1(this, "colors");
__publicField$1(this, "frameColors");
__publicField$1(this, "rawImageData");
__publicField$1(this, "lockFrame");
__publicField$1(this, "colorData");
__publicField$1(this, "frameColorData");
__publicField$1(this, "tilesPerLine");
__publicField$1(this, "imageStartLine");
__publicField$1(this, "canvasCreator");
__publicField$1(this, "imageDataCreator");
this.canvas = null;
this.tiles = [];
this.colors = [];
this.frameColors = [];
this.rawImageData = null;
this.lockFrame = false;
this.colorData = [...BW_PALETTE];
this.frameColorData = [...BW_PALETTE];
this.tilesPerLine = options?.tilesPerLine || TILES_PER_LINE;
this.imageStartLine = 2;
this.canvasCreator = options?.canvasCreator || createCanvasElement;
this.imageDataCreator = options?.imageDataCreator || createImageData;
}
update({
canvas,
tiles,
palette,
framePalette,
imageStartLine = 2
}) {
const startLineChanged = this.setImageStartLine(imageStartLine);
const canvasChanged = canvas ? this.setCanvas(canvas) : false;
const usedFramePalette = this.tilesPerLine === TILES_PER_LINE ? framePalette : palette;
const palettesChanged = this.setPalettes(palette, usedFramePalette);
if (startLineChanged || canvasChanged || palettesChanged || !this.tiles.length) {
this.tiles = [];
class UrlCache {
static rendered = {};
static rendering = {};
async getUrl(hash) {
if (UrlCache.rendered.hasOwnProperty(hash)) {
return UrlCache.rendered[hash];
}
const tilesChanged = this.setTiles(tiles);
const newHeight = this.getHeight();
const newWidth = this.getWidth();
if (!this.canvas) {
return;
if (UrlCache.rendering.hasOwnProperty(hash)) {
const url = await UrlCache.rendering[hash];
return url;
}
if (newHeight === 0) {
this.canvas.height = 0;
return;
}
if (this.canvas.height !== newHeight || this.canvas.width !== newWidth || !this.rawImageData?.length) {
this.canvas.height = newHeight;
this.canvas.width = newWidth;
const newRawImageData = new Uint8ClampedArray(newWidth * newHeight * 4);
this.rawImageData?.forEach((value, index) => {
newRawImageData[index] = value;
});
this.rawImageData = newRawImageData;
}
tilesChanged.forEach(({ index, newTile }) => {
this.renderTile(index, newTile);
});
this.updateCanvas(newWidth, newHeight);
return null;
}
setImageStartLine(imageStartLine) {
if (this.imageStartLine === imageStartLine) {
return false;
async setUrl(hash, promise) {
if (UrlCache.rendered.hasOwnProperty(hash) || UrlCache.rendering.hasOwnProperty(hash)) {
console.warn("hash already exists!");
}
this.imageStartLine = imageStartLine;
return true;
UrlCache.rendering[hash] = promise;
UrlCache.rendered[hash] = await promise;
delete UrlCache.rendering[hash];
}
updateCanvas(newWidth, newHeight) {
if (!this.canvas || !this.rawImageData?.length) {
}
const toObjectUrl = async (canvas) => new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Could not generate Blob from canvas"));
return;
}
const context = this.canvas.getContext("2d");
const imageData = this.imageDataCreator(this.rawImageData, newWidth, newHeight);
context?.putImageData(imageData, 0, 0);
}
getScaledCanvas(scaleFactor, handleExportFrame = ExportFrameMode.FRAMEMODE_KEEP) {
let handleFrameMode = handleExportFrame;
if (this.tilesPerLine !== TILES_PER_LINE && handleFrameMode !== ExportFrameMode.FRAMEMODE_KEEP) {
handleFrameMode = ExportFrameMode.FRAMEMODE_KEEP;
try {
resolve(URL.createObjectURL(blob));
} catch (error) {
reject(error);
}
const {
initialHeight,
initialWidth,
tilesPerLine
} = this.getScaledCanvasSize(handleFrameMode);
const canvas = this.canvasCreator();
});
});
const dataUrlFromRawOutput = async ({
data,
dimensions: { width, height }
}, scaleFactor, hash, canvasCreator) => {
const urlCache = new UrlCache();
urlCache.setUrl(hash, new Promise((resolve) => {
const canvas = canvasCreator();
canvas.width = width * scaleFactor;
canvas.height = height * scaleFactor;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("no canvas context");
}
canvas.width = initialWidth * scaleFactor;
canvas.height = initialHeight * scaleFactor;
this.getExportTiles(handleFrameMode).forEach((tile, index) => {
this.paintTileScaled(decodeTile(tile), index, context, scaleFactor, tilesPerLine, handleFrameMode);
});
return canvas;
const imageData = new ImageData(data, canvas.width, canvas.height);
context?.putImageData(imageData, 0, 0);
resolve(toObjectUrl(canvas));
}));
const url = await urlCache.getUrl(hash);
if (!url) {
throw new Error("error generating image");
}
getScaledCanvasSize(handleExportFrame) {
const width = this.getWidth();
const height = this.getHeight();
switch (handleExportFrame) {
case ExportFrameMode.FRAMEMODE_KEEP:
return {
initialHeight: height,
initialWidth: width,
tilesPerLine: this.tilesPerLine
};
case ExportFrameMode.FRAMEMODE_CROP:
return {
initialHeight: DEFAULT_FULL_PIXEL_HEIGHT - TILE_PIXEL_HEIGHT * FRAME_TILES,
initialWidth: DEFAULT_FULL_PIXEL_WIDTH - TILE_PIXEL_WIDTH * FRAME_TILES,
tilesPerLine: TILES_PER_LINE - FRAME_TILES
};
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE:
return {
initialHeight: width,
initialWidth: width,
tilesPerLine: this.tilesPerLine
};
default:
throw new Error(`unknown export mode ${handleExportFrame}`);
}
return url;
};
const createCanvasElement = () => {
try {
return document.createElement("canvas");
} catch (error) {
throw new Error("cannot create canvas element");
}
getExportTiles(handleExportFrame) {
if (!this.tiles) {
throw new Error("no tiles to export");
}
switch (handleExportFrame) {
case ExportFrameMode.FRAMEMODE_KEEP:
return this.tiles;
case ExportFrameMode.FRAMEMODE_CROP:
return this.getCroppedTiles();
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
return [
...BLACK_LINE,
...this.getDefaultImageRange(),
...BLACK_LINE
];
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE:
return [
...WHITE_LINE,
...this.getDefaultImageRange(),
...WHITE_LINE
];
default:
throw new Error(`unknown export mode ${handleExportFrame}`);
}
};
const padLines = {
[ExportFrameMode.FRAMEMODE_SQUARE_BLACK]: BLACK_LINE,
[ExportFrameMode.FRAMEMODE_SQUARE_WHITE]: WHITE_LINE
};
const getPalettes = (imagePalette, framePalette, tilesPerLine) => {
if (tilesPerLine !== TILES_PER_LINE) {
return {
imagePalette,
framePalette: imagePalette
};
}
getCroppedTiles() {
return this.tiles.reduce((acc, tile, index) => tileIndexIsPartOfFrame(index, this.imageStartLine, ExportFrameMode.FRAMEMODE_KEEP) ? acc : [...acc, tile], []);
return {
imagePalette,
framePalette
};
};
const getDimensions$1 = (tilesLength, tilesPerLine) => ({
width: TILE_PIXEL_WIDTH * tilesPerLine,
height: TILE_PIXEL_HEIGHT * Math.ceil(tilesLength / tilesPerLine)
});
const getCroppedTiles = (tiles, imageContext) => ({
tiles: tiles.reduce((acc, tile, index) => tileIndexIsPartOfFrame(index, imageContext.imageStartLine, ExportFrameMode.FRAMEMODE_KEEP) ? acc : [...acc, tile], []),
dimensions: { width: 128, height: 112 },
contextUpdates: { tilesPerLine: 16, imageStartLine: 0 }
});
const getPaddedSquare = (tiles, padLine, imageContext) => {
let wholeImageStartLine = imageContext.imageStartLine - FRAME_WIDTH - 1;
const paddedTiles = [...tiles];
const square20by20tiles = TILES_PER_LINE ** 2;
while (paddedTiles.length < square20by20tiles || wholeImageStartLine < 0) {
paddedTiles.unshift(...padLine);
paddedTiles.push(...padLine);
wholeImageStartLine += 1;
}
// for wild frame image, this returns the part of theimage
// which has the image data in the default position by cropping
// away part of the wild frame which does not fit into 160x144
getDefaultImageRange() {
const wholeImageStartLine = this.imageStartLine - FRAME_SIZE;
const startIndex = wholeImageStartLine * TILES_PER_LINE;
return this.tiles.slice(startIndex, startIndex + TILES_PER_LINE * TILES_PER_COLUMN);
}
setCanvas(canvas) {
if (this.canvas === canvas) {
return false;
const startIndex = wholeImageStartLine * TILES_PER_LINE;
return {
tiles: paddedTiles.slice(startIndex, startIndex + square20by20tiles),
dimensions: { width: 160, height: 160 },
contextUpdates: { tilesPerLine: 20, imageStartLine: 3 }
};
};
const applyCrop = (tiles, imageContext) => {
const checkFrameMode = imageContext.tilesPerLine !== TILES_PER_LINE ? ExportFrameMode.FRAMEMODE_KEEP : imageContext.handleExportFrame;
switch (checkFrameMode) {
case ExportFrameMode.FRAMEMODE_KEEP: {
return {
tiles,
dimensions: getDimensions$1(tiles.length, imageContext.tilesPerLine),
contextUpdates: {}
};
}
this.canvas = canvas;
return true;
}
setPalettes(palette, framePalette) {
if (this.colors[0] === palette[0] && this.colors[1] === palette[1] && this.colors[2] === palette[2] && this.colors[3] === palette[3] && this.frameColors[0] === framePalette[0] && this.frameColors[1] === framePalette[1] && this.frameColors[2] === framePalette[2] && this.frameColors[3] === framePalette[3]) {
return false;
case ExportFrameMode.FRAMEMODE_CROP: {
return getCroppedTiles(tiles, imageContext);
}
this.colors = palette;
this.frameColors = framePalette;
this.colors.forEach((color, index) => {
this.colorData[index] = color.length !== 7 ? BW_PALETTE[index] : parseInt(color.substring(1), 16);
});
this.frameColors.forEach((color, index) => {
this.frameColorData[index] = color.length !== 7 ? BW_PALETTE[index] : parseInt(color.substring(1), 16);
});
return true;
}
setTiles(tiles) {
const changedTiles = tiles.reduce((acc, newTile, index) => {
const changed = newTile !== this.tiles[index];
if (!changed) {
return acc;
}
return [
...acc,
{
index,
newTile
}
];
}, []);
this.tiles = tiles;
return changedTiles;
}
renderTile(tileIndex, rawLine) {
if (rawLine === SKIP_LINE) {
this.paintTile(null, tileIndex);
} else {
const tile = decodeTile(rawLine);
this.paintTile(tile, tileIndex);
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE: {
const padLine = padLines[checkFrameMode];
return getPaddedSquare(tiles, padLine, imageContext);
}
default:
throw new Error(`unknown export mode ${imageContext.handleExportFrame}`);
}
// This paints the tile with a specified offset and pixel width
paintTile(pixels, index) {
if (!this.rawImageData) {
return;
}
const tileXOffset = index % this.tilesPerLine;
const tileYOffset = Math.floor(index / this.tilesPerLine);
const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset;
for (let x = 0; x < TILE_PIXEL_WIDTH; x += 1) {
for (let y = 0; y < TILE_PIXEL_HEIGHT; y += 1) {
const rawIndex = (pixelXOffset + x + (pixelYOffset + y) * this.tilesPerLine * TILE_PIXEL_WIDTH) * 4;
if (pixels !== null) {
const color = getRGBValue({
pixels,
index: y * TILE_PIXEL_WIDTH + x,
tileIndex: index,
imageStartLine: this.imageStartLine,
handleExportFrame: ExportFrameMode.FRAMEMODE_KEEP,
colorData: this.colorData,
frameColorData: this.frameColorData
});
this.rawImageData[rawIndex] = color.r;
this.rawImageData[rawIndex + 1] = color.g;
this.rawImageData[rawIndex + 2] = color.b;
this.rawImageData[rawIndex + 3] = 255;
} else {
this.rawImageData[rawIndex] = 0;
this.rawImageData[rawIndex + 1] = 0;
this.rawImageData[rawIndex + 2] = 0;
this.rawImageData[rawIndex + 3] = 0;
}
}
}
}
paintTileScaled(pixels, index, canvasContext, pixelSize, tilesPerLine, handleExportFrame) {
const tileXOffset = index % tilesPerLine;
const tileYOffset = Math.floor(index / tilesPerLine);
const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset * pixelSize;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset * pixelSize;
for (let x = 0; x < TILE_PIXEL_WIDTH; x += 1) {
for (let y = 0; y < TILE_PIXEL_HEIGHT; y += 1) {
};
const paintTile = (rawImageData, pixels, index, imageContext) => {
const tileXOffset = index % imageContext.tilesPerLine;
const tileYOffset = Math.floor(index / imageContext.tilesPerLine);
const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset;
for (let x = 0; x < TILE_PIXEL_WIDTH; x += 1) {
for (let y = 0; y < TILE_PIXEL_HEIGHT; y += 1) {
const rawIndex = (pixelXOffset + x + (pixelYOffset + y) * imageContext.tilesPerLine * TILE_PIXEL_WIDTH) * 4;
if (pixels !== null) {
const color = getRGBValue({

@@ -445,219 +302,233 @@ pixels,

tileIndex: index,
imageStartLine: this.imageStartLine,
handleExportFrame,
colorData: this.colorData,
frameColorData: this.frameColorData
imageContext
});
canvasContext.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
canvasContext.fillRect(
pixelXOffset + x * pixelSize,
pixelYOffset + y * pixelSize,
pixelSize + 1,
pixelSize + 1
);
rawImageData[rawIndex] = color.r;
rawImageData[rawIndex + 1] = color.g;
rawImageData[rawIndex + 2] = color.b;
rawImageData[rawIndex + 3] = 255;
} else {
rawImageData[rawIndex] = 0;
rawImageData[rawIndex + 1] = 0;
rawImageData[rawIndex + 2] = 0;
rawImageData[rawIndex + 3] = 0;
}
}
}
getHeight() {
return TILE_PIXEL_HEIGHT * Math.ceil(this.tiles.length / this.tilesPerLine);
};
const renderTile = (rawImageData, rawLine, index, imageContext) => {
if (rawLine === SKIP_LINE) {
paintTile(rawImageData, null, index, imageContext);
} else {
const tile = decodeTile(rawLine);
paintTile(rawImageData, tile, index, imageContext);
}
getWidth() {
return TILE_PIXEL_WIDTH * this.tilesPerLine;
};
const getFullParams$1 = (params) => ({
tiles: params.tiles,
imagePalette: params.imagePalette,
framePalette: params.framePalette || params.imagePalette,
imageStartLine: typeof params.imageStartLine === "number" ? params.imageStartLine : FRAME_WIDTH,
tilesPerLine: params.tilesPerLine || TILES_PER_LINE,
scaleFactor: params.scaleFactor || 1,
handleExportFrame: params.handleExportFrame || ExportFrameMode.FRAMEMODE_KEEP
});
const scaleRawImageData = (data, width, height, scale) => {
const newWidth = width * scale;
const newHeight = height * scale;
const scaled = new Uint8ClampedArray(newWidth * newHeight * 4);
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
const srcIndex = (y * width + x) * 4;
const r = data[srcIndex];
const g = data[srcIndex + 1];
const b = data[srcIndex + 2];
const a = data[srcIndex + 3];
for (let dy = 0; dy < scale; dy += 1) {
for (let dx = 0; dx < scale; dx += 1) {
const destX = x * scale + dx;
const destY = y * scale + dy;
const destIndex = (destY * newWidth + destX) * 4;
scaled[destIndex] = r;
scaled[destIndex + 1] = g;
scaled[destIndex + 2] = b;
scaled[destIndex + 3] = a;
}
}
}
}
}
const hx2 = (n) => (n || 0).toString(16).padStart(2, "0");
const paletteTemplates = {
r: (v) => `#${hx2(v)}0000`,
g: (v) => `#00${hx2(v)}00`,
b: (v) => `#0000${hx2(v)}`,
n: (v) => `#${hx2(v)}${hx2(v)}${hx2(v)}`
};
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
const createChannel = (key, decoderOptions) => {
const canvas = decoderOptions.canvasCreator();
const decoder = new Decoder(decoderOptions);
decoder.update({
framePalette: [],
canvas,
tiles: [],
palette: []
});
return {
key,
decoder,
canvas
data: scaled,
dimensions: {
width: newWidth,
height: newHeight
}
};
};
class RGBNDecoder {
constructor(options) {
__publicField(this, "canvas");
__publicField(this, "palette");
__publicField(this, "lockFrame");
__publicField(this, "channels");
__publicField(this, "tilesPerLine");
__publicField(this, "canvasCreator");
__publicField(this, "imageDataCreator");
this.canvas = null;
this.palette = defaultPalette;
this.lockFrame = false;
this.tilesPerLine = options?.tilesPerLine || TILES_PER_LINE;
this.canvasCreator = options?.canvasCreator || createCanvasElement;
this.imageDataCreator = options?.imageDataCreator || createImageData;
const channelDecoderOptions = {
tilesPerLine: this.tilesPerLine,
canvasCreator: this.canvasCreator,
imageDataCreator: this.imageDataCreator
};
this.channels = {
r: createChannel(ChannelKey.R, channelDecoderOptions),
g: createChannel(ChannelKey.G, channelDecoderOptions),
b: createChannel(ChannelKey.B, channelDecoderOptions),
n: createChannel(ChannelKey.N, channelDecoderOptions)
};
}
update({
canvas,
const getRawMonochromeImageData = (params) => {
const {
tiles,
palette,
lockFrame = false
}) {
const canvasChanged = canvas ? this.setCanvas(canvas) : false;
const paletteChanged = this.setPalette(palette);
const lockFrameChanged = this.setLockFrame(lockFrame);
const shouldUpdate = canvasChanged || paletteChanged || lockFrameChanged;
const canvases = this.setTiles(tiles);
const { width: newWidth, height: newHeight } = this.getDimensions(canvases);
if (!shouldUpdate) {
return;
}
if (!this.canvas) {
return;
}
if (newHeight === 0) {
this.canvas.height = 0;
return;
}
if (this.canvas.height !== newHeight) {
this.canvas.height = newHeight;
}
if (this.canvas.width !== newWidth) {
this.canvas.width = newWidth;
}
const context = this.canvas?.getContext("2d");
if (!context) {
return;
}
this.blendCanvases(context, canvases);
imageStartLine,
imagePalette,
framePalette,
tilesPerLine,
handleExportFrame,
scaleFactor
} = params;
const imageContext = {
tilesPerLine,
imageStartLine,
handleExportFrame,
...getPalettes(imagePalette, framePalette, tilesPerLine || TILES_PER_LINE)
};
const {
tiles: usedTiles,
dimensions: {
width,
height
},
contextUpdates
} = applyCrop(tiles, imageContext);
const updatedContext = {
...imageContext,
...contextUpdates
};
if (!height || !width) {
throw new Error("Image has no dimensions");
}
setPalette(palette) {
if (!palette) {
return false;
}
if (JSON.stringify(this.palette) === JSON.stringify(palette)) {
return false;
}
this.palette = palette;
return true;
const rawImageData = new Uint8ClampedArray(width * height * 4);
usedTiles.forEach((newTile, index) => {
renderTile(rawImageData, newTile, index, updatedContext);
});
return scaleRawImageData(rawImageData, width, height, scaleFactor);
};
const getMonochromeImageUrl = async (params, canvasCreator = createCanvasElement) => {
const urlCache = new UrlCache();
const fullParams = getFullParams$1(params);
const hash = ohash.hash(fullParams);
const cachedUrl = await urlCache.getUrl(hash);
if (cachedUrl) {
return cachedUrl;
}
setCanvas(canvas) {
if (this.canvas === canvas) {
return false;
}
this.canvas = canvas;
return true;
const rawOutput = getRawMonochromeImageData(fullParams);
return dataUrlFromRawOutput(rawOutput, fullParams.scaleFactor, hash, canvasCreator);
};
const getFullParams = (params) => ({
tiles: params.tiles,
palette: params.palette,
lockFrame: params.lockFrame || false,
imageStartLine: typeof params.imageStartLine === "number" ? params.imageStartLine : FRAME_WIDTH,
tilesPerLine: params.tilesPerLine || TILES_PER_LINE,
scaleFactor: params.scaleFactor || 1,
handleExportFrame: params.handleExportFrame || ExportFrameMode.FRAMEMODE_KEEP
});
const singleChannelPalette = (palette, channelKey) => {
switch (channelKey) {
case ChannelKey.R:
return palette.map((shade) => (shade & 255) << 16).reverse();
case ChannelKey.G:
return palette.map((shade) => (shade & 255) << 8).reverse();
case ChannelKey.B:
return palette.map((shade) => shade & 255).reverse();
default:
return palette.map((shade) => {
const base = shade & 255;
return (base << 16) + (base << 8) + base;
}).reverse();
}
setLockFrame(lockFrame) {
if (lockFrame !== this.lockFrame) {
this.lockFrame = lockFrame;
return true;
}
return false;
};
const getDimensions = (canvases) => Object.values(canvases).reduce((acc, current) => {
if (current.width === 0 || current.height === 0) {
return acc;
}
setTiles(tiles) {
return channels.reduce((acc, key) => {
const channel = this.channels[key];
channel.tiles = tiles[key];
const channelColors = this.palette[key];
const paletteFunction = paletteTemplates[key];
if (!channel.tiles || !channelColors) {
return acc;
}
const palette = [
paletteFunction(channelColors[3]),
paletteFunction(channelColors[2]),
paletteFunction(channelColors[1]),
paletteFunction(channelColors[0])
];
channel.decoder.update({
canvas: channel.canvas,
tiles: channel.tiles,
framePalette: this.lockFrame ? BW_PALETTE_HEX : palette,
palette
});
return {
...acc,
[key]: channel.canvas
};
}, {});
}
blendCanvases(targetContext, sourceCanvases) {
channels.forEach((key) => {
const sourceCanvas = sourceCanvases[key];
if (sourceCanvas && sourceCanvas.width && sourceCanvas.height) {
if (key === ChannelKey.N) {
if (this.palette.blend === BlendMode.NORMAL) {
return;
}
targetContext.globalCompositeOperation = blendModeNewName(this.palette.blend);
} else {
targetContext.globalCompositeOperation = "lighter";
return {
width: Math.max(current.width, acc.width),
height: Math.max(current.height, acc.height)
};
}, { width: 0, height: 0 });
const blendCanvases = (sourceCanvases, blendMode, canvasCreator) => {
const dimensions = getDimensions(sourceCanvases);
const targetCanvas = canvasCreator();
targetCanvas.width = dimensions.width;
targetCanvas.height = dimensions.height;
const targetContext = targetCanvas.getContext("2d");
channels.forEach((key) => {
const sourceCanvas = sourceCanvases[key];
if (sourceCanvas && sourceCanvas.width && sourceCanvas.height) {
if (key === ChannelKey.N) {
if (blendMode === BlendMode.NORMAL) {
return;
}
targetContext.drawImage(sourceCanvas, 0, 0);
targetContext.globalCompositeOperation = blendModeNewName(blendMode);
} else {
targetContext.globalCompositeOperation = "lighter";
}
targetContext.drawImage(sourceCanvas, 0, 0);
}
});
const imageData = targetContext.getImageData(0, 0, dimensions.width, dimensions.height);
return {
data: imageData.data,
dimensions: {
width: imageData.width,
height: imageData.height
}
};
};
const getRawRGBNImageData = (params, canvasCreator) => {
const {
tiles,
imageStartLine,
palette,
lockFrame,
tilesPerLine,
handleExportFrame,
scaleFactor
} = params;
const canvases = Object.entries(tiles).reduce((acc, [key, channelTiles]) => {
const channelKey = key;
const channelShades = palette[channelKey] || RGBN_SHADES;
const channelPalette = singleChannelPalette(channelShades, channelKey);
const lockFramePalette = lockFrame ? singleChannelPalette(RGBN_SHADES, channelKey) : channelPalette;
const {
data,
dimensions
} = getRawMonochromeImageData({
imagePalette: channelPalette,
framePalette: lockFramePalette,
handleExportFrame,
imageStartLine,
scaleFactor,
tiles: channelTiles,
tilesPerLine
});
}
getScaledCanvas(scaleFactor, handleExportFrame = ExportFrameMode.FRAMEMODE_KEEP) {
const canvas = this.canvasCreator();
const canvases = channels.reduce((acc, key) => {
const channel = this.channels[key];
const channelCanvas = channel.decoder.getScaledCanvas(scaleFactor, handleExportFrame);
return {
...acc,
[key]: channelCanvas
};
}, {});
const { width, height } = this.getDimensions(canvases);
canvas.width = width;
canvas.height = height;
const canvas = canvasCreator();
canvas.width = dimensions.width;
canvas.height = dimensions.height;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("no canvas context");
}
context.fillStyle = "#000000";
context.fillRect(0, 0, 500, 500);
this.blendCanvases(context, canvases);
return canvas;
const imageData = new ImageData(data, dimensions.width, dimensions.height);
context.putImageData(imageData, 0, 0);
return {
...acc,
[channelKey]: canvas
};
}, {});
return blendCanvases(canvases, palette.blend || BlendMode.NORMAL, canvasCreator);
};
const getRGBNImageUrl = async (params, canvasCreator = createCanvasElement) => {
const urlCache = new UrlCache();
const fullParams = getFullParams(params);
const hash = ohash.hash(fullParams);
const cachedUrl = await urlCache.getUrl(hash);
if (cachedUrl) {
return cachedUrl;
}
getDimensions(canvases) {
return Object.values(canvases).reduce((acc, current) => {
if (current.width === 0 || current.height === 0) {
return acc;
}
return {
width: Math.max(current.width, acc.width),
height: Math.max(current.height, acc.height)
};
}, { width: 0, height: 0 });
}
}
const rawOutput = getRawRGBNImageData(fullParams, canvasCreator);
return dataUrlFromRawOutput(rawOutput, fullParams.scaleFactor, hash, canvasCreator);
};
const maxTiles = (rgbnTiles) => Math.max(...channels.map((key) => rgbnTiles[key]?.length || 0));
exports.BLACK = BLACK;
exports.BLACK_LINE = BLACK_LINE;
exports.BW_PALETTE = BW_PALETTE;

@@ -667,10 +538,25 @@ exports.BW_PALETTE_HEX = BW_PALETTE_HEX;

exports.ChannelKey = ChannelKey;
exports.Decoder = Decoder;
exports.ExportFrameMode = ExportFrameMode;
exports.RGBNDecoder = RGBNDecoder;
exports.FRAME_WIDTH = FRAME_WIDTH;
exports.RGBN_SHADES = RGBN_SHADES;
exports.SKIP_LINE = SKIP_LINE;
exports.TILES_PER_COLUMN = TILES_PER_COLUMN;
exports.TILES_PER_LINE = TILES_PER_LINE;
exports.TILE_PIXEL_HEIGHT = TILE_PIXEL_HEIGHT;
exports.TILE_PIXEL_WIDTH = TILE_PIXEL_WIDTH;
exports.UrlCache = UrlCache;
exports.WHITE = WHITE;
exports.WHITE_LINE = WHITE_LINE;
exports.blendModeNewName = blendModeNewName;
exports.channels = channels;
exports.decodeTile = decodeTile;
exports.defaultRGBNPalette = defaultRGBNPalette;
exports.getDimensions = getDimensions$1;
exports.getMonochromeImageUrl = getMonochromeImageUrl;
exports.getRGBNImageUrl = getRGBNImageUrl;
exports.getRGBValue = getRGBValue;
exports.getRawMonochromeImageData = getRawMonochromeImageData;
exports.getRawRGBNImageData = getRawRGBNImageData;
exports.maxTiles = maxTiles;
exports.scaleRawImageData = scaleRawImageData;
exports.tileIndexIsPartOfFrame = tileIndexIsPartOfFrame;

@@ -31,23 +31,7 @@ declare enum BlendMode {

}
declare const channels: ChannelKey[];
interface Channel<DecoderType> {
key: ChannelKey;
tiles?: string[];
decoder: DecoderType;
canvas: HTMLCanvasElement;
}
type RGBNTiles = Partial<Record<ChannelKey, string[]>>;
type Channels<DecoderType> = Record<ChannelKey, Channel<DecoderType>>;
type SourceCanvases = Partial<Record<ChannelKey, HTMLCanvasElement>>;
type CanvasCreator = () => HTMLCanvasElement;
type ImageDataCreator = (rawImageData: Uint8ClampedArray, width: number, height: number) => ImageData;
interface ChangedTile {
index: number;
newTile: string;
}
interface ScaledCanvasSize {
initialHeight: number;
initialWidth: number;
tilesPerLine: number;
}
interface RGBValue {

@@ -67,70 +51,55 @@ r: number;

}
interface DecoderOptions {
interface BaseImageCreationParams<TilesType> {
tiles: TilesType;
imageStartLine?: number;
tilesPerLine?: number;
canvasCreator?: CanvasCreator;
imageDataCreator?: ImageDataCreator;
scaleFactor?: number;
handleExportFrame?: ExportFrameMode;
}
interface DecoderUpdateParams {
canvas: HTMLCanvasElement | null;
tiles: string[];
palette: string[];
framePalette: string[];
imageStartLine?: number;
interface MonochromeImageCreationParams extends BaseImageCreationParams<string[]> {
imagePalette: BWPalette;
framePalette?: BWPalette;
}
interface RGBNDecoderUpdateParams {
canvas: HTMLCanvasElement | null;
tiles: RGBNTiles;
type FullMonochromeImageCreationParams = Required<MonochromeImageCreationParams>;
interface RGBNImageCreationParams extends BaseImageCreationParams<RGBNTiles> {
palette: RGBNPalette;
lockFrame?: boolean;
}
declare class Decoder {
private canvas;
private tiles;
private colors;
private frameColors;
private rawImageData;
private lockFrame;
private colorData;
private frameColorData;
private tilesPerLine;
private imageStartLine;
private canvasCreator;
private imageDataCreator;
constructor(options?: DecoderOptions);
update({ canvas, tiles, palette, framePalette, imageStartLine, }: DecoderUpdateParams): void;
private setImageStartLine;
private updateCanvas;
getScaledCanvas(scaleFactor: number, handleExportFrame?: ExportFrameMode): HTMLCanvasElement;
private getScaledCanvasSize;
private getExportTiles;
private getCroppedTiles;
private getDefaultImageRange;
private setCanvas;
private setPalettes;
private setTiles;
private renderTile;
private paintTile;
private paintTileScaled;
private getHeight;
private getWidth;
type FullRGBNImageCreationParams = Required<RGBNImageCreationParams>;
interface PixelDimensions {
width: number;
height: number;
}
interface BaseImageContext {
tilesPerLine: number;
imageStartLine: number;
handleExportFrame: ExportFrameMode;
}
interface MonochromeImageContext extends BaseImageContext {
imagePalette: BWPalette;
framePalette: BWPalette;
}
interface CropResult<ContextType> {
tiles: string[];
dimensions: PixelDimensions;
contextUpdates: Partial<ContextType>;
}
interface RawOutput {
data: Uint8ClampedArray;
dimensions: PixelDimensions;
}
declare class RGBNDecoder {
private canvas;
private palette;
private lockFrame;
private channels;
private tilesPerLine;
private canvasCreator;
private imageDataCreator;
constructor(options?: DecoderOptions);
update({ canvas, tiles, palette, lockFrame, }: RGBNDecoderUpdateParams): void;
private setPalette;
private setCanvas;
private setLockFrame;
private setTiles;
private blendCanvases;
getScaledCanvas(scaleFactor: number, handleExportFrame?: ExportFrameMode): HTMLCanvasElement;
private getDimensions;
declare const getDimensions: (tilesLength: number, tilesPerLine: number) => PixelDimensions;
declare const scaleRawImageData: (data: Uint8ClampedArray, width: number, height: number, scale: number) => RawOutput;
declare const getRawMonochromeImageData: (params: FullMonochromeImageCreationParams) => RawOutput;
declare const getMonochromeImageUrl: (params: MonochromeImageCreationParams, canvasCreator?: CanvasCreator) => Promise<string>;
declare const getRawRGBNImageData: (params: FullRGBNImageCreationParams, canvasCreator: CanvasCreator) => RawOutput;
declare const getRGBNImageUrl: (params: RGBNImageCreationParams, canvasCreator?: CanvasCreator) => Promise<string>;
declare class UrlCache {
static rendered: Record<string, string>;
static rendering: Record<string, Promise<string>>;
getUrl(hash: string): Promise<string | null>;
setUrl(hash: string, promise: Promise<string>): Promise<void>;
}

@@ -140,20 +109,28 @@

declare const getRGBValue: ({ pixels, index, tileIndex, imageStartLine, handleExportFrame, colorData, frameColorData, }: {
declare const getRGBValue: ({ pixels, index, tileIndex, imageContext: { imageStartLine, handleExportFrame, imagePalette, framePalette, }, }: {
pixels: IndexedTilePixels;
index: number;
tileIndex: number;
imageStartLine: number;
handleExportFrame: ExportFrameMode;
colorData: BWPalette;
frameColorData: BWPalette;
imageContext: MonochromeImageContext;
}) => RGBValue;
declare const tileIndexIsPartOfFrame: (tileIndex: number, imageStartLine: number, handleExportFrame?: ExportFrameMode) => boolean;
declare const tileIndexIsPartOfFrame: (tileIndex: number, imageStartLine: number, handleExportFrame: ExportFrameMode) => boolean;
declare const maxTiles: (rgbnTiles: RGBNTiles) => number;
declare const BLACK = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
declare const WHITE = "00000000000000000000000000000000";
declare const SKIP_LINE = "skip";
declare const TILE_PIXEL_WIDTH = 8;
declare const TILE_PIXEL_HEIGHT = 8;
declare const TILES_PER_LINE = 20;
declare const TILES_PER_COLUMN = 18;
declare const FRAME_WIDTH = 2;
declare const BW_PALETTE: number[];
declare const BW_PALETTE_HEX: string[];
declare const BLACK_LINE: string[];
declare const WHITE_LINE: string[];
declare const RGBN_SHADES: number[];
declare const defaultRGBNPalette: RGBNPalette;
export { type BWPalette, BW_PALETTE, BW_PALETTE_HEX, BlendMode, type CanvasCreator, type ChangedTile, type Channel, ChannelKey, type Channels, Decoder, type DecoderOptions, type DecoderUpdateParams, ExportFrameMode, type ImageDataCreator, type IndexedTilePixels, RGBNDecoder, type RGBNDecoderUpdateParams, type RGBNPalette, type RGBNTiles, type RGBValue, SKIP_LINE, type ScaledCanvasSize, type SourceCanvases, blendModeNewName, decodeTile, getRGBValue, maxTiles, tileIndexIsPartOfFrame };
export { BLACK, BLACK_LINE, type BWPalette, BW_PALETTE, BW_PALETTE_HEX, type BaseImageContext, type BaseImageCreationParams, BlendMode, type CanvasCreator, ChannelKey, type CropResult, ExportFrameMode, FRAME_WIDTH, type FullMonochromeImageCreationParams, type FullRGBNImageCreationParams, type IndexedTilePixels, type MonochromeImageContext, type MonochromeImageCreationParams, type PixelDimensions, type RGBNImageCreationParams, type RGBNPalette, type RGBNTiles, RGBN_SHADES, type RGBValue, type RawOutput, SKIP_LINE, type SourceCanvases, TILES_PER_COLUMN, TILES_PER_LINE, TILE_PIXEL_HEIGHT, TILE_PIXEL_WIDTH, UrlCache, WHITE, WHITE_LINE, blendModeNewName, channels, decodeTile, defaultRGBNPalette, getDimensions, getMonochromeImageUrl, getRGBNImageUrl, getRGBValue, getRawMonochromeImageData, getRawRGBNImageData, maxTiles, scaleRawImageData, tileIndexIsPartOfFrame };

@@ -31,23 +31,7 @@ declare enum BlendMode {

}
declare const channels: ChannelKey[];
interface Channel<DecoderType> {
key: ChannelKey;
tiles?: string[];
decoder: DecoderType;
canvas: HTMLCanvasElement;
}
type RGBNTiles = Partial<Record<ChannelKey, string[]>>;
type Channels<DecoderType> = Record<ChannelKey, Channel<DecoderType>>;
type SourceCanvases = Partial<Record<ChannelKey, HTMLCanvasElement>>;
type CanvasCreator = () => HTMLCanvasElement;
type ImageDataCreator = (rawImageData: Uint8ClampedArray, width: number, height: number) => ImageData;
interface ChangedTile {
index: number;
newTile: string;
}
interface ScaledCanvasSize {
initialHeight: number;
initialWidth: number;
tilesPerLine: number;
}
interface RGBValue {

@@ -67,70 +51,55 @@ r: number;

}
interface DecoderOptions {
interface BaseImageCreationParams<TilesType> {
tiles: TilesType;
imageStartLine?: number;
tilesPerLine?: number;
canvasCreator?: CanvasCreator;
imageDataCreator?: ImageDataCreator;
scaleFactor?: number;
handleExportFrame?: ExportFrameMode;
}
interface DecoderUpdateParams {
canvas: HTMLCanvasElement | null;
tiles: string[];
palette: string[];
framePalette: string[];
imageStartLine?: number;
interface MonochromeImageCreationParams extends BaseImageCreationParams<string[]> {
imagePalette: BWPalette;
framePalette?: BWPalette;
}
interface RGBNDecoderUpdateParams {
canvas: HTMLCanvasElement | null;
tiles: RGBNTiles;
type FullMonochromeImageCreationParams = Required<MonochromeImageCreationParams>;
interface RGBNImageCreationParams extends BaseImageCreationParams<RGBNTiles> {
palette: RGBNPalette;
lockFrame?: boolean;
}
declare class Decoder {
private canvas;
private tiles;
private colors;
private frameColors;
private rawImageData;
private lockFrame;
private colorData;
private frameColorData;
private tilesPerLine;
private imageStartLine;
private canvasCreator;
private imageDataCreator;
constructor(options?: DecoderOptions);
update({ canvas, tiles, palette, framePalette, imageStartLine, }: DecoderUpdateParams): void;
private setImageStartLine;
private updateCanvas;
getScaledCanvas(scaleFactor: number, handleExportFrame?: ExportFrameMode): HTMLCanvasElement;
private getScaledCanvasSize;
private getExportTiles;
private getCroppedTiles;
private getDefaultImageRange;
private setCanvas;
private setPalettes;
private setTiles;
private renderTile;
private paintTile;
private paintTileScaled;
private getHeight;
private getWidth;
type FullRGBNImageCreationParams = Required<RGBNImageCreationParams>;
interface PixelDimensions {
width: number;
height: number;
}
interface BaseImageContext {
tilesPerLine: number;
imageStartLine: number;
handleExportFrame: ExportFrameMode;
}
interface MonochromeImageContext extends BaseImageContext {
imagePalette: BWPalette;
framePalette: BWPalette;
}
interface CropResult<ContextType> {
tiles: string[];
dimensions: PixelDimensions;
contextUpdates: Partial<ContextType>;
}
interface RawOutput {
data: Uint8ClampedArray;
dimensions: PixelDimensions;
}
declare class RGBNDecoder {
private canvas;
private palette;
private lockFrame;
private channels;
private tilesPerLine;
private canvasCreator;
private imageDataCreator;
constructor(options?: DecoderOptions);
update({ canvas, tiles, palette, lockFrame, }: RGBNDecoderUpdateParams): void;
private setPalette;
private setCanvas;
private setLockFrame;
private setTiles;
private blendCanvases;
getScaledCanvas(scaleFactor: number, handleExportFrame?: ExportFrameMode): HTMLCanvasElement;
private getDimensions;
declare const getDimensions: (tilesLength: number, tilesPerLine: number) => PixelDimensions;
declare const scaleRawImageData: (data: Uint8ClampedArray, width: number, height: number, scale: number) => RawOutput;
declare const getRawMonochromeImageData: (params: FullMonochromeImageCreationParams) => RawOutput;
declare const getMonochromeImageUrl: (params: MonochromeImageCreationParams, canvasCreator?: CanvasCreator) => Promise<string>;
declare const getRawRGBNImageData: (params: FullRGBNImageCreationParams, canvasCreator: CanvasCreator) => RawOutput;
declare const getRGBNImageUrl: (params: RGBNImageCreationParams, canvasCreator?: CanvasCreator) => Promise<string>;
declare class UrlCache {
static rendered: Record<string, string>;
static rendering: Record<string, Promise<string>>;
getUrl(hash: string): Promise<string | null>;
setUrl(hash: string, promise: Promise<string>): Promise<void>;
}

@@ -140,20 +109,28 @@

declare const getRGBValue: ({ pixels, index, tileIndex, imageStartLine, handleExportFrame, colorData, frameColorData, }: {
declare const getRGBValue: ({ pixels, index, tileIndex, imageContext: { imageStartLine, handleExportFrame, imagePalette, framePalette, }, }: {
pixels: IndexedTilePixels;
index: number;
tileIndex: number;
imageStartLine: number;
handleExportFrame: ExportFrameMode;
colorData: BWPalette;
frameColorData: BWPalette;
imageContext: MonochromeImageContext;
}) => RGBValue;
declare const tileIndexIsPartOfFrame: (tileIndex: number, imageStartLine: number, handleExportFrame?: ExportFrameMode) => boolean;
declare const tileIndexIsPartOfFrame: (tileIndex: number, imageStartLine: number, handleExportFrame: ExportFrameMode) => boolean;
declare const maxTiles: (rgbnTiles: RGBNTiles) => number;
declare const BLACK = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
declare const WHITE = "00000000000000000000000000000000";
declare const SKIP_LINE = "skip";
declare const TILE_PIXEL_WIDTH = 8;
declare const TILE_PIXEL_HEIGHT = 8;
declare const TILES_PER_LINE = 20;
declare const TILES_PER_COLUMN = 18;
declare const FRAME_WIDTH = 2;
declare const BW_PALETTE: number[];
declare const BW_PALETTE_HEX: string[];
declare const BLACK_LINE: string[];
declare const WHITE_LINE: string[];
declare const RGBN_SHADES: number[];
declare const defaultRGBNPalette: RGBNPalette;
export { type BWPalette, BW_PALETTE, BW_PALETTE_HEX, BlendMode, type CanvasCreator, type ChangedTile, type Channel, ChannelKey, type Channels, Decoder, type DecoderOptions, type DecoderUpdateParams, ExportFrameMode, type ImageDataCreator, type IndexedTilePixels, RGBNDecoder, type RGBNDecoderUpdateParams, type RGBNPalette, type RGBNTiles, type RGBValue, SKIP_LINE, type ScaledCanvasSize, type SourceCanvases, blendModeNewName, decodeTile, getRGBValue, maxTiles, tileIndexIsPartOfFrame };
export { BLACK, BLACK_LINE, type BWPalette, BW_PALETTE, BW_PALETTE_HEX, type BaseImageContext, type BaseImageCreationParams, BlendMode, type CanvasCreator, ChannelKey, type CropResult, ExportFrameMode, FRAME_WIDTH, type FullMonochromeImageCreationParams, type FullRGBNImageCreationParams, type IndexedTilePixels, type MonochromeImageContext, type MonochromeImageCreationParams, type PixelDimensions, type RGBNImageCreationParams, type RGBNPalette, type RGBNTiles, RGBN_SHADES, type RGBValue, type RawOutput, SKIP_LINE, type SourceCanvases, TILES_PER_COLUMN, TILES_PER_LINE, TILE_PIXEL_HEIGHT, TILE_PIXEL_WIDTH, UrlCache, WHITE, WHITE_LINE, blendModeNewName, channels, decodeTile, defaultRGBNPalette, getDimensions, getMonochromeImageUrl, getRGBNImageUrl, getRGBValue, getRawMonochromeImageData, getRawRGBNImageData, maxTiles, scaleRawImageData, tileIndexIsPartOfFrame };

@@ -31,23 +31,7 @@ declare enum BlendMode {

}
declare const channels: ChannelKey[];
interface Channel<DecoderType> {
key: ChannelKey;
tiles?: string[];
decoder: DecoderType;
canvas: HTMLCanvasElement;
}
type RGBNTiles = Partial<Record<ChannelKey, string[]>>;
type Channels<DecoderType> = Record<ChannelKey, Channel<DecoderType>>;
type SourceCanvases = Partial<Record<ChannelKey, HTMLCanvasElement>>;
type CanvasCreator = () => HTMLCanvasElement;
type ImageDataCreator = (rawImageData: Uint8ClampedArray, width: number, height: number) => ImageData;
interface ChangedTile {
index: number;
newTile: string;
}
interface ScaledCanvasSize {
initialHeight: number;
initialWidth: number;
tilesPerLine: number;
}
interface RGBValue {

@@ -67,70 +51,55 @@ r: number;

}
interface DecoderOptions {
interface BaseImageCreationParams<TilesType> {
tiles: TilesType;
imageStartLine?: number;
tilesPerLine?: number;
canvasCreator?: CanvasCreator;
imageDataCreator?: ImageDataCreator;
scaleFactor?: number;
handleExportFrame?: ExportFrameMode;
}
interface DecoderUpdateParams {
canvas: HTMLCanvasElement | null;
tiles: string[];
palette: string[];
framePalette: string[];
imageStartLine?: number;
interface MonochromeImageCreationParams extends BaseImageCreationParams<string[]> {
imagePalette: BWPalette;
framePalette?: BWPalette;
}
interface RGBNDecoderUpdateParams {
canvas: HTMLCanvasElement | null;
tiles: RGBNTiles;
type FullMonochromeImageCreationParams = Required<MonochromeImageCreationParams>;
interface RGBNImageCreationParams extends BaseImageCreationParams<RGBNTiles> {
palette: RGBNPalette;
lockFrame?: boolean;
}
declare class Decoder {
private canvas;
private tiles;
private colors;
private frameColors;
private rawImageData;
private lockFrame;
private colorData;
private frameColorData;
private tilesPerLine;
private imageStartLine;
private canvasCreator;
private imageDataCreator;
constructor(options?: DecoderOptions);
update({ canvas, tiles, palette, framePalette, imageStartLine, }: DecoderUpdateParams): void;
private setImageStartLine;
private updateCanvas;
getScaledCanvas(scaleFactor: number, handleExportFrame?: ExportFrameMode): HTMLCanvasElement;
private getScaledCanvasSize;
private getExportTiles;
private getCroppedTiles;
private getDefaultImageRange;
private setCanvas;
private setPalettes;
private setTiles;
private renderTile;
private paintTile;
private paintTileScaled;
private getHeight;
private getWidth;
type FullRGBNImageCreationParams = Required<RGBNImageCreationParams>;
interface PixelDimensions {
width: number;
height: number;
}
interface BaseImageContext {
tilesPerLine: number;
imageStartLine: number;
handleExportFrame: ExportFrameMode;
}
interface MonochromeImageContext extends BaseImageContext {
imagePalette: BWPalette;
framePalette: BWPalette;
}
interface CropResult<ContextType> {
tiles: string[];
dimensions: PixelDimensions;
contextUpdates: Partial<ContextType>;
}
interface RawOutput {
data: Uint8ClampedArray;
dimensions: PixelDimensions;
}
declare class RGBNDecoder {
private canvas;
private palette;
private lockFrame;
private channels;
private tilesPerLine;
private canvasCreator;
private imageDataCreator;
constructor(options?: DecoderOptions);
update({ canvas, tiles, palette, lockFrame, }: RGBNDecoderUpdateParams): void;
private setPalette;
private setCanvas;
private setLockFrame;
private setTiles;
private blendCanvases;
getScaledCanvas(scaleFactor: number, handleExportFrame?: ExportFrameMode): HTMLCanvasElement;
private getDimensions;
declare const getDimensions: (tilesLength: number, tilesPerLine: number) => PixelDimensions;
declare const scaleRawImageData: (data: Uint8ClampedArray, width: number, height: number, scale: number) => RawOutput;
declare const getRawMonochromeImageData: (params: FullMonochromeImageCreationParams) => RawOutput;
declare const getMonochromeImageUrl: (params: MonochromeImageCreationParams, canvasCreator?: CanvasCreator) => Promise<string>;
declare const getRawRGBNImageData: (params: FullRGBNImageCreationParams, canvasCreator: CanvasCreator) => RawOutput;
declare const getRGBNImageUrl: (params: RGBNImageCreationParams, canvasCreator?: CanvasCreator) => Promise<string>;
declare class UrlCache {
static rendered: Record<string, string>;
static rendering: Record<string, Promise<string>>;
getUrl(hash: string): Promise<string | null>;
setUrl(hash: string, promise: Promise<string>): Promise<void>;
}

@@ -140,20 +109,28 @@

declare const getRGBValue: ({ pixels, index, tileIndex, imageStartLine, handleExportFrame, colorData, frameColorData, }: {
declare const getRGBValue: ({ pixels, index, tileIndex, imageContext: { imageStartLine, handleExportFrame, imagePalette, framePalette, }, }: {
pixels: IndexedTilePixels;
index: number;
tileIndex: number;
imageStartLine: number;
handleExportFrame: ExportFrameMode;
colorData: BWPalette;
frameColorData: BWPalette;
imageContext: MonochromeImageContext;
}) => RGBValue;
declare const tileIndexIsPartOfFrame: (tileIndex: number, imageStartLine: number, handleExportFrame?: ExportFrameMode) => boolean;
declare const tileIndexIsPartOfFrame: (tileIndex: number, imageStartLine: number, handleExportFrame: ExportFrameMode) => boolean;
declare const maxTiles: (rgbnTiles: RGBNTiles) => number;
declare const BLACK = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
declare const WHITE = "00000000000000000000000000000000";
declare const SKIP_LINE = "skip";
declare const TILE_PIXEL_WIDTH = 8;
declare const TILE_PIXEL_HEIGHT = 8;
declare const TILES_PER_LINE = 20;
declare const TILES_PER_COLUMN = 18;
declare const FRAME_WIDTH = 2;
declare const BW_PALETTE: number[];
declare const BW_PALETTE_HEX: string[];
declare const BLACK_LINE: string[];
declare const WHITE_LINE: string[];
declare const RGBN_SHADES: number[];
declare const defaultRGBNPalette: RGBNPalette;
export { type BWPalette, BW_PALETTE, BW_PALETTE_HEX, BlendMode, type CanvasCreator, type ChangedTile, type Channel, ChannelKey, type Channels, Decoder, type DecoderOptions, type DecoderUpdateParams, ExportFrameMode, type ImageDataCreator, type IndexedTilePixels, RGBNDecoder, type RGBNDecoderUpdateParams, type RGBNPalette, type RGBNTiles, type RGBValue, SKIP_LINE, type ScaledCanvasSize, type SourceCanvases, blendModeNewName, decodeTile, getRGBValue, maxTiles, tileIndexIsPartOfFrame };
export { BLACK, BLACK_LINE, type BWPalette, BW_PALETTE, BW_PALETTE_HEX, type BaseImageContext, type BaseImageCreationParams, BlendMode, type CanvasCreator, ChannelKey, type CropResult, ExportFrameMode, FRAME_WIDTH, type FullMonochromeImageCreationParams, type FullRGBNImageCreationParams, type IndexedTilePixels, type MonochromeImageContext, type MonochromeImageCreationParams, type PixelDimensions, type RGBNImageCreationParams, type RGBNPalette, type RGBNTiles, RGBN_SHADES, type RGBValue, type RawOutput, SKIP_LINE, type SourceCanvases, TILES_PER_COLUMN, TILES_PER_LINE, TILE_PIXEL_HEIGHT, TILE_PIXEL_WIDTH, UrlCache, WHITE, WHITE_LINE, blendModeNewName, channels, decodeTile, defaultRGBNPalette, getDimensions, getMonochromeImageUrl, getRGBNImageUrl, getRGBValue, getRawMonochromeImageData, getRawRGBNImageData, maxTiles, scaleRawImageData, tileIndexIsPartOfFrame };

@@ -0,1 +1,3 @@

import { hash } from 'ohash';
var BlendMode = /* @__PURE__ */ ((BlendMode2) => {

@@ -60,6 +62,3 @@ BlendMode2["NORMAL"] = "normal";

const TILES_PER_COLUMN = 18;
const FRAME_SIZE = 2;
const FRAME_TILES = FRAME_SIZE * 2;
const DEFAULT_FULL_PIXEL_HEIGHT = TILES_PER_COLUMN * TILE_PIXEL_HEIGHT;
const DEFAULT_FULL_PIXEL_WIDTH = TILES_PER_LINE * TILE_PIXEL_WIDTH;
const FRAME_WIDTH = 2;
const BW_PALETTE = [16777215, 11184810, 5592405, 0];

@@ -69,7 +68,8 @@ const BW_PALETTE_HEX = ["#ffffff", "#aaaaaa", "#555555", "#000000"];

const WHITE_LINE = Array(TILES_PER_LINE).fill(WHITE);
const defaultPalette = {
r: [0, 84, 172, 255],
g: [0, 84, 172, 255],
b: [0, 84, 172, 255],
n: [0, 85, 170, 255],
const RGBN_SHADES = [0, 85, 170, 255];
const defaultRGBNPalette = {
r: RGBN_SHADES,
g: RGBN_SHADES,
b: RGBN_SHADES,
n: RGBN_SHADES,
blend: BlendMode.MULTIPLY

@@ -111,14 +111,13 @@ };

const tileIndexIsPartOfFrame = (tileIndex, imageStartLine, handleExportFrame = ExportFrameMode.FRAMEMODE_KEEP) => {
const tileIndexIsPartOfFrame = (tileIndex, imageStartLine, handleExportFrame) => {
if (handleExportFrame === ExportFrameMode.FRAMEMODE_CROP) {
return false;
}
const checkIndex = tileIndex - (handleExportFrame === ExportFrameMode.FRAMEMODE_KEEP ? 0 : 20);
if (checkIndex < imageStartLine * 20) {
if (tileIndex < imageStartLine * 20) {
return true;
}
if (checkIndex >= imageStartLine * 20 + 280) {
if (tileIndex >= imageStartLine * 20 + 280) {
return true;
}
switch (checkIndex % 20) {
switch (tileIndex % 20) {
case 0:

@@ -134,14 +133,2 @@ case 1:

const calculateImageStartLine = (handleExportFrame, imageStartLine) => {
switch (handleExportFrame) {
case ExportFrameMode.FRAMEMODE_CROP:
return 0;
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE:
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
return 2;
case ExportFrameMode.FRAMEMODE_KEEP:
default:
return imageStartLine;
}
};
const getRGBValue = ({

@@ -151,9 +138,10 @@ pixels,

tileIndex,
imageStartLine,
handleExportFrame,
colorData,
frameColorData
imageContext: {
imageStartLine,
handleExportFrame,
imagePalette,
framePalette
}
}) => {
const calculatedImageStartLine = calculateImageStartLine(handleExportFrame, imageStartLine);
const palette = tileIndexIsPartOfFrame(tileIndex, calculatedImageStartLine, handleExportFrame) ? frameColorData : colorData;
const palette = tileIndexIsPartOfFrame(tileIndex, imageStartLine, handleExportFrame) ? framePalette : imagePalette;
const value = palette[pixels[index]];

@@ -170,270 +158,139 @@ return {

const createCanvasElement = () => {
try {
return document.createElement("canvas");
} catch (error) {
throw new Error("cannot create canvas element");
}
};
const createImageData = (rawImageData, width, height) => new ImageData(rawImageData, width, height);
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class Decoder {
constructor(options) {
__publicField$1(this, "canvas");
__publicField$1(this, "tiles");
__publicField$1(this, "colors");
__publicField$1(this, "frameColors");
__publicField$1(this, "rawImageData");
__publicField$1(this, "lockFrame");
__publicField$1(this, "colorData");
__publicField$1(this, "frameColorData");
__publicField$1(this, "tilesPerLine");
__publicField$1(this, "imageStartLine");
__publicField$1(this, "canvasCreator");
__publicField$1(this, "imageDataCreator");
this.canvas = null;
this.tiles = [];
this.colors = [];
this.frameColors = [];
this.rawImageData = null;
this.lockFrame = false;
this.colorData = [...BW_PALETTE];
this.frameColorData = [...BW_PALETTE];
this.tilesPerLine = options?.tilesPerLine || TILES_PER_LINE;
this.imageStartLine = 2;
this.canvasCreator = options?.canvasCreator || createCanvasElement;
this.imageDataCreator = options?.imageDataCreator || createImageData;
}
update({
canvas,
tiles,
palette,
framePalette,
imageStartLine = 2
}) {
const startLineChanged = this.setImageStartLine(imageStartLine);
const canvasChanged = canvas ? this.setCanvas(canvas) : false;
const usedFramePalette = this.tilesPerLine === TILES_PER_LINE ? framePalette : palette;
const palettesChanged = this.setPalettes(palette, usedFramePalette);
if (startLineChanged || canvasChanged || palettesChanged || !this.tiles.length) {
this.tiles = [];
class UrlCache {
static rendered = {};
static rendering = {};
async getUrl(hash) {
if (UrlCache.rendered.hasOwnProperty(hash)) {
return UrlCache.rendered[hash];
}
const tilesChanged = this.setTiles(tiles);
const newHeight = this.getHeight();
const newWidth = this.getWidth();
if (!this.canvas) {
return;
if (UrlCache.rendering.hasOwnProperty(hash)) {
const url = await UrlCache.rendering[hash];
return url;
}
if (newHeight === 0) {
this.canvas.height = 0;
return;
}
if (this.canvas.height !== newHeight || this.canvas.width !== newWidth || !this.rawImageData?.length) {
this.canvas.height = newHeight;
this.canvas.width = newWidth;
const newRawImageData = new Uint8ClampedArray(newWidth * newHeight * 4);
this.rawImageData?.forEach((value, index) => {
newRawImageData[index] = value;
});
this.rawImageData = newRawImageData;
}
tilesChanged.forEach(({ index, newTile }) => {
this.renderTile(index, newTile);
});
this.updateCanvas(newWidth, newHeight);
return null;
}
setImageStartLine(imageStartLine) {
if (this.imageStartLine === imageStartLine) {
return false;
async setUrl(hash, promise) {
if (UrlCache.rendered.hasOwnProperty(hash) || UrlCache.rendering.hasOwnProperty(hash)) {
console.warn("hash already exists!");
}
this.imageStartLine = imageStartLine;
return true;
UrlCache.rendering[hash] = promise;
UrlCache.rendered[hash] = await promise;
delete UrlCache.rendering[hash];
}
updateCanvas(newWidth, newHeight) {
if (!this.canvas || !this.rawImageData?.length) {
}
const toObjectUrl = async (canvas) => new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Could not generate Blob from canvas"));
return;
}
const context = this.canvas.getContext("2d");
const imageData = this.imageDataCreator(this.rawImageData, newWidth, newHeight);
context?.putImageData(imageData, 0, 0);
}
getScaledCanvas(scaleFactor, handleExportFrame = ExportFrameMode.FRAMEMODE_KEEP) {
let handleFrameMode = handleExportFrame;
if (this.tilesPerLine !== TILES_PER_LINE && handleFrameMode !== ExportFrameMode.FRAMEMODE_KEEP) {
handleFrameMode = ExportFrameMode.FRAMEMODE_KEEP;
try {
resolve(URL.createObjectURL(blob));
} catch (error) {
reject(error);
}
const {
initialHeight,
initialWidth,
tilesPerLine
} = this.getScaledCanvasSize(handleFrameMode);
const canvas = this.canvasCreator();
});
});
const dataUrlFromRawOutput = async ({
data,
dimensions: { width, height }
}, scaleFactor, hash, canvasCreator) => {
const urlCache = new UrlCache();
urlCache.setUrl(hash, new Promise((resolve) => {
const canvas = canvasCreator();
canvas.width = width * scaleFactor;
canvas.height = height * scaleFactor;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("no canvas context");
}
canvas.width = initialWidth * scaleFactor;
canvas.height = initialHeight * scaleFactor;
this.getExportTiles(handleFrameMode).forEach((tile, index) => {
this.paintTileScaled(decodeTile(tile), index, context, scaleFactor, tilesPerLine, handleFrameMode);
});
return canvas;
const imageData = new ImageData(data, canvas.width, canvas.height);
context?.putImageData(imageData, 0, 0);
resolve(toObjectUrl(canvas));
}));
const url = await urlCache.getUrl(hash);
if (!url) {
throw new Error("error generating image");
}
getScaledCanvasSize(handleExportFrame) {
const width = this.getWidth();
const height = this.getHeight();
switch (handleExportFrame) {
case ExportFrameMode.FRAMEMODE_KEEP:
return {
initialHeight: height,
initialWidth: width,
tilesPerLine: this.tilesPerLine
};
case ExportFrameMode.FRAMEMODE_CROP:
return {
initialHeight: DEFAULT_FULL_PIXEL_HEIGHT - TILE_PIXEL_HEIGHT * FRAME_TILES,
initialWidth: DEFAULT_FULL_PIXEL_WIDTH - TILE_PIXEL_WIDTH * FRAME_TILES,
tilesPerLine: TILES_PER_LINE - FRAME_TILES
};
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE:
return {
initialHeight: width,
initialWidth: width,
tilesPerLine: this.tilesPerLine
};
default:
throw new Error(`unknown export mode ${handleExportFrame}`);
}
return url;
};
const createCanvasElement = () => {
try {
return document.createElement("canvas");
} catch (error) {
throw new Error("cannot create canvas element");
}
getExportTiles(handleExportFrame) {
if (!this.tiles) {
throw new Error("no tiles to export");
}
switch (handleExportFrame) {
case ExportFrameMode.FRAMEMODE_KEEP:
return this.tiles;
case ExportFrameMode.FRAMEMODE_CROP:
return this.getCroppedTiles();
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
return [
...BLACK_LINE,
...this.getDefaultImageRange(),
...BLACK_LINE
];
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE:
return [
...WHITE_LINE,
...this.getDefaultImageRange(),
...WHITE_LINE
];
default:
throw new Error(`unknown export mode ${handleExportFrame}`);
}
};
const padLines = {
[ExportFrameMode.FRAMEMODE_SQUARE_BLACK]: BLACK_LINE,
[ExportFrameMode.FRAMEMODE_SQUARE_WHITE]: WHITE_LINE
};
const getPalettes = (imagePalette, framePalette, tilesPerLine) => {
if (tilesPerLine !== TILES_PER_LINE) {
return {
imagePalette,
framePalette: imagePalette
};
}
getCroppedTiles() {
return this.tiles.reduce((acc, tile, index) => tileIndexIsPartOfFrame(index, this.imageStartLine, ExportFrameMode.FRAMEMODE_KEEP) ? acc : [...acc, tile], []);
return {
imagePalette,
framePalette
};
};
const getDimensions$1 = (tilesLength, tilesPerLine) => ({
width: TILE_PIXEL_WIDTH * tilesPerLine,
height: TILE_PIXEL_HEIGHT * Math.ceil(tilesLength / tilesPerLine)
});
const getCroppedTiles = (tiles, imageContext) => ({
tiles: tiles.reduce((acc, tile, index) => tileIndexIsPartOfFrame(index, imageContext.imageStartLine, ExportFrameMode.FRAMEMODE_KEEP) ? acc : [...acc, tile], []),
dimensions: { width: 128, height: 112 },
contextUpdates: { tilesPerLine: 16, imageStartLine: 0 }
});
const getPaddedSquare = (tiles, padLine, imageContext) => {
let wholeImageStartLine = imageContext.imageStartLine - FRAME_WIDTH - 1;
const paddedTiles = [...tiles];
const square20by20tiles = TILES_PER_LINE ** 2;
while (paddedTiles.length < square20by20tiles || wholeImageStartLine < 0) {
paddedTiles.unshift(...padLine);
paddedTiles.push(...padLine);
wholeImageStartLine += 1;
}
// for wild frame image, this returns the part of theimage
// which has the image data in the default position by cropping
// away part of the wild frame which does not fit into 160x144
getDefaultImageRange() {
const wholeImageStartLine = this.imageStartLine - FRAME_SIZE;
const startIndex = wholeImageStartLine * TILES_PER_LINE;
return this.tiles.slice(startIndex, startIndex + TILES_PER_LINE * TILES_PER_COLUMN);
}
setCanvas(canvas) {
if (this.canvas === canvas) {
return false;
const startIndex = wholeImageStartLine * TILES_PER_LINE;
return {
tiles: paddedTiles.slice(startIndex, startIndex + square20by20tiles),
dimensions: { width: 160, height: 160 },
contextUpdates: { tilesPerLine: 20, imageStartLine: 3 }
};
};
const applyCrop = (tiles, imageContext) => {
const checkFrameMode = imageContext.tilesPerLine !== TILES_PER_LINE ? ExportFrameMode.FRAMEMODE_KEEP : imageContext.handleExportFrame;
switch (checkFrameMode) {
case ExportFrameMode.FRAMEMODE_KEEP: {
return {
tiles,
dimensions: getDimensions$1(tiles.length, imageContext.tilesPerLine),
contextUpdates: {}
};
}
this.canvas = canvas;
return true;
}
setPalettes(palette, framePalette) {
if (this.colors[0] === palette[0] && this.colors[1] === palette[1] && this.colors[2] === palette[2] && this.colors[3] === palette[3] && this.frameColors[0] === framePalette[0] && this.frameColors[1] === framePalette[1] && this.frameColors[2] === framePalette[2] && this.frameColors[3] === framePalette[3]) {
return false;
case ExportFrameMode.FRAMEMODE_CROP: {
return getCroppedTiles(tiles, imageContext);
}
this.colors = palette;
this.frameColors = framePalette;
this.colors.forEach((color, index) => {
this.colorData[index] = color.length !== 7 ? BW_PALETTE[index] : parseInt(color.substring(1), 16);
});
this.frameColors.forEach((color, index) => {
this.frameColorData[index] = color.length !== 7 ? BW_PALETTE[index] : parseInt(color.substring(1), 16);
});
return true;
}
setTiles(tiles) {
const changedTiles = tiles.reduce((acc, newTile, index) => {
const changed = newTile !== this.tiles[index];
if (!changed) {
return acc;
}
return [
...acc,
{
index,
newTile
}
];
}, []);
this.tiles = tiles;
return changedTiles;
}
renderTile(tileIndex, rawLine) {
if (rawLine === SKIP_LINE) {
this.paintTile(null, tileIndex);
} else {
const tile = decodeTile(rawLine);
this.paintTile(tile, tileIndex);
case ExportFrameMode.FRAMEMODE_SQUARE_BLACK:
case ExportFrameMode.FRAMEMODE_SQUARE_WHITE: {
const padLine = padLines[checkFrameMode];
return getPaddedSquare(tiles, padLine, imageContext);
}
default:
throw new Error(`unknown export mode ${imageContext.handleExportFrame}`);
}
// This paints the tile with a specified offset and pixel width
paintTile(pixels, index) {
if (!this.rawImageData) {
return;
}
const tileXOffset = index % this.tilesPerLine;
const tileYOffset = Math.floor(index / this.tilesPerLine);
const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset;
for (let x = 0; x < TILE_PIXEL_WIDTH; x += 1) {
for (let y = 0; y < TILE_PIXEL_HEIGHT; y += 1) {
const rawIndex = (pixelXOffset + x + (pixelYOffset + y) * this.tilesPerLine * TILE_PIXEL_WIDTH) * 4;
if (pixels !== null) {
const color = getRGBValue({
pixels,
index: y * TILE_PIXEL_WIDTH + x,
tileIndex: index,
imageStartLine: this.imageStartLine,
handleExportFrame: ExportFrameMode.FRAMEMODE_KEEP,
colorData: this.colorData,
frameColorData: this.frameColorData
});
this.rawImageData[rawIndex] = color.r;
this.rawImageData[rawIndex + 1] = color.g;
this.rawImageData[rawIndex + 2] = color.b;
this.rawImageData[rawIndex + 3] = 255;
} else {
this.rawImageData[rawIndex] = 0;
this.rawImageData[rawIndex + 1] = 0;
this.rawImageData[rawIndex + 2] = 0;
this.rawImageData[rawIndex + 3] = 0;
}
}
}
}
paintTileScaled(pixels, index, canvasContext, pixelSize, tilesPerLine, handleExportFrame) {
const tileXOffset = index % tilesPerLine;
const tileYOffset = Math.floor(index / tilesPerLine);
const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset * pixelSize;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset * pixelSize;
for (let x = 0; x < TILE_PIXEL_WIDTH; x += 1) {
for (let y = 0; y < TILE_PIXEL_HEIGHT; y += 1) {
};
const paintTile = (rawImageData, pixels, index, imageContext) => {
const tileXOffset = index % imageContext.tilesPerLine;
const tileYOffset = Math.floor(index / imageContext.tilesPerLine);
const pixelXOffset = TILE_PIXEL_WIDTH * tileXOffset;
const pixelYOffset = TILE_PIXEL_HEIGHT * tileYOffset;
for (let x = 0; x < TILE_PIXEL_WIDTH; x += 1) {
for (let y = 0; y < TILE_PIXEL_HEIGHT; y += 1) {
const rawIndex = (pixelXOffset + x + (pixelYOffset + y) * imageContext.tilesPerLine * TILE_PIXEL_WIDTH) * 4;
if (pixels !== null) {
const color = getRGBValue({

@@ -443,219 +300,231 @@ pixels,

tileIndex: index,
imageStartLine: this.imageStartLine,
handleExportFrame,
colorData: this.colorData,
frameColorData: this.frameColorData
imageContext
});
canvasContext.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
canvasContext.fillRect(
pixelXOffset + x * pixelSize,
pixelYOffset + y * pixelSize,
pixelSize + 1,
pixelSize + 1
);
rawImageData[rawIndex] = color.r;
rawImageData[rawIndex + 1] = color.g;
rawImageData[rawIndex + 2] = color.b;
rawImageData[rawIndex + 3] = 255;
} else {
rawImageData[rawIndex] = 0;
rawImageData[rawIndex + 1] = 0;
rawImageData[rawIndex + 2] = 0;
rawImageData[rawIndex + 3] = 0;
}
}
}
getHeight() {
return TILE_PIXEL_HEIGHT * Math.ceil(this.tiles.length / this.tilesPerLine);
};
const renderTile = (rawImageData, rawLine, index, imageContext) => {
if (rawLine === SKIP_LINE) {
paintTile(rawImageData, null, index, imageContext);
} else {
const tile = decodeTile(rawLine);
paintTile(rawImageData, tile, index, imageContext);
}
getWidth() {
return TILE_PIXEL_WIDTH * this.tilesPerLine;
};
const getFullParams$1 = (params) => ({
tiles: params.tiles,
imagePalette: params.imagePalette,
framePalette: params.framePalette || params.imagePalette,
imageStartLine: typeof params.imageStartLine === "number" ? params.imageStartLine : FRAME_WIDTH,
tilesPerLine: params.tilesPerLine || TILES_PER_LINE,
scaleFactor: params.scaleFactor || 1,
handleExportFrame: params.handleExportFrame || ExportFrameMode.FRAMEMODE_KEEP
});
const scaleRawImageData = (data, width, height, scale) => {
const newWidth = width * scale;
const newHeight = height * scale;
const scaled = new Uint8ClampedArray(newWidth * newHeight * 4);
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
const srcIndex = (y * width + x) * 4;
const r = data[srcIndex];
const g = data[srcIndex + 1];
const b = data[srcIndex + 2];
const a = data[srcIndex + 3];
for (let dy = 0; dy < scale; dy += 1) {
for (let dx = 0; dx < scale; dx += 1) {
const destX = x * scale + dx;
const destY = y * scale + dy;
const destIndex = (destY * newWidth + destX) * 4;
scaled[destIndex] = r;
scaled[destIndex + 1] = g;
scaled[destIndex + 2] = b;
scaled[destIndex + 3] = a;
}
}
}
}
}
const hx2 = (n) => (n || 0).toString(16).padStart(2, "0");
const paletteTemplates = {
r: (v) => `#${hx2(v)}0000`,
g: (v) => `#00${hx2(v)}00`,
b: (v) => `#0000${hx2(v)}`,
n: (v) => `#${hx2(v)}${hx2(v)}${hx2(v)}`
};
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
const createChannel = (key, decoderOptions) => {
const canvas = decoderOptions.canvasCreator();
const decoder = new Decoder(decoderOptions);
decoder.update({
framePalette: [],
canvas,
tiles: [],
palette: []
});
return {
key,
decoder,
canvas
data: scaled,
dimensions: {
width: newWidth,
height: newHeight
}
};
};
class RGBNDecoder {
constructor(options) {
__publicField(this, "canvas");
__publicField(this, "palette");
__publicField(this, "lockFrame");
__publicField(this, "channels");
__publicField(this, "tilesPerLine");
__publicField(this, "canvasCreator");
__publicField(this, "imageDataCreator");
this.canvas = null;
this.palette = defaultPalette;
this.lockFrame = false;
this.tilesPerLine = options?.tilesPerLine || TILES_PER_LINE;
this.canvasCreator = options?.canvasCreator || createCanvasElement;
this.imageDataCreator = options?.imageDataCreator || createImageData;
const channelDecoderOptions = {
tilesPerLine: this.tilesPerLine,
canvasCreator: this.canvasCreator,
imageDataCreator: this.imageDataCreator
};
this.channels = {
r: createChannel(ChannelKey.R, channelDecoderOptions),
g: createChannel(ChannelKey.G, channelDecoderOptions),
b: createChannel(ChannelKey.B, channelDecoderOptions),
n: createChannel(ChannelKey.N, channelDecoderOptions)
};
}
update({
canvas,
const getRawMonochromeImageData = (params) => {
const {
tiles,
palette,
lockFrame = false
}) {
const canvasChanged = canvas ? this.setCanvas(canvas) : false;
const paletteChanged = this.setPalette(palette);
const lockFrameChanged = this.setLockFrame(lockFrame);
const shouldUpdate = canvasChanged || paletteChanged || lockFrameChanged;
const canvases = this.setTiles(tiles);
const { width: newWidth, height: newHeight } = this.getDimensions(canvases);
if (!shouldUpdate) {
return;
}
if (!this.canvas) {
return;
}
if (newHeight === 0) {
this.canvas.height = 0;
return;
}
if (this.canvas.height !== newHeight) {
this.canvas.height = newHeight;
}
if (this.canvas.width !== newWidth) {
this.canvas.width = newWidth;
}
const context = this.canvas?.getContext("2d");
if (!context) {
return;
}
this.blendCanvases(context, canvases);
imageStartLine,
imagePalette,
framePalette,
tilesPerLine,
handleExportFrame,
scaleFactor
} = params;
const imageContext = {
tilesPerLine,
imageStartLine,
handleExportFrame,
...getPalettes(imagePalette, framePalette, tilesPerLine || TILES_PER_LINE)
};
const {
tiles: usedTiles,
dimensions: {
width,
height
},
contextUpdates
} = applyCrop(tiles, imageContext);
const updatedContext = {
...imageContext,
...contextUpdates
};
if (!height || !width) {
throw new Error("Image has no dimensions");
}
setPalette(palette) {
if (!palette) {
return false;
}
if (JSON.stringify(this.palette) === JSON.stringify(palette)) {
return false;
}
this.palette = palette;
return true;
const rawImageData = new Uint8ClampedArray(width * height * 4);
usedTiles.forEach((newTile, index) => {
renderTile(rawImageData, newTile, index, updatedContext);
});
return scaleRawImageData(rawImageData, width, height, scaleFactor);
};
const getMonochromeImageUrl = async (params, canvasCreator = createCanvasElement) => {
const urlCache = new UrlCache();
const fullParams = getFullParams$1(params);
const hash$1 = hash(fullParams);
const cachedUrl = await urlCache.getUrl(hash$1);
if (cachedUrl) {
return cachedUrl;
}
setCanvas(canvas) {
if (this.canvas === canvas) {
return false;
}
this.canvas = canvas;
return true;
const rawOutput = getRawMonochromeImageData(fullParams);
return dataUrlFromRawOutput(rawOutput, fullParams.scaleFactor, hash$1, canvasCreator);
};
const getFullParams = (params) => ({
tiles: params.tiles,
palette: params.palette,
lockFrame: params.lockFrame || false,
imageStartLine: typeof params.imageStartLine === "number" ? params.imageStartLine : FRAME_WIDTH,
tilesPerLine: params.tilesPerLine || TILES_PER_LINE,
scaleFactor: params.scaleFactor || 1,
handleExportFrame: params.handleExportFrame || ExportFrameMode.FRAMEMODE_KEEP
});
const singleChannelPalette = (palette, channelKey) => {
switch (channelKey) {
case ChannelKey.R:
return palette.map((shade) => (shade & 255) << 16).reverse();
case ChannelKey.G:
return palette.map((shade) => (shade & 255) << 8).reverse();
case ChannelKey.B:
return palette.map((shade) => shade & 255).reverse();
default:
return palette.map((shade) => {
const base = shade & 255;
return (base << 16) + (base << 8) + base;
}).reverse();
}
setLockFrame(lockFrame) {
if (lockFrame !== this.lockFrame) {
this.lockFrame = lockFrame;
return true;
}
return false;
};
const getDimensions = (canvases) => Object.values(canvases).reduce((acc, current) => {
if (current.width === 0 || current.height === 0) {
return acc;
}
setTiles(tiles) {
return channels.reduce((acc, key) => {
const channel = this.channels[key];
channel.tiles = tiles[key];
const channelColors = this.palette[key];
const paletteFunction = paletteTemplates[key];
if (!channel.tiles || !channelColors) {
return acc;
}
const palette = [
paletteFunction(channelColors[3]),
paletteFunction(channelColors[2]),
paletteFunction(channelColors[1]),
paletteFunction(channelColors[0])
];
channel.decoder.update({
canvas: channel.canvas,
tiles: channel.tiles,
framePalette: this.lockFrame ? BW_PALETTE_HEX : palette,
palette
});
return {
...acc,
[key]: channel.canvas
};
}, {});
}
blendCanvases(targetContext, sourceCanvases) {
channels.forEach((key) => {
const sourceCanvas = sourceCanvases[key];
if (sourceCanvas && sourceCanvas.width && sourceCanvas.height) {
if (key === ChannelKey.N) {
if (this.palette.blend === BlendMode.NORMAL) {
return;
}
targetContext.globalCompositeOperation = blendModeNewName(this.palette.blend);
} else {
targetContext.globalCompositeOperation = "lighter";
return {
width: Math.max(current.width, acc.width),
height: Math.max(current.height, acc.height)
};
}, { width: 0, height: 0 });
const blendCanvases = (sourceCanvases, blendMode, canvasCreator) => {
const dimensions = getDimensions(sourceCanvases);
const targetCanvas = canvasCreator();
targetCanvas.width = dimensions.width;
targetCanvas.height = dimensions.height;
const targetContext = targetCanvas.getContext("2d");
channels.forEach((key) => {
const sourceCanvas = sourceCanvases[key];
if (sourceCanvas && sourceCanvas.width && sourceCanvas.height) {
if (key === ChannelKey.N) {
if (blendMode === BlendMode.NORMAL) {
return;
}
targetContext.drawImage(sourceCanvas, 0, 0);
targetContext.globalCompositeOperation = blendModeNewName(blendMode);
} else {
targetContext.globalCompositeOperation = "lighter";
}
targetContext.drawImage(sourceCanvas, 0, 0);
}
});
const imageData = targetContext.getImageData(0, 0, dimensions.width, dimensions.height);
return {
data: imageData.data,
dimensions: {
width: imageData.width,
height: imageData.height
}
};
};
const getRawRGBNImageData = (params, canvasCreator) => {
const {
tiles,
imageStartLine,
palette,
lockFrame,
tilesPerLine,
handleExportFrame,
scaleFactor
} = params;
const canvases = Object.entries(tiles).reduce((acc, [key, channelTiles]) => {
const channelKey = key;
const channelShades = palette[channelKey] || RGBN_SHADES;
const channelPalette = singleChannelPalette(channelShades, channelKey);
const lockFramePalette = lockFrame ? singleChannelPalette(RGBN_SHADES, channelKey) : channelPalette;
const {
data,
dimensions
} = getRawMonochromeImageData({
imagePalette: channelPalette,
framePalette: lockFramePalette,
handleExportFrame,
imageStartLine,
scaleFactor,
tiles: channelTiles,
tilesPerLine
});
}
getScaledCanvas(scaleFactor, handleExportFrame = ExportFrameMode.FRAMEMODE_KEEP) {
const canvas = this.canvasCreator();
const canvases = channels.reduce((acc, key) => {
const channel = this.channels[key];
const channelCanvas = channel.decoder.getScaledCanvas(scaleFactor, handleExportFrame);
return {
...acc,
[key]: channelCanvas
};
}, {});
const { width, height } = this.getDimensions(canvases);
canvas.width = width;
canvas.height = height;
const canvas = canvasCreator();
canvas.width = dimensions.width;
canvas.height = dimensions.height;
const context = canvas.getContext("2d");
if (!context) {
throw new Error("no canvas context");
}
context.fillStyle = "#000000";
context.fillRect(0, 0, 500, 500);
this.blendCanvases(context, canvases);
return canvas;
const imageData = new ImageData(data, dimensions.width, dimensions.height);
context.putImageData(imageData, 0, 0);
return {
...acc,
[channelKey]: canvas
};
}, {});
return blendCanvases(canvases, palette.blend || BlendMode.NORMAL, canvasCreator);
};
const getRGBNImageUrl = async (params, canvasCreator = createCanvasElement) => {
const urlCache = new UrlCache();
const fullParams = getFullParams(params);
const hash$1 = hash(fullParams);
const cachedUrl = await urlCache.getUrl(hash$1);
if (cachedUrl) {
return cachedUrl;
}
getDimensions(canvases) {
return Object.values(canvases).reduce((acc, current) => {
if (current.width === 0 || current.height === 0) {
return acc;
}
return {
width: Math.max(current.width, acc.width),
height: Math.max(current.height, acc.height)
};
}, { width: 0, height: 0 });
}
}
const rawOutput = getRawRGBNImageData(fullParams, canvasCreator);
return dataUrlFromRawOutput(rawOutput, fullParams.scaleFactor, hash$1, canvasCreator);
};
const maxTiles = (rgbnTiles) => Math.max(...channels.map((key) => rgbnTiles[key]?.length || 0));
export { BW_PALETTE, BW_PALETTE_HEX, BlendMode, ChannelKey, Decoder, ExportFrameMode, RGBNDecoder, SKIP_LINE, blendModeNewName, decodeTile, getRGBValue, maxTiles, tileIndexIsPartOfFrame };
export { BLACK, BLACK_LINE, BW_PALETTE, BW_PALETTE_HEX, BlendMode, ChannelKey, ExportFrameMode, FRAME_WIDTH, RGBN_SHADES, SKIP_LINE, TILES_PER_COLUMN, TILES_PER_LINE, TILE_PIXEL_HEIGHT, TILE_PIXEL_WIDTH, UrlCache, WHITE, WHITE_LINE, blendModeNewName, channels, decodeTile, defaultRGBNPalette, getDimensions$1 as getDimensions, getMonochromeImageUrl, getRGBNImageUrl, getRGBValue, getRawMonochromeImageData, getRawRGBNImageData, maxTiles, scaleRawImageData, tileIndexIsPartOfFrame };
{
"name": "gb-image-decoder",
"version": "1.3.1",
"version": "2.0.0",
"description": "Decoder classes for GameBoy-encoded images",

@@ -33,3 +33,3 @@ "repository": "",

"@typescript-eslint/eslint-plugin": "^7.9.0",
"canvas": "^2.11.2",
"canvas": "^3.1.0",
"eslint": "^8.57.0",

@@ -39,8 +39,11 @@ "eslint-config-airbnb-base": "^15.0.0",

"eslint-plugin-import": "^2.29.1",
"jsdom": "^25.0.1",
"ohash": "^1.1.4",
"jsdom": "^26.1.0",
"ohash": "^2.0.11",
"typescript": "^5.4.5",
"unbuild": "^2.0.0",
"vitest": "^2.1.3"
"unbuild": "^3.5.0",
"vitest": "^3.1.2"
},
"peerDependencies": {
"ohash": "^2.0.11"
}
}
+72
-50
# GameBoy-Tile format Image Decoders
This package renders GameBoy-encoded images to a canvas element.
This package renders GameBoy-encoded images.
You may need the [gbp-decode package](https://npmjs.com/gbp-decode) to decode various sources.
You may use the [gbp-decode package](https://npmjs.com/gbp-decode) to decode various sources.
Or you may want to use the [arduino-gameboy-printer-emulator](https://github.com/mofosyne/arduino-gameboy-printer-emulator).

@@ -10,46 +10,34 @@ There are a lot more similar projects for aquiring raw tile data.

## Usage
Import within your Project
```typescript
import { RGBNDecoder, Decoder, RGBNTiles } from 'gb-image-decoder';
```
Have a canvas element inside your DOM
### Browser
#### Monochrome images
```typescript
const myCanvas: HTMLCanvasElement = document.createElement('canvas');
```
import { getMonochromeImageUrl, ExportFrameMode } from 'gb-image-decoder';
### For monochrome images:
Have a set of tiles (each string represents a 8x8 tile)
```typescript
// For classic monochrome images:
const myTiles: string[] = [ // need 360 of these for a 160x144 image
const tiles: string[] = [ // need 360 of these for a 160x144 image
'7D FF 0A FF 7D FF FF FF 5F FF BB FF 5D FF FF FF', '75 FF A2 FF 44 FF FF FF 5D FF FF FF FF FF FF FF', '55 FF 1F FF 57 FF FF FF 7D FF FF FF FF FF FF FF', '77 FF FD FF 57 FF FF FF 75 FF FF FF DF FF FF FF',
];
```
const monoPalette = ['#ffffff', '#aaaaaa', '#555555', '#000000'];
Have a palette for your monochrome image
```typescript
const monoPalette = ['#ffffff', '#aaaaaa', '#555555', '#000000'];
const imageSrc = await getMonochromeImageUrl({
imagePalette: monoPalette,
tiles,
// Optional parameters (with it's defaults):
// framePalette: monoPalette,
// imageStartLine: 2,
// tilesPerLine: 20,
// scaleFactor: 1,
// handleExportFrame: ExportFrameMode.FRAMEMODE_KEEP,
})
```
Initialize the decoder:
#### RGB(N) images
`getRGBNImageUrl` provides a convenience function to combine separate monochrome tile sets into one colored image.
the `RGBNTiles` type contains the four possible channels (`r`, `g`, `b`, `n`)
The optional neutral layer will be overlayed on the previously combined rgb image.
```typescript
const decoder = new Decoder();
decoder.update({
canvas: myCanvas,
tiles: myTiles,
palette: monoPalette,
framePalette: monoPalette,
lockFrame: false,
});
```
import { getRGBNImageUrl, ExportFrameMode, RGBNTiles, RGBNPalette, defaultRGBNPalette } from 'gb-image-decoder';
### For RGBN images:
RGB(N) Images can contain any of the channels. All channels are optional, but the r,g,b channels are recommended.
Your image needs a set of RGBN tiles:
```typescript
// For RGBN images
const myTilesRGBN: RGBNTiles = { // need 360 of each for a 160x144 image
const tiles: RGBNTiles = {
r: ['7D FF 0A FF 7D FF FF FF 5F FF BB FF 5D FF FF FF', '75 FF A2 FF 44 FF FF FF 5D FF FF FF FF FF FF FF', '55 FF 1F FF 57 FF FF FF 7D FF FF FF FF FF FF FF', '77 FF FD FF 57 FF FF FF 75 FF FF FF DF FF FF FF'],

@@ -60,25 +48,59 @@ g: ['7F FF BF FF FF FF FF FF DF FF FF FF FF FF FF FF', 'A2 D5 FC C3 4C D3 FE C1 D8 E4 ED F2 FF F1 DB FC', 'EA 55 83 FC 48 F5 07 F8 A4 53 A8 07 A3 5C FE 01', 'FF 3F FF 3F FF 3F 7F BF FF 3F FF 7F 7D FF FF FF'],

};
const imageSrc = await getRGBNImageUrl({
palette: defaultRGBNPalette,
tiles,
// Optional parameters (with it's defaults):
// imageStartLine: 2,
// tilesPerLine: 20,
// scaleFactor: 1,
// handleExportFrame: ExportFrameMode.FRAMEMODE_KEEP,
})
```
For RGBN there is a default blend palette available:
### Nodejs
Within a node environment, `getMonochromeImageUrl` and `getRGBNImageUrl` cannot be called, as they rely on the use of `URL.createObjectURL(blob)`.
Instead the internally used functions need to be called. All parameters are mandatory.
The resulting data and dimensions can be used to create an image e.g. by using the [canvas package](https://www.npmjs.com/package/canvas)
#### Monochrome images
```typescript
import { defaultPalette as rgbnPalette } from 'gb-image-decoder';
import { getRawMonochromeImageData, FullMonochromeImageCreationParams, ExportFrameMode } from 'gb-image-decoder';
const monoPalette = ['#ffffff', '#aaaaaa', '#555555', '#000000'];
const fullParams: FullMonochromeImageCreationParams = {
framePalette: monoPalette,
imagePalette: monoPalette,
tiles, // as in browser example
imageStartLine: 2,
tilesPerLine: 20,
scaleFactor: 1,
handleExportFrame: ExportFrameMode.FRAMEMODE_KEEP,
};
const { data, dimensions } = getRawMonochromeImageData(fullParams);
```
Initialize the decoder:
#### RGB(N) images
```typescript
const decoder = new RGBNDecoder();
decoder.update({
canvas: myCanvas,
tiles: myTilesRGBN,
palette: rgbnPalette,
lockFrame: false,
});
import { getRawRGBNImageData, FullRGBNImageCreationParams, defaultRGBNPalette, ExportFrameMode } from 'gb-image-decoder';
const monoPalette = ['#ffffff', '#aaaaaa', '#555555', '#000000'];
const fullParams: FullRGBNImageCreationParams = {
palette: defaultRGBNPalette,
tiles, // as in browser example
imageStartLine: 2,
tilesPerLine: 20,
scaleFactor: 1,
handleExportFrame: ExportFrameMode.FRAMEMODE_KEEP,
};
const { data, dimensions } = getRawRGBNImageData(fullParams);
```
## ToDos
* improve docs
* tests
## License: MIT
[LICENSE](./LICENSE)