@jjlmoya/utils-games-development 1.66.0 → 1.68.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 (72) 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/category_seo_quality.test.ts +74 -0
  30. package/src/tests/diacritics_density.test.ts +118 -118
  31. package/src/tests/faq_count.test.ts +19 -19
  32. package/src/tests/i18n_coverage.test.ts +36 -36
  33. package/src/tests/inverted_punctuation.test.ts +84 -84
  34. package/src/tests/mocks/astro_mock.js +2 -2
  35. package/src/tests/no_en_dash.test.ts +70 -70
  36. package/src/tests/no_h1_in_components.test.ts +48 -48
  37. package/src/tests/pagespeed_best_practices.test.ts +198 -198
  38. package/src/tests/qa-test-helpers.ts +32 -32
  39. package/src/tests/qa_bibliography_links.test.ts +41 -41
  40. package/src/tests/qa_claim_evidence.test.ts +69 -69
  41. package/src/tests/qa_logic_reference_coverage.test.ts +46 -46
  42. package/src/tests/qa_runtime_i18n.test.ts +100 -100
  43. package/src/tests/schemas_fulfillment.test.ts +23 -23
  44. package/src/tests/script_density.test.ts +94 -94
  45. package/src/tests/seo_length.test.ts +23 -23
  46. package/src/tests/seo_translation_completeness.test.ts +18 -12
  47. package/src/tests/slug_language_code_format.test.ts +23 -23
  48. package/src/tests/slug_uniqueness.test.ts +80 -80
  49. package/src/tests/title_quality.test.ts +56 -56
  50. package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -322
  51. package/src/tool/audioLoopPointFinder/client.ts +262 -262
  52. package/src/tool/hitboxHurtboxAnimator/hitbox-hurtbox-animator.css +614 -614
  53. package/src/tool/saveFileEditor/component.astro +351 -351
  54. package/src/tool/saveFileEditor/game-save-file-editor.css +250 -250
  55. package/src/tool/spriteSheetPacker/app-client.ts +248 -248
  56. package/src/tool/spriteSheetPacker/bibliography.ts +12 -12
  57. package/src/tool/spriteSheetPacker/component.astro +229 -229
  58. package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -55
  59. package/src/tool/spriteSheetPacker/entry.ts +28 -28
  60. package/src/tool/spriteSheetPacker/exporter.ts +116 -116
  61. package/src/tool/spriteSheetPacker/extractor-client.ts +127 -127
  62. package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -60
  63. package/src/tool/spriteSheetPacker/logic.test.ts +155 -155
  64. package/src/tool/spriteSheetPacker/logic.ts +32 -32
  65. package/src/tool/spriteSheetPacker/packer-core.ts +121 -121
  66. package/src/tool/spriteSheetPacker/packer-ui.ts +86 -86
  67. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +578 -578
  68. package/src/tool/spriteSheetPacker/types.ts +89 -89
  69. package/src/tool/spriteSheetPacker/ui.ts +42 -42
  70. package/src/tool/steamCapsuleGenerator/index.ts +9 -9
  71. package/src/tool/steamCapsuleGenerator/validation.ts +14 -14
  72. 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
+ }