@jjlmoya/utils-games-development 1.65.0 → 1.67.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/package.json +67 -67
- package/scripts/postinstall.mjs +27 -27
- package/src/category/GamesCategorySEO.astro +19 -19
- package/src/category/i18n/de.ts +15 -15
- package/src/category/i18n/en.ts +15 -15
- package/src/category/i18n/es.ts +15 -15
- package/src/category/i18n/fr.ts +15 -15
- package/src/category/i18n/id.ts +10 -10
- package/src/category/i18n/it.ts +10 -10
- package/src/category/i18n/ja.ts +10 -10
- package/src/category/i18n/ko.ts +10 -10
- package/src/category/i18n/nl.ts +10 -10
- package/src/category/i18n/pl.ts +10 -10
- package/src/category/i18n/pt.ts +10 -10
- package/src/category/i18n/ru.ts +10 -10
- package/src/category/i18n/sv.ts +10 -10
- package/src/category/i18n/tr.ts +10 -10
- package/src/category/i18n/zh.ts +10 -10
- package/src/category/seo.astro +15 -15
- package/src/components/PreviewNavSidebar.astro +116 -116
- package/src/components/PreviewToolbar.astro +143 -143
- package/src/data.ts +11 -11
- package/src/env.d.ts +5 -5
- package/src/index.ts +20 -20
- package/src/layouts/PreviewLayout.astro +122 -122
- package/src/pages/[locale]/[slug].astro +165 -165
- package/src/pages/[locale].astro +250 -250
- package/src/pages/index.astro +4 -4
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +74 -0
- package/src/tests/diacritics_density.test.ts +118 -118
- package/src/tests/faq_count.test.ts +19 -19
- package/src/tests/i18n_coverage.test.ts +36 -36
- package/src/tests/inverted_punctuation.test.ts +84 -84
- package/src/tests/mocks/astro_mock.js +2 -2
- package/src/tests/no_en_dash.test.ts +70 -70
- package/src/tests/no_h1_in_components.test.ts +48 -48
- package/src/tests/pagespeed_best_practices.test.ts +198 -198
- package/src/tests/qa-test-helpers.ts +32 -32
- package/src/tests/qa_bibliography_links.test.ts +41 -41
- package/src/tests/qa_claim_evidence.test.ts +69 -69
- package/src/tests/qa_logic_reference_coverage.test.ts +46 -46
- package/src/tests/qa_runtime_i18n.test.ts +100 -100
- package/src/tests/schemas_fulfillment.test.ts +23 -23
- package/src/tests/script_density.test.ts +94 -94
- package/src/tests/seo_length.test.ts +23 -23
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/slug_language_code_format.test.ts +23 -23
- package/src/tests/slug_uniqueness.test.ts +80 -80
- package/src/tests/title_quality.test.ts +56 -56
- package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -322
- package/src/tool/audioLoopPointFinder/client.ts +262 -262
- package/src/tool/damageFormulaLab/bibliography.astro +3 -21
- package/src/tool/damageFormulaLab/seo.astro +7 -5
- package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +3 -19
- package/src/tool/hitboxHurtboxAnimator/hitbox-hurtbox-animator.css +614 -614
- package/src/tool/hitboxHurtboxAnimator/seo.astro +7 -5
- package/src/tool/saveFileEditor/component.astro +351 -351
- package/src/tool/saveFileEditor/game-save-file-editor.css +250 -250
- package/src/tool/spriteSheetPacker/app-client.ts +248 -248
- package/src/tool/spriteSheetPacker/bibliography.ts +12 -12
- package/src/tool/spriteSheetPacker/component.astro +229 -229
- package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -55
- package/src/tool/spriteSheetPacker/entry.ts +28 -28
- package/src/tool/spriteSheetPacker/exporter.ts +116 -116
- package/src/tool/spriteSheetPacker/extractor-client.ts +127 -127
- package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -60
- package/src/tool/spriteSheetPacker/logic.test.ts +155 -155
- package/src/tool/spriteSheetPacker/logic.ts +32 -32
- package/src/tool/spriteSheetPacker/packer-core.ts +121 -121
- package/src/tool/spriteSheetPacker/packer-ui.ts +86 -86
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +578 -578
- package/src/tool/spriteSheetPacker/types.ts +89 -89
- package/src/tool/spriteSheetPacker/ui.ts +42 -42
- package/src/tool/steamCapsuleGenerator/index.ts +9 -9
- package/src/tool/steamCapsuleGenerator/validation.ts +14 -14
- package/src/types.ts +72 -72
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
calculateBinPacking,
|
|
4
|
-
calculateGridSlices,
|
|
5
|
-
generateAtlasJson,
|
|
6
|
-
generateCssSnippet,
|
|
7
|
-
generateEngineCodeSnippet,
|
|
8
|
-
nextPowerOfTwo,
|
|
9
|
-
type PackerOptions,
|
|
10
|
-
type SpriteFrameInput,
|
|
11
|
-
} from './logic';
|
|
12
|
-
|
|
13
|
-
describe('SpriteSheetPacker Logic', () => {
|
|
14
|
-
it('calculates next power of two correctly', () => {
|
|
15
|
-
expect(nextPowerOfTwo(1)).toBe(1);
|
|
16
|
-
expect(nextPowerOfTwo(3)).toBe(4);
|
|
17
|
-
expect(nextPowerOfTwo(500)).toBe(512);
|
|
18
|
-
expect(nextPowerOfTwo(1024)).toBe(1024);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('packs empty frames array returning default empty result', () => {
|
|
22
|
-
const options: PackerOptions = {
|
|
23
|
-
padding: 2,
|
|
24
|
-
borderExtrusion: 0,
|
|
25
|
-
forcePowerOfTwo: true,
|
|
26
|
-
maxTextureWidth: 1024,
|
|
27
|
-
maxTextureHeight: 1024,
|
|
28
|
-
allowRotation: false,
|
|
29
|
-
trimTransparency: false,
|
|
30
|
-
format: 'generic-json-hash',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const result = calculateBinPacking([], options);
|
|
34
|
-
expect(result.totalFrames).toBe(0);
|
|
35
|
-
expect(result.efficiency).toBe(0);
|
|
36
|
-
expect(result.drawCallsBefore).toBe(0);
|
|
37
|
-
expect(result.drawCallsAfter).toBe(0);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('packs frames and calculates texture dimensions and efficiency', () => {
|
|
41
|
-
const frames: SpriteFrameInput[] = [
|
|
42
|
-
{ id: '1', name: 'hero_walk_0', width: 32, height: 32 },
|
|
43
|
-
{ id: '2', name: 'hero_walk_1', width: 32, height: 32 },
|
|
44
|
-
{ id: '3', name: 'hero_idle_0', width: 64, height: 64 },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const options: PackerOptions = {
|
|
48
|
-
padding: 0,
|
|
49
|
-
borderExtrusion: 0,
|
|
50
|
-
forcePowerOfTwo: false,
|
|
51
|
-
maxTextureWidth: 1024,
|
|
52
|
-
maxTextureHeight: 1024,
|
|
53
|
-
allowRotation: false,
|
|
54
|
-
trimTransparency: false,
|
|
55
|
-
format: 'generic-json-hash',
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const result = calculateBinPacking(frames, options);
|
|
59
|
-
expect(result.totalFrames).toBe(3);
|
|
60
|
-
expect(result.drawCallsBefore).toBe(3);
|
|
61
|
-
expect(result.drawCallsAfter).toBe(1);
|
|
62
|
-
expect(result.frames.length).toBe(3);
|
|
63
|
-
expect(result.efficiency).toBeGreaterThan(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('forces power of two dimensions when requested', () => {
|
|
67
|
-
const frames: SpriteFrameInput[] = [
|
|
68
|
-
{ id: '1', name: 'frame_1', width: 100, height: 100 },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
const options: PackerOptions = {
|
|
72
|
-
padding: 0,
|
|
73
|
-
borderExtrusion: 0,
|
|
74
|
-
forcePowerOfTwo: true,
|
|
75
|
-
maxTextureWidth: 2048,
|
|
76
|
-
maxTextureHeight: 2048,
|
|
77
|
-
allowRotation: false,
|
|
78
|
-
trimTransparency: false,
|
|
79
|
-
format: 'generic-json-array',
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const result = calculateBinPacking(frames, options);
|
|
83
|
-
expect(result.textureWidth).toBe(128);
|
|
84
|
-
expect(result.textureHeight).toBe(128);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('generates atlas JSON formats for different engines', () => {
|
|
88
|
-
const frames: SpriteFrameInput[] = [
|
|
89
|
-
{ id: '1', name: 'coin', width: 16, height: 16 },
|
|
90
|
-
];
|
|
91
|
-
const options: PackerOptions = {
|
|
92
|
-
padding: 0,
|
|
93
|
-
borderExtrusion: 0,
|
|
94
|
-
forcePowerOfTwo: false,
|
|
95
|
-
maxTextureWidth: 512,
|
|
96
|
-
maxTextureHeight: 512,
|
|
97
|
-
allowRotation: false,
|
|
98
|
-
trimTransparency: false,
|
|
99
|
-
format: 'godot',
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const result = calculateBinPacking(frames, options);
|
|
103
|
-
expect(result.atlasJson).toContain('spritesheet.png');
|
|
104
|
-
expect(result.atlasJson).toContain('coin');
|
|
105
|
-
|
|
106
|
-
const unityJson = generateAtlasJson(result.frames, 512, 512, 'unity');
|
|
107
|
-
expect(unityJson).toContain('sprites');
|
|
108
|
-
|
|
109
|
-
const phaserJson = generateAtlasJson(result.frames, 512, 512, 'phaser');
|
|
110
|
-
expect(phaserJson).toContain('meta');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('generates css snippets accurately', () => {
|
|
114
|
-
const frames: SpriteFrameInput[] = [
|
|
115
|
-
{ id: '1', name: 'player_idle', width: 24, height: 24 },
|
|
116
|
-
];
|
|
117
|
-
const options: PackerOptions = {
|
|
118
|
-
padding: 0,
|
|
119
|
-
borderExtrusion: 0,
|
|
120
|
-
forcePowerOfTwo: false,
|
|
121
|
-
maxTextureWidth: 512,
|
|
122
|
-
maxTextureHeight: 512,
|
|
123
|
-
allowRotation: false,
|
|
124
|
-
trimTransparency: false,
|
|
125
|
-
format: 'css',
|
|
126
|
-
};
|
|
127
|
-
const result = calculateBinPacking(frames, options);
|
|
128
|
-
const css = generateCssSnippet(result.frames);
|
|
129
|
-
expect(css).toContain('.sprite-player_idle');
|
|
130
|
-
expect(css).toContain('background-position');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('generates engine code snippets correctly', () => {
|
|
134
|
-
expect(generateEngineCodeSnippet('unity')).toContain('Resources.LoadAll');
|
|
135
|
-
expect(generateEngineCodeSnippet('godot')).toContain('AtlasTexture');
|
|
136
|
-
expect(generateEngineCodeSnippet('phaser')).toContain('this.load.atlas');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('calculates grid slices for extractor mode', () => {
|
|
140
|
-
const slices = calculateGridSlices({
|
|
141
|
-
imageWidth: 128,
|
|
142
|
-
imageHeight: 64,
|
|
143
|
-
frameWidth: 32,
|
|
144
|
-
frameHeight: 32,
|
|
145
|
-
margin: 0,
|
|
146
|
-
spacing: 0,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
expect(slices.length).toBe(8);
|
|
150
|
-
expect(slices[0]?.x).toBe(0);
|
|
151
|
-
expect(slices[0]?.y).toBe(0);
|
|
152
|
-
expect(slices[7]?.x).toBe(96);
|
|
153
|
-
expect(slices[7]?.y).toBe(32);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
calculateBinPacking,
|
|
4
|
+
calculateGridSlices,
|
|
5
|
+
generateAtlasJson,
|
|
6
|
+
generateCssSnippet,
|
|
7
|
+
generateEngineCodeSnippet,
|
|
8
|
+
nextPowerOfTwo,
|
|
9
|
+
type PackerOptions,
|
|
10
|
+
type SpriteFrameInput,
|
|
11
|
+
} from './logic';
|
|
12
|
+
|
|
13
|
+
describe('SpriteSheetPacker Logic', () => {
|
|
14
|
+
it('calculates next power of two correctly', () => {
|
|
15
|
+
expect(nextPowerOfTwo(1)).toBe(1);
|
|
16
|
+
expect(nextPowerOfTwo(3)).toBe(4);
|
|
17
|
+
expect(nextPowerOfTwo(500)).toBe(512);
|
|
18
|
+
expect(nextPowerOfTwo(1024)).toBe(1024);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('packs empty frames array returning default empty result', () => {
|
|
22
|
+
const options: PackerOptions = {
|
|
23
|
+
padding: 2,
|
|
24
|
+
borderExtrusion: 0,
|
|
25
|
+
forcePowerOfTwo: true,
|
|
26
|
+
maxTextureWidth: 1024,
|
|
27
|
+
maxTextureHeight: 1024,
|
|
28
|
+
allowRotation: false,
|
|
29
|
+
trimTransparency: false,
|
|
30
|
+
format: 'generic-json-hash',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = calculateBinPacking([], options);
|
|
34
|
+
expect(result.totalFrames).toBe(0);
|
|
35
|
+
expect(result.efficiency).toBe(0);
|
|
36
|
+
expect(result.drawCallsBefore).toBe(0);
|
|
37
|
+
expect(result.drawCallsAfter).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('packs frames and calculates texture dimensions and efficiency', () => {
|
|
41
|
+
const frames: SpriteFrameInput[] = [
|
|
42
|
+
{ id: '1', name: 'hero_walk_0', width: 32, height: 32 },
|
|
43
|
+
{ id: '2', name: 'hero_walk_1', width: 32, height: 32 },
|
|
44
|
+
{ id: '3', name: 'hero_idle_0', width: 64, height: 64 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const options: PackerOptions = {
|
|
48
|
+
padding: 0,
|
|
49
|
+
borderExtrusion: 0,
|
|
50
|
+
forcePowerOfTwo: false,
|
|
51
|
+
maxTextureWidth: 1024,
|
|
52
|
+
maxTextureHeight: 1024,
|
|
53
|
+
allowRotation: false,
|
|
54
|
+
trimTransparency: false,
|
|
55
|
+
format: 'generic-json-hash',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = calculateBinPacking(frames, options);
|
|
59
|
+
expect(result.totalFrames).toBe(3);
|
|
60
|
+
expect(result.drawCallsBefore).toBe(3);
|
|
61
|
+
expect(result.drawCallsAfter).toBe(1);
|
|
62
|
+
expect(result.frames.length).toBe(3);
|
|
63
|
+
expect(result.efficiency).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('forces power of two dimensions when requested', () => {
|
|
67
|
+
const frames: SpriteFrameInput[] = [
|
|
68
|
+
{ id: '1', name: 'frame_1', width: 100, height: 100 },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const options: PackerOptions = {
|
|
72
|
+
padding: 0,
|
|
73
|
+
borderExtrusion: 0,
|
|
74
|
+
forcePowerOfTwo: true,
|
|
75
|
+
maxTextureWidth: 2048,
|
|
76
|
+
maxTextureHeight: 2048,
|
|
77
|
+
allowRotation: false,
|
|
78
|
+
trimTransparency: false,
|
|
79
|
+
format: 'generic-json-array',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = calculateBinPacking(frames, options);
|
|
83
|
+
expect(result.textureWidth).toBe(128);
|
|
84
|
+
expect(result.textureHeight).toBe(128);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('generates atlas JSON formats for different engines', () => {
|
|
88
|
+
const frames: SpriteFrameInput[] = [
|
|
89
|
+
{ id: '1', name: 'coin', width: 16, height: 16 },
|
|
90
|
+
];
|
|
91
|
+
const options: PackerOptions = {
|
|
92
|
+
padding: 0,
|
|
93
|
+
borderExtrusion: 0,
|
|
94
|
+
forcePowerOfTwo: false,
|
|
95
|
+
maxTextureWidth: 512,
|
|
96
|
+
maxTextureHeight: 512,
|
|
97
|
+
allowRotation: false,
|
|
98
|
+
trimTransparency: false,
|
|
99
|
+
format: 'godot',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = calculateBinPacking(frames, options);
|
|
103
|
+
expect(result.atlasJson).toContain('spritesheet.png');
|
|
104
|
+
expect(result.atlasJson).toContain('coin');
|
|
105
|
+
|
|
106
|
+
const unityJson = generateAtlasJson(result.frames, 512, 512, 'unity');
|
|
107
|
+
expect(unityJson).toContain('sprites');
|
|
108
|
+
|
|
109
|
+
const phaserJson = generateAtlasJson(result.frames, 512, 512, 'phaser');
|
|
110
|
+
expect(phaserJson).toContain('meta');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('generates css snippets accurately', () => {
|
|
114
|
+
const frames: SpriteFrameInput[] = [
|
|
115
|
+
{ id: '1', name: 'player_idle', width: 24, height: 24 },
|
|
116
|
+
];
|
|
117
|
+
const options: PackerOptions = {
|
|
118
|
+
padding: 0,
|
|
119
|
+
borderExtrusion: 0,
|
|
120
|
+
forcePowerOfTwo: false,
|
|
121
|
+
maxTextureWidth: 512,
|
|
122
|
+
maxTextureHeight: 512,
|
|
123
|
+
allowRotation: false,
|
|
124
|
+
trimTransparency: false,
|
|
125
|
+
format: 'css',
|
|
126
|
+
};
|
|
127
|
+
const result = calculateBinPacking(frames, options);
|
|
128
|
+
const css = generateCssSnippet(result.frames);
|
|
129
|
+
expect(css).toContain('.sprite-player_idle');
|
|
130
|
+
expect(css).toContain('background-position');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('generates engine code snippets correctly', () => {
|
|
134
|
+
expect(generateEngineCodeSnippet('unity')).toContain('Resources.LoadAll');
|
|
135
|
+
expect(generateEngineCodeSnippet('godot')).toContain('AtlasTexture');
|
|
136
|
+
expect(generateEngineCodeSnippet('phaser')).toContain('this.load.atlas');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('calculates grid slices for extractor mode', () => {
|
|
140
|
+
const slices = calculateGridSlices({
|
|
141
|
+
imageWidth: 128,
|
|
142
|
+
imageHeight: 64,
|
|
143
|
+
frameWidth: 32,
|
|
144
|
+
frameHeight: 32,
|
|
145
|
+
margin: 0,
|
|
146
|
+
spacing: 0,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(slices.length).toBe(8);
|
|
150
|
+
expect(slices[0]?.x).toBe(0);
|
|
151
|
+
expect(slices[0]?.y).toBe(0);
|
|
152
|
+
expect(slices[7]?.x).toBe(96);
|
|
153
|
+
expect(slices[7]?.y).toBe(32);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import type { ExtractionGridConfig, ExtractedFrameSlice } from './types';
|
|
2
|
-
|
|
3
|
-
export * from './types';
|
|
4
|
-
export * from './packer-core';
|
|
5
|
-
export * from './exporter';
|
|
6
|
-
|
|
7
|
-
export function calculateGridSlices(config: ExtractionGridConfig): ExtractedFrameSlice[] {
|
|
8
|
-
const { imageWidth, imageHeight, frameWidth, frameHeight, margin, spacing } = config;
|
|
9
|
-
|
|
10
|
-
if (frameWidth <= 0 || frameHeight <= 0) {
|
|
11
|
-
return [];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const slices: ExtractedFrameSlice[] = [];
|
|
15
|
-
let index = 0;
|
|
16
|
-
|
|
17
|
-
for (let y = margin; y + frameHeight <= imageHeight; y += frameHeight + spacing) {
|
|
18
|
-
for (let x = margin; x + frameWidth <= imageWidth; x += frameWidth + spacing) {
|
|
19
|
-
slices.push({
|
|
20
|
-
id: `slice_${index}`,
|
|
21
|
-
index,
|
|
22
|
-
x,
|
|
23
|
-
y,
|
|
24
|
-
width: frameWidth,
|
|
25
|
-
height: frameHeight,
|
|
26
|
-
});
|
|
27
|
-
index++;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return slices;
|
|
32
|
-
}
|
|
1
|
+
import type { ExtractionGridConfig, ExtractedFrameSlice } from './types';
|
|
2
|
+
|
|
3
|
+
export * from './types';
|
|
4
|
+
export * from './packer-core';
|
|
5
|
+
export * from './exporter';
|
|
6
|
+
|
|
7
|
+
export function calculateGridSlices(config: ExtractionGridConfig): ExtractedFrameSlice[] {
|
|
8
|
+
const { imageWidth, imageHeight, frameWidth, frameHeight, margin, spacing } = config;
|
|
9
|
+
|
|
10
|
+
if (frameWidth <= 0 || frameHeight <= 0) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const slices: ExtractedFrameSlice[] = [];
|
|
15
|
+
let index = 0;
|
|
16
|
+
|
|
17
|
+
for (let y = margin; y + frameHeight <= imageHeight; y += frameHeight + spacing) {
|
|
18
|
+
for (let x = margin; x + frameWidth <= imageWidth; x += frameWidth + spacing) {
|
|
19
|
+
slices.push({
|
|
20
|
+
id: `slice_${index}`,
|
|
21
|
+
index,
|
|
22
|
+
x,
|
|
23
|
+
y,
|
|
24
|
+
width: frameWidth,
|
|
25
|
+
height: frameHeight,
|
|
26
|
+
});
|
|
27
|
+
index++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return slices;
|
|
32
|
+
}
|
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import type { PackerOptions, PackedFrame, PackedResult, SpriteFrameInput } from './types';
|
|
2
|
-
import { generateAtlasJson, generateCssSnippet, generateEngineCodeSnippet } from './exporter';
|
|
3
|
-
|
|
4
|
-
export function nextPowerOfTwo(value: number): number {
|
|
5
|
-
let power = 1;
|
|
6
|
-
while (power < value) {
|
|
7
|
-
power *= 2;
|
|
8
|
-
}
|
|
9
|
-
return power;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function emptyPackingResult(): PackedResult {
|
|
13
|
-
return {
|
|
14
|
-
textureWidth: 0,
|
|
15
|
-
textureHeight: 0,
|
|
16
|
-
efficiency: 0,
|
|
17
|
-
totalFrames: 0,
|
|
18
|
-
frames: [],
|
|
19
|
-
atlasJson: '{}',
|
|
20
|
-
cssSnippet: '',
|
|
21
|
-
codeSnippet: '',
|
|
22
|
-
drawCallsBefore: 0,
|
|
23
|
-
drawCallsAfter: 0,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getFrameOffsets(item: SpriteFrameInput) {
|
|
28
|
-
const tx = item.trimmedX ? item.trimmedX : 0;
|
|
29
|
-
const ty = item.trimmedY ? item.trimmedY : 0;
|
|
30
|
-
const ow = item.originalWidth ? item.originalWidth : item.width;
|
|
31
|
-
const oh = item.originalHeight ? item.originalHeight : item.height;
|
|
32
|
-
const px = item.pivotX !== undefined ? item.pivotX : 0.5;
|
|
33
|
-
const py = item.pivotY !== undefined ? item.pivotY : 0.5;
|
|
34
|
-
return { tx, ty, ow, oh, px, py };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function createPackedFrame(item: SpriteFrameInput, x: number, y: number): PackedFrame {
|
|
38
|
-
const { tx, ty, ow, oh, px, py } = getFrameOffsets(item);
|
|
39
|
-
return {
|
|
40
|
-
id: item.id,
|
|
41
|
-
name: item.name,
|
|
42
|
-
x,
|
|
43
|
-
y,
|
|
44
|
-
width: item.width,
|
|
45
|
-
height: item.height,
|
|
46
|
-
rotated: false,
|
|
47
|
-
trimmed: tx > 0 || ty > 0,
|
|
48
|
-
spriteSourceSize: { x: tx, y: ty, w: item.width, h: item.height },
|
|
49
|
-
sourceSize: { w: ow, h: oh },
|
|
50
|
-
pivot: { x: px, y: py },
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function packSortedFrames(sorted: SpriteFrameInput[], options: PackerOptions) {
|
|
55
|
-
const padding = Math.max(0, options.padding);
|
|
56
|
-
const extrusion = Math.max(0, options.borderExtrusion);
|
|
57
|
-
const totalOffset = padding + extrusion * 2;
|
|
58
|
-
|
|
59
|
-
let currentX = padding;
|
|
60
|
-
let currentY = padding;
|
|
61
|
-
let rowHeight = 0;
|
|
62
|
-
let maxW = 0;
|
|
63
|
-
let maxH = 0;
|
|
64
|
-
const packedFrames: PackedFrame[] = [];
|
|
65
|
-
|
|
66
|
-
for (const item of sorted) {
|
|
67
|
-
const itemW = item.width + totalOffset;
|
|
68
|
-
const itemH = item.height + totalOffset;
|
|
69
|
-
|
|
70
|
-
if (currentX + itemW > options.maxTextureWidth && currentX > padding) {
|
|
71
|
-
currentX = padding;
|
|
72
|
-
currentY += rowHeight + padding;
|
|
73
|
-
rowHeight = 0;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
packedFrames.push(createPackedFrame(item, currentX + extrusion, currentY + extrusion));
|
|
77
|
-
|
|
78
|
-
currentX += itemW + padding;
|
|
79
|
-
rowHeight = Math.max(rowHeight, itemH);
|
|
80
|
-
maxW = Math.max(maxW, currentX);
|
|
81
|
-
maxH = Math.max(maxH, currentY + rowHeight + padding);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { packedFrames, maxW, maxH };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function calculateBinPacking(frames: SpriteFrameInput[], options: PackerOptions): PackedResult {
|
|
88
|
-
if (frames.length === 0) {
|
|
89
|
-
return emptyPackingResult();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const sorted = [...frames].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height));
|
|
93
|
-
const { packedFrames, maxW, maxH } = packSortedFrames(sorted, options);
|
|
94
|
-
|
|
95
|
-
let finalWidth = options.forcePowerOfTwo ? nextPowerOfTwo(maxW) : maxW;
|
|
96
|
-
let finalHeight = options.forcePowerOfTwo ? nextPowerOfTwo(maxH) : maxH;
|
|
97
|
-
|
|
98
|
-
finalWidth = Math.min(finalWidth, options.maxTextureWidth);
|
|
99
|
-
finalHeight = Math.min(finalHeight, options.maxTextureHeight);
|
|
100
|
-
|
|
101
|
-
let usedArea = 0;
|
|
102
|
-
for (const f of packedFrames) {
|
|
103
|
-
usedArea += f.width * f.height;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const totalArea = Math.max(1, finalWidth * finalHeight);
|
|
107
|
-
const efficiency = Math.min(100, Math.round((usedArea / totalArea) * 1000) / 10);
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
textureWidth: finalWidth,
|
|
111
|
-
textureHeight: finalHeight,
|
|
112
|
-
efficiency,
|
|
113
|
-
totalFrames: packedFrames.length,
|
|
114
|
-
frames: packedFrames,
|
|
115
|
-
atlasJson: generateAtlasJson(packedFrames, finalWidth, finalHeight, options.format),
|
|
116
|
-
cssSnippet: generateCssSnippet(packedFrames),
|
|
117
|
-
codeSnippet: generateEngineCodeSnippet(options.format),
|
|
118
|
-
drawCallsBefore: frames.length,
|
|
119
|
-
drawCallsAfter: 1,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
1
|
+
import type { PackerOptions, PackedFrame, PackedResult, SpriteFrameInput } from './types';
|
|
2
|
+
import { generateAtlasJson, generateCssSnippet, generateEngineCodeSnippet } from './exporter';
|
|
3
|
+
|
|
4
|
+
export function nextPowerOfTwo(value: number): number {
|
|
5
|
+
let power = 1;
|
|
6
|
+
while (power < value) {
|
|
7
|
+
power *= 2;
|
|
8
|
+
}
|
|
9
|
+
return power;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function emptyPackingResult(): PackedResult {
|
|
13
|
+
return {
|
|
14
|
+
textureWidth: 0,
|
|
15
|
+
textureHeight: 0,
|
|
16
|
+
efficiency: 0,
|
|
17
|
+
totalFrames: 0,
|
|
18
|
+
frames: [],
|
|
19
|
+
atlasJson: '{}',
|
|
20
|
+
cssSnippet: '',
|
|
21
|
+
codeSnippet: '',
|
|
22
|
+
drawCallsBefore: 0,
|
|
23
|
+
drawCallsAfter: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getFrameOffsets(item: SpriteFrameInput) {
|
|
28
|
+
const tx = item.trimmedX ? item.trimmedX : 0;
|
|
29
|
+
const ty = item.trimmedY ? item.trimmedY : 0;
|
|
30
|
+
const ow = item.originalWidth ? item.originalWidth : item.width;
|
|
31
|
+
const oh = item.originalHeight ? item.originalHeight : item.height;
|
|
32
|
+
const px = item.pivotX !== undefined ? item.pivotX : 0.5;
|
|
33
|
+
const py = item.pivotY !== undefined ? item.pivotY : 0.5;
|
|
34
|
+
return { tx, ty, ow, oh, px, py };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createPackedFrame(item: SpriteFrameInput, x: number, y: number): PackedFrame {
|
|
38
|
+
const { tx, ty, ow, oh, px, py } = getFrameOffsets(item);
|
|
39
|
+
return {
|
|
40
|
+
id: item.id,
|
|
41
|
+
name: item.name,
|
|
42
|
+
x,
|
|
43
|
+
y,
|
|
44
|
+
width: item.width,
|
|
45
|
+
height: item.height,
|
|
46
|
+
rotated: false,
|
|
47
|
+
trimmed: tx > 0 || ty > 0,
|
|
48
|
+
spriteSourceSize: { x: tx, y: ty, w: item.width, h: item.height },
|
|
49
|
+
sourceSize: { w: ow, h: oh },
|
|
50
|
+
pivot: { x: px, y: py },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function packSortedFrames(sorted: SpriteFrameInput[], options: PackerOptions) {
|
|
55
|
+
const padding = Math.max(0, options.padding);
|
|
56
|
+
const extrusion = Math.max(0, options.borderExtrusion);
|
|
57
|
+
const totalOffset = padding + extrusion * 2;
|
|
58
|
+
|
|
59
|
+
let currentX = padding;
|
|
60
|
+
let currentY = padding;
|
|
61
|
+
let rowHeight = 0;
|
|
62
|
+
let maxW = 0;
|
|
63
|
+
let maxH = 0;
|
|
64
|
+
const packedFrames: PackedFrame[] = [];
|
|
65
|
+
|
|
66
|
+
for (const item of sorted) {
|
|
67
|
+
const itemW = item.width + totalOffset;
|
|
68
|
+
const itemH = item.height + totalOffset;
|
|
69
|
+
|
|
70
|
+
if (currentX + itemW > options.maxTextureWidth && currentX > padding) {
|
|
71
|
+
currentX = padding;
|
|
72
|
+
currentY += rowHeight + padding;
|
|
73
|
+
rowHeight = 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
packedFrames.push(createPackedFrame(item, currentX + extrusion, currentY + extrusion));
|
|
77
|
+
|
|
78
|
+
currentX += itemW + padding;
|
|
79
|
+
rowHeight = Math.max(rowHeight, itemH);
|
|
80
|
+
maxW = Math.max(maxW, currentX);
|
|
81
|
+
maxH = Math.max(maxH, currentY + rowHeight + padding);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { packedFrames, maxW, maxH };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function calculateBinPacking(frames: SpriteFrameInput[], options: PackerOptions): PackedResult {
|
|
88
|
+
if (frames.length === 0) {
|
|
89
|
+
return emptyPackingResult();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sorted = [...frames].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height));
|
|
93
|
+
const { packedFrames, maxW, maxH } = packSortedFrames(sorted, options);
|
|
94
|
+
|
|
95
|
+
let finalWidth = options.forcePowerOfTwo ? nextPowerOfTwo(maxW) : maxW;
|
|
96
|
+
let finalHeight = options.forcePowerOfTwo ? nextPowerOfTwo(maxH) : maxH;
|
|
97
|
+
|
|
98
|
+
finalWidth = Math.min(finalWidth, options.maxTextureWidth);
|
|
99
|
+
finalHeight = Math.min(finalHeight, options.maxTextureHeight);
|
|
100
|
+
|
|
101
|
+
let usedArea = 0;
|
|
102
|
+
for (const f of packedFrames) {
|
|
103
|
+
usedArea += f.width * f.height;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const totalArea = Math.max(1, finalWidth * finalHeight);
|
|
107
|
+
const efficiency = Math.min(100, Math.round((usedArea / totalArea) * 1000) / 10);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
textureWidth: finalWidth,
|
|
111
|
+
textureHeight: finalHeight,
|
|
112
|
+
efficiency,
|
|
113
|
+
totalFrames: packedFrames.length,
|
|
114
|
+
frames: packedFrames,
|
|
115
|
+
atlasJson: generateAtlasJson(packedFrames, finalWidth, finalHeight, options.format),
|
|
116
|
+
cssSnippet: generateCssSnippet(packedFrames),
|
|
117
|
+
codeSnippet: generateEngineCodeSnippet(options.format),
|
|
118
|
+
drawCallsBefore: frames.length,
|
|
119
|
+
drawCallsAfter: 1,
|
|
120
|
+
};
|
|
121
|
+
}
|