tools: Introduce Array2D class and simplify Image and TileMap.
4 files changed, 114 insertions(+), 112 deletions(-)

A => tools/array2d.js
M tools/images.js
M tools/sprites.js
M tools/tilemaps.js
A => tools/array2d.js +65 -0
@@ 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;

          
M tools/images.js +28 -66
@@ 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 @@ class Image {
 		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 Image {
 	}
 }
 
-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 @@ class PNGLoader {
 		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 @@ class PNGLoader {
 	}
 }
 
-module.exports = { Image, ImageData, ImageSlice, Palette, PaletteColor, PNGLoader };
+module.exports = { Image, Palette, PaletteColor, PNGLoader };

          
M tools/sprites.js +5 -11
@@ 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 Sprite {
 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 @@ class SpritePattern {
 		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 @@ class SpritePattern {
 				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 @@ class AsepriteSheet {
 		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));
 		}
 	}

          
M tools/tilemaps.js +16 -35
@@ 4,36 4,30 @@ const path = require("path");
 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 TileMap {
 }
 
 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 @@ class Tmx {
 	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) {