@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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/tests/locale_completeness.test.ts +1 -20
- package/src/tests/tool_validation.test.ts +2 -3
- package/src/tool/itchioGameTester/client.ts +12 -23
- package/src/tool/itchioGameTester/component.astro +0 -7
- package/src/tool/itchioGameTester/i18n/de.ts +0 -2
- package/src/tool/itchioGameTester/i18n/en.ts +0 -2
- package/src/tool/itchioGameTester/i18n/es.ts +0 -2
- package/src/tool/itchioGameTester/i18n/fr.ts +0 -2
- package/src/tool/itchioGameTester/i18n/id.ts +0 -2
- package/src/tool/itchioGameTester/i18n/it.ts +0 -2
- package/src/tool/itchioGameTester/i18n/ja.ts +0 -2
- package/src/tool/itchioGameTester/i18n/ko.ts +0 -2
- package/src/tool/itchioGameTester/i18n/nl.ts +0 -2
- package/src/tool/itchioGameTester/i18n/pl.ts +0 -2
- package/src/tool/itchioGameTester/i18n/pt.ts +0 -2
- package/src/tool/itchioGameTester/i18n/ru.ts +0 -2
- package/src/tool/itchioGameTester/i18n/sv.ts +0 -2
- package/src/tool/itchioGameTester/i18n/tr.ts +0 -2
- package/src/tool/itchioGameTester/i18n/zh.ts +0 -2
- package/src/tool/itchioGameTester/ui.ts +0 -2
- package/src/tool/spriteSheetPacker/app-client.ts +248 -0
- package/src/tool/spriteSheetPacker/bibliography.astro +6 -0
- package/src/tool/spriteSheetPacker/bibliography.ts +12 -0
- package/src/tool/spriteSheetPacker/component.astro +229 -0
- package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -0
- package/src/tool/spriteSheetPacker/entry.ts +28 -0
- package/src/tool/spriteSheetPacker/exporter.ts +116 -0
- package/src/tool/spriteSheetPacker/extractor-client.ts +127 -0
- package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -0
- package/src/tool/spriteSheetPacker/i18n/de.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/en.ts +275 -0
- package/src/tool/spriteSheetPacker/i18n/es.ts +275 -0
- package/src/tool/spriteSheetPacker/i18n/fr.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/id.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/it.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/ja.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/ko.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/nl.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/pl.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/pt.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/ru.ts +266 -0
- package/src/tool/spriteSheetPacker/i18n/sv.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/tr.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/zh.ts +270 -0
- package/src/tool/spriteSheetPacker/index.ts +11 -0
- package/src/tool/spriteSheetPacker/logic.test.ts +155 -0
- package/src/tool/spriteSheetPacker/logic.ts +32 -0
- package/src/tool/spriteSheetPacker/packer-core.ts +121 -0
- package/src/tool/spriteSheetPacker/packer-ui.ts +86 -0
- package/src/tool/spriteSheetPacker/seo.astro +15 -0
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +542 -0
- package/src/tool/spriteSheetPacker/types.ts +89 -0
- package/src/tool/spriteSheetPacker/ui.ts +42 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -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(
|
|
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
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
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,
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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', () => {
|
|
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 =
|
|
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(
|
|
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,
|
|
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(
|
|
152
|
-
bindPresetEvents(
|
|
153
|
-
bindLiveDragResizer(
|
|
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嵌入设置',
|
|
@@ -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,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
|
+
];
|