@city41/gba-convertpng 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Matt Greer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # GBA Convert png
2
+
3
+ This is a nodejs based tool for converting png images to the tile data format used in GBA games.
4
+
5
+ HEADS UP: I make games for the e-Reader, and so far have not done mainstream GBA development. So possibly this tool is missing things for GBA dev. If so, please let me know.
package/dist/asm.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare function toAsm(data: number[], width: "b" | "w", numbersPerRow: number): string;
2
+ export { toAsm };
package/dist/asm.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toAsm = toAsm;
4
+ const toHex_1 = require("./toHex");
5
+ function toAsm(data, width, numbersPerRow) {
6
+ const hexFn = width === "b" ? toHex_1.toHexByte : toHex_1.toHexWord;
7
+ const rows = [];
8
+ let row = [];
9
+ for (let i = 0; i < data.length; ++i) {
10
+ if (row.length === numbersPerRow) {
11
+ rows.push(` .d${width} ${row.join(",")}`);
12
+ row = [];
13
+ }
14
+ row.push(hexFn(data[i]));
15
+ }
16
+ rows.push(` .d${width} ${row.join(",")}`);
17
+ return rows.join("\r\n") + "\r\n";
18
+ }
19
+ //# sourceMappingURL=asm.js.map
@@ -0,0 +1,8 @@
1
+ import { BackgroundSpec } from "./types";
2
+ type ProcessBackgroundResult = {
3
+ tilesAsmSrc: string;
4
+ paletteAsmSrc: string;
5
+ mapAsmSrc: string;
6
+ };
7
+ declare function processBackground(bg: BackgroundSpec): Promise<ProcessBackgroundResult>;
8
+ export { processBackground };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.processBackground = processBackground;
7
+ const asm_1 = require("./asm");
8
+ const canvas_1 = require("./canvas");
9
+ const palette_1 = require("./palette");
10
+ const tile_1 = require("./tile");
11
+ const isEqual_1 = __importDefault(require("lodash/isEqual"));
12
+ function extractMap(allTilesThatFormImage, dedupedTiles) {
13
+ const map = [];
14
+ allTilesThatFormImage.forEach((tile, i) => {
15
+ const index = dedupedTiles.findIndex((dt) => {
16
+ return (0, isEqual_1.default)(dt, tile);
17
+ });
18
+ if (index < 0) {
19
+ throw new Error("extractMap: failed to find a matching tile in the deduped tile set");
20
+ }
21
+ map.push(index);
22
+ });
23
+ return map;
24
+ }
25
+ async function processBackground(bg) {
26
+ const canvas = await (0, canvas_1.reduceColors)(await (0, canvas_1.createCanvasFromPath)(bg.file), 16);
27
+ const palette = (0, palette_1.extractPalette)(canvas, !bg.trimPalette);
28
+ const allTilesThatFormImage = (0, tile_1.extractTiles)(canvas, palette, 1);
29
+ const dedupedTiles = (0, tile_1.dedupeTiles)(allTilesThatFormImage);
30
+ const map = extractMap(allTilesThatFormImage, dedupedTiles);
31
+ return {
32
+ tilesAsmSrc: (0, asm_1.toAsm)(dedupedTiles.flat(1), "b", 4),
33
+ paletteAsmSrc: (0, asm_1.toAsm)(palette, "w", 4),
34
+ mapAsmSrc: (0, asm_1.toAsm)(map, "w", 8),
35
+ };
36
+ }
37
+ //# sourceMappingURL=background.js.map
package/dist/c.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare function toC(data: number[], width: "b" | "w", numbersPerRow: number): string;
2
+ export { toC };
package/dist/c.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toC = toC;
4
+ const toHex_1 = require("./toHex");
5
+ function toC(data, width, numbersPerRow) {
6
+ const hexFn = width === "b" ? toHex_1.toHexByte : toHex_1.toHexWord;
7
+ const rows = [];
8
+ let row = [];
9
+ for (let i = 0; i < data.length; ++i) {
10
+ if (row.length === numbersPerRow) {
11
+ rows.push(row.join(",") + ",");
12
+ row = [];
13
+ }
14
+ row.push(hexFn(data[i]));
15
+ }
16
+ rows.push(row.join(","));
17
+ return `{ ${rows.join("\r\n")} }`;
18
+ }
19
+ //# sourceMappingURL=c.js.map
@@ -0,0 +1,5 @@
1
+ import { Canvas } from "canvas";
2
+ declare function reduceColors(c: Canvas, maxColors: number): Promise<Canvas>;
3
+ declare function createCanvasFromPath(pngPath: string): Promise<Canvas>;
4
+ declare function forceCanvasToPalette(canvas: Canvas, palette: Canvas): Promise<Canvas>;
5
+ export { createCanvasFromPath, reduceColors, forceCanvasToPalette };
package/dist/canvas.js ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createCanvasFromPath = createCanvasFromPath;
40
+ exports.reduceColors = reduceColors;
41
+ exports.forceCanvasToPalette = forceCanvasToPalette;
42
+ const path = __importStar(require("node:path"));
43
+ const os = __importStar(require("node:os"));
44
+ const fsp = __importStar(require("node:fs/promises"));
45
+ const canvas_1 = require("canvas");
46
+ const imagemagick_1 = __importDefault(require("imagemagick"));
47
+ const mkdirp_1 = require("mkdirp");
48
+ const nearest_color_1 = __importDefault(require("nearest-color"));
49
+ async function _reduceColorsWithMagick(renderedFilePath, maxColors) {
50
+ const outputPath = `${renderedFilePath}.reduced.png`;
51
+ return new Promise((resolve, reject) => {
52
+ imagemagick_1.default.convert([
53
+ renderedFilePath,
54
+ "-dither",
55
+ "none",
56
+ "-colors",
57
+ maxColors.toString(),
58
+ `png8:${outputPath}`,
59
+ ], (err) => {
60
+ if (err) {
61
+ reject(err);
62
+ }
63
+ else {
64
+ resolve(outputPath);
65
+ }
66
+ });
67
+ });
68
+ }
69
+ async function reduceColors(c, maxColors) {
70
+ const tmpDir = path.resolve(os.tmpdir(), `reduceColors_${Date.now()}`);
71
+ await (0, mkdirp_1.mkdirp)(tmpDir);
72
+ const tmpPath = path.resolve(tmpDir, `_reduceColors_${maxColors}_${Date.now()}.png`);
73
+ const b = c.toBuffer();
74
+ await fsp.writeFile(tmpPath, b);
75
+ const reducedPath = await _reduceColorsWithMagick(tmpPath, maxColors);
76
+ return createCanvasFromPath(reducedPath);
77
+ }
78
+ async function createCanvasFromPath(pngPath) {
79
+ return new Promise((resolve, reject) => {
80
+ const img = new canvas_1.Image();
81
+ img.onload = () => {
82
+ const canvas = (0, canvas_1.createCanvas)(img.width, img.height);
83
+ const context = canvas.getContext("2d");
84
+ context.drawImage(img, 0, 0);
85
+ resolve(canvas);
86
+ };
87
+ img.onerror = reject;
88
+ img.src = pngPath;
89
+ });
90
+ }
91
+ function findNearestColor(pixel, palette) {
92
+ const colorsInput = [];
93
+ for (let p = 0; p < palette.length; p += 4) {
94
+ colorsInput.push({
95
+ name: `color${p / 4}`,
96
+ source: "",
97
+ rgb: {
98
+ r: palette[p],
99
+ g: palette[p + 1],
100
+ b: palette[p + 2],
101
+ },
102
+ });
103
+ }
104
+ const pixelInput = {
105
+ r: pixel[0],
106
+ g: pixel[1],
107
+ b: pixel[2],
108
+ };
109
+ const nearestResult = (0, nearest_color_1.default)(pixelInput, colorsInput);
110
+ pixel[0] = nearestResult.rgb.r;
111
+ pixel[1] = nearestResult.rgb.g;
112
+ pixel[2] = nearestResult.rgb.b;
113
+ return pixel;
114
+ }
115
+ function isMagenta(pixel) {
116
+ return pixel[0] === 255 && pixel[1] === 0 && pixel[2] === 255;
117
+ }
118
+ async function forceCanvasToPalette(canvas, palette) {
119
+ if (palette.width !== 15) {
120
+ throw new Error("forceCanvasToPalette: palette needs to be 15px wide (it should not have zero/magenta in it)");
121
+ }
122
+ if (palette.height !== 1) {
123
+ throw new Error("forceCanvasToPalette: palette needs to be 1px tall");
124
+ }
125
+ const canvasImageData = canvas
126
+ .getContext("2d")
127
+ .getImageData(0, 0, canvas.width, canvas.height);
128
+ const paletteImageData = palette
129
+ .getContext("2d")
130
+ .getImageData(0, 0, palette.width, palette.height);
131
+ for (let p = 0; p < canvasImageData.data.length; p += 4) {
132
+ const pixel = canvasImageData.data.slice(p, p + 4);
133
+ if (isMagenta(pixel)) {
134
+ continue;
135
+ }
136
+ const nearestPixel = findNearestColor(pixel, paletteImageData.data);
137
+ canvasImageData.data.set(nearestPixel, p);
138
+ }
139
+ canvas.getContext("2d").putImageData(canvasImageData, 0, 0);
140
+ return canvas;
141
+ }
142
+ //# sourceMappingURL=canvas.js.map
@@ -0,0 +1,2 @@
1
+ declare function rgbToGBA16(r: number, g: number, b: number): number;
2
+ export { rgbToGBA16 };
package/dist/colors.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rgbToGBA16 = rgbToGBA16;
4
+ function rgbToGBA16(r, g, b) {
5
+ const gbaR = Math.floor((31 * r) / 255);
6
+ const gbaG = Math.floor((31 * g) / 255);
7
+ const gbaB = Math.floor((31 * b) / 255);
8
+ return (gbaB << 10) | (gbaG << 5) | gbaR;
9
+ }
10
+ //# sourceMappingURL=colors.js.map
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const path = __importStar(require("node:path"));
38
+ const fsp = __importStar(require("fs/promises"));
39
+ const sprite_1 = require("./sprite");
40
+ const background_1 = require("./background");
41
+ /**
42
+ * Loads the json spec from the file path and converts all file paths
43
+ * inside to absolute paths so the rest of the tool doesn't have to think about it
44
+ */
45
+ function hydrateJsonSpec(jsonSpecPath) {
46
+ const rootDir = path.dirname(jsonSpecPath);
47
+ const initialSpec = require(jsonSpecPath);
48
+ return {
49
+ ...initialSpec,
50
+ outputDir: path.resolve(rootDir, initialSpec.outputDir),
51
+ format: initialSpec.format ?? "z80",
52
+ sprites: (initialSpec.sprites ?? []).map((s) => {
53
+ if ((0, sprite_1.isBasicSpriteSpec)(s)) {
54
+ return {
55
+ ...s,
56
+ file: path.resolve(rootDir, s.file),
57
+ forcePalette: s.forcePalette
58
+ ? path.resolve(rootDir, s.forcePalette)
59
+ : undefined,
60
+ };
61
+ }
62
+ else {
63
+ return {
64
+ ...s,
65
+ forcePalette: s.forcePalette
66
+ ? path.resolve(rootDir, s.forcePalette)
67
+ : undefined,
68
+ sharedPalette: s.sharedPalette.map((ss) => {
69
+ return {
70
+ ...ss,
71
+ file: path.resolve(rootDir, ss.file),
72
+ };
73
+ }),
74
+ };
75
+ }
76
+ }),
77
+ backgrounds: (initialSpec.backgrounds ?? []).map((bg) => {
78
+ return {
79
+ ...bg,
80
+ file: path.resolve(rootDir, bg.file),
81
+ };
82
+ }),
83
+ };
84
+ }
85
+ async function main(jsonSpec) {
86
+ if (jsonSpec.format === "bin") {
87
+ throw new Error("convertpng does not support bin format");
88
+ }
89
+ const ext = jsonSpec.format === "z80" ? "asm" : "c.inc";
90
+ for (const sprite of jsonSpec.sprites) {
91
+ const processResult = await (0, sprite_1.processSprite)(sprite, jsonSpec.format, sprite.forcePalette);
92
+ if ((0, sprite_1.isBasicSpriteSpec)(sprite)) {
93
+ const fileRoot = path.basename(sprite.file, path.extname(sprite.file));
94
+ const tilesOutputPath = path.resolve(jsonSpec.outputDir, `${fileRoot}.tiles.${ext}`);
95
+ const paletteOutputPath = path.resolve(jsonSpec.outputDir, `${fileRoot}.palette.${ext}`);
96
+ await fsp.writeFile(tilesOutputPath, processResult.tilesSrc[0]);
97
+ console.log("wrote", tilesOutputPath);
98
+ await fsp.writeFile(paletteOutputPath, processResult.paletteSrc);
99
+ console.log("wrote", paletteOutputPath);
100
+ }
101
+ else {
102
+ for (let i = 0; i < sprite.sharedPalette.length; ++i) {
103
+ const subsprite = sprite.sharedPalette[i];
104
+ const fileRoot = path.basename(subsprite.file, path.extname(subsprite.file));
105
+ const tilesOutputPath = path.resolve(jsonSpec.outputDir, `${fileRoot}.tiles.${ext}`);
106
+ await fsp.writeFile(tilesOutputPath, processResult.tilesSrc[i]);
107
+ console.log("wrote", tilesOutputPath);
108
+ }
109
+ const paletteOutputPath = path.resolve(jsonSpec.outputDir, `${sprite.name}.shared.palette.${ext}`);
110
+ await fsp.writeFile(paletteOutputPath, processResult.paletteSrc);
111
+ console.log("wrote", paletteOutputPath);
112
+ }
113
+ }
114
+ for (const bg of jsonSpec.backgrounds) {
115
+ const processResult = await (0, background_1.processBackground)(bg);
116
+ const fileRoot = path.basename(bg.file, path.extname(bg.file));
117
+ const tilesAsmPath = path.resolve(jsonSpec.outputDir, `${fileRoot}.tiles.asm`);
118
+ const paletteAsmPath = path.resolve(jsonSpec.outputDir, `${fileRoot}.palette.asm`);
119
+ const mapAsmPath = path.resolve(jsonSpec.outputDir, `${fileRoot}.map.asm`);
120
+ await fsp.writeFile(tilesAsmPath, processResult.tilesAsmSrc);
121
+ console.log("wrote", tilesAsmPath);
122
+ await fsp.writeFile(paletteAsmPath, processResult.paletteAsmSrc);
123
+ console.log("wrote", paletteAsmPath);
124
+ await fsp.writeFile(mapAsmPath, processResult.mapAsmSrc);
125
+ console.log("wrote", mapAsmPath);
126
+ }
127
+ }
128
+ if (require.main === module) {
129
+ const [_tsNode, _convertpng, jsonSpecPath] = process.argv;
130
+ if (!jsonSpecPath) {
131
+ console.error("usage: gba-convertpng <json-spec-path>");
132
+ process.exit(1);
133
+ }
134
+ const jsonSpec = hydrateJsonSpec(path.resolve(jsonSpecPath));
135
+ main(jsonSpec)
136
+ .then(() => console.log("done"))
137
+ .catch((e) => console.error(e));
138
+ }
139
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,4 @@
1
+ import { Canvas } from "canvas";
2
+ declare function extractPalette(c: Canvas, pad?: boolean): number[];
3
+ declare function reducePalettes(palettes: number[][]): number[];
4
+ export { extractPalette, reducePalettes };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractPalette = extractPalette;
4
+ exports.reducePalettes = reducePalettes;
5
+ const colors_1 = require("./colors");
6
+ const MAGENTA = (0, colors_1.rgbToGBA16)(255, 0, 255);
7
+ function extractPalette(c, pad = true) {
8
+ const gbaColors = new Set();
9
+ const imageData = c.getContext("2d").getImageData(0, 0, c.width, c.height);
10
+ for (let p = 0; p < imageData.data.length; p += 4) {
11
+ // skip anything not fully opaque
12
+ if (imageData.data[p + 3] !== 255) {
13
+ continue;
14
+ }
15
+ const r = imageData.data[p];
16
+ const g = imageData.data[p + 1];
17
+ const b = imageData.data[p + 2];
18
+ const gbaColor = (0, colors_1.rgbToGBA16)(r, g, b);
19
+ gbaColors.add(gbaColor);
20
+ }
21
+ const rawPalette = Array.from(gbaColors);
22
+ // make sure there is no magenta in the palette
23
+ const paletteWithoutMangenta = rawPalette.filter((c) => c !== MAGENTA);
24
+ // then append magenta as the first color, to become transparent
25
+ const palette = [MAGENTA].concat(paletteWithoutMangenta);
26
+ while (pad && palette.length < 16) {
27
+ palette.push(0);
28
+ }
29
+ return palette;
30
+ }
31
+ function reducePalettes(palettes) {
32
+ const colorMap = {};
33
+ const mergedPalette = [];
34
+ for (const palette of palettes) {
35
+ for (const color of palette) {
36
+ if (!colorMap[color]) {
37
+ colorMap[color] = true;
38
+ mergedPalette.push(color);
39
+ }
40
+ }
41
+ }
42
+ if (mergedPalette.length > 16) {
43
+ throw new Error(`reducePalette: final palette is too large: ${mergedPalette.length}`);
44
+ }
45
+ return mergedPalette;
46
+ }
47
+ //# sourceMappingURL=palette.js.map
@@ -0,0 +1,11 @@
1
+ import { BasicSpriteSpec, Format, SpriteSpec } from "./types";
2
+ import { Canvas } from "canvas";
3
+ type ProcessSpriteResult = {
4
+ canvas: Canvas;
5
+ tilesSrc: string[] | number[];
6
+ paletteSrc: string | number[];
7
+ };
8
+ declare function isBasicSpriteSpec(sprite: SpriteSpec): sprite is BasicSpriteSpec;
9
+ declare function processSprite(sprite: SpriteSpec, format: Format, forcedPalettePath?: string): Promise<ProcessSpriteResult>;
10
+ export { isBasicSpriteSpec, processSprite };
11
+ export type { ProcessSpriteResult };
package/dist/sprite.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isBasicSpriteSpec = isBasicSpriteSpec;
4
+ exports.processSprite = processSprite;
5
+ const asm_1 = require("./asm");
6
+ const c_1 = require("./c");
7
+ const canvas_1 = require("./canvas");
8
+ const palette_1 = require("./palette");
9
+ const tile_1 = require("./tile");
10
+ function isBasicSpriteSpec(sprite) {
11
+ return "file" in sprite;
12
+ }
13
+ async function processBasicSprite(sprite, format, forcedPalette) {
14
+ let canvas = await (0, canvas_1.reduceColors)(await (0, canvas_1.createCanvasFromPath)(sprite.file), 16);
15
+ let palette;
16
+ if (forcedPalette) {
17
+ canvas = await (0, canvas_1.forceCanvasToPalette)(canvas, forcedPalette);
18
+ palette = (0, palette_1.extractPalette)(forcedPalette, false);
19
+ }
20
+ else {
21
+ palette = (0, palette_1.extractPalette)(canvas, !sprite.trimPalette);
22
+ }
23
+ const tiles = (0, tile_1.extractTiles)(canvas, palette, sprite.frames).flat(1);
24
+ if (format === "bin") {
25
+ return {
26
+ canvas,
27
+ tilesSrc: tiles,
28
+ paletteSrc: palette,
29
+ };
30
+ }
31
+ const tileSrcFun = format === "z80" ? asm_1.toAsm : c_1.toC;
32
+ const paletteSrcFun = format === "z80" ? asm_1.toAsm : c_1.toC;
33
+ return {
34
+ canvas,
35
+ tilesSrc: [tileSrcFun(tiles, "b", 4)],
36
+ paletteSrc: paletteSrcFun(palette, "w", 4),
37
+ };
38
+ }
39
+ async function processSharedPaletteSprites(sharedPaletteSprite, format, forcedPalette) {
40
+ const canvases = [];
41
+ const palettes = [];
42
+ for (let i = 0; i < sharedPaletteSprite.sharedPalette.length; ++i) {
43
+ let c = await (0, canvas_1.reduceColors)(await (0, canvas_1.createCanvasFromPath)(sharedPaletteSprite.sharedPalette[i].file), 16);
44
+ if (forcedPalette) {
45
+ c = await (0, canvas_1.forceCanvasToPalette)(c, forcedPalette);
46
+ }
47
+ canvases.push(c);
48
+ palettes.push((0, palette_1.extractPalette)(c, !sharedPaletteSprite.trimPalette));
49
+ }
50
+ const commonPalette = forcedPalette
51
+ ? (0, palette_1.extractPalette)(forcedPalette, false)
52
+ : (0, palette_1.reducePalettes)(palettes);
53
+ const tiles = [];
54
+ for (let i = 0; i < sharedPaletteSprite.sharedPalette.length; ++i) {
55
+ const t = (0, tile_1.extractTiles)(canvases[i], commonPalette, sharedPaletteSprite.sharedPalette[i].frames).flat(1);
56
+ tiles.push(t);
57
+ }
58
+ const tileSrcFun = format === "z80" ? asm_1.toAsm : c_1.toC;
59
+ const paletteSrcFun = format === "z80" ? asm_1.toAsm : c_1.toC;
60
+ return {
61
+ // this is useless in this scenario, but canvas
62
+ // really only exists for the puzzle generator
63
+ canvas: canvases[0],
64
+ tilesSrc: tiles.map((t) => tileSrcFun(t, "b", 4)),
65
+ paletteSrc: paletteSrcFun(commonPalette, "w", 4),
66
+ };
67
+ }
68
+ async function processSprite(sprite, format, forcedPalettePath) {
69
+ const forcedPalette = forcedPalettePath
70
+ ? await (0, canvas_1.createCanvasFromPath)(forcedPalettePath)
71
+ : undefined;
72
+ if (isBasicSpriteSpec(sprite)) {
73
+ return processBasicSprite(sprite, format, forcedPalette);
74
+ }
75
+ else {
76
+ return processSharedPaletteSprites(sprite, format, forcedPalette);
77
+ }
78
+ }
79
+ //# sourceMappingURL=sprite.js.map
package/dist/tile.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Canvas } from "canvas";
2
+ declare function extractTiles(c: Canvas, palette: number[], frameCount: number): number[][];
3
+ declare function dedupeTiles(tiles: number[][]): number[][];
4
+ export { extractTiles, dedupeTiles };
package/dist/tile.js ADDED
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractTiles = extractTiles;
4
+ exports.dedupeTiles = dedupeTiles;
5
+ const colors_1 = require("./colors");
6
+ function extractTile(imageData, palette) {
7
+ const tileData = [];
8
+ for (let p = 0; p < imageData.data.length; p += 8) {
9
+ // first pixel in tile, high nibble
10
+ // if this pixel has any transparency, then use palette index 0
11
+ // which will make it fully transparent on the gba
12
+ let hindex = 0;
13
+ if (imageData.data[p + 7] === 255) {
14
+ // this is a fully opaque pixel, so use it
15
+ const hr = imageData.data[p + 4];
16
+ const hg = imageData.data[p + 5];
17
+ const hb = imageData.data[p + 6];
18
+ const hgbaColor = (0, colors_1.rgbToGBA16)(hr, hg, hb);
19
+ hindex = palette.indexOf(hgbaColor);
20
+ }
21
+ // second pixel in tile, low nibble
22
+ // if this pixel has any transparency, then use palette index 0
23
+ // which will make it fully transparent on the gba
24
+ let lindex = 0;
25
+ if (imageData.data[p + 3] === 255) {
26
+ // this is a fully opaque pixel, so use it
27
+ const lr = imageData.data[p + 0];
28
+ const lg = imageData.data[p + 1];
29
+ const lb = imageData.data[p + 2];
30
+ const lgbaColor = (0, colors_1.rgbToGBA16)(lr, lg, lb);
31
+ lindex = palette.indexOf(lgbaColor);
32
+ }
33
+ const tileByte = ((hindex & 0xf) << 4) | (lindex & 0xf);
34
+ tileData.push(tileByte);
35
+ }
36
+ return tileData;
37
+ }
38
+ // a GBA tile using a 16 color palette has two indexes per byte
39
+ // so one 8x8 tile is 4x8 bytes
40
+ //
41
+ // [ p1|p2, p3|p4, p5|p6, p7|p8]
42
+ // [p9|p10, p11|p12, p13|p14, p15|p16]
43
+ // ...
44
+ //
45
+ // it is stored row oriented
46
+ // for an image that has multiple tiles, it is stored in a flat 1d array
47
+ //
48
+ // [a|b]
49
+ // [c|d]
50
+ //
51
+ // becomes [a][b][c][d]
52
+ //
53
+ // sprites with more than one frame look like this in the png
54
+ //
55
+ // [a1|b1][a2|b2]
56
+ // [c1|d1][c2|d2]
57
+ //
58
+ // and become this in the data
59
+ // [a1][b1][c1][d1][a2][b2][c2][d2]
60
+ function extractTiles(c, palette, frameCount) {
61
+ const context = c.getContext("2d");
62
+ const tilesData = [];
63
+ const frameWidthT = c.width / 8 / frameCount;
64
+ const frameHeightT = c.height / 8;
65
+ for (let f = 0; f < frameCount; ++f) {
66
+ for (let y = 0; y < frameHeightT; ++y) {
67
+ for (let x = f * frameWidthT; x < (f + 1) * frameWidthT; ++x) {
68
+ const imageData = context.getImageData(x * 8, y * 8, 8, 8);
69
+ const curTileData = extractTile(imageData, palette);
70
+ tilesData.push(curTileData);
71
+ }
72
+ }
73
+ }
74
+ return tilesData;
75
+ }
76
+ function dedupeTiles(tiles) {
77
+ const dedupedMap = tiles.reduce((accum, tile) => {
78
+ const key = tile.join(",");
79
+ accum[key] = tile;
80
+ return accum;
81
+ }, {});
82
+ return Object.values(dedupedMap);
83
+ }
84
+ //# sourceMappingURL=tile.js.map
@@ -0,0 +1,2 @@
1
+ export declare function toHexByte(num: number): string;
2
+ export declare function toHexWord(num: number): string;
package/dist/toHex.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toHexByte = toHexByte;
4
+ exports.toHexWord = toHexWord;
5
+ function toHexByte(num) {
6
+ const rawHex = num.toString(16);
7
+ const neededFiller = Math.max(2 - rawHex.length, 0);
8
+ const filler = new Array(neededFiller).fill("0").join("");
9
+ return `0x${filler}${rawHex}`;
10
+ }
11
+ function toHexWord(num) {
12
+ const rawHex = num.toString(16);
13
+ const neededFiller = Math.max(4 - rawHex.length, 0);
14
+ const filler = new Array(neededFiller).fill("0").join("");
15
+ return `0x${filler}${rawHex}`;
16
+ }
17
+ //# sourceMappingURL=toHex.js.map
@@ -0,0 +1,25 @@
1
+ export type Format = "C" | "z80" | "bin";
2
+ export type BasicSpriteSpec = {
3
+ file: string;
4
+ frames: number;
5
+ trimPalette?: boolean;
6
+ forcePalette?: string;
7
+ };
8
+ export type SharedPaletteSpriteSpec = {
9
+ name: string;
10
+ trimPalette?: boolean;
11
+ sharedPalette: BasicSpriteSpec[];
12
+ forcePalette?: string;
13
+ };
14
+ export type SpriteSpec = BasicSpriteSpec | SharedPaletteSpriteSpec;
15
+ export type BackgroundSpec = {
16
+ file: string;
17
+ trimPalette?: boolean;
18
+ };
19
+ export type ImportedJsonSpec = {
20
+ outputDir: string;
21
+ format?: Format;
22
+ sprites?: SpriteSpec[];
23
+ backgrounds?: BackgroundSpec[];
24
+ };
25
+ export type JsonSpec = Required<ImportedJsonSpec>;
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@city41/gba-convertpng",
3
+ "version": "0.0.2",
4
+ "description": "Converts png images to GBA tile format",
5
+ "main": "index.js",
6
+ "repository": "github.com/city41/gba-convertpng",
7
+ "author": "Matt Greer <matt.e.greer@gmail.com>",
8
+ "license": "MIT",
9
+ "files": [
10
+ "dist/**/*.js",
11
+ "dist/**/*.d.ts"
12
+ ],
13
+ "bin": {
14
+ "gba-convertpng": "./dist/main.js"
15
+ },
16
+ "scripts": {
17
+ "type-check": "yarn tsc --noEmit",
18
+ "prebuild": "yarn type-check",
19
+ "build": "tsc",
20
+ "prepublish": "yarn build"
21
+ },
22
+ "dependencies": {
23
+ "@types/imagemagick": "^0.0.35",
24
+ "@types/lodash": "^4.17.20",
25
+ "@types/nearest-color": "^0.4.1",
26
+ "canvas": "^3.2.0",
27
+ "imagemagick": "^0.1.3",
28
+ "lodash": "^4.17.21",
29
+ "mkdirp": "^3.0.1",
30
+ "nearest-color": "^0.4.4"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^24.9.1",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }