@jjlmoya/utils-games-development 1.47.0 → 1.49.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 (57) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +1 -20
  5. package/src/tests/tool_validation.test.ts +2 -3
  6. package/src/tool/itchioGameTester/client.ts +12 -23
  7. package/src/tool/itchioGameTester/component.astro +0 -7
  8. package/src/tool/itchioGameTester/i18n/de.ts +0 -2
  9. package/src/tool/itchioGameTester/i18n/en.ts +0 -2
  10. package/src/tool/itchioGameTester/i18n/es.ts +0 -2
  11. package/src/tool/itchioGameTester/i18n/fr.ts +0 -2
  12. package/src/tool/itchioGameTester/i18n/id.ts +0 -2
  13. package/src/tool/itchioGameTester/i18n/it.ts +0 -2
  14. package/src/tool/itchioGameTester/i18n/ja.ts +0 -2
  15. package/src/tool/itchioGameTester/i18n/ko.ts +0 -2
  16. package/src/tool/itchioGameTester/i18n/nl.ts +0 -2
  17. package/src/tool/itchioGameTester/i18n/pl.ts +0 -2
  18. package/src/tool/itchioGameTester/i18n/pt.ts +0 -2
  19. package/src/tool/itchioGameTester/i18n/ru.ts +0 -2
  20. package/src/tool/itchioGameTester/i18n/sv.ts +0 -2
  21. package/src/tool/itchioGameTester/i18n/tr.ts +0 -2
  22. package/src/tool/itchioGameTester/i18n/zh.ts +0 -2
  23. package/src/tool/itchioGameTester/ui.ts +0 -2
  24. package/src/tool/spriteSheetPacker/app-client.ts +248 -0
  25. package/src/tool/spriteSheetPacker/bibliography.astro +6 -0
  26. package/src/tool/spriteSheetPacker/bibliography.ts +12 -0
  27. package/src/tool/spriteSheetPacker/component.astro +229 -0
  28. package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -0
  29. package/src/tool/spriteSheetPacker/entry.ts +28 -0
  30. package/src/tool/spriteSheetPacker/exporter.ts +116 -0
  31. package/src/tool/spriteSheetPacker/extractor-client.ts +127 -0
  32. package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -0
  33. package/src/tool/spriteSheetPacker/i18n/de.ts +270 -0
  34. package/src/tool/spriteSheetPacker/i18n/en.ts +275 -0
  35. package/src/tool/spriteSheetPacker/i18n/es.ts +275 -0
  36. package/src/tool/spriteSheetPacker/i18n/fr.ts +270 -0
  37. package/src/tool/spriteSheetPacker/i18n/id.ts +270 -0
  38. package/src/tool/spriteSheetPacker/i18n/it.ts +270 -0
  39. package/src/tool/spriteSheetPacker/i18n/ja.ts +270 -0
  40. package/src/tool/spriteSheetPacker/i18n/ko.ts +270 -0
  41. package/src/tool/spriteSheetPacker/i18n/nl.ts +270 -0
  42. package/src/tool/spriteSheetPacker/i18n/pl.ts +270 -0
  43. package/src/tool/spriteSheetPacker/i18n/pt.ts +270 -0
  44. package/src/tool/spriteSheetPacker/i18n/ru.ts +266 -0
  45. package/src/tool/spriteSheetPacker/i18n/sv.ts +270 -0
  46. package/src/tool/spriteSheetPacker/i18n/tr.ts +270 -0
  47. package/src/tool/spriteSheetPacker/i18n/zh.ts +270 -0
  48. package/src/tool/spriteSheetPacker/index.ts +11 -0
  49. package/src/tool/spriteSheetPacker/logic.test.ts +155 -0
  50. package/src/tool/spriteSheetPacker/logic.ts +32 -0
  51. package/src/tool/spriteSheetPacker/packer-core.ts +121 -0
  52. package/src/tool/spriteSheetPacker/packer-ui.ts +86 -0
  53. package/src/tool/spriteSheetPacker/seo.astro +15 -0
  54. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +542 -0
  55. package/src/tool/spriteSheetPacker/types.ts +89 -0
  56. package/src/tool/spriteSheetPacker/ui.ts +42 -0
  57. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-games-development",
3
- "version": "1.47.0",
3
+ "version": "1.49.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,9 +1,10 @@
1
1
  import type { GamesCategoryEntry } from '../types';
2
2
  import { steamCapsuleGenerator } from '../tool/steamCapsuleGenerator/entry';
3
+ import { spriteSheetPacker } from '../tool/spriteSheetPacker/entry';
3
4
 
