@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.
Files changed (77) hide show
  1. package/package.json +67 -67
  2. package/scripts/postinstall.mjs +27 -27
  3. package/src/category/GamesCategorySEO.astro +19 -19
  4. package/src/category/i18n/de.ts +15 -15
  5. package/src/category/i18n/en.ts +15 -15
  6. package/src/category/i18n/es.ts +15 -15
  7. package/src/category/i18n/fr.ts +15 -15
  8. package/src/category/i18n/id.ts +10 -10
  9. package/src/category/i18n/it.ts +10 -10
  10. package/src/category/i18n/ja.ts +10 -10
  11. package/src/category/i18n/ko.ts +10 -10
  12. package/src/category/i18n/nl.ts +10 -10
  13. package/src/category/i18n/pl.ts +10 -10
  14. package/src/category/i18n/pt.ts +10 -10
  15. package/src/category/i18n/ru.ts +10 -10
  16. package/src/category/i18n/sv.ts +10 -10
  17. package/src/category/i18n/tr.ts +10 -10
  18. package/src/category/i18n/zh.ts +10 -10
  19. package/src/category/seo.astro +15 -15
  20. package/src/components/PreviewNavSidebar.astro +116 -116
  21. package/src/components/PreviewToolbar.astro +143 -143
  22. package/src/data.ts +11 -11
  23. package/src/env.d.ts +5 -5
  24. package/src/index.ts +20 -20
  25. package/src/layouts/PreviewLayout.astro +122 -122
  26. package/src/pages/[locale]/[slug].astro +165 -165
  27. package/src/pages/[locale].astro +250 -250
  28. package/src/pages/index.astro +4 -4
  29. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  30. package/src/tests/category_seo_quality.test.ts +74 -0
  31. package/src/tests/diacritics_density.test.ts +118 -118
  32. package/src/tests/faq_count.test.ts +19 -19
  33. package/src/tests/i18n_coverage.test.ts +36 -36
  34. package/src/tests/inverted_punctuation.test.ts +84 -84
  35. package/src/tests/mocks/astro_mock.js +2 -2
  36. package/src/tests/no_en_dash.test.ts +70 -70
  37. package/src/tests/no_h1_in_components.test.ts +48 -48
  38. package/src/tests/pagespeed_best_practices.test.ts +198 -198
  39. package/src/tests/qa-test-helpers.ts +32 -32
  40. package/src/tests/qa_bibliography_links.test.ts +41 -41
  41. package/src/tests/qa_claim_evidence.test.ts +69 -69
  42. package/src/tests/qa_logic_reference_coverage.test.ts +46 -46
  43. package/src/tests/qa_runtime_i18n.test.ts +100 -100
  44. package/src/tests/schemas_fulfillment.test.ts +23 -23
  45. package/src/tests/script_density.test.ts +94 -94
  46. package/src/tests/seo_length.test.ts +23 -23
  47. package/src/tests/seo_wellformed_export.test.ts +65 -0
  48. package/src/tests/slug_language_code_format.test.ts +23 -23
  49. package/src/tests/slug_uniqueness.test.ts +80 -80
  50. package/src/tests/title_quality.test.ts +56 -56
  51. package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -322
  52. package/src/tool/audioLoopPointFinder/client.ts +262 -262
  53. package/src/tool/damageFormulaLab/bibliography.astro +3 -21
  54. package/src/tool/damageFormulaLab/seo.astro +7 -5
  55. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +3 -19
  56. package/src/tool/hitboxHurtboxAnimator/hitbox-hurtbox-animator.css +614 -614
  57. package/src/tool/hitboxHurtboxAnimator/seo.astro +7 -5
  58. package/src/tool/saveFileEditor/component.astro +351 -351
  59. package/src/tool/saveFileEditor/game-save-file-editor.css +250 -250
  60. package/src/tool/spriteSheetPacker/app-client.ts +248 -248
  61. package/src/tool/spriteSheetPacker/bibliography.ts +12 -12
  62. package/src/tool/spriteSheetPacker/component.astro +229 -229
  63. package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -55
  64. package/src/tool/spriteSheetPacker/entry.ts +28 -28
  65. package/src/tool/spriteSheetPacker/exporter.ts +116 -116
  66. package/src/tool/spriteSheetPacker/extractor-client.ts +127 -127
  67. package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -60
  68. package/src/tool/spriteSheetPacker/logic.test.ts +155 -155
  69. package/src/tool/spriteSheetPacker/logic.ts +32 -32
  70. package/src/tool/spriteSheetPacker/packer-core.ts +121 -121
  71. package/src/tool/spriteSheetPacker/packer-ui.ts +86 -86
  72. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +578 -578
  73. package/src/tool/spriteSheetPacker/types.ts +89 -89
  74. package/src/tool/spriteSheetPacker/ui.ts +42 -42
  75. package/src/tool/steamCapsuleGenerator/index.ts +9 -9
  76. package/src/tool/steamCapsuleGenerator/validation.ts +14 -14
  77. 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
+ }