@remvst/asset-catalog 1.0.5 → 1.2.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.
- package/lib/generate-image-catalog.js +92 -7
- package/lib/generate-sound-catalog.js +90 -3
- package/lib/utils.d.ts +1 -1
- package/lib/utils.js +2 -2
- package/package.json +9 -4
- package/src/generate-image-catalog.ts +107 -7
- package/src/generate-sound-catalog.ts +105 -4
- package/src/utils.ts +2 -2
- package/tsconfig.json +0 -1
|
@@ -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: '
|
|
135
|
+
default: '.',
|
|
81
136
|
alias: 'a',
|
|
82
|
-
describe: '
|
|
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();
|
|
@@ -9,6 +9,7 @@ const utils_1 = require("./utils");
|
|
|
9
9
|
const yargs_1 = __importDefault(require("yargs/yargs"));
|
|
10
10
|
const helpers_1 = require("yargs/helpers");
|
|
11
11
|
const path_1 = require("path");
|
|
12
|
+
const audiosprite_1 = __importDefault(require("audiosprite"));
|
|
12
13
|
async function main() {
|
|
13
14
|
const argv = await (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
14
15
|
.options({
|
|
@@ -20,9 +21,9 @@ async function main() {
|
|
|
20
21
|
},
|
|
21
22
|
'assetDir': {
|
|
22
23
|
type: 'string',
|
|
23
|
-
default: '
|
|
24
|
+
default: '.',
|
|
24
25
|
alias: 'a',
|
|
25
|
-
describe: '
|
|
26
|
+
describe: 'Asset directory where all the PNGs are located',
|
|
26
27
|
},
|
|
27
28
|
'wav': {
|
|
28
29
|
type: 'boolean',
|
|
@@ -39,6 +40,19 @@ async function main() {
|
|
|
39
40
|
default: true,
|
|
40
41
|
describe: 'Include .mp3 files',
|
|
41
42
|
},
|
|
43
|
+
'outSpritesheet': {
|
|
44
|
+
type: 'string',
|
|
45
|
+
required: false,
|
|
46
|
+
describe: 'Path to the exported spritesheet (without the extension)',
|
|
47
|
+
},
|
|
48
|
+
'spritesheetExcludeCategory': {
|
|
49
|
+
type: 'string',
|
|
50
|
+
array: true,
|
|
51
|
+
required: false,
|
|
52
|
+
alias: 'x',
|
|
53
|
+
default: [],
|
|
54
|
+
describe: 'Exclude certain categories from the spritesheet',
|
|
55
|
+
},
|
|
42
56
|
})
|
|
43
57
|
.argv;
|
|
44
58
|
const extensions = [];
|
|
@@ -54,7 +68,8 @@ async function main() {
|
|
|
54
68
|
}
|
|
55
69
|
catch (e) { }
|
|
56
70
|
const files = await (0, utils_1.allFiles)(argv.assetDir);
|
|
57
|
-
const sounds = files.filter(file => extensions.indexOf((0, path_1.extname)(file)) >= 0)
|
|
71
|
+
const sounds = files.filter(file => extensions.indexOf((0, path_1.extname)(file)) >= 0)
|
|
72
|
+
.filter(file => (0, path_1.basename)(file, (0, path_1.extname)(file)) !== 'sprites');
|
|
58
73
|
const defs = new Map();
|
|
59
74
|
for (const sound of sounds) {
|
|
60
75
|
const category = (0, utils_1.categoryPath)(argv.assetDir, sound).join('_');
|
|
@@ -68,6 +83,48 @@ async function main() {
|
|
|
68
83
|
}
|
|
69
84
|
defs.get(category).get(filenameWithoutExt).add(sound);
|
|
70
85
|
}
|
|
86
|
+
const spriteSounds = new Map();
|
|
87
|
+
let spritesheetJson = null;
|
|
88
|
+
if (argv.outSpritesheet) {
|
|
89
|
+
for (const category of defs.keys()) {
|
|
90
|
+
let isCategoryExcluded = false;
|
|
91
|
+
for (const exclusion of argv.spritesheetExcludeCategory) {
|
|
92
|
+
if (category.indexOf(exclusion) !== -1) {
|
|
93
|
+
isCategoryExcluded = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (isCategoryExcluded) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const filenameWithoutExt of defs.get(category).keys()) {
|
|
101
|
+
const fileSet = defs.get(category).get(filenameWithoutExt);
|
|
102
|
+
const files = Array.from(fileSet);
|
|
103
|
+
const pickedFile = files.find(file => (0, path_1.extname)(file) === '.wav') ||
|
|
104
|
+
files.find(file => (0, path_1.extname)(file) === '.ogg') ||
|
|
105
|
+
files.find(file => (0, path_1.extname)(file) === '.mp3');
|
|
106
|
+
if (!pickedFile) {
|
|
107
|
+
throw new Error('Unable to pick file for sprite in ' + category);
|
|
108
|
+
}
|
|
109
|
+
spriteSounds.set(category + '/' + filenameWithoutExt, pickedFile);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
spritesheetJson = await new Promise((resolve, reject) => {
|
|
113
|
+
const options = {
|
|
114
|
+
output: argv.outSpritesheet,
|
|
115
|
+
format: 'howler',
|
|
116
|
+
export: extensions.map(ext => ext.slice(1)).join(','),
|
|
117
|
+
};
|
|
118
|
+
(0, audiosprite_1.default)(Array.from(spriteSounds.values()), options, (err, res) => {
|
|
119
|
+
if (err) {
|
|
120
|
+
reject(err);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
resolve(res);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
71
128
|
const imports = [];
|
|
72
129
|
const definitions = [];
|
|
73
130
|
const funcs = [];
|
|
@@ -76,10 +133,38 @@ async function main() {
|
|
|
76
133
|
soundDefinition += ' constructor(\n';
|
|
77
134
|
soundDefinition += ' readonly basename: string,\n';
|
|
78
135
|
soundDefinition += ' readonly files: string[],\n';
|
|
136
|
+
soundDefinition += ' readonly sprite: string | null,\n';
|
|
79
137
|
soundDefinition += ' readonly averageFileSize: number,\n';
|
|
80
138
|
soundDefinition += ' ) {}\n';
|
|
81
139
|
soundDefinition += '}\n';
|
|
82
140
|
definitions.push(soundDefinition);
|
|
141
|
+
if (argv.outSpritesheet) {
|
|
142
|
+
const modifiedSpritesheetJson = { ...spritesheetJson };
|
|
143
|
+
modifiedSpritesheetJson.urls = extensions.map(ext => argv.outSpritesheet + ext);
|
|
144
|
+
let spriteSheetFunc = 'export function sound_spritesheet() {\n';
|
|
145
|
+
spriteSheetFunc += ' return {\n';
|
|
146
|
+
spriteSheetFunc += ' urls: [\n';
|
|
147
|
+
const idealOrder = ['.ogg', '.mp3', '.wav'];
|
|
148
|
+
const sortedExtensions = extensions.sort((a, b) => idealOrder.indexOf(a) - idealOrder.indexOf(b));
|
|
149
|
+
for (const extension of sortedExtensions) {
|
|
150
|
+
const importName = 'sprites_' + extension.slice(1);
|
|
151
|
+
imports.push(`import ${importName} from '${(0, path_1.relative)((0, path_1.dirname)(argv.outFile), argv.outSpritesheet + extension).replace(/\\/g, '/')}';`);
|
|
152
|
+
spriteSheetFunc += ` ${importName},\n`;
|
|
153
|
+
}
|
|
154
|
+
let totalFileSize = 0;
|
|
155
|
+
for (const extension of sortedExtensions) {
|
|
156
|
+
const soundFile = argv.outSpritesheet + extension;
|
|
157
|
+
const stats = await fs_1.promises.stat(soundFile);
|
|
158
|
+
totalFileSize += stats.size;
|
|
159
|
+
}
|
|
160
|
+
const averageFileSize = Math.round(totalFileSize / files.length);
|
|
161
|
+
spriteSheetFunc += ' ],\n';
|
|
162
|
+
spriteSheetFunc += ` sprite: ${JSON.stringify(spritesheetJson.sprite)},\n`;
|
|
163
|
+
spriteSheetFunc += ` averageFileSize: ${averageFileSize},\n`;
|
|
164
|
+
spriteSheetFunc += ' }\n';
|
|
165
|
+
spriteSheetFunc += '};\n';
|
|
166
|
+
funcs.push(spriteSheetFunc);
|
|
167
|
+
}
|
|
83
168
|
for (const category of defs.keys()) {
|
|
84
169
|
let func = `export function sound_${category}(): SoundDefinition[] {\n`;
|
|
85
170
|
func += ` return [\n`;
|
|
@@ -101,7 +186,9 @@ async function main() {
|
|
|
101
186
|
totalFileSize += stats.size;
|
|
102
187
|
}
|
|
103
188
|
const averageFileSize = Math.round(totalFileSize / files.length);
|
|
189
|
+
const hasSprite = spriteSounds.get(category + '/' + filenameWithoutExt);
|
|
104
190
|
func += ' ],\n';
|
|
191
|
+
func += ` ${JSON.stringify(hasSprite ? filenameWithoutExt : null)},\n`;
|
|
105
192
|
func += ` ${averageFileSize},\n`;
|
|
106
193
|
func += ' ),\n';
|
|
107
194
|
}
|
package/lib/utils.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export declare function allFiles(path: string): Promise<string[]>;
|
|
|
2
2
|
export declare function sanitize(string: string): string;
|
|
3
3
|
export declare function camelize(str: string): string;
|
|
4
4
|
export declare function lowerCamelize(str: string): string;
|
|
5
|
-
export declare function categoryPath(assetDir: string,
|
|
5
|
+
export declare function categoryPath(assetDir: string, filepath: string): string[];
|
package/lib/utils.js
CHANGED
|
@@ -34,8 +34,8 @@ function lowerCamelize(str) {
|
|
|
34
34
|
return camelized.slice(0, 1).toLowerCase() + camelized.slice(1);
|
|
35
35
|
}
|
|
36
36
|
exports.lowerCamelize = lowerCamelize;
|
|
37
|
-
function categoryPath(assetDir,
|
|
38
|
-
const dir = (0, path_1.dirname)(
|
|
37
|
+
function categoryPath(assetDir, filepath) {
|
|
38
|
+
const dir = (0, path_1.dirname)(filepath);
|
|
39
39
|
const trimmedDir = (0, path_1.relative)(assetDir, dir);
|
|
40
40
|
return trimmedDir
|
|
41
41
|
.split(/[\/\\]/g)
|
package/package.json
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remvst/asset-catalog",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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",
|
|
12
|
-
"test:sounds": "ts-node src/generate-sound-catalog.ts --assetDir=./testData/sounds --outFile=testOut/sounds.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
|
+
"test:sounds": "ts-node src/generate-sound-catalog.ts --assetDir=./testData/sounds --outFile=testOut/sounds.ts --outSpritesheet=testData/sounds/sprites --spritesheetExcludeCategory=jump",
|
|
13
13
|
"test": "npm run test:images && npm run test:sounds",
|
|
14
14
|
"prepublishOnly": "npm i && npm run build"
|
|
15
15
|
},
|
|
16
16
|
"author": "Rémi Vansteelandt",
|
|
17
17
|
"license": "UNLICENSED",
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"audiosprite": "^0.7.2",
|
|
20
|
+
"bin-pack": "^1.0.2",
|
|
21
|
+
"canvas": "^2.11.2",
|
|
19
22
|
"image-size": "^1.0.2",
|
|
20
23
|
"yargs": "^17.7.2"
|
|
21
24
|
},
|
|
22
25
|
"devDependencies": {
|
|
26
|
+
"@types/audiosprite": "^0.7.3",
|
|
27
|
+
"@types/bin-pack": "^1.0.3",
|
|
23
28
|
"@types/node": "^18.11.5",
|
|
24
29
|
"@types/yargs": "^17.0.32",
|
|
25
30
|
"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
141
|
'outFile': {
|
|
74
142
|
type: 'string',
|
|
75
|
-
default: '.',
|
|
143
|
+
default: 'textures.ts',
|
|
76
144
|
alias: 'o',
|
|
77
145
|
describe: 'Directory to generate the files into',
|
|
78
146
|
},
|
|
79
147
|
'assetDir': {
|
|
80
148
|
type: 'string',
|
|
81
|
-
default: '
|
|
149
|
+
default: '.',
|
|
82
150
|
alias: 'a',
|
|
83
|
-
describe: '
|
|
84
|
-
}
|
|
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
|
}
|
|
@@ -4,7 +4,8 @@ import { promises as fs } from 'fs';
|
|
|
4
4
|
import { sanitize, allFiles, categoryPath } from './utils';
|
|
5
5
|
import yargs from 'yargs/yargs';
|
|
6
6
|
import { hideBin } from 'yargs/helpers';
|
|
7
|
-
import { dirname, extname, basename, relative } from 'path';
|
|
7
|
+
import { dirname, extname, basename, relative, parse } from 'path';
|
|
8
|
+
import audiosprite from 'audiosprite';
|
|
8
9
|
|
|
9
10
|
async function main() {
|
|
10
11
|
const argv = await yargs(hideBin(process.argv))
|
|
@@ -17,9 +18,9 @@ async function main() {
|
|
|
17
18
|
},
|
|
18
19
|
'assetDir': {
|
|
19
20
|
type: 'string',
|
|
20
|
-
default: '
|
|
21
|
+
default: '.',
|
|
21
22
|
alias: 'a',
|
|
22
|
-
describe: '
|
|
23
|
+
describe: 'Asset directory where all the PNGs are located',
|
|
23
24
|
},
|
|
24
25
|
'wav': {
|
|
25
26
|
type: 'boolean',
|
|
@@ -36,6 +37,19 @@ async function main() {
|
|
|
36
37
|
default: true,
|
|
37
38
|
describe: 'Include .mp3 files',
|
|
38
39
|
},
|
|
40
|
+
'outSpritesheet': {
|
|
41
|
+
type: 'string',
|
|
42
|
+
required: false,
|
|
43
|
+
describe: 'Path to the exported spritesheet (without the extension)',
|
|
44
|
+
},
|
|
45
|
+
'spritesheetExcludeCategory': {
|
|
46
|
+
type: 'string',
|
|
47
|
+
array: true,
|
|
48
|
+
required: false,
|
|
49
|
+
alias: 'x',
|
|
50
|
+
default: [] as string[],
|
|
51
|
+
describe: 'Exclude certain categories from the spritesheet',
|
|
52
|
+
},
|
|
39
53
|
})
|
|
40
54
|
.argv;
|
|
41
55
|
|
|
@@ -51,7 +65,8 @@ async function main() {
|
|
|
51
65
|
|
|
52
66
|
|
|
53
67
|
const files = await allFiles(argv.assetDir);
|
|
54
|
-
const sounds = files.filter(file => extensions.indexOf(extname(file)) >= 0)
|
|
68
|
+
const sounds = files.filter(file => extensions.indexOf(extname(file)) >= 0)
|
|
69
|
+
.filter(file => basename(file, extname(file)) !== 'sprites');
|
|
55
70
|
|
|
56
71
|
const defs = new Map<string, Map<string, Set<string>>>();
|
|
57
72
|
|
|
@@ -71,6 +86,53 @@ async function main() {
|
|
|
71
86
|
defs.get(category)!.get(filenameWithoutExt)!.add(sound);
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
const spriteSounds: Map<string, string> = new Map();
|
|
90
|
+
|
|
91
|
+
let spritesheetJson: any = null;
|
|
92
|
+
if (argv.outSpritesheet) {
|
|
93
|
+
for (const category of defs.keys()) {
|
|
94
|
+
let isCategoryExcluded = false;
|
|
95
|
+
for (const exclusion of argv.spritesheetExcludeCategory) {
|
|
96
|
+
if (category.indexOf(exclusion) !== -1) {
|
|
97
|
+
isCategoryExcluded = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (isCategoryExcluded) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const filenameWithoutExt of defs.get(category)!.keys()) {
|
|
106
|
+
const fileSet = defs.get(category)!.get(filenameWithoutExt)!;
|
|
107
|
+
|
|
108
|
+
const files = Array.from(fileSet);
|
|
109
|
+
const pickedFile = files.find(file => extname(file) === '.wav') ||
|
|
110
|
+
files.find(file => extname(file) === '.ogg') ||
|
|
111
|
+
files.find(file => extname(file) === '.mp3');
|
|
112
|
+
if (!pickedFile) {
|
|
113
|
+
throw new Error('Unable to pick file for sprite in ' + category);
|
|
114
|
+
}
|
|
115
|
+
spriteSounds.set(category + '/' + filenameWithoutExt, pickedFile);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
spritesheetJson = await new Promise((resolve, reject) => {
|
|
120
|
+
const options: audiosprite.Option = {
|
|
121
|
+
output: argv.outSpritesheet,
|
|
122
|
+
format: 'howler',
|
|
123
|
+
export: extensions.map(ext => ext.slice(1)).join(','),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
audiosprite(Array.from(spriteSounds.values()), options, (err, res) => {
|
|
127
|
+
if (err) {
|
|
128
|
+
reject(err);
|
|
129
|
+
} else {
|
|
130
|
+
resolve(res);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
74
136
|
const imports = [];
|
|
75
137
|
const definitions = [];
|
|
76
138
|
const funcs = [];
|
|
@@ -80,11 +142,47 @@ async function main() {
|
|
|
80
142
|
soundDefinition += ' constructor(\n';
|
|
81
143
|
soundDefinition += ' readonly basename: string,\n';
|
|
82
144
|
soundDefinition += ' readonly files: string[],\n';
|
|
145
|
+
soundDefinition += ' readonly sprite: string | null,\n';
|
|
83
146
|
soundDefinition += ' readonly averageFileSize: number,\n';
|
|
84
147
|
soundDefinition += ' ) {}\n';
|
|
85
148
|
soundDefinition += '}\n';
|
|
86
149
|
definitions.push(soundDefinition);
|
|
87
150
|
|
|
151
|
+
if (argv.outSpritesheet) {
|
|
152
|
+
const modifiedSpritesheetJson = { ...spritesheetJson };
|
|
153
|
+
modifiedSpritesheetJson.urls = extensions.map(ext => argv.outSpritesheet + ext);
|
|
154
|
+
|
|
155
|
+
let spriteSheetFunc = 'export function sound_spritesheet() {\n';
|
|
156
|
+
spriteSheetFunc += ' return {\n';
|
|
157
|
+
spriteSheetFunc += ' urls: [\n';
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
const idealOrder = ['.ogg', '.mp3', '.wav'];
|
|
161
|
+
const sortedExtensions = extensions.sort((a, b) => idealOrder.indexOf(a) - idealOrder.indexOf(b));
|
|
162
|
+
for (const extension of sortedExtensions) {
|
|
163
|
+
const importName = 'sprites_' + extension.slice(1);
|
|
164
|
+
imports.push(`import ${importName} from '${relative(dirname(argv.outFile), argv.outSpritesheet + extension).replace(/\\/g, '/')}';`);
|
|
165
|
+
|
|
166
|
+
spriteSheetFunc += ` ${importName},\n`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let totalFileSize = 0;
|
|
170
|
+
for (const extension of sortedExtensions) {
|
|
171
|
+
const soundFile = argv.outSpritesheet + extension;
|
|
172
|
+
const stats = await fs.stat(soundFile);
|
|
173
|
+
totalFileSize += stats.size;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const averageFileSize = Math.round(totalFileSize / files.length);
|
|
177
|
+
|
|
178
|
+
spriteSheetFunc += ' ],\n';
|
|
179
|
+
spriteSheetFunc += ` sprite: ${JSON.stringify(spritesheetJson.sprite)},\n`;
|
|
180
|
+
spriteSheetFunc += ` averageFileSize: ${averageFileSize},\n`;
|
|
181
|
+
spriteSheetFunc += ' }\n';
|
|
182
|
+
spriteSheetFunc += '};\n';
|
|
183
|
+
funcs.push(spriteSheetFunc);
|
|
184
|
+
}
|
|
185
|
+
|
|
88
186
|
for (const category of defs.keys()) {
|
|
89
187
|
let func = `export function sound_${category}(): SoundDefinition[] {\n`;
|
|
90
188
|
func += ` return [\n`;
|
|
@@ -111,7 +209,10 @@ async function main() {
|
|
|
111
209
|
|
|
112
210
|
const averageFileSize = Math.round(totalFileSize / files.length);
|
|
113
211
|
|
|
212
|
+
const hasSprite = spriteSounds.get(category + '/' + filenameWithoutExt)!;
|
|
213
|
+
|
|
114
214
|
func += ' ],\n';
|
|
215
|
+
func += ` ${JSON.stringify(hasSprite ? filenameWithoutExt : null)},\n`;
|
|
115
216
|
func += ` ${averageFileSize},\n`;
|
|
116
217
|
func += ' ),\n';
|
|
117
218
|
}
|
package/src/utils.ts
CHANGED
|
@@ -34,8 +34,8 @@ export function lowerCamelize(str: string): string {
|
|
|
34
34
|
return camelized.slice(0, 1).toLowerCase() + camelized.slice(1);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function categoryPath(assetDir: string,
|
|
38
|
-
const dir = dirname(
|
|
37
|
+
export function categoryPath(assetDir: string, filepath: string): string[] {
|
|
38
|
+
const dir = dirname(filepath);
|
|
39
39
|
const trimmedDir = relative(assetDir, dir);
|
|
40
40
|
return trimmedDir
|
|
41
41
|
.split(/[\/\\]/g)
|