4
5
  export const gamesCategory: GamesCategoryEntry = {
5
6
  icon: 'mdi:gamepad-variant',
6
- tools: [steamCapsuleGenerator],
7
+ tools: [steamCapsuleGenerator, spriteSheetPacker],
7
8
  i18n: {
8
9
  de: () => import('./i18n/de').then((m) => m.content),
9
10
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -2,8 +2,11 @@ export { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
2
2
  export type { SteamCapsuleGeneratorUI, SteamCapsuleGeneratorLocaleContent } from './tool/steamCapsuleGenerator/entry';
3
3
  export { itchioGameTester } from './tool/itchioGameTester/entry';
4
4
  export type { ItchioGameTesterUI, ItchioGameTesterLocaleContent } from './tool/itchioGameTester/entry';
5
+ export { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
6
+ export type { SpriteSheetPackerUI, SpriteSheetPackerLocaleContent } from './tool/spriteSheetPacker/entry';
5
7
  export { gamesCategory } from './category';
6
8
  import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
7
9
  import { itchioGameTester } from './tool/itchioGameTester/entry';
10
+ import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
8
11
 
9
- export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester];
12
+ export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker];
@@ -1,27 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { ALL_TOOLS } from '../tools';
3
- import type { ToolLocaleContent } from '../types';
4
3
 
5
4
  describe('Locale Completeness Validation', () => {
6
- ALL_TOOLS.forEach((tool) => {
7
- describe(`Tool: ${tool.entry.id}`, () => {
8
- Object.keys(tool.entry.i18n).forEach((locale) => {
9
- describe(`Locale: ${locale}`, () => {
10
- it('bibliography should be an array', async () => {
11
- const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
- const content = (await loader?.()) as ToolLocaleContent;
13
-
14
- expect(
15
- Array.isArray(content.bibliography),
16
- `Tool "${tool.entry.id}" locale "${locale}" bibliography is not an array`,
17
- ).toBeTruthy();
18
- });
19
- });
20
- });
21
- });
22
- });
23
-
24
5
  it('one tool is registered', () => {
25
- expect(ALL_TOOLS.length).toBe(2);
6
+ expect(ALL_TOOLS.length).toBe(3);
26
7
  });
27
8
  });
@@ -4,8 +4,8 @@ import { gamesCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 2 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(2);
7
+ it('should have 3 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(3);
9
9
  });
10
10
 
11
11
  it('gamesCategory should be defined', () => {
@@ -14,4 +14,3 @@ describe('Tool Validation Suite', () => {
14
14
  });
15
15
  });
16
16
  });
17
-
@@ -1,5 +1,5 @@
1
1
  import JSZip from 'jszip';
2
- import { validateGameFiles, calculateAspectRatio, generateItchioEmbedConfig, type WebGameFileInfo } from './logic';
2
+ import { validateGameFiles, generateItchioEmbedConfig, type WebGameFileInfo } from './logic';
3
3
  import { displayAudit } from './audit';
4
4
 
5
5
  const STATE_KEY = 'jjl_itchio_game_tester_state';
@@ -50,28 +50,23 @@ function applyViewportStyle(w: number, h: number) {
50
50
  }
51
51
  const wInput = document.getElementById('igt-width-input') as HTMLInputElement;
52
52
  const hInput = document.getElementById('igt-height-input') as HTMLInputElement;
53
- const rDisplay = document.getElementById('igt-ratio-display');
54
53
  if (wInput) wInput.value = w.toString();
55
54
  if (hInput) hInput.value = h.toString();
56
- if (rDisplay) rDisplay.textContent = calculateAspectRatio(w, h);
57
55
  localStorage.setItem(STATE_KEY, JSON.stringify({ w, h }));
58
56
  }
59
57
 
