@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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SpriteSheetPackerUI } from './ui';
|
|
3
|
+
import './sprite-sheet-packer.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: SpriteSheetPackerUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="ssp-container">
|
|
13
|
+
<div class="ssp-main-card">
|
|
14
|
+
<div class="ssp-header-tabs" role="tablist">
|
|
15
|
+
<button id="ssp-tab-packer" class="ssp-tab-btn active" role="tab" aria-selected="true" aria-controls="ssp-packer-panel">
|
|
16
|
+
{ui.packerTab}
|
|
17
|
+
</button>
|
|
18
|
+
<button id="ssp-tab-extractor" class="ssp-tab-btn" role="tab" aria-selected="false" aria-controls="ssp-extractor-panel">
|
|
19
|
+
{ui.extractorTab}
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div id="ssp-packer-panel" class="ssp-body-grid" role="tabpanel" aria-labelledby="ssp-tab-packer">
|
|
24
|
+
<div class="ssp-sidebar-section">
|
|
25
|
+
<div id="ssp-dropzone" class="ssp-dropzone">
|
|
26
|
+
<div class="ssp-drop-title">{ui.dropZoneTitle}</div>
|
|
27
|
+
<div class="ssp-drop-sub">{ui.dropZoneSubtitle}</div>
|
|
28
|
+
<input type="file" id="ssp-file-input" class="ssp-file-input" multiple accept="image/png,image/jpeg,image/webp" aria-label={ui.selectFilesButton} />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="ssp-btn-row">
|
|
32
|
+
<button id="ssp-btn-select" class="ssp-btn ssp-btn-primary" type="button">{ui.selectFilesButton}</button>
|
|
33
|
+
<button id="ssp-btn-clear" class="ssp-btn ssp-btn-danger" type="button">{ui.clearAllButton}</button>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="ssp-presets-row">
|
|
37
|
+
<button id="ssp-preset-pixel" class="ssp-chip-btn" type="button">{ui.presetPixelArt}</button>
|
|
38
|
+
<button id="ssp-preset-hd" class="ssp-chip-btn" type="button">{ui.presetHdUi}</button>
|
|
39
|
+
<button id="ssp-preset-mobile" class="ssp-chip-btn" type="button">{ui.presetMobile}</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="ssp-controls-list">
|
|
43
|
+
<div class="ssp-control-group">
|
|
44
|
+
<label for="ssp-export-format" class="ssp-label"><span>{ui.exportFormatLabel}</span></label>
|
|
45
|
+
<select id="ssp-export-format" class="ssp-select">
|
|
46
|
+
<option value="generic-json-hash">{ui.formatGenericHash}</option>
|
|
47
|
+
<option value="generic-json-array">{ui.formatGenericArray}</option>
|
|
48
|
+
<option value="unity">{ui.formatUnity}</option>
|
|
49
|
+
<option value="godot">{ui.formatGodot}</option>
|
|
50
|
+
<option value="phaser">{ui.formatPhaser}</option>
|
|
51
|
+
<option value="css">{ui.formatCss}</option>
|
|
52
|
+
</select>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="ssp-control-group">
|
|
56
|
+
<label for="ssp-padding" class="ssp-label">
|
|
57
|
+
<span>{ui.paddingLabel}</span>
|
|
58
|
+
<span id="ssp-padding-val">2</span>
|
|
59
|
+
</label>
|
|
60
|
+
<div class="ssp-stepper-box">
|
|
61
|
+
<button id="ssp-pad-dec" class="ssp-step-btn" type="button" aria-label="Decrease Padding">-</button>
|
|
62
|
+
<input type="range" id="ssp-padding" class="ssp-slider" min="0" max="32" value="2" aria-label={ui.paddingLabel} />
|
|
63
|
+
<button id="ssp-pad-inc" class="ssp-step-btn" type="button" aria-label="Increase Padding">+</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="ssp-control-group">
|
|
68
|
+
<label for="ssp-extrusion" class="ssp-label">
|
|
69
|
+
<span>{ui.borderExtrusionLabel}</span>
|
|
70
|
+
<span id="ssp-extrusion-val">0</span>
|
|
71
|
+
</label>
|
|
72
|
+
<div class="ssp-stepper-box">
|
|
73
|
+
<button id="ssp-ext-dec" class="ssp-step-btn" type="button" aria-label="Decrease Extrusion">-</button>
|
|
74
|
+
<input type="range" id="ssp-extrusion" class="ssp-slider" min="0" max="8" value="0" aria-label={ui.borderExtrusionLabel} />
|
|
75
|
+
<button id="ssp-ext-inc" class="ssp-step-btn" type="button" aria-label="Increase Extrusion">+</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="ssp-control-group">
|
|
80
|
+
<label for="ssp-max-size" class="ssp-label"><span>{ui.maxTextureSizeLabel}</span></label>
|
|
81
|
+
<select id="ssp-max-size" class="ssp-select">
|
|
82
|
+
<option value="512">512 x 512 px</option>
|
|
83
|
+
<option value="1024">1024 x 1024 px</option>
|
|
84
|
+
<option value="2048" selected>2048 x 2048 px</option>
|
|
85
|
+
<option value="4096">4096 x 4096 px</option>
|
|
86
|
+
</select>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="ssp-toggle-group">
|
|
90
|
+
<label for="ssp-power-two" class="ssp-label">{ui.powerOfTwoLabel}</label>
|
|
91
|
+
<input type="checkbox" id="ssp-power-two" class="ssp-toggle-input" checked aria-label={ui.powerOfTwoLabel} />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="ssp-main-section">
|
|
97
|
+
<div class="ssp-stats-grid">
|
|
98
|
+
<div class="ssp-stat-badge">
|
|
99
|
+
<div id="ssp-stat-efficiency" class="ssp-stat-val">0%</div>
|
|
100
|
+
<div class="ssp-stat-lbl">{ui.efficiencyBadge}</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="ssp-stat-badge">
|
|
103
|
+
<div id="ssp-stat-drawcalls" class="ssp-stat-val">0</div>
|
|
104
|
+
<div class="ssp-stat-lbl">{ui.drawCallsBadge}</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="ssp-stat-badge">
|
|
107
|
+
<div id="ssp-stat-frames" class="ssp-stat-val">0</div>
|
|
108
|
+
<div class="ssp-stat-lbl">{ui.totalFramesBadge}</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="ssp-stat-badge">
|
|
111
|
+
<div id="ssp-stat-size" class="ssp-stat-val">0x0</div>
|
|
112
|
+
<div class="ssp-stat-lbl">{ui.textureSizeBadge}</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="ssp-canvas-wrapper">
|
|
117
|
+
<canvas id="ssp-atlas-canvas" class="ssp-canvas" width="512" height="512" aria-label={ui.previewTitle}></canvas>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="ssp-btn-row">
|
|
121
|
+
<button id="ssp-btn-download-zip" class="ssp-btn ssp-btn-primary" type="button">{ui.downloadZipButton}</button>
|
|
122
|
+
<button id="ssp-btn-copy-json" class="ssp-btn ssp-btn-secondary" type="button">{ui.copyJsonButton}</button>
|
|
123
|
+
<button id="ssp-btn-download-png" class="ssp-btn ssp-btn-secondary" type="button">{ui.downloadSheetPngButton}</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="ssp-flipbook-box">
|
|
127
|
+
<div class="ssp-flipbook-header">{ui.flipbookTitle}</div>
|
|
128
|
+
<div class="ssp-flipbook-body">
|
|
129
|
+
<canvas id="ssp-flipbook-canvas" class="ssp-flipbook-canvas" width="128" height="128" aria-label={ui.flipbookTitle}></canvas>
|
|
130
|
+
<div class="ssp-flipbook-controls">
|
|
131
|
+
<div class="ssp-control-group">
|
|
132
|
+
<label for="ssp-fps-slider" class="ssp-label">
|
|
133
|
+
<span>{ui.flipbookFpsLabel}</span>
|
|
134
|
+
<span id="ssp-fps-val">12 FPS</span>
|
|
135
|
+
</label>
|
|
136
|
+
<input type="range" id="ssp-fps-slider" class="ssp-slider" min="1" max="60" value="12" aria-label={ui.flipbookFpsLabel} />
|
|
137
|
+
</div>
|
|
138
|
+
<div class="ssp-btn-row">
|
|
139
|
+
<button id="ssp-btn-play" class="ssp-btn ssp-btn-secondary" type="button">{ui.playAnimation}</button>
|
|
140
|
+
<button id="ssp-btn-pause" class="ssp-btn ssp-btn-secondary" type="button">{ui.pauseAnimation}</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="ssp-code-container">
|
|
147
|
+
<div class="ssp-code-header">
|
|
148
|
+
<span class="ssp-code-title">{ui.codeSnippetTitle}</span>
|
|
149
|
+
<button id="ssp-btn-copy-code" class="ssp-copy-btn" type="button">{ui.copySnippetButton}</button>
|
|
150
|
+
</div>
|
|
151
|
+
<pre id="ssp-code-snippet" class="ssp-code-block"></pre>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div id="ssp-extractor-panel" class="ssp-body-grid" role="tabpanel" aria-labelledby="ssp-tab-extractor" style="display: none;">
|
|
157
|
+
<div class="ssp-sidebar-section">
|
|
158
|
+
<div id="ssp-ext-dropzone" class="ssp-dropzone">
|
|
159
|
+
<div class="ssp-drop-title">{ui.dropZoneTitle}</div>
|
|
160
|
+
<div class="ssp-drop-sub">{ui.dropZoneSubtitle}</div>
|
|
161
|
+
<input type="file" id="ssp-ext-file-input" class="ssp-file-input" accept="image/png,image/jpeg,image/webp" aria-label={ui.selectFilesButton} />
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="ssp-controls-list">
|
|
165
|
+
<div class="ssp-control-group">
|
|
166
|
+
<label for="ssp-ext-width" class="ssp-label">{ui.frameWidthLabel}</label>
|
|
167
|
+
<div class="ssp-stepper-box">
|
|
168
|
+
<button id="ssp-fw-dec" class="ssp-step-btn" type="button" aria-label="Decrease Frame Width">-</button>
|
|
169
|
+
<input type="number" id="ssp-ext-width" class="ssp-input" value="32" min="1" aria-label={ui.frameWidthLabel} />
|
|
170
|
+
<button id="ssp-fw-inc" class="ssp-step-btn" type="button" aria-label="Increase Frame Width">+</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div class="ssp-control-group">
|
|
175
|
+
<label for="ssp-ext-height" class="ssp-label">{ui.frameHeightLabel}</label>
|
|
176
|
+
<div class="ssp-stepper-box">
|
|
177
|
+
<button id="ssp-fh-dec" class="ssp-step-btn" type="button" aria-label="Decrease Frame Height">-</button>
|
|
178
|
+
<input type="number" id="ssp-ext-height" class="ssp-input" value="32" min="1" aria-label={ui.frameHeightLabel} />
|
|
179
|
+
<button id="ssp-fh-inc" class="ssp-step-btn" type="button" aria-label="Increase Frame Height">+</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="ssp-control-group">
|
|
184
|
+
<label for="ssp-ext-margin" class="ssp-label">{ui.marginLabel}</label>
|
|
185
|
+
<div class="ssp-stepper-box">
|
|
186
|
+
<button id="ssp-mar-dec" class="ssp-step-btn" type="button" aria-label="Decrease Margin">-</button>
|
|
187
|
+
<input type="number" id="ssp-ext-margin" class="ssp-input" value="0" min="0" aria-label={ui.marginLabel} />
|
|
188
|
+
<button id="ssp-mar-inc" class="ssp-step-btn" type="button" aria-label="Increase Margin">+</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="ssp-control-group">
|
|
193
|
+
<label for="ssp-ext-spacing" class="ssp-label">{ui.spacingLabel}</label>
|
|
194
|
+
<div class="ssp-stepper-box">
|
|
195
|
+
<button id="ssp-spc-dec" class="ssp-step-btn" type="button" aria-label="Decrease Spacing">-</button>
|
|
196
|
+
<input type="number" id="ssp-ext-spacing" class="ssp-input" value="0" min="0" aria-label={ui.spacingLabel} />
|
|
197
|
+
<button id="ssp-spc-inc" class="ssp-step-btn" type="button" aria-label="Increase Spacing">+</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<button id="ssp-btn-extract-zip" class="ssp-btn ssp-btn-primary" type="button">{ui.downloadZipButton}</button>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="ssp-main-section">
|
|
206
|
+
<div class="ssp-stat-badge" style="align-self: flex-start;">
|
|
207
|
+
<div id="ssp-ext-count" class="ssp-stat-val">0</div>
|
|
208
|
+
<div class="ssp-stat-lbl">{ui.extractedCountLabel}</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="ssp-canvas-wrapper">
|
|
211
|
+
<canvas id="ssp-ext-canvas" class="ssp-canvas" width="512" height="512" aria-label={ui.previewTitle}></canvas>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div id="ssp-toast" class="ssp-toast">{ui.copiedToast}</div>
|
|
217
|
+
<div id="ssp-particle-container" class="ssp-particle-container"></div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<script>
|
|
222
|
+
import { initSpriteSheetPackerApp } from './app-client';
|
|
223
|
+
|
|
224
|
+
if (document.readyState === 'loading') {
|
|
225
|
+
document.addEventListener('DOMContentLoaded', initSpriteSheetPackerApp);
|
|
226
|
+
} else {
|
|
227
|
+
initSpriteSheetPackerApp();
|
|
228
|
+
}
|
|
229
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawnParticle, type LoadedImageItem } from './packer-ui';
|
|
2
|
+
|
|
3
|
+
export function handleFilesUpload(files: File[], loadedImages: LoadedImageItem[], updatePacker: () => void): void {
|
|
4
|
+
const promises = files.map((file) => {
|
|
5
|
+
return new Promise<void>((resolve) => {
|
|
6
|
+
const img = new Image();
|
|
7
|
+
const url = URL.createObjectURL(file);
|
|
8
|
+
img.onload = () => {
|
|
9
|
+
loadedImages.push({
|
|
10
|
+
file,
|
|
11
|
+
img,
|
|
12
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
13
|
+
name: file.name.replace(/\.[^/.]+$/, ''),
|
|
14
|
+
});
|
|
15
|
+
resolve();
|
|
16
|
+
};
|
|
17
|
+
img.src = url;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
Promise.all(promises).then(() => updatePacker());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function bindDropzone(loadedImages: LoadedImageItem[], updatePacker: () => void): void {
|
|
25
|
+
const dropzone = document.getElementById('ssp-dropzone');
|
|
26
|
+
const fileInput = document.getElementById('ssp-file-input') as HTMLInputElement | null;
|
|
27
|
+
|
|
28
|
+
document.getElementById('ssp-btn-select')?.addEventListener('click', () => fileInput?.click());
|
|
29
|
+
dropzone?.addEventListener('click', () => fileInput?.click());
|
|
30
|
+
|
|
31
|
+
dropzone?.addEventListener('dragover', (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
dropzone.classList.add('dragover');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
dropzone?.addEventListener('dragleave', (e) => {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
dropzone.classList.remove('dragover');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
dropzone?.addEventListener('drop', (e: DragEvent) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
dropzone.classList.remove('dragover');
|
|
44
|
+
if (e.dataTransfer?.files?.length) {
|
|
45
|
+
handleFilesUpload(Array.from(e.dataTransfer.files), loadedImages, updatePacker);
|
|
46
|
+
spawnParticle(e.clientX, e.clientY, 'PACKED!');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
fileInput?.addEventListener('change', () => {
|
|
51
|
+
if (fileInput.files?.length) {
|
|
52
|
+
handleFilesUpload(Array.from(fileInput.files), loadedImages, updatePacker);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GamesToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { SpriteSheetPackerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { SpriteSheetPackerUI };
|
|
5
|
+
|
|
6
|
+
export type SpriteSheetPackerLocaleContent = ToolLocaleContent<SpriteSheetPackerUI>;
|
|
7
|
+
|
|
8
|
+
export const spriteSheetPacker: GamesToolEntry<SpriteSheetPackerUI> = {
|
|
9
|
+
id: 'sprite-sheet-packer',
|
|
10
|
+
icons: { bg: 'mdi:gamepad-variant', fg: 'mdi:texture-box' },
|
|
11
|
+
i18n: {
|
|
12
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
13
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
14
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
15
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
16
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
17
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
18
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
19
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
20
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
21
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
22
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
23
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
24
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
25
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
26
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ExportFormat, PackedFrame } from './types';
|
|
2
|
+
|
|
3
|
+
function buildGenericHashJson(frames: PackedFrame[], width: number, height: number): string {
|
|
4
|
+
const frameHash: Record<string, unknown> = {};
|
|
5
|
+
for (const f of frames) {
|
|
6
|
+
frameHash[f.name] = {
|
|
7
|
+
frame: { x: f.x, y: f.y, w: f.width, h: f.height },
|
|
8
|
+
rotated: f.rotated,
|
|
9
|
+
trimmed: f.trimmed,
|
|
10
|
+
spriteSourceSize: f.spriteSourceSize,
|
|
11
|
+
sourceSize: f.sourceSize,
|
|
12
|
+
pivot: f.pivot,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
frames: frameHash,
|
|
18
|
+
meta: { app: 'GameBob Sprite Sheet Packer', version: '1.0', size: { w: width, h: height }, scale: 1 },
|
|
19
|
+
},
|
|
20
|
+
null,
|
|
21
|
+
2
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildGenericArrayJson(frames: PackedFrame[], width: number, height: number): string {
|
|
26
|
+
const frameArray = frames.map((f) => ({
|
|
27
|
+
filename: f.name,
|
|
28
|
+
frame: { x: f.x, y: f.y, w: f.width, h: f.height },
|
|
29
|
+
rotated: f.rotated,
|
|
30
|
+
trimmed: f.trimmed,
|
|
31
|
+
spriteSourceSize: f.spriteSourceSize,
|
|
32
|
+
sourceSize: f.sourceSize,
|
|
33
|
+
pivot: f.pivot,
|
|
34
|
+
}));
|
|
35
|
+
return JSON.stringify(
|
|
36
|
+
{
|
|
37
|
+
frames: frameArray,
|
|
38
|
+
meta: { app: 'GameBob Sprite Sheet Packer', version: '1.0', size: { w: width, h: height }, scale: 1 },
|
|
39
|
+
},
|
|
40
|
+
null,
|
|
41
|
+
2
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildGodotJson(frames: PackedFrame[], width: number, height: number): string {
|
|
46
|
+
const godotData = {
|
|
47
|
+
textures: [
|
|
48
|
+
{
|
|
49
|
+
image: 'spritesheet.png',
|
|
50
|
+
size: { w: width, h: height },
|
|
51
|
+
sprites: frames.map((f) => ({
|
|
52
|
+
name: f.name,
|
|
53
|
+
region: [f.x, f.y, f.width, f.height],
|
|
54
|
+
margin: [f.spriteSourceSize.x, f.spriteSourceSize.y, f.sourceSize.w, f.sourceSize.h],
|
|
55
|
+
})),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
return JSON.stringify(godotData, null, 2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildUnityJson(frames: PackedFrame[], width: number, height: number): string {
|
|
63
|
+
const unityFrames = frames.map((f) => ({
|
|
64
|
+
name: f.name,
|
|
65
|
+
x: f.x,
|
|
66
|
+
y: f.y,
|
|
67
|
+
width: f.width,
|
|
68
|
+
height: f.height,
|
|
69
|
+
pivotX: f.pivot.x,
|
|
70
|
+
pivotY: f.pivot.y,
|
|
71
|
+
}));
|
|
72
|
+
return JSON.stringify({ sprites: unityFrames, meta: { width, height } }, null, 2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function generateAtlasJson(
|
|
76
|
+
frames: PackedFrame[],
|
|
77
|
+
width: number,
|
|
78
|
+
height: number,
|
|
79
|
+
format: ExportFormat
|
|
80
|
+
): string {
|
|
81
|
+
if (format === 'generic-json-hash' || format === 'phaser') {
|
|
82
|
+
return buildGenericHashJson(frames, width, height);
|
|
83
|
+
}
|
|
84
|
+
if (format === 'generic-json-array') {
|
|
85
|
+
return buildGenericArrayJson(frames, width, height);
|
|
86
|
+
}
|
|
87
|
+
if (format === 'godot') {
|
|
88
|
+
return buildGodotJson(frames, width, height);
|
|
89
|
+
}
|
|
90
|
+
if (format === 'unity') {
|
|
91
|
+
return buildUnityJson(frames, width, height);
|
|
92
|
+
}
|
|
93
|
+
return JSON.stringify({ frames, meta: { width, height } }, null, 2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function generateCssSnippet(frames: PackedFrame[]): string {
|
|
97
|
+
return frames
|
|
98
|
+
.map(
|
|
99
|
+
(f) =>
|
|
100
|
+
`.sprite-${f.name.replace(/[^a-zA-Z0-9_-]/g, '_')} {\n width: ${f.width}px;\n height: ${f.height}px;\n background-position: -${f.x}px -${f.y}px;\n}`
|
|
101
|
+
)
|
|
102
|
+
.join('\n\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function generateEngineCodeSnippet(format: ExportFormat): string {
|
|
106
|
+
if (format === 'unity') {
|
|
107
|
+
return `// Unity C# Load Example\nSprite[] sprites = Resources.LoadAll<Sprite>("spritesheet");\nSprite myFrame = System.Array.Find(sprites, s => s.name == "frame_0");`;
|
|
108
|
+
}
|
|
109
|
+
if (format === 'godot') {
|
|
110
|
+
return `# Godot GDScript Load Example\nvar atlas_texture = AtlasTexture.new()\natlas_texture.atlas = load("res://spritesheet.png")\natlas_texture.region = Rect2(0, 0, 32, 32)`;
|
|
111
|
+
}
|
|
112
|
+
if (format === 'phaser') {
|
|
113
|
+
return `// Phaser 3 Load Example\nthis.load.atlas('sheetKey', 'spritesheet.png', 'spritesheet.json');\nconst sprite = this.add.sprite(400, 300, 'sheetKey', 'frame_0');`;
|
|
114
|
+
}
|
|
115
|
+
return `// HTML / JS Load Example\nconst image = new Image();\nimage.src = 'spritesheet.png';`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { calculateGridSlices } from './logic';
|
|
3
|
+
import { drawEmptyCanvasMessage, setupStepper, triggerButtonFeedback } from './packer-ui';
|
|
4
|
+
|
|
5
|
+
let extImage: HTMLImageElement | null = null;
|
|
6
|
+
|
|
7
|
+
function parseInputValue(id: string, fallback: string): number {
|
|
8
|
+
const el = document.getElementById(id) as HTMLInputElement | null;
|
|
9
|
+
return parseInt(el?.value || fallback, 10);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getExtractorInputs() {
|
|
13
|
+
return {
|
|
14
|
+
fw: parseInputValue('ssp-ext-width', '32'),
|
|
15
|
+
fh: parseInputValue('ssp-ext-height', '32'),
|
|
16
|
+
margin: parseInputValue('ssp-ext-margin', '0'),
|
|
17
|
+
spacing: parseInputValue('ssp-ext-spacing', '0'),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function updateExtractor(): void {
|
|
22
|
+
const canvas = document.getElementById('ssp-ext-canvas') as HTMLCanvasElement | null;
|
|
23
|
+
if (!canvas) return;
|
|
24
|
+
|
|
25
|
+
if (!extImage) {
|
|
26
|
+
drawEmptyCanvasMessage(canvas, 'Upload sprite sheet to slice frames');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
canvas.width = extImage.width;
|
|
31
|
+
canvas.height = extImage.height;
|
|
32
|
+
|
|
33
|
+
const ctx = canvas.getContext('2d');
|
|
34
|
+
if (!ctx) return;
|
|
35
|
+
|
|
36
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
37
|
+
ctx.drawImage(extImage, 0, 0);
|
|
38
|
+
|
|
39
|
+
const { fw, fh, margin, spacing } = getExtractorInputs();
|
|
40
|
+
const slices = calculateGridSlices({
|
|
41
|
+
imageWidth: extImage.width,
|
|
42
|
+
imageHeight: extImage.height,
|
|
43
|
+
frameWidth: fw,
|
|
44
|
+
frameHeight: fh,
|
|
45
|
+
margin,
|
|
46
|
+
spacing,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const countEl = document.getElementById('ssp-ext-count');
|
|
50
|
+
if (countEl) countEl.textContent = slices.length.toString();
|
|
51
|
+
|
|
52
|
+
ctx.strokeStyle = '#38bdf8';
|
|
53
|
+
ctx.lineWidth = 1;
|
|
54
|
+
for (const slice of slices) {
|
|
55
|
+
ctx.strokeRect(slice.x, slice.y, slice.width, slice.height);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function processExtractedZip(e: MouseEvent, fw: number, fh: number, slices: ReturnType<typeof calculateGridSlices>) {
|
|
60
|
+
triggerButtonFeedback(document.getElementById('ssp-btn-extract-zip'), 'SPLIT ZIP!', e);
|
|
61
|
+
|
|
62
|
+
const zip = new JSZip();
|
|
63
|
+
const tempCanvas = document.createElement('canvas');
|
|
64
|
+
tempCanvas.width = fw;
|
|
65
|
+
tempCanvas.height = fh;
|
|
66
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
67
|
+
if (!tempCtx || !extImage) return;
|
|
68
|
+
|
|
69
|
+
slices.forEach((slice) => {
|
|
70
|
+
tempCtx.clearRect(0, 0, fw, fh);
|
|
71
|
+
tempCtx.drawImage(extImage!, slice.x, slice.y, slice.width, slice.height, 0, 0, fw, fh);
|
|
72
|
+
const dataUrl = tempCanvas.toDataURL('image/png').split(',')[1] ?? '';
|
|
73
|
+
zip.file(`frame_${slice.index}.png`, dataUrl, { base64: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
zip.generateAsync({ type: 'blob' }).then((content) => {
|
|
77
|
+
const link = document.createElement('a');
|
|
78
|
+
link.href = URL.createObjectURL(content);
|
|
79
|
+
link.download = 'extracted-sprites.zip';
|
|
80
|
+
link.click();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function downloadExtractedZip(e: MouseEvent): void {
|
|
85
|
+
if (!extImage) return;
|
|
86
|
+
const { fw, fh, margin, spacing } = getExtractorInputs();
|
|
87
|
+
const slices = calculateGridSlices({
|
|
88
|
+
imageWidth: extImage.width,
|
|
89
|
+
imageHeight: extImage.height,
|
|
90
|
+
frameWidth: fw,
|
|
91
|
+
frameHeight: fh,
|
|
92
|
+
margin,
|
|
93
|
+
spacing,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (slices.length === 0) return;
|
|
97
|
+
processExtractedZip(e, fw, fh, slices);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function initExtractor(): void {
|
|
101
|
+
const extDrop = document.getElementById('ssp-ext-dropzone');
|
|
102
|
+
const extInput = document.getElementById('ssp-ext-file-input') as HTMLInputElement | null;
|
|
103
|
+
|
|
104
|
+
extDrop?.addEventListener('click', () => extInput?.click());
|
|
105
|
+
extInput?.addEventListener('change', () => {
|
|
106
|
+
if (extInput.files && extInput.files[0]) {
|
|
107
|
+
const file = extInput.files[0];
|
|
108
|
+
const img = new Image();
|
|
109
|
+
img.onload = () => {
|
|
110
|
+
extImage = img;
|
|
111
|
+
updateExtractor();
|
|
112
|
+
};
|
|
113
|
+
img.src = URL.createObjectURL(file);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
setupStepper('ssp-fw-dec', 'ssp-fw-inc', 'ssp-ext-width', updateExtractor);
|
|
118
|
+
setupStepper('ssp-fh-dec', 'ssp-fh-inc', 'ssp-ext-height', updateExtractor);
|
|
119
|
+
setupStepper('ssp-mar-dec', 'ssp-mar-inc', 'ssp-ext-margin', updateExtractor);
|
|
120
|
+
setupStepper('ssp-spc-dec', 'ssp-spc-inc', 'ssp-ext-spacing', updateExtractor);
|
|
121
|
+
|
|
122
|
+
['ssp-ext-width', 'ssp-ext-height', 'ssp-ext-margin', 'ssp-ext-spacing'].forEach((id) => {
|
|
123
|
+
document.getElementById(id)?.addEventListener('input', updateExtractor);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
document.getElementById('ssp-btn-extract-zip')?.addEventListener('click', (e) => downloadExtractedZip(e));
|
|
127
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawnParticle, type LoadedImageItem } from './packer-ui';
|
|
2
|
+
|
|
3
|
+
let currentAnimFrameIndex = 0;
|
|
4
|
+
let animFps = 12;
|
|
5
|
+
let isPlayingAnim = true;
|
|
6
|
+
let lastFrameTime = 0;
|
|
7
|
+
|
|
8
|
+
export function renderFlipbookFrame(loadedImages: LoadedImageItem[]): void {
|
|
9
|
+
const canvas = document.getElementById('ssp-flipbook-canvas') as HTMLCanvasElement | null;
|
|
10
|
+
if (!canvas || loadedImages.length === 0) return;
|
|
11
|
+
|
|
12
|
+
const ctx = canvas.getContext('2d');
|
|
13
|
+
if (!ctx) return;
|
|
14
|
+
|
|
15
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
16
|
+
const item = loadedImages[currentAnimFrameIndex % loadedImages.length];
|
|
17
|
+
|
|
18
|
+
if (item && item.img) {
|
|
19
|
+
const scale = Math.min(canvas.width / item.img.width, canvas.height / item.img.height);
|
|
20
|
+
const drawW = item.img.width * scale;
|
|
21
|
+
const drawH = item.img.height * scale;
|
|
22
|
+
const drawX = (canvas.width - drawW) / 2;
|
|
23
|
+
const drawY = (canvas.height - drawH) / 2;
|
|
24
|
+
ctx.drawImage(item.img, drawX, drawY, drawW, drawH);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function startFlipbookLoop(loadedImages: LoadedImageItem[]): void {
|
|
29
|
+
function loop(timestamp: number) {
|
|
30
|
+
if (isPlayingAnim && loadedImages.length > 0) {
|
|
31
|
+
const interval = 1000 / animFps;
|
|
32
|
+
if (timestamp - lastFrameTime > interval) {
|
|
33
|
+
currentAnimFrameIndex = (currentAnimFrameIndex + 1) % loadedImages.length;
|
|
34
|
+
lastFrameTime = timestamp;
|
|
35
|
+
renderFlipbookFrame(loadedImages);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
requestAnimationFrame(loop);
|
|
39
|
+
}
|
|
40
|
+
requestAnimationFrame(loop);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function bindFlipbook(): void {
|
|
44
|
+
const fpsSlider = document.getElementById('ssp-fps-slider') as HTMLInputElement | null;
|
|
45
|
+
fpsSlider?.addEventListener('input', () => {
|
|
46
|
+
animFps = parseInt(fpsSlider.value, 10);
|
|
47
|
+
const valEl = document.getElementById('ssp-fps-val');
|
|
48
|
+
if (valEl) valEl.textContent = `${animFps} FPS`;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
document.getElementById('ssp-btn-play')?.addEventListener('click', (e) => {
|
|
52
|
+
isPlayingAnim = true;
|
|
53
|
+
spawnParticle(e.clientX, e.clientY, 'PLAYING');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
document.getElementById('ssp-btn-pause')?.addEventListener('click', (e) => {
|
|
57
|
+
isPlayingAnim = false;
|
|
58
|
+
spawnParticle(e.clientX, e.clientY, 'PAUSED');
|
|
59
|
+
});
|
|
60
|
+
}
|