gb-image-decoder
Advanced tools
+366
-480
| '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; |
+61
-84
@@ -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 }; |
+61
-84
@@ -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 }; |
+61
-84
@@ -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 }; |
+348
-479
@@ -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 }; |
+9
-6
| { | ||
| "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) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
105
26.51%60619
-9.44%1
Infinity%1176
-18.78%1
Infinity%