@jjlmoya/utils-games-development 1.49.0 → 1.51.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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +5 -1
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/audioLoopPointFinder/audio-loop-point-finder.css +322 -0
  7. package/src/tool/audioLoopPointFinder/audio-player.ts +84 -0
  8. package/src/tool/audioLoopPointFinder/bibliography.astro +6 -0
  9. package/src/tool/audioLoopPointFinder/bibliography.ts +16 -0
  10. package/src/tool/audioLoopPointFinder/client.ts +262 -0
  11. package/src/tool/audioLoopPointFinder/component.astro +147 -0
  12. package/src/tool/audioLoopPointFinder/dom-utils.ts +55 -0
  13. package/src/tool/audioLoopPointFinder/entry.ts +27 -0
  14. package/src/tool/audioLoopPointFinder/i18n/de.ts +211 -0
  15. package/src/tool/audioLoopPointFinder/i18n/en.ts +211 -0
  16. package/src/tool/audioLoopPointFinder/i18n/es.ts +211 -0
  17. package/src/tool/audioLoopPointFinder/i18n/fr.ts +211 -0
  18. package/src/tool/audioLoopPointFinder/i18n/id.ts +211 -0
  19. package/src/tool/audioLoopPointFinder/i18n/it.ts +211 -0
  20. package/src/tool/audioLoopPointFinder/i18n/ja.ts +211 -0
  21. package/src/tool/audioLoopPointFinder/i18n/ko.ts +211 -0
  22. package/src/tool/audioLoopPointFinder/i18n/nl.ts +211 -0
  23. package/src/tool/audioLoopPointFinder/i18n/pl.ts +211 -0
  24. package/src/tool/audioLoopPointFinder/i18n/pt.ts +211 -0
  25. package/src/tool/audioLoopPointFinder/i18n/ru.ts +211 -0
  26. package/src/tool/audioLoopPointFinder/i18n/sv.ts +211 -0
  27. package/src/tool/audioLoopPointFinder/i18n/tr.ts +211 -0
  28. package/src/tool/audioLoopPointFinder/i18n/zh.ts +211 -0
  29. package/src/tool/audioLoopPointFinder/index.ts +11 -0
  30. package/src/tool/audioLoopPointFinder/logic.test.ts +72 -0
  31. package/src/tool/audioLoopPointFinder/logic.ts +214 -0
  32. package/src/tool/audioLoopPointFinder/metadata-parser.ts +94 -0
  33. package/src/tool/audioLoopPointFinder/seo.astro +15 -0
  34. package/src/tool/audioLoopPointFinder/ui.ts +42 -0
  35. package/src/tool/audioLoopPointFinder/waveform-renderer.ts +53 -0
  36. package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +55 -19
  37. package/src/tools.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-games-development",
3
- "version": "1.49.0",
3
+ "version": "1.51.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,10 +1,11 @@
1
1
  import type { GamesCategoryEntry } from '../types';
2
2
  import { steamCapsuleGenerator } from '../tool/steamCapsuleGenerator/entry';
3
3
  import { spriteSheetPacker } from '../tool/spriteSheetPacker/entry';
4
+ import { audioLoopPointFinder } from '../tool/audioLoopPointFinder/entry';
4
5
 
5
6
  export const gamesCategory: GamesCategoryEntry = {
6
7
  icon: 'mdi:gamepad-variant',
7
- tools: [steamCapsuleGenerator, spriteSheetPacker],
8
+ tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder],
8
9
  i18n: {
9
10
  de: () => import('./i18n/de').then((m) => m.content),
10
11
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -4,9 +4,13 @@ export { itchioGameTester } from './tool/itchioGameTester/entry';
4
4
  export type { ItchioGameTesterUI, ItchioGameTesterLocaleContent } from './tool/itchioGameTester/entry';
5
5
  export { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
6
6
  export type { SpriteSheetPackerUI, SpriteSheetPackerLocaleContent } from './tool/spriteSheetPacker/entry';
7
+ export { audioLoopPointFinder } from './tool/audioLoopPointFinder/entry';
8
+ export type { AudioLoopPointFinderUI, AudioLoopPointFinderLocaleContent } from './tool/audioLoopPointFinder/entry';
7
9
  export { gamesCategory } from './category';
8
10
  import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
9
11
  import { itchioGameTester } from './tool/itchioGameTester/entry';
10
12
  import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
13
+ import { audioLoopPointFinder } from './tool/audioLoopPointFinder/entry';
14
+
15
+ export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder];
11
16
 
12
- export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker];
@@ -3,6 +3,6 @@ import { ALL_TOOLS } from '../tools';
3
3
 
