@remvst/asset-catalog 1.0.4 → 1.1.0

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.
@@ -11,6 +11,8 @@ const path_1 = require("path");
11
11
  const yargs_1 = __importDefault(require("yargs/yargs"));
12
12
  const helpers_1 = require("yargs/helpers");
13
13
  const tree_1 = require("./tree");
14
+ const bin_pack_1 = __importDefault(require("bin-pack"));
15
+ const canvas_1 = require("canvas");
14
16
  function importName(assetDir, png) {
15
17
  let importName = png;
16
18
  importName = (0, path_1.resolve)(png);
@@ -37,7 +39,7 @@ function generatedTemplateInterface(tree, name, indent = '') {
37
39
  generated += indent + '}\n';
38
40
  return generated;
39
41
  }
40
- async function generatedCreateCatalogFunction(assetDir, tree) {
42
+ async function generatedCreateCatalogFunction(assetDir, tree, spritesheet) {
41
43
  async function rec(tree, indent = '') {
42
44
  let generated = '{\n';
43
45
  for (const [subname, item] of tree.entries()) {
@@ -49,11 +51,20 @@ async function generatedCreateCatalogFunction(assetDir, tree) {
49
51
  const dimensions = (0, image_size_1.default)(item);
50
52
  const stats = await fs_1.promises.stat(item);
51
53
  const withoutExt = (0, path_1.basename)(subname, (0, path_1.extname)(subname));
54
+ const spriteData = spritesheet?.get((0, path_1.resolve)(item)) || null;
55
+ let spriteDataStr = 'null';
56
+ if (spriteData) {
57
+ spriteDataStr = `{
58
+ sheet: SpriteSheetPng,
59
+ frame: ${JSON.stringify(spriteData)},
60
+ }`;
61
+ }
52
62
  generated += indent + ` ${(0, utils_1.lowerCamelize)(withoutExt)}: createItem({
53
63
  path: ${importName(assetDir, item)},
54
64
  width: ${dimensions.width},
55
65
  height: ${dimensions.height},
56
66
  size: ${stats.size},
67
+ spriteData: ${spriteDataStr},
57
68
  }),\n`;
58
69
  }
59
70
  }
@@ -61,26 +72,83 @@ async function generatedCreateCatalogFunction(assetDir, tree) {
61
72
  return generated;
62
73
  }
63
74
  let generated = '\n';
64
- generated += 'export function createTextureCatalog<T>(createItem: (opts: {path: string, width: number, height: number, size: number}) => T): TextureCatalog<T> {\n';
75
+ generated += 'export function createTextureCatalog<T>(createItem: (opts: {path: string, width: number, height: number, size: number, spriteData: SpriteData}) => T): TextureCatalog<T> {\n';
65
76
  generated += ` return ${await rec(tree, ' ')};\n`;
66
77
  generated += '}\n';
67
78
  return generated;
68
79
  }
80
+ async function createSpritesheet(tree, outFile, excludes) {
81
+ const bins = [];
82
+ const padding = 1;
83
+ function generateBins(tree) {
84
+ itemLoop: for (const item of tree.values()) {
85
+ if (item instanceof Map) {
86
+ generateBins(item);
87
+ }
88
+ else {
89
+ for (const exclude of excludes) {
90
+ if (item.includes(exclude)) {
91
+ continue itemLoop;
92
+ }
93
+ }
94
+ const dimensions = (0, image_size_1.default)(item);
95
+ bins.push({
96
+ width: dimensions.width + padding * 2,
97
+ height: dimensions.height + padding * 2,
98
+ path: (0, path_1.resolve)(item),
99
+ });
100
+ }
101
+ }
102
+ }
103
+ generateBins(tree);
104
+ const packed = (0, bin_pack_1.default)(bins);
105
+ const canvas = (0, canvas_1.createCanvas)(packed.width, packed.height);
106
+ const ctx = canvas.getContext('2d');
107
+ for (const item of packed.items) {
108
+ const image = await (0, canvas_1.loadImage)(item.item.path);
109
+ ctx.drawImage(image, item.x + padding, item.y + padding);
110
+ }
111
+ const buffer = canvas.toBuffer("image/png");
112
+ await fs_1.promises.writeFile(outFile, buffer);
113
+ const resultMap = new Map();
114
+ for (const item of packed.items) {
115
+ resultMap.set(item.item.path, {
116
+ x: item.x + padding,
117
+ y: item.y + padding,
118
+ width: item.width - padding * 2,
119
+ height: item.height - padding * 2,
120
+ });
121
+ }
122
+ return resultMap;
123
+ }
69
124
  async function main() {
70
125
  const argv = await (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
71
126
  .options({
72
127
  'outFile': {
73
128
  type: 'string',
74
- default: '.',
129
+ default: 'textures.ts',
75
130
  alias: 'o',
76
131
  describe: 'Directory to generate the files into',
77
132
  },
78
133
  'assetDir': {
79
134
  type: 'string',
80
- default: './package.json',
135
+ default: '.',
81
136
  alias: 'a',
82
- describe: 'package.json file to use for the version',
83
- }
137
+ describe: 'Asset directory where all the PNGs are located',
138
+ },
139
+ 'outSpritesheet': {
140
+ type: 'string',
141
+ required: false,
142
+ alias: 's',
143
+ describe: 'Path to the generated spritesheet',
144
+ },
145
+ 'spritesheetExclude': {
146
+ type: 'string',
147
+ array: true,
148
+ required: false,
149
+ alias: 'x',
150
+ describe: 'Exclude certain paths from the spritesheet',
151
+ },
84
152
  })
85
153
  .argv;
86
154
  const texturesRoot = argv.assetDir;
@@ -97,12 +165,29 @@ async function main() {
97
165
  const importPath = (0, path_1.relative)((0, path_1.dirname)(argv.outFile), png).replace(/\\/g, '/');
98
166
  imports.push(`import ${importName(argv.assetDir, png)} from './${importPath}';`);
99
167
  }
168
+ let spritesheet = null;
169
+ if (argv.outSpritesheet) {
170
+ spritesheet = await createSpritesheet(tree, argv.outSpritesheet, argv.spritesheetExclude || []);
171
+ const importPath = (0, path_1.relative)((0, path_1.dirname)(argv.outFile), (0, path_1.resolve)(argv.outSpritesheet)).replace(/\\/g, '/');
172
+ imports.push(`import SpriteSheetPng from './${importPath}';`);
173
+ }
100
174
  let generatedFileContent = '';
101
175
  generatedFileContent += imports.join('\n');
102
176
  generatedFileContent += '\n\n';
177
+ generatedFileContent += `export interface Rectangle {
178
+ x: number;
179
+ y: number;
180
+ width: number;
181
+ height: number;
182
+ }\n`;
183
+ generatedFileContent += `export interface SpriteData {
184
+ sheet: string;
185
+ frame: Rectangle;
186
+ }\n`;
187
+ generatedFileContent += '\n\n';
103
188
  generatedFileContent += 'export type TextureCatalog<T> = ' + generatedTemplateInterface(tree, 'TextureCatalog');
104
189
  generatedFileContent += '\n\n';
105
- generatedFileContent += await generatedCreateCatalogFunction(argv.assetDir, tree);
190
+ generatedFileContent += await generatedCreateCatalogFunction(argv.assetDir, tree, spritesheet);
106
191
  await fs_1.promises.writeFile(generatedTs, generatedFileContent);
107
192
  }
108
193
  main();
File without changes
package/lib/utils.js CHANGED
@@ -38,7 +38,7 @@ function categoryPath(assetDir, png) {
38
38
  const dir = (0, path_1.dirname)(png);
39
39
  const trimmedDir = (0, path_1.relative)(assetDir, dir);
40
40
  return trimmedDir
41
- .split('/')
41
+ .split(/[\/\\]/g)
42
42
  .map(component => lowerCamelize(component))
43
43
  .filter(component => component.length > 0);
44
44
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@remvst/asset-catalog",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "generate-image-catalog": "./lib/generate-image-catalog.js",
7
7
  "generate-sound-catalog": "./lib/generate-sound-catalog.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "rm -rf lib && tsc",
11
- "test:images": "ts-node src/generate-image-catalog.ts --assetDir=./testData --outFile=testOut/images.ts",
10
+ "build": "rm -rf lib && tsc && chmod +x lib/generate-image-catalog.js lib/generate-sound-catalog.js",
11
+ "test:images": "ts-node src/generate-image-catalog.ts --assetDir=./testData --outFile=testOut/images.ts --outSpritesheet=testOut/spritesheet.png",
12
12
  "test:sounds": "ts-node src/generate-sound-catalog.ts --assetDir=./testData/sounds --outFile=testOut/sounds.ts",
13
13
  "test": "npm run test:images && npm run test:sounds",
14
14
  "prepublishOnly": "npm i && npm run build"
@@ -16,10 +16,13 @@
16
16
  "author": "Rémi Vansteelandt",
17
17
  "license": "UNLICENSED",
18
18
  "dependencies": {
19
+ "bin-pack": "^1.0.2",
20
+ "canvas": "^2.11.2",
19
21
  "image-size": "^1.0.2",
20
22
  "yargs": "^17.7.2"
21
23
  },
22
24
  "devDependencies": {
25
+ "@types/bin-pack": "^1.0.3",
23
26
  "@types/node": "^18.11.5",
24
27
  "@types/yargs": "^17.0.32",
25
28
  "ts-node": "^10.9.1",
@@ -7,6 +7,11 @@ import { resolve, relative, dirname, extname, basename } from 'path';
7
7
  import yargs from 'yargs/yargs';
8
8
  import { hideBin } from 'yargs/helpers';
9
9
  import { Tree, generateTree } from './tree';
10
+ import pack from 'bin-pack';
11
+ import { createCanvas, loadImage } from 'canvas';
12
+
13
+ type Rectangle = {x: number, y: number, width: number, height: number};
14
+ type SpritesheetResult = Map<string, Rectangle>;
10
15
 
11
16
  function importName(assetDir: string, png: string): string {
12
17
  let importName = png;
@@ -37,7 +42,7 @@ function generatedTemplateInterface(tree: Tree, name: string, indent: string = '
37
42
  return generated;
38
43
  }
39
44
 
40
- async function generatedCreateCatalogFunction(assetDir: string, tree: Tree): Promise<string> {
45
+ async function generatedCreateCatalogFunction(assetDir: string, tree: Tree, spritesheet: SpritesheetResult | null): Promise<string> {
41
46
  async function rec(tree: Tree, indent: string = '') {
42
47
  let generated = '{\n';
43
48
  for (const [subname, item] of tree.entries()) {
@@ -48,11 +53,21 @@ async function generatedCreateCatalogFunction(assetDir: string, tree: Tree): Pro
48
53
  const dimensions = sizeOf(item);
49
54
  const stats = await fs.stat(item);
50
55
  const withoutExt = basename(subname, extname(subname));
56
+ const spriteData = spritesheet?.get(resolve(item)) || null;
57
+ let spriteDataStr = 'null';
58
+ if (spriteData) {
59
+ spriteDataStr = `{
60
+ sheet: SpriteSheetPng,
61
+ frame: ${JSON.stringify(spriteData)},
62
+ }`;
63
+ }
64
+
51
65
  generated += indent + ` ${lowerCamelize(withoutExt)}: createItem({
52
66
  path: ${importName(assetDir, item)},
53
67
  width: ${dimensions.width},
54
68
  height: ${dimensions.height},
55
69
  size: ${stats.size},
70
+ spriteData: ${spriteDataStr},
56
71
  }),\n`;
57
72
  }
58
73
  }
@@ -61,27 +76,93 @@ async function generatedCreateCatalogFunction(assetDir: string, tree: Tree): Pro
61
76
  }
62
77
 
63
78
  let generated = '\n';
64
- generated += 'export function createTextureCatalog<T>(createItem: (opts: {path: string, width: number, height: number, size: number}) => T): TextureCatalog<T> {\n';
79
+ generated += 'export function createTextureCatalog<T>(createItem: (opts: {path: string, width: number, height: number, size: number, spriteData: SpriteData}) => T): TextureCatalog<T> {\n';
65
80
  generated += ` return ${await rec(tree, ' ')};\n`;
66
81
  generated += '}\n';
67
82
  return generated;
68
83
  }
69
84
 
85
+ async function createSpritesheet(tree: Tree, outFile: string, excludes: string[]): Promise<SpritesheetResult> {
86
+ const bins: (pack.Bin & {path: string})[] = [];
87
+
88
+ const padding = 1;
89
+
90
+ function generateBins(tree: Tree) {
91
+ itemLoop: for (const item of tree.values()) {
92
+ if (item instanceof Map) {
93
+ generateBins(item);
94
+ } else {
95
+ for (const exclude of excludes) {
96
+ if (item.includes(exclude)) {
97
+ continue itemLoop;
98
+ }
99
+ }
100
+
101
+ const dimensions = sizeOf(item);
102
+ bins.push({
103
+ width: dimensions.width! + padding * 2,
104
+ height: dimensions.height! + padding * 2,
105
+ path: resolve(item),
106
+ });
107
+ }
108
+ }
109
+ }
110
+
111
+ generateBins(tree);
112
+
113
+ const packed = pack(bins);
114
+
115
+ const canvas = createCanvas(packed.width, packed.height);
116
+ const ctx = canvas.getContext('2d');
117
+
118
+ for (const item of packed.items) {
119
+ const image = await loadImage(item.item.path);
120
+ ctx.drawImage(image, item.x + padding, item.y + padding);
121
+ }
122
+
123
+ const buffer = canvas.toBuffer("image/png");
124
+ await fs.writeFile(outFile, buffer);
125
+
126
+ const resultMap = new Map<string, {x: number, y: number, width: number, height: number}>();
127
+ for (const item of packed.items) {
128
+ resultMap.set(item.item.path, {
129
+ x: item.x + padding,
130
+ y: item.y + padding,
131
+ width: item.width - padding * 2,
132
+ height: item.height - padding * 2,
133
+ });
134
+ }
135
+ return resultMap;
136
+ }
137
+
70
138
  async function main() {
71
139
  const argv = await yargs(hideBin(process.argv))
72
140
  .options({
73
- 'outFile': {
74
- type: 'string',
75
- default: '.',
76
- alias: 'o',
141
+ 'outFile': {
142
+ type: 'string',
143
+ default: 'textures.ts',
144
+ alias: 'o',
77
145
  describe: 'Directory to generate the files into',
78
146
  },
79
- 'assetDir': {
80
- type: 'string',
81
- default: './package.json',
82
- alias: 'a',
83
- describe: 'package.json file to use for the version',
84
- }
147
+ 'assetDir': {
148
+ type: 'string',
149
+ default: '.',
150
+ alias: 'a',
151
+ describe: 'Asset directory where all the PNGs are located',
152
+ },
153
+ 'outSpritesheet': {
154
+ type: 'string',
155
+ required: false,
156
+ alias: 's',
157
+ describe: 'Path to the generated spritesheet',
158
+ },
159
+ 'spritesheetExclude': {
160
+ type: 'string',
161
+ array: true,
162
+ required: false,
163
+ alias: 'x',
164
+ describe: 'Exclude certain paths from the spritesheet',
165
+ },
85
166
  })
86
167
  .argv;
87
168
 
@@ -102,12 +183,31 @@ async function main() {
102
183
  imports.push(`import ${importName(argv.assetDir, png)} from './${importPath}';`);
103
184
  }
104
185
 
186
+ let spritesheet: SpritesheetResult | null = null;
187
+ if (argv.outSpritesheet) {
188
+ spritesheet = await createSpritesheet(tree, argv.outSpritesheet, argv.spritesheetExclude || []);
189
+
190
+ const importPath = relative(dirname(argv.outFile), resolve(argv.outSpritesheet)).replace(/\\/g, '/');
191
+ imports.push(`import SpriteSheetPng from './${importPath}';`);
192
+ }
193
+
105
194
  let generatedFileContent = '';
106
195
  generatedFileContent += imports.join('\n');
107
196
  generatedFileContent += '\n\n';
197
+ generatedFileContent += `export interface Rectangle {
198
+ x: number;
199
+ y: number;
200
+ width: number;
201
+ height: number;
202
+ }\n`;
203
+ generatedFileContent += `export interface SpriteData {
204
+ sheet: string;
205
+ frame: Rectangle;
206
+ }\n`;
207
+ generatedFileContent += '\n\n';
108
208
  generatedFileContent += 'export type TextureCatalog<T> = ' + generatedTemplateInterface(tree, 'TextureCatalog');
109
209
  generatedFileContent += '\n\n';
110
- generatedFileContent += await generatedCreateCatalogFunction(argv.assetDir, tree);
210
+ generatedFileContent += await generatedCreateCatalogFunction(argv.assetDir, tree, spritesheet);
111
211
 
112
212
  await fs.writeFile(generatedTs, generatedFileContent);
113
213
  }
package/src/utils.ts CHANGED
@@ -38,7 +38,7 @@ export function categoryPath(assetDir: string, png: string): string[] {
38
38
  const dir = dirname(png);
39
39
  const trimmedDir = relative(assetDir, dir);
40
40
  return trimmedDir
41
- .split('/')
41
+ .split(/[\/\\]/g)
42
42
  .map(component => lowerCamelize(component))
43
43
  .filter(component => component.length > 0);
44
44
  }
package/tsconfig.json CHANGED
@@ -5,7 +5,6 @@
5
5
  "declaration": true,
6
6
  "strict": true,
7
7
  "esModuleInterop": true,
8
- "allowJs": false,
9
8
  "outDir": "./lib",
10
9
  "skipLibCheck": true
11
10
  },
package/testOut/images.ts DELETED
@@ -1,38 +0,0 @@
1
- import _directions_left_png from './../testData/directions/left.png';
2
- import _directions_up_png from './../testData/directions/up.png';
3
- import _fire_png from './../testData/fire.png';
4
-
5
- export type TextureCatalog<T> = {
6
- directions: {
7
- left: T,
8
- up: T,
9
- }
10
- fire: T,
11
- }
12
-
13
-
14
-
15
- export function createTextureCatalog<T>(createItem: (opts: {path: string, width: number, height: number, size: number}) => T): TextureCatalog<T> {
16
- return {
17
- directions: {
18
- left: createItem({
19
- path: _directions_left_png,
20
- width: 100,
21
- height: 100,
22
- size: 4424,
23
- }),
24
- up: createItem({
25
- path: _directions_up_png,
26
- width: 100,
27
- height: 100,
28
- size: 601,
29
- }),
30
- },
31
- fire: createItem({
32
- path: _fire_png,
33
- width: 384,
34
- height: 384,
35
- size: 10908,
36
- }),
37
- };
38
- }
package/testOut/sounds.ts DELETED
@@ -1,45 +0,0 @@
1
- import __testData_sounds_click_UI_Click_Metallic_mono_ogg from '../testData/sounds/click/UI_Click_Metallic_mono.ogg';
2
- import __testData_sounds_click_UI_Click_Metallic_mono_mp3 from '../testData/sounds/click/UI_Click_Metallic_mono.mp3';
3
- import __testData_sounds_jump_jump2_ogg from '../testData/sounds/jump/jump2.ogg';
4
- import __testData_sounds_jump_xhGkA9Px_mp3 from '../testData/sounds/jump/xhGkA9Px.mp3';
5
-
6
- export class SoundDefinition {
7
- constructor(
8
- readonly basename: string,
9
- readonly files: string[],
10
- readonly averageFileSize: number,
11
- ) {}
12
- }
13
-
14
-
15
- export function sound_click(): SoundDefinition[] {
16
- return [
17
- new SoundDefinition(
18
- 'UI_Click_Metallic_mono',
19
- [
20
- __testData_sounds_click_UI_Click_Metallic_mono_ogg,
21
- __testData_sounds_click_UI_Click_Metallic_mono_mp3,
22
- ],
23
- 2836,
24
- ),
25
- ];
26
- }
27
-
28
- export function sound_jump(): SoundDefinition[] {
29
- return [
30
- new SoundDefinition(
31
- 'jump2',
32
- [
33
- __testData_sounds_jump_jump2_ogg,
34
- ],
35
- 5631,
36
- ),
37
- new SoundDefinition(
38
- 'xhGkA9Px',
39
- [
40
- __testData_sounds_jump_xhGkA9Px_mp3,
41
- ],
42
- 2106,
43
- ),
44
- ];
45
- }