# HG changeset patch # User Laurens Holst # Date 1591735276 -7200 # Tue Jun 09 22:41:16 2020 +0200 # Node ID 11fa65ee0e454364b1509631547d06894ed8996c # Parent 13ce6b1804e1a5a05aed99d0fa3f7db4f6ad5f8b tools: Introduce Array2D class and simplify Image and TileMap. diff --git a/tools/array2d.js b/tools/array2d.js new file mode 100644 --- /dev/null +++ b/tools/array2d.js @@ -0,0 +1,65 @@ +const { checkTypes } = require("./utils"); + +class Array2D { + constructor(width, height, getter) { + checkTypes(arguments, "number", "number", Function); + this.width = width; + this.height = height; + this.getter = getter; + } + + get(x, y) { + checkTypes(arguments, "number", "number"); + if (x < 0 || x >= this.width || y < 0 || y >= this.height) + throw new Error("Coordinates out of range."); + return this.getter(x, y); + } + + static ofArray(array, width, height) { + checkTypes(arguments, Array, "number", "number"); + if (array.length != width * height) + throw new Error("Array data incomplete."); + return new Array2D(width, height, (x, y) => array[y * width + x]); + } + + toArray() { + const array = []; + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + array.push(this.get(x, y)); + } + } + return array; + } + + apply() { + return Array2D.ofArray(this.toArray(), this.width, this.height); + } + + map(mapFunction) { + checkTypes(arguments, Function); + return new Array2D(this.width, this.height, (x, y) => mapFunction(this.get(x, y), x, y)); + } + + tile(width, height) { + checkTypes(arguments, "number", "number"); + if (this.width % width != 0 || this.height % height != 0) + throw new Error("Not an integer multiple of tile dimensions."); + const tiles = []; + for (let y = 0; y < this.height; y += height) { + for (let x = 0; x < this.width; x += width) { + tiles.push(this.slice(x, y, width, height)); + } + } + return Array2D.ofArray(tiles, Math.floor(this.width / width), Math.floor(this.height / height)); + } + + slice(xOffset, yOffset, width, height) { + checkTypes(arguments, "number", "number", "number", "number"); + if (xOffset < 0 || (xOffset + width) > this.width || yOffset < 0 || (yOffset + height) > this.height) + throw new Error("Slice coordinates out of range."); + return new Array2D(width, height, (x, y) => this.get(xOffset + x, yOffset + y)); + } +} + +exports.Array2D = Array2D; diff --git a/tools/images.js b/tools/images.js --- a/tools/images.js +++ b/tools/images.js @@ -2,48 +2,48 @@ const fs = require("fs"); const PNG = require("png-js"); const { checkTypes } = require("./utils"); +const { Array2D } = require("./array2d"); class Image { - constructor(width, height, palette) { - checkTypes(arguments, "number", "number", Palette); - this.width = width; - this.height = height; + constructor(pixels, palette) { + checkTypes(arguments, Array2D, Palette); + this.pixels = pixels; this.palette = palette; } - getPixel() { - throw new Error("Not implemented."); + getPixel(x, y) { + return this.pixels.get(x, y); } getPixelData(screenMode) { - if (this.width & 1) + if (this.pixels.width & 1) throw new Error("Image width is not even."); const pixelData = []; if (screenMode >= 5 && screenMode <= 6) { - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x += 2) { - pixelData.push((this.getPixel(x, y).index & 15) << 4 | (this.getPixel(x + 1, y).index & 15)); + for (let y = 0; y < this.pixels.height; y++) { + for (let x = 0; x < this.pixels.width; x += 2) { + pixelData.push((this.pixels.get(x, y).index & 15) << 4 | (this.pixels.get(x + 1, y).index & 15)); } } } else if (screenMode == 7) { - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - const pixel = this.getPixel(x, y); + for (let y = 0; y < this.pixels.height; y++) { + for (let x = 0; x < this.pixels.width; x++) { + const pixel = this.pixels.get(x, y); pixelData.push((pixel.index & 15) << 4 | (pixel.index & 15)); } } } else if (screenMode == 8) { - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - const pixel = this.getPixel(x, y).project(7).round(); + for (let y = 0; y < this.pixels.height; y++) { + for (let x = 0; x < this.pixels.width; x++) { + const pixel = this.pixels.get(x, y).project(7).round(); pixelData.push(pixel.g << 5 | pixel.r << 2 | pixel.b >> 1); } } } else if (screenMode >= 10 && screenMode <= 11) { - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - const pixel = this.getPixel(x, y); + for (let y = 0; y < this.pixels.height; y++) { + for (let x = 0; x < this.pixels.width; x++) { + const pixel = this.pixels.get(x, y); pixelData.push((pixel.index & 15) << 4 | 8); } } @@ -51,20 +51,19 @@ return pixelData; } - toTiles(width, height) { - checkTypes(arguments, "number", "number"); - const tiles = []; - for (let y = 0; y < this.height; y += height) - for (let x = 0; x < this.width; x += width) - tiles.push(new ImageSlice(this, x, y, width, height)); - return tiles; + tile(width, height) { + return this.pixels.tile(width, height).map(pixels => new Image(pixels, this.palette)).apply(); + } + + slice(x, y, width, height) { + return new Image(this.pixels.slice(x, y, width, height), this.palette); } toAsm(screenMode) { checkTypes(arguments, "number"); const lines = []; if (this.name) { - lines.push(`${this.name}_instance: Image ${this.width}, ${this.height}, ${this.name}_palette, ${this.name}_pixelData`); + lines.push(`${this.name}_instance: Image ${this.pixels.width}, ${this.pixels.height}, ${this.name}_palette, ${this.name}_pixelData`); lines.push(`${this.name}_palette: ${this.palette.toAsm()}`); lines.push("\tSECTION ROM_DATA"); lines.push(`${this.name}_pixelData:`); @@ -77,43 +76,6 @@ } } -class ImageData extends Image { - constructor(pixels, width, height, palette) { - checkTypes(arguments, Array, "number", "number", Palette); - if (pixels.length != width * height) - throw new Error("Pixel data incomplete."); - super(width, height, palette); - this.pixels = pixels; - this.name = null; - } - - getPixel(x, y) { - checkTypes(arguments, "number", "number"); - if (x < 0 || x >= this.width || y < 0 || y >= this.height) - throw new Error("Coordinates out of range."); - return this.pixels[y * this.width + x]; - } -} - -class ImageSlice extends Image { - constructor(image, x, y, width, height) { - checkTypes(arguments, Image, "number", "number", "number", "number"); - if (x < 0 || (x + width) > image.width || y < 0 || (y + height) > image.height) - throw new Error("Slice coordinates out of range."); - super(width, height, image.palette); - this.image = image; - this.x = x; - this.y = y; - } - - getPixel(x, y) { - checkTypes(arguments, "number", "number"); - if (x < 0 || x >= this.width || y < 0 || y >= this.height) - throw new Error("Coordinates out of range."); - return this.image.getPixel(this.x + x, this.y + y); - } -} - class Palette { constructor() { this.colors = []; @@ -208,7 +170,7 @@ for (const index of pngPixels) pixels.push(palette.getColor(index)); - return new ImageData(pixels, png.width, png.height, palette); + return new Image(Array2D.ofArray(pixels, png.width, png.height), palette); } toPalette(palette8bit) { @@ -223,4 +185,4 @@ } } -module.exports = { Image, ImageData, ImageSlice, Palette, PaletteColor, PNGLoader }; +module.exports = { Image, Palette, PaletteColor, PNGLoader }; diff --git a/tools/sprites.js b/tools/sprites.js --- a/tools/sprites.js +++ b/tools/sprites.js @@ -2,7 +2,7 @@ const fs = require("fs"); const path = require("path"); const { checkTypes } = require("./utils"); -const { Image, ImageSlice, PNGLoader } = require("./images"); +const { Image, PNGLoader } = require("./images"); class Sprite { constructor(name, sheet) { @@ -38,14 +38,12 @@ class SpriteFrame { constructor(name, image, duration) { checkTypes(arguments, "string", Image, "number"); - if (image.width % 16 != 0 || image.height % 16 != 0) - throw new Error(`Unsupported sprite dimensions .`); this.name = name; this.image = image; this.duration = duration; this.patterns = []; - for (const tile of image.toTiles(16, 16)) { + for (const tile of image.tile(16, 16).toArray()) { this.patterns.push(new SpritePattern(tile, 1)); this.patterns.push(new SpritePattern(tile, 2)); this.patterns.push(new SpritePattern(tile, 4)); @@ -74,7 +72,8 @@ checkTypes(arguments, Image, "number"); this.data = []; - const addPatternData = image => { + const images = image.tile(8, 8); + for (const image of [images.get(0, 0), images.get(0, 1), images.get(1, 0), images.get(1, 1)]) { for (let y = 0; y < 8; y++) { let byte = 0; for (let x = 0; x < 8; x++) { @@ -84,11 +83,6 @@ this.data.push(byte); } } - - addPatternData(new ImageSlice(image, 0, 0, 8, 8)); - addPatternData(new ImageSlice(image, 0, 8, 8, 8)); - addPatternData(new ImageSlice(image, 8, 0, 8, 8)); - addPatternData(new ImageSlice(image, 8, 8, 8, 8)); } toAsm() { @@ -122,7 +116,7 @@ checkTypes(arguments, Object, Sprite); for (const jsonFrameName in jsonFrames) { const json = jsonFrames[jsonFrameName]; - const image = new ImageSlice(sprite.sheet, json.frame.x, json.frame.y, json.frame.w, json.frame.h); + const image = sprite.sheet.slice(json.frame.x, json.frame.y, json.frame.w, json.frame.h); sprite.addFrame(new SpriteFrame(jsonFrameName, image, json.duration / 1000)); } } diff --git a/tools/tilemaps.js b/tools/tilemaps.js --- a/tools/tilemaps.js +++ b/tools/tilemaps.js @@ -4,36 +4,30 @@ const xml2js = require("xml2js"); const { checkTypes } = require("./utils"); const { Image, Palette, PNGLoader } = require("./images"); +const { Array2D } = require("./array2d"); class TileMap { - constructor(name, width, height, tileSet) { - checkTypes(arguments, "string", "number", "number", TileSet); + constructor(name, tiles, tileSet) { + checkTypes(arguments, "string", Array2D, TileSet); this.name = name; - this.width = width; - this.height = height; + this.tiles = tiles; this.tileSet = tileSet; - this.tiles = []; - } - - addTileByIndex(index) { - checkTypes(arguments, "number"); - this.tiles.push(this.tileSet.getTileByIndex(index)); } optimize() { - this.tileSet.optimize(this.tiles); + this.tileSet.optimize(this.tiles.toArray()); } toAsm(screenMode) { checkTypes(arguments, "number"); const lines = []; lines.push(`${this.name}_instance:`) - lines.push(`\tTileMap ${this.width}, ${this.height}, ${this.tileSet.name}_palette, ${this.name}_data`); + lines.push(`\tTileMap ${this.tiles.width}, ${this.tiles.height}, ${this.tileSet.name}_palette, ${this.name}_data`); lines.push(""); lines.push("\tSECTION ROM_DATA"); lines.push("\tALIGN ROMMapper_BANK_SIZE"); lines.push(`${this.name}_data:`); - for (const tile of this.tiles) { + for (const tile of this.tiles.toArray()) { lines.push(`\tdw ${this.tileSet.name}_${tile.getName()}`); } lines.push("\tENDS"); @@ -44,18 +38,11 @@ } class TileSet { - constructor(name, width, height, palette) { - checkTypes(arguments, "string", "number", "number", Palette); + constructor(name, tiles, palette) { + checkTypes(arguments, "string", Array, Palette); this.name = name; - this.width = width; - this.height = height; + this.tiles = tiles; this.palette = palette; - this.tiles = []; - } - - addTile(tile) { - checkTypes(arguments, Tile); - this.tiles.push(tile); } getTileByIndex(index) { @@ -137,25 +124,19 @@ async parseMap(mapXml, gamma) { checkTypes(arguments, Object, "number"); const tileSet = await this.parseSet(mapXml.tileset[0], gamma); - const tileMap = new TileMap(this.name, ~~mapXml.$.width, ~~mapXml.$.height, tileSet); const indices = mapXml.layer[0].data[0]._.replace(/\s/g, "").split(",").map(i => ~~i - 1); - for (const index of indices) { - tileMap.addTileByIndex(index); - } - return tileMap; + const indexMap = Array2D.ofArray(indices, ~~mapXml.$.width, ~~mapXml.$.height); + return new TileMap(this.name, indexMap.map(index => tileSet.getTileByIndex(index)).apply(), tileSet); } async parseSet(setXml, gamma) { checkTypes(arguments, Object, "number"); const imagepath = path.resolve(path.dirname(this.path), setXml.image[0].$.source); const image = await new PNGLoader(imagepath, gamma).loadImage(); - const tileSet = new TileSet(setXml.$.name, ~~setXml.$.tilewidth, ~~setXml.$.tileheight, image.palette); - const tileImages = image.toTiles(tileSet.width, tileSet.height); - for (let i = 0; i < tileImages.length; i++) { - const tileXml = (setXml.tile || []).find(t => t.$.id == i) || { $: { id: i } }; - tileSet.addTile(this.parseTile(tileXml, tileImages[i])); - } - return tileSet; + const tileImages = image.tile(~~setXml.$.tilewidth, ~~setXml.$.tileheight); + const tiles = tileImages.toArray().map((image, index) => + this.parseTile((setXml.tile || []).find(t => t.$.id == index) || { $: { id: index } }, image)); + return new TileSet(setXml.$.name, tiles, image.palette); } parseTile(tileXml, image) {