4
4
  describe('Locale Completeness Validation', () => {
5
5
  it('one tool is registered', () => {
6
- expect(ALL_TOOLS.length).toBe(3);
6
+ expect(ALL_TOOLS.length).toBe(4);
7
7
  });
8
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 3 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(3);
7
+ it('should have 4 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(4);
9
9
  });
10
10
 
11
11
  it('gamesCategory should be defined', () => {
@@ -0,0 +1,322 @@
1
+ .alpf-main-card {
2
+ background: var(--bg-surface, #fff);
3
+ color: var(--text-base, #0f172a);
4
+ border: 1px solid var(--border-base, #e2e8f0);
5
+ border-radius: 1.25rem;
6
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01);
7
+ padding: 1.75rem;
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 1.5rem;
11
+ position: relative;
12
+ overflow: hidden;
13
+ transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
14
+ }
15
+
16
+ .theme-dark .alpf-main-card {
17
+ background: var(--bg-surface, #0f172a);
18
+ color: var(--text-base, #f8fafc);
19
+ border-color: var(--border-base, #1e293b);
20
+ box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.4);
21
+ }
22
+
23
+ .alpf-dropzone {
24
+ border: 2px dashed var(--border-base, #cbd5e1);
25
+ border-radius: 1rem;
26
+ padding: 2.5rem 1.5rem;
27
+ text-align: center;
28
+ background: rgba(241, 245, 249, 0.4);
29
+ cursor: pointer;
30
+ transition: all 0.2s ease;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: center;
34
+ gap: 0.75rem;
35
+ }
36
+
37
+ .theme-dark .alpf-dropzone {
38
+ border-color: var(--border-base, #334155);
39
+ background: rgba(30, 41, 59, 0.3);
40
+ }
41
+
42
+ .alpf-dropzone:hover,
43
+ .alpf-dropzone.drag-over {
44
+ border-color: var(--accent-base, #6366f1);
45
+ background: rgba(99, 102, 241, 0.06);
46
+ transform: translateY(-2px);
47
+ }
48
+
49
+ .alpf-dropzone-icon {
50
+ width: 3rem;
51
+ height: 3rem;
52
+ color: var(--accent-base, #6366f1);
53
+ }
54
+
55
+ .alpf-dropzone-title {
56
+ font-size: 1.125rem;
57
+ font-weight: 600;
58
+ }
59
+
60
+ .alpf-dropzone-subtitle {
61
+ font-size: 0.875rem;
62
+ opacity: 0.7;
63
+ }
64
+
65
+ .alpf-file-input {
66
+ display: none;
67
+ }
68
+
69
+ .alpf-btn {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ gap: 0.5rem;
74
+ padding: 0.625rem 1.25rem;
75
+ font-weight: 600;
76
+ font-size: 0.9375rem;
77
+ border-radius: 0.625rem;
78
+ border: none;
79
+ cursor: pointer;
80
+ transition: all 0.15s ease;
81
+ user-select: none;
82
+ }
83
+
84
+ .alpf-btn-primary {
85
+ background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
86
+ color: var(--text-inverse, #fff);
87
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
88
+ }
89
+
90
+ .alpf-btn-primary:hover {
91
+ background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
92
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
93
+ transform: translateY(-1px);
94
+ }
95
+
96
+ .alpf-btn-primary:active {
97
+ transform: translateY(0);
98
+ }
99
+
100
+ .alpf-btn-secondary {
101
+ background: var(--bg-muted, #f1f5f9);
102
+ color: var(--text-base, #334155);
103
+ border: 1px solid var(--border-base, #cbd5e1);
104
+ }
105
+
106
+ .theme-dark .alpf-btn-secondary {
107
+ background: var(--bg-muted, #1e293b);
108
+ color: var(--text-base, #e2e8f0);
109
+ border-color: var(--border-base, #334155);
110
+ }
111
+
112
+ .alpf-btn-secondary:hover {
113
+ border-color: var(--accent-base, #6366f1);
114
+ color: var(--accent-base, #6366f1);
115
+ }
116
+
117
+ .alpf-btn-success {
118
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
119
+ color: var(--text-inverse, #fff);
120
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
121
+ }
122
+
123
+ .alpf-btn-success:hover {
124
+ background: linear-gradient(135deg, #059669 0%, #047857 100%);
125
+ box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
126
+ transform: translateY(-1px);
127
+ }
128
+
129
+ .alpf-workspace {
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 1.5rem;
133
+ }
134
+
135
+ .alpf-stats-grid {
136
+ display: grid;
137
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
138
+ gap: 1rem;
139
+ }
140
+
141
+ .alpf-stat-card {
142
+ background: var(--bg-muted, #f8fafc);
143
+ border: 1px solid var(--border-base, #e2e8f0);
144
+ border-radius: 0.75rem;
145
+ padding: 0.875rem 1rem;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 0.25rem;
149
+ }
150
+
151
+ .theme-dark .alpf-stat-card {
152
+ background: var(--bg-muted, #1e293b);
153
+ border-color: var(--border-base, #334155);
154
+ }
155
+
156
+ .alpf-stat-label {
157
+ font-size: 0.75rem;
158
+ text-transform: uppercase;
159
+ letter-spacing: 0.05em;
160
+ opacity: 0.7;
161
+ }
162
+
163
+ .alpf-stat-value {
164
+ font-size: 1.125rem;
165
+ font-weight: 700;
166
+ color: var(--accent-base, #6366f1);
167
+ }
168
+
169
+ .theme-dark .alpf-stat-value {
170
+ color: var(--accent-light, #818cf8);
171
+ }
172
+
173
+ .alpf-waveform-container {
174
+ position: relative;
175
+ background: #020617;
176
+ border-radius: 0.875rem;
177
+ border: 1px solid var(--border-base, #334155);
178
+ padding: 0.5rem;
179
+ overflow: hidden;
180
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5);
181
+ }
182
+
183
+ .alpf-waveform-canvas {
184
+ width: 100%;
185
+ height: 180px;
186
+ display: block;
187
+ cursor: crosshair;
188
+ }
189
+
190
+ .alpf-toolbar {
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ align-items: center;
194
+ justify-content: space-between;
195
+ gap: 1rem;
196
+ padding: 0.75rem;
197
+ background: var(--bg-muted, #f8fafc);
198
+ border: 1px solid var(--border-base, #e2e8f0);
199
+ border-radius: 0.75rem;
200
+ }
201
+
202
+ .theme-dark .alpf-toolbar {
203
+ background: var(--bg-muted, #1e293b);
204
+ border-color: var(--border-base, #334155);
205
+ }
206
+
207
+ .alpf-controls-group {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 0.75rem;
211
+ flex-wrap: wrap;
212
+ }
213
+
214
+ .alpf-marker-inputs {
215
+ display: grid;
216
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
217
+ gap: 1.25rem;
218
+ }
219
+
220
+ .alpf-input-field {
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 0.375rem;
224
+ }
225
+
226
+ .alpf-label {
227
+ font-size: 0.875rem;
228
+ font-weight: 600;
229
+ }
230
+
231
+ .alpf-input-row {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 0.5rem;
235
+ }
236
+
237
+ .alpf-input {
238
+ width: 100%;
239
+ padding: 0.5rem 0.75rem;
240
+ border-radius: 0.5rem;
241
+ border: 1px solid var(--border-base, #cbd5e1);
242
+ background: var(--bg-surface, #fff);
243
+ color: var(--text-base, #0f172a);
244
+ font-size: 0.9375rem;
245
+ }
246
+
247
+ .theme-dark .alpf-input {
248
+ border-color: var(--border-base, #334155);
249
+ background: var(--bg-surface, #0f172a);
250
+ color: var(--text-base, #f8fafc);
251
+ }
252
+
253
+ .alpf-input:focus {
254
+ outline: none;
255
+ border-color: var(--accent-base, #6366f1);
256
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
257
+ }
258
+
259
+ .alpf-preset-chips {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 0.5rem;
263
+ flex-wrap: wrap;
264
+ }
265
+
266
+ .alpf-chip {
267
+ padding: 0.375rem 0.75rem;
268
+ border-radius: 9999px;
269
+ font-size: 0.8125rem;
270
+ font-weight: 600;
271
+ border: 1px solid var(--border-base, #cbd5e1);
272
+ background: var(--bg-surface, #fff);
273
+ color: var(--text-base, #475569);
274
+ cursor: pointer;
275
+ transition: all 0.15s ease;
276
+ }
277
+
278
+ .theme-dark .alpf-chip {
279
+ border-color: var(--border-base, #334155);
280
+ background: var(--bg-surface, #1e293b);
281
+ color: var(--text-base, #94a3b8);
282
+ }
283
+
284
+ .alpf-chip:hover {
285
+ border-color: var(--accent-base, #6366f1);
286
+ color: var(--accent-base, #6366f1);
287
+ transform: translateY(-1px);
288
+ }
289
+
290
+ .alpf-status-bar {
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: space-between;
294
+ font-size: 0.875rem;
295
+ padding: 0.5rem 0.75rem;
296
+ border-radius: 0.5rem;
297
+ background: var(--bg-muted, #f1f5f9);
298
+ }
299
+
300
+ .theme-dark .alpf-status-bar {
301
+ background: var(--bg-muted, #1e293b);
302
+ }
303
+
304
+ .alpf-particle {
305
+ position: absolute;
306
+ pointer-events: none;
307
+ font-weight: bold;
308
+ font-size: 0.875rem;
309
+ color: var(--accent-base, #6366f1);
310
+ animation: alpf-particle-float 0.8s ease-out forwards;
311
+ }
312
+
313
+ @keyframes alpf-particle-float {
314
+ 0% {
315
+ opacity: 1;
316
+ transform: translate(0, 0) scale(1);
317
+ }
318
+ 100% {
319
+ opacity: 0;
320
+ transform: translate(0, -30px) scale(1.3);
321
+ }
322
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ samplesToSeconds,
3
+ validateLoopPoints,
4
+ createWavWithLoopMetadata,
5
+ } from './logic';
6
+ import {
7
+ playBtn,
8
+ stopBtn,
9
+ statusText,
10
+ setElementText,
11
+ getI18nText,
12
+ } from './dom-utils';
13
+
14
+ let audioContext: AudioContext | null = null;
15
+ let sourceNode: AudioBufferSourceNode | null = null;
16
+
17
+ export function initAudioContext() {
18
+ if (!audioContext) {
19
+ audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
20
+ }
21
+ return audioContext;
22
+ }
23
+
24
+ export function stopLoop() {
25
+ if (sourceNode) {
26
+ try {
27
+ sourceNode.stop();
28
+ sourceNode.disconnect();
29
+ } catch {
30
+ sourceNode = null;
31
+ }
32
+ sourceNode = null;
33
+ }
34
+ if (playBtn) playBtn.style.display = 'inline-flex';
35
+ if (stopBtn) stopBtn.style.display = 'none';
36
+ setElementText(statusText, getI18nText('stopped'));
37
+ }
38
+
39
+ export function playLoop(
40
+ audioBuffer: AudioBuffer | null,
41
+ loopStartSample: number,
42
+ loopEndSample: number,
43
+ totalSamples: number
44
+ ) {
45
+ if (!audioBuffer) return;
46
+ const ctx = initAudioContext();
47
+ if (!ctx) return;
48
+
49
+ const validation = validateLoopPoints(loopStartSample, loopEndSample, totalSamples);
50
+ if (!validation.isValid) {
51
+ if (statusText && validation.error) setElementText(statusText, validation.error);
52
+ return;
53
+ }
54
+
55
+ stopLoop();
56
+ sourceNode = ctx.createBufferSource();
57
+ sourceNode.buffer = audioBuffer;
58
+ sourceNode.loop = true;
59
+ sourceNode.loopStart = samplesToSeconds(loopStartSample, audioBuffer.sampleRate);
60
+ sourceNode.loopEnd = samplesToSeconds(loopEndSample, audioBuffer.sampleRate);
61
+ sourceNode.connect(ctx.destination);
62
+ sourceNode.start(0, sourceNode.loopStart);
63
+
64
+ if (playBtn) playBtn.style.display = 'none';
65
+ if (stopBtn) stopBtn.style.display = 'inline-flex';
66
+ setElementText(statusText, getI18nText('looping'));
67
+ }
68
+
69
+ export function exportWav(
70
+ audioBuffer: AudioBuffer | null,
71
+ loopStartSample: number,
72
+ loopEndSample: number
73
+ ) {
74
+ if (!audioBuffer) return;
75
+ const wavBytes = createWavWithLoopMetadata(audioBuffer, loopStartSample, loopEndSample);
76
+ const blob = new Blob([wavBytes.buffer as ArrayBuffer], { type: 'audio/wav' });
77
+ const url = URL.createObjectURL(blob);
78
+ const a = document.createElement('a');
79
+ a.href = url;
80
+ a.download = 'looped_track_with_metadata.wav';
81
+ a.click();
82
+ URL.revokeObjectURL(url);
83
+ setElementText(statusText, getI18nText('exported'));
84
+ }
@@ -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,16 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliographyEntries: BibliographyEntry[] = [
4
+ {
5
+ name: 'W3C Web Audio API Recommendation Specification',
6
+ url: 'https://www.w3.org/TR/webaudio/',
7
+ },
8
+ {
9
+ name: 'Microsoft Multimedia Programming Interface: RIFF WAVE Format',
10
+ url: 'https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html',
11
+ },
12
+ {
13
+ name: 'Godot Engine Audio Loop Points Documentation',
14
+ url: 'https://docs.godotengine.org/en/stable/tutorials/audio/audio_streams.html',
15
+ },
16
+ ];