60
- function bindInputEvents(currentRatio: { value: number }) {
58
+ function bindInputEvents() {
61
59
  const widthInput = document.getElementById('igt-width-input') as HTMLInputElement;
62
60
  const heightInput = document.getElementById('igt-height-input') as HTMLInputElement;
63
- const ratioLock = document.getElementById('igt-ratio-lock') as HTMLInputElement;
64
61
  const autoScale = document.getElementById('igt-auto-scale') as HTMLInputElement;
65
62
  widthInput?.addEventListener('input', () => {
66
63
  const w = parseInt(widthInput.value, 10) || 960;
67
- const h = ratioLock?.checked ? Math.round(w / currentRatio.value) : (parseInt(heightInput.value, 10) || 540);
68
- if (!ratioLock?.checked) currentRatio.value = w / h;
64
+ const h = parseInt(heightInput.value, 10) || 540;
69
65
  applyViewportStyle(w, h);
70
66
  });
71
67
  heightInput?.addEventListener('input', () => {
72
68
  const h = parseInt(heightInput.value, 10) || 540;
73
- const w = ratioLock?.checked ? Math.round(h * currentRatio.value) : (parseInt(widthInput.value, 10) || 960);
74
- if (!ratioLock?.checked) currentRatio.value = w / h;
69
+ const w = parseInt(widthInput.value, 10) || 960;
75
70
  applyViewportStyle(w, h);
76
71
  });
77
72
  autoScale?.addEventListener('change', () => {
@@ -79,7 +74,7 @@ function bindInputEvents(currentRatio: { value: number }) {
79
74
  });
80
75
  }
81
76
 
82
- function bindPresetEvents(currentRatio: { value: number }) {
77
+ function bindPresetEvents() {
83
78
  const resetBtn = document.getElementById('igt-reset-btn');
84
79
  const presetsContainer = document.getElementById('igt-presets-container');
85
80
  presetsContainer?.addEventListener('click', (e) => {
@@ -87,19 +82,16 @@ function bindPresetEvents(currentRatio: { value: number }) {
87
82
  if (target.classList.contains('igt-chip')) {
88
83
  const w = parseInt(target.getAttribute('data-w') || '960', 10);
89
84
  const h = parseInt(target.getAttribute('data-h') || '540', 10);
90
- currentRatio.value = w / h;
91
85
  applyViewportStyle(w, h);
92
86
  presetsContainer.querySelectorAll('.igt-chip').forEach((c) => c.classList.remove('active'));
93
87
  target.classList.add('active');
94
88
  }
95
89
  });
96
- resetBtn?.addEventListener('click', () => { currentRatio.value = 960 / 540; applyViewportStyle(960, 540); });
90
+ resetBtn?.addEventListener('click', () => { applyViewportStyle(960, 540); });
97
91
  }
98
92
 
99
93
  interface DragState {
100
94
  iframe: HTMLElement | null;
101
- ratioLock: HTMLInputElement | null;
102
- currentRatio: { value: number };
103
95
  start: { x: number; y: number; w: number; h: number };
104
96
  }
105
97
 
@@ -109,8 +101,7 @@ function attachDragListeners(st: DragState) {
109
101
  const onMouseMove = (e: MouseEvent) => {
110
102
  if (!isResizing) return;
111
103
  const constW = Math.max(320, Math.min(1920, st.start.w + (e.clientX - st.start.x)));
112
- const constH = st.ratioLock?.checked ? Math.round(constW / st.currentRatio.value) : Math.max(240, Math.min(1080, st.start.h + (e.clientY - st.start.y)));
113
- if (!st.ratioLock?.checked) st.currentRatio.value = constW / constH;
104
+ const constH = Math.max(240, Math.min(1080, st.start.h + (e.clientY - st.start.y)));
114
105
  applyViewportStyle(constW, constH);
115
106
  };
116
107
  const onMouseUp = () => {
@@ -123,22 +114,20 @@ function attachDragListeners(st: DragState) {
123
114
  window.addEventListener('mouseup', onMouseUp);
124
115
  }
125
116
 
126
- function bindLiveDragResizer(currentRatio: { value: number }) {
117
+ function bindLiveDragResizer() {
127
118
  const handle = document.getElementById('igt-resize-handle');
128
119
  const frame = document.getElementById('igt-emulator-frame');
129
120
  const iframe = document.getElementById('igt-iframe');
130
- const ratioLock = document.getElementById('igt-ratio-lock') as HTMLInputElement;
131
121
  if (!handle || !frame) return;
132
122
  handle.addEventListener('mousedown', (e) => {
133
123
  e.preventDefault();
134
- attachDragListeners({ iframe, ratioLock, currentRatio, start: { x: e.clientX, y: e.clientY, w: currentViewport.w, h: currentViewport.h } });
124
+ attachDragListeners({ iframe, start: { x: e.clientX, y: e.clientY, w: currentViewport.w, h: currentViewport.h } });
135
125
  });
136
126
  }
137
127
 
138
128
  function setupViewportController() {
139
129
  const widthInput = document.getElementById('igt-width-input') as HTMLInputElement;
140
130
  const heightInput = document.getElementById('igt-height-input') as HTMLInputElement;
141
- const currentRatio = { value: 16 / 9 };
142
131
  const savedState = localStorage.getItem(STATE_KEY);
143
132
  if (savedState) {
144
133
  try {
@@ -148,9 +137,9 @@ function setupViewportController() {
148
137
  applyViewportStyle(parsed.w, parsed.h);
149
138
  } catch {}
150
139
  }
151
- bindInputEvents(currentRatio);
152
- bindPresetEvents(currentRatio);
153
- bindLiveDragResizer(currentRatio);
140
+ bindInputEvents();
141
+ bindPresetEvents();
142
+ bindLiveDragResizer();
154
143
  }
155
144
 
156
145
  function getMimeType(filename: string): string {
@@ -61,13 +61,6 @@ const { ui } = Astro.props;
61
61
  <label for="igt-height-input" class="igt-label">{ui.viewportHeight}</label>
62
62
  <input type="number" id="igt-height-input" class="igt-input" min="240" max="1080" value="540" aria-label={ui.viewportHeight} />
63
63
  </div>
64
- <div class="igt-input-group">
65
- <label for="igt-ratio-lock" class="igt-label">{ui.aspectRatio}</label>
66
- <div style="display: flex; align-items: center; gap: 0.5rem; height: 100%;">
67
- <input type="checkbox" id="igt-ratio-lock" checked aria-label={ui.lockAspectRatio} />
68
- <span style="font-size: 0.9rem;" id="igt-ratio-display">16:9</span>
69
- </div>
70
- </div>
71
64
  <div class="igt-input-group" style="grid-column: 1 / -1;">
72
65
  <div style="display: flex; align-items: center; gap: 0.5rem;">
73
66
  <input type="checkbox" id="igt-auto-scale" checked aria-label={ui.autoScaleToggle} />
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io-Kompatibilitätsbewertung',
15
15
  viewportWidth: 'Viewport-Breite (px)',
16
16
  viewportHeight: 'Viewport-Höhe (px)',
17
- aspectRatio: 'Seitenverhältnis',
18
- lockAspectRatio: 'Seitenverhältnis sperren',
19
17
  presets: 'Schnelle Auflösungs-Presets',
20
18
  fitTest: 'Live-Layout- und Scrollbar-Test',
21
19
  copySettings: 'Itch.io Embed-Einstellungen kopieren',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io Compatibility Score',
15
15
  viewportWidth: 'Viewport Width (px)',
16
16
  viewportHeight: 'Viewport Height (px)',
17
- aspectRatio: 'Aspect Ratio',
18
- lockAspectRatio: 'Lock Aspect Ratio',
19
17
  presets: 'Quick Resolution Presets',
20
18
  fitTest: 'Live Layout & Scrollbar Test',
21
19
  copySettings: 'Copy Itch.io Embed Settings',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Puntuación de Compatibilidad con Itch.io',
15
15
  viewportWidth: 'Ancho del Viewport (px)',
16
16
  viewportHeight: 'Alto del Viewport (px)',
17
- aspectRatio: 'Relación de Aspecto',
18
- lockAspectRatio: 'Bloquear Relación de Aspecto',
19
17
  presets: 'Presets de Resolución Rápidos',
20
18
  fitTest: 'Prueba de Diseño y Scrollbars en Vivo',
21
19
  copySettings: 'Copiar Configuración de Embed para Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Score de Compatibilité Itch.io',
15
15
  viewportWidth: 'Largeur du Viewport (px)',
16
16
  viewportHeight: 'Hauteur du Viewport (px)',
17
- aspectRatio: 'Rapport d\'Aspect',
18
- lockAspectRatio: 'Verrouiller le Rapport d\'Aspect',
19
17
  presets: 'Préréglages de Résolution Rapides',
20
18
  fitTest: 'Test de Mise en Page et Scrollbars en Direct',
21
19
  copySettings: 'Copier les Paramètres d\'Intégration Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Skor Kompatibilitas Itch.io',
15
15
  viewportWidth: 'Lebar Viewport (px)',
16
16
  viewportHeight: 'Tinggi Viewport (px)',
17
- aspectRatio: 'Rasio Aspek',
18
- lockAspectRatio: 'Kunci Rasio Aspek',
19
17
  presets: 'Preset Resolusi Cepat',
20
18
  fitTest: 'Uji Tata Letak dan Scrollbar Langsung',
21
19
  copySettings: 'Salin Pengaturan Embed Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Punteggio di Compatibilità Itch.io',
15
15
  viewportWidth: 'Larghezza Viewport (px)',
16
16
  viewportHeight: 'Altezza Viewport (px)',
17
- aspectRatio: 'Rapporto di Aspetto',
18
- lockAspectRatio: 'Blocca Rapporto di Aspetto',
19
17
  presets: 'Preset di Risoluzione Rapidi',
20
18
  fitTest: 'Test Live di Layout e Scrollbar',
21
19
  copySettings: 'Copia Impostazioni di Incorporamento Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io互換性スコア',
15
15
  viewportWidth: 'ビューポート幅 (px)',
16
16
  viewportHeight: 'ビューポート高さ (px)',
17
- aspectRatio: 'アスペクト比',
18
- lockAspectRatio: 'アスペクト比をロック',
19
17
  presets: 'クイック解像度プリセット',
20
18
  fitTest: 'ライブレイアウトとスクロールバーテスト',
21
19
  copySettings: 'Itch.io埋め込み設定をコピー',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io 호환성 점수',
15
15
  viewportWidth: '뷰포트 너비 (px)',
16
16
  viewportHeight: '뷰포트 높이 (px)',
17
- aspectRatio: '화면 비율',
18
- lockAspectRatio: '화면 비율 잠금',
19
17
  presets: '빠른 해상도 프리셋',
20
18
  fitTest: '라이브 레이아웃 및 스크롤바 테스트',
21
19
  copySettings: 'Itch.io 임베드 설정 복사',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io compatibiliteitsscore',
15
15
  viewportWidth: 'Viewportbreedte (px)',
16
16
  viewportHeight: 'Viewporthoogte (px)',
17
- aspectRatio: 'Beeldverhouding',
18
- lockAspectRatio: 'Beeldverhouding vergrendelen',
19
17
  presets: 'Snelle resolutiepresets',
20
18
  fitTest: 'Live layout- en scrollbartest',
21
19
  copySettings: 'Itch.io embed-instellingen kopiëren',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Wynik zgodności z Itch.io',
15
15
  viewportWidth: 'Szerokość viewportu (px)',
16
16
  viewportHeight: 'Wysokość viewportu (px)',
17
- aspectRatio: 'Proporcje ekranu',
18
- lockAspectRatio: 'Zablokuj proporcje ekranu',
19
17
  presets: 'Szybkie presety rozdzielczości',
20
18
  fitTest: 'Test układu i pasków przewijania na żywo',
21
19
  copySettings: 'Skopiuj ustawienia osadzania Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Pontuação de Compatibilidade Itch.io',
15
15
  viewportWidth: 'Largura do Viewport (px)',
16
16
  viewportHeight: 'Altura do Viewport (px)',
17
- aspectRatio: 'Proporção de Tela',
18
- lockAspectRatio: 'Bloquear Proporção de Tela',
19
17
  presets: 'Predefinições de Resolução Rápida',
20
18
  fitTest: 'Teste de Layout e Barras de Rolagem ao Vivo',
21
19
  copySettings: 'Copiar Configurações de Incorporação Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Оценка совместимости с Itch.io',
15
15
  viewportWidth: 'Ширина viewport (px)',
16
16
  viewportHeight: 'Высота viewport (px)',
17
- aspectRatio: 'Соотношение сторон',
18
- lockAspectRatio: 'Зафиксировать соотношение сторон',
19
17
  presets: 'Быстрые пресеты разрешений',
20
18
  fitTest: 'Тест макета и полос прокрутки в реальном времени',
21
19
  copySettings: 'Скопировать настройки встраивания Itch.io',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io-kompatibilitetspoäng',
15
15
  viewportWidth: 'Viewportbredd (px)',
16
16
  viewportHeight: 'Viewporthöjd (px)',
17
- aspectRatio: 'Bildförhållande',
18
- lockAspectRatio: 'Lås bildförhållande',
19
17
  presets: 'Snabba upplösningsförinställningar',
20
18
  fitTest: 'Live layout- och rullningsliststest',
21
19
  copySettings: 'Kopiera Itch.io-inbäddningsinställningar',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io Uyumluluk Puanı',
15
15
  viewportWidth: 'Görünüm Alanı Genişliği (px)',
16
16
  viewportHeight: 'Görünüm Alanı Yüksekliği (px)',
17
- aspectRatio: 'En-Boy Oranı',
18
- lockAspectRatio: 'En-Boy Oranını Kilitle',
19
17
  presets: 'Hızlı Çözünürlük Ön Ayarları',
20
18
  fitTest: 'Canlı Düzen ve Kaydırma Çubuğu Testi',
21
19
  copySettings: 'Itch.io Yerleştirme Ayarlarını Kopyala',
@@ -14,8 +14,6 @@ export const content: ToolLocaleContent<ItchioGameTesterUI> = {
14
14
  compatibilityScore: 'Itch.io兼容性评分',
15
15
  viewportWidth: '视口宽度 (px)',
16
16
  viewportHeight: '视口高度 (px)',
17
- aspectRatio: '宽高比',
18
- lockAspectRatio: '锁定宽高比',
19
17
  presets: '快速分辨率预设',
20
18
  fitTest: '实时布局与滚动条测试',
21
19
  copySettings: '复制Itch.io嵌入设置',
@@ -6,8 +6,6 @@ export interface ItchioGameTesterUI extends Record<string, string> {
6
6
  compatibilityScore: string;
7
7
  viewportWidth: string;
8
8
  viewportHeight: string;
9
- aspectRatio: string;
10
- lockAspectRatio: string;
11
9
  presets: string;
12
10
  fitTest: string;
13
11
  copySettings: string;
@@ -0,0 +1,248 @@
1
+ import JSZip from 'jszip';
2
+ import { bindDropzone } from './dropzone-client';
3
+ import { initExtractor, updateExtractor } from './extractor-client';
4
+ import { bindFlipbook, startFlipbookLoop } from './flipbook-client';
5
+ import { calculateBinPacking, type ExportFormat, type SpriteFrameInput } from './logic';
6
+ import { drawEmptyCanvasMessage, setupStepper, spawnParticle, triggerButtonFeedback, type LoadedImageItem } from './packer-ui';
7
+
8
+ let loadedImages: LoadedImageItem[] = [];
9
+ let currentAtlasData: ReturnType<typeof calculateBinPacking> | null = null;
10
+
11
+ function parseInputValue(id: string, fallback: string): number {
12
+ const el = document.getElementById(id) as HTMLInputElement | null;
13
+ return parseInt(el?.value || fallback, 10);
14
+ }
15
+
16
+ function renderAtlasCanvas(): void {
17
+ const canvas = document.getElementById('ssp-atlas-canvas') as HTMLCanvasElement | null;
18
+ if (!canvas) return;
19
+
20
+ if (!currentAtlasData || loadedImages.length === 0) {
21
+ drawEmptyCanvasMessage(canvas, 'Upload PNG frames to preview packed texture atlas');
22
+ return;
23
+ }
24
+
25
+ canvas.width = currentAtlasData.textureWidth;
26
+ canvas.height = currentAtlasData.textureHeight;
27
+
28
+ const ctx = canvas.getContext('2d');
29
+ if (!ctx) return;
30
+
31
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
32
+ for (const frame of currentAtlasData.frames) {
33
+ const found = loadedImages.find((img) => img.id === frame.id);
34
+ if (found) {
35
+ ctx.drawImage(found.img, frame.x, frame.y, frame.width, frame.height);
36
+ ctx.strokeStyle = 'rgba(99, 102, 241, 0.4)';
37
+ ctx.lineWidth = 1;
38
+ ctx.strokeRect(frame.x, frame.y, frame.width, frame.height);
39
+ }
40
+ }
41
+ }
42
+
43
+ function updateStatsUI(): void {
44
+ if (!currentAtlasData) return;
45
+
46
+ const effEl = document.getElementById('ssp-stat-efficiency');
47
+ const dcEl = document.getElementById('ssp-stat-drawcalls');
48
+ const framesEl = document.getElementById('ssp-stat-frames');
49
+ const sizeEl = document.getElementById('ssp-stat-size');
50
+ const codeEl = document.getElementById('ssp-code-snippet');
51
+
52
+ if (effEl) effEl.textContent = `${currentAtlasData.efficiency}%`;
53
+ if (dcEl) dcEl.textContent = `${currentAtlasData.drawCallsBefore} -> ${currentAtlasData.drawCallsAfter}`;
54
+ if (framesEl) framesEl.textContent = currentAtlasData.totalFrames.toString();
55
+ if (sizeEl) sizeEl.textContent = `${currentAtlasData.textureWidth}x${currentAtlasData.textureHeight}`;
56
+ if (codeEl) codeEl.textContent = currentAtlasData.codeSnippet;
57
+ }
58
+
59
+ function getPackerSettings() {
60
+ const pad = parseInputValue('ssp-padding', '2');
61
+ const ext = parseInputValue('ssp-extrusion', '0');
62
+ const max = parseInputValue('ssp-max-size', '2048');
63
+ const fmt = ((document.getElementById('ssp-export-format') as HTMLSelectElement)?.value || 'generic-json-hash') as ExportFormat;
64
+ const pot = (document.getElementById('ssp-power-two') as HTMLInputElement)?.checked ?? true;
65
+ return { padding: pad, borderExtrusion: ext, format: fmt, maxTextureWidth: max, forcePowerOfTwo: pot };
66
+ }
67
+
68
+ function updatePacker(): void {
69
+ const { padding, borderExtrusion, format, maxTextureWidth, forcePowerOfTwo } = getPackerSettings();
70
+
71
+ const frameInputs: SpriteFrameInput[] = loadedImages.map((item) => ({
72
+ id: item.id,
73
+ name: item.name,
74
+ width: item.img.width,
75
+ height: item.img.height,
76
+ }));
77
+
78
+ currentAtlasData = calculateBinPacking(frameInputs, {
79
+ padding,
80
+ borderExtrusion,
81
+ forcePowerOfTwo,
82
+ maxTextureWidth,
83
+ maxTextureHeight: maxTextureWidth,
84
+ allowRotation: false,
85
+ trimTransparency: false,
86
+ format,
87
+ });
88
+
89
+ renderAtlasCanvas();
90
+ updateStatsUI();
91
+ }
92
+
93
+ function setPresetValues(padding: number, extrusion: number, maxSize: string, pot: boolean): void {
94
+ const padInput = document.getElementById('ssp-padding') as HTMLInputElement | null;
95
+ const extInput = document.getElementById('ssp-extrusion') as HTMLInputElement | null;
96
+ const maxSelect = document.getElementById('ssp-max-size') as HTMLSelectElement | null;
97
+ const potCheck = document.getElementById('ssp-power-two') as HTMLInputElement | null;
98
+
99
+ if (padInput) {
100
+ padInput.value = padding.toString();
101
+ const valEl = document.getElementById('ssp-padding-val');
102
+ if (valEl) valEl.textContent = padding.toString();
103
+ }
104
+ if (extInput) {
105
+ extInput.value = extrusion.toString();
106
+ const valEl = document.getElementById('ssp-extrusion-val');
107
+ if (valEl) valEl.textContent = extrusion.toString();
108
+ }
109
+ if (maxSelect) maxSelect.value = maxSize;
110
+ if (potCheck) potCheck.checked = pot;
111
+
112
+ updatePacker();
113
+ }
114
+
115
+ function downloadZipPackage(e: MouseEvent): void {
116
+ if (!currentAtlasData || currentAtlasData.frames.length === 0) return;
117
+ const canvas = document.getElementById('ssp-atlas-canvas') as HTMLCanvasElement | null;
118
+ if (!canvas) return;
119
+
120
+ triggerButtonFeedback(document.getElementById('ssp-btn-download-zip'), 'ZIP Generated!', e);
121
+
122
+ const zip = new JSZip();
123
+ canvas.toBlob((blob) => {
124
+ if (!blob) return;
125
+ zip.file('spritesheet.png', blob);
126
+ zip.file('spritesheet.json', currentAtlasData!.atlasJson);
127
+ zip.generateAsync({ type: 'blob' }).then((content) => {
128
+ const link = document.createElement('a');
129
+ link.href = URL.createObjectURL(content);
130
+ link.download = 'sprite-sheet-package.zip';
131
+ link.click();
132
+ });
133
+ });
134
+ }
135
+
136
+ function bindTabs(): void {
137
+ const tabPacker = document.getElementById('ssp-tab-packer');
138
+ const tabExtractor = document.getElementById('ssp-tab-extractor');
139
+ const panelPacker = document.getElementById('ssp-packer-panel');
140
+ const panelExtractor = document.getElementById('ssp-extractor-panel');
141
+
142
+ tabPacker?.addEventListener('click', () => {
143
+ tabPacker.classList.add('active');
144
+ tabExtractor?.classList.remove('active');
145
+ if (panelPacker) panelPacker.style.display = 'grid';
146
+ if (panelExtractor) panelExtractor.style.display = 'none';
147
+ renderAtlasCanvas();
148
+ });
149
+
150
+ tabExtractor?.addEventListener('click', () => {
151
+ tabExtractor.classList.add('active');
152
+ tabPacker?.classList.remove('active');
153
+ if (panelPacker) panelPacker.style.display = 'none';
154
+ if (panelExtractor) panelExtractor.style.display = 'grid';
155
+ updateExtractor();
156
+ });
157
+ }
158
+
159
+ function bindPresets(): void {
160
+ const setActivePreset = (target: HTMLElement) => {
161
+ document.querySelectorAll('.ssp-chip-btn').forEach((el) => el.classList.remove('active'));
162
+ target.classList.add('active');
163
+ };
164
+
165
+ document.getElementById('ssp-preset-pixel')?.addEventListener('click', (e) => {
166
+ setActivePreset(e.target as HTMLElement);
167
+ setPresetValues(2, 0, '512', true);
168
+ spawnParticle(e.clientX, e.clientY, 'PIXEL 16x16');
169
+ });
170
+ document.getElementById('ssp-preset-hd')?.addEventListener('click', (e) => {
171
+ setActivePreset(e.target as HTMLElement);
172
+ setPresetValues(4, 1, '1024', true);
173
+ spawnParticle(e.clientX, e.clientY, 'HD 1024');
174
+ });
175
+ document.getElementById('ssp-preset-mobile')?.addEventListener('click', (e) => {
176
+ setActivePreset(e.target as HTMLElement);
177
+ setPresetValues(2, 0, '2048', true);
178
+ spawnParticle(e.clientX, e.clientY, 'MOBILE 2048');
179
+ });
180
+ }
181
+
182
+ function bindSliders(): void {
183
+ const paddingSlider = document.getElementById('ssp-padding') as HTMLInputElement | null;
184
+ const extrusionSlider = document.getElementById('ssp-extrusion') as HTMLInputElement | null;
185
+
186
+ paddingSlider?.addEventListener('input', () => {
187
+ const valEl = document.getElementById('ssp-padding-val');
188
+ if (valEl) valEl.textContent = paddingSlider.value;
189
+ updatePacker();
190
+ });
191
+
192
+ extrusionSlider?.addEventListener('input', () => {
193
+ const valEl = document.getElementById('ssp-extrusion-val');
194
+ if (valEl) valEl.textContent = extrusionSlider.value;
195
+ updatePacker();
196
+ });
197
+ }
198
+
199
+ function bindExportCopies(): void {
200
+ document.getElementById('ssp-btn-copy-json')?.addEventListener('click', (e) => {
201
+ if (!currentAtlasData) return;
202
+ triggerButtonFeedback(document.getElementById('ssp-btn-copy-json'), 'Copied!', e);
203
+ navigator.clipboard.writeText(currentAtlasData.atlasJson);
204
+ });
205
+ document.getElementById('ssp-btn-download-png')?.addEventListener('click', (e) => {
206
+ const canvas = document.getElementById('ssp-atlas-canvas') as HTMLCanvasElement | null;
207
+ if (!canvas) return;
208
+ triggerButtonFeedback(document.getElementById('ssp-btn-download-png'), 'Downloaded!', e);
209
+ const link = document.createElement('a');
210
+ link.href = canvas.toDataURL('image/png');
211
+ link.download = 'spritesheet.png';
212
+ link.click();
213
+ });
214
+ document.getElementById('ssp-btn-copy-code')?.addEventListener('click', (e) => {
215
+ if (!currentAtlasData) return;
216
+ triggerButtonFeedback(document.getElementById('ssp-btn-copy-code'), 'Copied Code!', e);
217
+ navigator.clipboard.writeText(currentAtlasData.codeSnippet);
218
+ });
219
+ }
220
+
221
+ function bindActionButtons(): void {
222
+ setupStepper('ssp-pad-dec', 'ssp-pad-inc', 'ssp-padding', updatePacker);
223
+ setupStepper('ssp-ext-dec', 'ssp-ext-inc', 'ssp-extrusion', updatePacker);
224
+ bindSliders();
225
+ bindExportCopies();
226
+
227
+ document.getElementById('ssp-export-format')?.addEventListener('change', updatePacker);
228
+ document.getElementById('ssp-max-size')?.addEventListener('change', updatePacker);
229
+ document.getElementById('ssp-power-two')?.addEventListener('change', updatePacker);
230
+ document.getElementById('ssp-btn-download-zip')?.addEventListener('click', (e) => downloadZipPackage(e));
231
+ document.getElementById('ssp-btn-clear')?.addEventListener('click', (e) => {
232
+ loadedImages = [];
233
+ updatePacker();
234
+ spawnParticle(e.clientX, e.clientY, 'CLEARED!');
235
+ });
236
+ }
237
+
238
+ export function initSpriteSheetPackerApp(): void {
239
+ bindTabs();
240
+ bindPresets();
241
+ bindDropzone(loadedImages, updatePacker);
242
+ bindActionButtons();
243
+ bindFlipbook();
244
+ initExtractor();
245
+ startFlipbookLoop(loadedImages);
246
+ renderAtlasCanvas();
247
+ updateExtractor();
248
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { bibliographyEntries } from './bibliography';
4
+ ---
5
+
6
+ <SharedBibliography links={bibliographyEntries} />
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliographyEntries: BibliographyEntry[] = [
4
+ {
5
+ name: 'Godot Engine 2D Sprite Sheets Documentation',
6
+ url: 'https://docs.godotengine.org/en/stable/tutorials/2d/2d_sprite_animation.html',
7
+ },
8
+ {
9
+ name: 'Unity Sprite Atlas Manual',
10
+ url: 'https://docs.unity3d.com/Manual/class-SpriteAtlas.html',
11
+ },
12
+ ];