Nymbo commited on
Commit
c431ae8
·
verified ·
1 Parent(s): 9e092f7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +799 -19
index.html CHANGED
@@ -1,19 +1,799 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sprite Animation Viewer</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ font-family: Arial, sans-serif;
13
+ }
14
+
15
+ body {
16
+ background-color: #121212;
17
+ color: #e0e0e0;
18
+ padding: 20px;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ min-height: 100vh;
23
+ }
24
+
25
+ .container {
26
+ width: 100%;
27
+ max-width: 900px;
28
+ background-color: #1e1e1e;
29
+ border-radius: 10px;
30
+ padding: 20px;
31
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
32
+ }
33
+
34
+ h1 {
35
+ text-align: center;
36
+ margin-bottom: 20px;
37
+ color: #3498db;
38
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
39
+ }
40
+
41
+ .settings {
42
+ display: grid;
43
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
44
+ gap: 10px;
45
+ margin-bottom: 20px;
46
+ }
47
+
48
+ .form-group {
49
+ margin-bottom: 15px;
50
+ }
51
+
52
+ label {
53
+ display: block;
54
+ margin-bottom: 5px;
55
+ font-weight: bold;
56
+ color: #3498db;
57
+ }
58
+
59
+ input[type="number"], input[type="file"], input[type="range"] {
60
+ width: 100%;
61
+ padding: 8px;
62
+ border-radius: 5px;
63
+ border: 1px solid #333;
64
+ background-color: #2a2a2a;
65
+ color: #e0e0e0;
66
+ }
67
+
68
+ button {
69
+ background-color: #3498db;
70
+ color: #121212;
71
+ border: none;
72
+ padding: 10px 15px;
73
+ border-radius: 5px;
74
+ cursor: pointer;
75
+ font-weight: bold;
76
+ transition: all 0.3s;
77
+ margin-right: 10px;
78
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
79
+ }
80
+
81
+ button:hover {
82
+ background-color: #2980b9;
83
+ transform: translateY(-2px);
84
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
85
+ }
86
+
87
+ .controls {
88
+ display: flex;
89
+ justify-content: center;
90
+ align-items: center;
91
+ margin: 15px 0;
92
+ flex-wrap: wrap;
93
+ gap: 10px;
94
+ }
95
+
96
+ .canvas-container {
97
+ width: 100%;
98
+ display: flex;
99
+ justify-content: center;
100
+ margin-top: 20px;
101
+ border: 2px solid #3498db;
102
+ border-radius: 8px;
103
+ overflow: hidden;
104
+ box-shadow: 0 0 20px rgba(52, 152, 219, 0.2);
105
+ padding: 10px;
106
+ background-color: #0a0a0a;
107
+ }
108
+
109
+ canvas {
110
+ background-color: #0a0a0a;
111
+ max-width: 100%;
112
+ height: auto;
113
+ image-rendering: pixelated;
114
+ }
115
+
116
+ .sprite-info {
117
+ margin-top: 15px;
118
+ padding: 10px;
119
+ background-color: #242424;
120
+ border-radius: 5px;
121
+ border-left: 3px solid #3498db;
122
+ }
123
+
124
+ .fps-display {
125
+ margin-left: 10px;
126
+ font-weight: bold;
127
+ color: #3498db;
128
+ }
129
+
130
+ .preview-container {
131
+ margin-top: 20px;
132
+ display: flex;
133
+ flex-wrap: wrap;
134
+ gap: 10px;
135
+ justify-content: center;
136
+ }
137
+
138
+ .frame-preview {
139
+ border: 2px solid #333;
140
+ border-radius: 5px;
141
+ overflow: hidden;
142
+ cursor: pointer;
143
+ transition: transform 0.2s, border-color 0.2s;
144
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
145
+ }
146
+
147
+ .frame-preview:hover {
148
+ transform: scale(1.05);
149
+ }
150
+
151
+ .frame-preview.active {
152
+ border-color: #3498db;
153
+ box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
154
+ }
155
+
156
+ .full-sprite-preview-container {
157
+ margin-top: 15px;
158
+ padding: 15px;
159
+ background-color: #242424;
160
+ border-radius: 5px;
161
+ border-left: 3px solid #3498db;
162
+ }
163
+
164
+ .full-sprite-preview-container h3 {
165
+ margin-top: 0;
166
+ margin-bottom: 10px;
167
+ color: #3498db;
168
+ font-size: 16px;
169
+ }
170
+
171
+ .preview-area {
172
+ width: 100%;
173
+ overflow-x: auto;
174
+ overflow-y: auto;
175
+ background-color: #1a1a1a;
176
+ border-radius: 4px;
177
+ padding: 10px;
178
+ max-height: 200px;
179
+ display: flex;
180
+ justify-content: center;
181
+ align-items: center;
182
+ }
183
+
184
+ .preview-area img {
185
+ max-width: 100%;
186
+ max-height: 180px;
187
+ object-fit: contain;
188
+ image-rendering: pixelated;
189
+ }
190
+
191
+ .no-sprite-message {
192
+ color: #666;
193
+ text-align: center;
194
+ font-style: italic;
195
+ }
196
+
197
+ select.row-select {
198
+ width: 100%;
199
+ padding: 8px;
200
+ border-radius: 5px;
201
+ border: 1px solid #333;
202
+ background-color: #2a2a2a;
203
+ color: #e0e0e0;
204
+ cursor: pointer;
205
+ }
206
+
207
+ select.row-select:focus {
208
+ border-color: #3498db;
209
+ outline: none;
210
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3);
211
+ }
212
+
213
+ input[type="range"].slider {
214
+ -webkit-appearance: none;
215
+ appearance: none;
216
+ width: 100%;
217
+ height: 6px;
218
+ border-radius: 3px;
219
+ background: #1a1a1a;
220
+ outline: none;
221
+ margin-top: 8px;
222
+ }
223
+
224
+ input[type="range"].slider::-webkit-slider-thumb {
225
+ -webkit-appearance: none;
226
+ appearance: none;
227
+ width: 16px;
228
+ height: 16px;
229
+ border-radius: 50%;
230
+ background: #3498db;
231
+ cursor: pointer;
232
+ transition: all 0.2s;
233
+ }
234
+
235
+ input[type="range"].slider::-webkit-slider-thumb:hover {
236
+ background: #2980b9;
237
+ transform: scale(1.1);
238
+ }
239
+
240
+ input[type="range"].slider::-moz-range-thumb {
241
+ width: 16px;
242
+ height: 16px;
243
+ border-radius: 50%;
244
+ background: #3498db;
245
+ cursor: pointer;
246
+ border: none;
247
+ transition: all 0.2s;
248
+ }
249
+
250
+ input[type="range"].slider::-moz-range-thumb:hover {
251
+ background: #2980b9;
252
+ transform: scale(1.1);
253
+ }
254
+
255
+ .example-desc {
256
+ font-size: 0.85em;
257
+ margin-top: 5px;
258
+ color: #777;
259
+ font-style: italic;
260
+ }
261
+
262
+ .speed-control {
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 10px;
266
+ }
267
+
268
+ .speed-control input[type="range"] {
269
+ flex: 1;
270
+ }
271
+ </style>
272
+ </head>
273
+ <body>
274
+ <div class="container">
275
+ <h1>Sprite Animation Viewer</h1>
276
+
277
+ <div class="settings">
278
+ <div class="form-group">
279
+ <label for="spriteFile">Upload Sprite Sheet:</label>
280
+ <input type="file" id="spriteFile" accept="image/png,image/jpeg">
281
+ </div>
282
+
283
+ <div class="form-group">
284
+ <button id="loadExampleBtn" aria-label="Load example sprite sheet">Load Example</button>
285
+ <p class="example-desc">Try a lotus flower blooming animation</p>
286
+ </div>
287
+
288
+ <div class="form-group">
289
+ <label for="tileWidth" id="tileWidthLabel">Tile Width (px):</label>
290
+ <input type="number" id="tileWidth" value="64" min="1" max="512">
291
+ <input type="range" id="tileWidthSlider" min="1" max="512" value="64" class="slider" aria-labelledby="tileWidthLabel">
292
+ </div>
293
+
294
+ <div class="form-group">
295
+ <label for="tileHeight" id="tileHeightLabel">Tile Height (px):</label>
296
+ <input type="number" id="tileHeight" value="64" min="1" max="512">
297
+ <input type="range" id="tileHeightSlider" min="1" max="512" value="64" class="slider" aria-labelledby="tileHeightLabel">
298
+ </div>
299
+
300
+ <div class="form-group">
301
+ <label for="frameCount" id="frameCountLabel">Frame Count:</label>
302
+ <input type="number" id="frameCount" value="10" min="1" max="50">
303
+ <input type="range" id="frameCountSlider" min="1" max="50" value="10" class="slider" aria-labelledby="frameCountLabel">
304
+ </div>
305
+
306
+ <div class="form-group">
307
+ <label for="rowSelect">Color Variant:</label>
308
+ <select id="rowSelect" class="row-select">
309
+ <option value="0">Row 1</option>
310
+ <option value="1">Row 2</option>
311
+ <option value="2">Row 3</option>
312
+ <option value="3">Row 4</option>
313
+ <option value="4">Row 5</option>
314
+ </select>
315
+ </div>
316
+
317
+ <div class="form-group">
318
+ <label for="directionSelect">Animation Direction:</label>
319
+ <select id="directionSelect" class="row-select">
320
+ <option value="ltr">Left to Right</option>
321
+ <option value="rtl">Right to Left</option>
322
+ <option value="ttb">Top to Bottom</option>
323
+ <option value="btt">Bottom to Top</option>
324
+ <option value="alternate">Alternate (Ping-Pong)</option>
325
+ <option value="random">Random</option>
326
+ </select>
327
+ </div>
328
+ </div>
329
+
330
+ <div class="sprite-info" id="spriteInfo">
331
+ No sprite loaded yet.
332
+ </div>
333
+
334
+ <div class="full-sprite-preview-container">
335
+ <h3>Full Sprite Sheet Preview</h3>
336
+ <div class="preview-area" id="fullSpritePreview">
337
+ <p class="no-sprite-message">Upload a sprite sheet to see preview</p>
338
+ </div>
339
+ </div>
340
+
341
+ <div class="controls">
342
+ <button id="playPauseBtn" aria-label="Play animation">Play</button>
343
+ <button id="resetBtn" aria-label="Reset animation">Reset</button>
344
+ <div class="speed-control">
345
+ <label for="fpsRange" id="fpsLabel">Speed: </label>
346
+ <input type="range" id="fpsRange" min="1" max="60" value="10" aria-labelledby="fpsLabel">
347
+ <span class="fps-display" id="fpsDisplay" aria-live="polite">10 FPS</span>
348
+ </div>
349
+ </div>
350
+
351
+ <div class="canvas-container">
352
+ <canvas id="animationCanvas" aria-label="Sprite animation display"></canvas>
353
+ </div>
354
+
355
+ <div class="preview-container" id="framePreviewContainer" role="list" aria-label="Animation frames preview"></div>
356
+ </div>
357
+
358
+ <script>
359
+ // Get all the DOM elements we'll need
360
+ const spriteFileInput = document.getElementById('spriteFile');
361
+ const tileWidthInput = document.getElementById('tileWidth');
362
+ const tileHeightInput = document.getElementById('tileHeight');
363
+ const frameCountInput = document.getElementById('frameCount');
364
+ const tileWidthSlider = document.getElementById('tileWidthSlider');
365
+ const tileHeightSlider = document.getElementById('tileHeightSlider');
366
+ const frameCountSlider = document.getElementById('frameCountSlider');
367
+ const rowSelectInput = document.getElementById('rowSelect');
368
+ const directionSelectInput = document.getElementById('directionSelect');
369
+ const playPauseBtn = document.getElementById('playPauseBtn');
370
+ const resetBtn = document.getElementById('resetBtn');
371
+ const fpsRange = document.getElementById('fpsRange');
372
+ const fpsDisplay = document.getElementById('fpsDisplay');
373
+ const canvas = document.getElementById('animationCanvas');
374
+ const ctx = canvas.getContext('2d');
375
+ const spriteInfo = document.getElementById('spriteInfo');
376
+ const framePreviewContainer = document.getElementById('framePreviewContainer');
377
+ const fullSpritePreview = document.getElementById('fullSpritePreview');
378
+ const loadExampleBtn = document.getElementById('loadExampleBtn');
379
+
380
+ // Animation state
381
+ let spriteImage = null;
382
+ let currentFrame = 0;
383
+ let isPlaying = false;
384
+ let animationInterval = null;
385
+ let fps = 10;
386
+ let currentRow = 0; // Track which row/color variant is selected
387
+ let direction = 'ltr'; // Animation direction
388
+ let isReversing = false; // For alternating animation
389
+
390
+ // Initialize with default sizes
391
+ canvas.width = 128;
392
+ canvas.height = 128;
393
+
394
+ // Load the example image
395
+ loadExampleBtn.addEventListener('click', () => {
396
+ // Example image - the lotus flower blooming animation
397
+ const exampleImageUrl = 'lotus_bloom.png'; // This should be the path to your lotus flower image
398
+
399
+ // Create an image from the example URL
400
+ spriteImage = new Image();
401
+ spriteImage.onload = () => {
402
+ // Set appropriate defaults for this specific sprite sheet
403
+ tileWidthInput.value = 64;
404
+ tileWidthSlider.value = 64;
405
+ tileHeightInput.value = 64;
406
+ tileHeightSlider.value = 64;
407
+ frameCountInput.value = 16; // Assuming 16 frames in the animation
408
+ frameCountSlider.value = 16;
409
+
410
+ updateSpriteInfo();
411
+ updateFullSpritePreview();
412
+ reset();
413
+ createFramePreviews();
414
+ };
415
+ spriteImage.onerror = () => {
416
+ alert('Could not load example image. Make sure "lotus_bloom.png" is in the same directory as this HTML file.');
417
+ };
418
+ spriteImage.src = exampleImageUrl;
419
+ });
420
+
421
+ // Handle file upload
422
+ spriteFileInput.addEventListener('change', (e) => {
423
+ const file = e.target.files[0];
424
+ if (file) {
425
+ // Create image from uploaded file
426
+ const reader = new FileReader();
427
+ reader.onload = (event) => {
428
+ spriteImage = new Image();
429
+ spriteImage.onload = () => {
430
+ updateSpriteInfo();
431
+ updateFullSpritePreview();
432
+ reset();
433
+ createFramePreviews();
434
+ };
435
+ spriteImage.src = event.target.result;
436
+ };
437
+ reader.readAsDataURL(file);
438
+ }
439
+ });
440
+
441
+ // Update the full sprite sheet preview
442
+ function updateFullSpritePreview() {
443
+ fullSpritePreview.innerHTML = '';
444
+
445
+ if (!spriteImage) {
446
+ fullSpritePreview.innerHTML = '<p class="no-sprite-message">Upload a sprite sheet to see preview</p>';
447
+ return;
448
+ }
449
+
450
+ // Create a copy of the sprite image for the preview
451
+ const previewImg = document.createElement('img');
452
+ previewImg.src = spriteImage.src;
453
+ previewImg.alt = 'Full sprite sheet preview';
454
+
455
+ // Add the image to the preview container
456
+ fullSpritePreview.appendChild(previewImg);
457
+ }
458
+
459
+ // Update sprite sheet information display
460
+ function updateSpriteInfo() {
461
+ if (!spriteImage) return;
462
+
463
+ const tileWidth = parseInt(tileWidthInput.value);
464
+ const tileHeight = parseInt(tileHeightInput.value);
465
+ const frameCount = parseInt(frameCountInput.value);
466
+
467
+ // Update canvas size to match tile size (scaled up by 2x for better visibility)
468
+ canvas.width = tileWidth * 2;
469
+ canvas.height = tileHeight * 2;
470
+
471
+ // Calculate how many frames can fit in the image
472
+ const framesPerRow = Math.floor(spriteImage.width / tileWidth);
473
+ const totalRows = Math.floor(spriteImage.height / tileHeight);
474
+ const maxPossibleFrames = framesPerRow * totalRows;
475
+
476
+ // Update the row selector to match the actual number of rows in the sprite sheet
477
+ rowSelectInput.innerHTML = '';
478
+ for (let i = 0; i < totalRows; i++) {
479
+ const option = document.createElement('option');
480
+ option.value = i;
481
+ option.textContent = `Row ${i+1} ${i === 0 ? '(Top)' : i === totalRows-1 ? '(Bottom)' : ''}`;
482
+ rowSelectInput.appendChild(option);
483
+ }
484
+
485
+ spriteInfo.innerHTML = `
486
+ Sprite Sheet: ${spriteImage.width}x${spriteImage.height}px<br>
487
+ Tile Size: ${tileWidth}x${tileHeight}px<br>
488
+ Frames: ${frameCount} per row<br>
489
+ Rows/Variants: ${totalRows}<br>
490
+ Layout: ${framesPerRow} frames per row<br>
491
+ Animation: ${getDirectionDescription()}
492
+ `;
493
+
494
+ // Set current row to match the select value
495
+ currentRow = parseInt(rowSelectInput.value);
496
+ }
497
+
498
+ // Get a user-friendly description of the current animation direction
499
+ function getDirectionDescription() {
500
+ switch (directionSelectInput.value) {
501
+ case 'ltr': return 'Left to Right';
502
+ case 'rtl': return 'Right to Left';
503
+ case 'ttb': return 'Top to Bottom';
504
+ case 'btt': return 'Bottom to Top';
505
+ case 'alternate': return 'Alternating (Ping-Pong)';
506
+ case 'random': return 'Random Frame Order';
507
+ default: return 'Left to Right';
508
+ }
509
+ }
510
+
511
+ // Create individual frame previews
512
+ function createFramePreviews() {
513
+ framePreviewContainer.innerHTML = '';
514
+ if (!spriteImage) return;
515
+
516
+ const tileWidth = parseInt(tileWidthInput.value);
517
+ const tileHeight = parseInt(tileHeightInput.value);
518
+ const frameCount = parseInt(frameCountInput.value);
519
+ const row = currentRow;
520
+
521
+ for (let i = 0; i < frameCount; i++) {
522
+ // Create a canvas for each frame preview
523
+ const previewCanvas = document.createElement('canvas');
524
+ previewCanvas.width = tileWidth;
525
+ previewCanvas.height = tileHeight;
526
+ previewCanvas.style.width = '64px'; // Smaller display size
527
+ previewCanvas.style.height = '64px';
528
+ previewCanvas.className = 'frame-preview';
529
+ previewCanvas.setAttribute('role', 'listitem');
530
+ previewCanvas.setAttribute('aria-label', `Frame ${i+1}`);
531
+
532
+ if (i === currentFrame) {
533
+ previewCanvas.classList.add('active');
534
+ previewCanvas.setAttribute('aria-current', 'true');
535
+ }
536
+
537
+ const previewCtx = previewCanvas.getContext('2d');
538
+ previewCtx.imageSmoothingEnabled = false;
539
+
540
+ // Calculate position in sprite sheet
541
+ const framesPerRow = Math.floor(spriteImage.width / tileWidth);
542
+ const col = i % framesPerRow;
543
+
544
+ // Draw the frame from the current row
545
+ previewCtx.drawImage(
546
+ spriteImage,
547
+ col * tileWidth,
548
+ row * tileHeight,
549
+ tileWidth,
550
+ tileHeight,
551
+ 0,
552
+ 0,
553
+ tileWidth,
554
+ tileHeight
555
+ );
556
+
557
+ // Add click handler to select frame
558
+ previewCanvas.addEventListener('click', () => {
559
+ currentFrame = i;
560
+ updateActivePreviews();
561
+ drawFrame();
562
+ });
563
+
564
+ framePreviewContainer.appendChild(previewCanvas);
565
+ }
566
+ }
567
+
568
+ // Update which preview is marked as active
569
+ function updateActivePreviews() {
570
+ const previews = document.querySelectorAll('.frame-preview');
571
+ previews.forEach((preview, index) => {
572
+ if (index === currentFrame) {
573
+ preview.classList.add('active');
574
+ preview.setAttribute('aria-current', 'true');
575
+ } else {
576
+ preview.classList.remove('active');
577
+ preview.removeAttribute('aria-current');
578
+ }
579
+ });
580
+ }
581
+
582
+ // Draw the current frame
583
+ function drawFrame() {
584
+ if (!spriteImage) return;
585
+
586
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
587
+
588
+ const tileWidth = parseInt(tileWidthInput.value);
589
+ const tileHeight = parseInt(tileHeightInput.value);
590
+ const frameCount = parseInt(frameCountInput.value);
591
+ const row = currentRow;
592
+
593
+ // Ensure currentFrame is within bounds
594
+ currentFrame = currentFrame % frameCount;
595
+
596
+ // Calculate position in sprite sheet
597
+ const framesPerRow = Math.floor(spriteImage.width / tileWidth);
598
+ const col = currentFrame % framesPerRow;
599
+
600
+ // Draw the frame - maintain crisp pixel art by using imageSmoothingEnabled = false
601
+ ctx.imageSmoothingEnabled = false;
602
+ ctx.drawImage(
603
+ spriteImage,
604
+ col * tileWidth,
605
+ row * tileHeight,
606
+ tileWidth,
607
+ tileHeight,
608
+ 0,
609
+ 0,
610
+ canvas.width,
611
+ canvas.height
612
+ );
613
+
614
+ updateActivePreviews();
615
+
616
+ // Update canvas aria-label for accessibility
617
+ canvas.setAttribute('aria-label', `Sprite animation display - Frame ${currentFrame + 1} of ${frameCount}, Row ${currentRow + 1}, Direction: ${getDirectionDescription()}`);
618
+ }
619
+
620
+ // Animation loop
621
+ function animate() {
622
+ if (!isPlaying) return;
623
+
624
+ clearInterval(animationInterval);
625
+
626
+ animationInterval = setInterval(() => {
627
+ // Update the current frame based on direction
628
+ updateFrameBasedOnDirection();
629
+ drawFrame();
630
+ }, 1000 / fps);
631
+ }
632
+
633
+ // Update the frame based on selected direction
634
+ function updateFrameBasedOnDirection() {
635
+ const frameCount = parseInt(frameCountInput.value);
636
+ direction = directionSelectInput.value;
637
+
638
+ switch (direction) {
639
+ case 'ltr':
640
+ // Left to right (increment frame horizontally)
641
+ currentFrame = (currentFrame + 1) % frameCount;
642
+ break;
643
+
644
+ case 'rtl':
645
+ // Right to left (decrement frame horizontally)
646
+ currentFrame = (currentFrame - 1 + frameCount) % frameCount;
647
+ break;
648
+
649
+ case 'ttb':
650
+ // Top to bottom (increment row)
651
+ currentFrame = currentFrame; // Keep the same frame
652
+ const totalRows = Math.floor(spriteImage.height / parseInt(tileHeightInput.value));
653
+ currentRow = (currentRow + 1) % totalRows;
654
+ break;
655
+
656
+ case 'btt':
657
+ // Bottom to top (decrement row)
658
+ currentFrame = currentFrame; // Keep the same frame
659
+ const rows = Math.floor(spriteImage.height / parseInt(tileHeightInput.value));
660
+ currentRow = (currentRow - 1 + rows) % rows;
661
+ break;
662
+
663
+ case 'alternate':
664
+ // Ping-pong between first and last frame
665
+ if (!isReversing) {
666
+ currentFrame++;
667
+ if (currentFrame >= frameCount - 1) {
668
+ isReversing = true;
669
+ }
670
+ } else {
671
+ currentFrame--;
672
+ if (currentFrame <= 0) {
673
+ isReversing = false;
674
+ }
675
+ }
676
+ break;
677
+
678
+ case 'random':
679
+ // Jump to a random frame
680
+ let newFrame;
681
+ do {
682
+ newFrame = Math.floor(Math.random() * frameCount);
683
+ } while (newFrame === currentFrame && frameCount > 1);
684
+ currentFrame = newFrame;
685
+ break;
686
+ }
687
+ }
688
+
689
+ // Play/Pause button
690
+ playPauseBtn.addEventListener('click', () => {
691
+ isPlaying = !isPlaying;
692
+ playPauseBtn.textContent = isPlaying ? 'Pause' : 'Play';
693
+ playPauseBtn.setAttribute('aria-label', isPlaying ? 'Pause animation' : 'Play animation');
694
+
695
+ if (isPlaying) {
696
+ animate();
697
+ } else {
698
+ clearInterval(animationInterval);
699
+ }
700
+ });
701
+
702
+ // Reset button
703
+ resetBtn.addEventListener('click', reset);
704
+
705
+ function reset() {
706
+ currentFrame = 0;
707
+ isReversing = false; // Reset the alternating direction state
708
+ drawFrame();
709
+ }
710
+
711
+ // FPS (speed) control
712
+ fpsRange.addEventListener('input', (e) => {
713
+ fps = parseInt(e.target.value);
714
+ fpsDisplay.textContent = `${fps} FPS`;
715
+
716
+ if (isPlaying) {
717
+ animate();
718
+ }
719
+ });
720
+
721
+ // Update when parameters change
722
+ tileWidthInput.addEventListener('input', () => {
723
+ tileWidthSlider.value = tileWidthInput.value;
724
+ updateSpriteInfo();
725
+ createFramePreviews();
726
+ drawFrame();
727
+ });
728
+
729
+ tileHeightInput.addEventListener('input', () => {
730
+ tileHeightSlider.value = tileHeightInput.value;
731
+ updateSpriteInfo();
732
+ createFramePreviews();
733
+ drawFrame();
734
+ });
735
+
736
+ frameCountInput.addEventListener('input', () => {
737
+ frameCountSlider.value = frameCountInput.value;
738
+ updateSpriteInfo();
739
+ createFramePreviews();
740
+ drawFrame();
741
+ });
742
+
743
+ // Slider event listeners
744
+ tileWidthSlider.addEventListener('input', () => {
745
+ tileWidthInput.value = tileWidthSlider.value;
746
+ updateSpriteInfo();
747
+ createFramePreviews();
748
+ drawFrame();
749
+ });
750
+
751
+ tileHeightSlider.addEventListener('input', () => {
752
+ tileHeightInput.value = tileHeightSlider.value;
753
+ updateSpriteInfo();
754
+ createFramePreviews();
755
+ drawFrame();
756
+ });
757
+
758
+ frameCountSlider.addEventListener('input', () => {
759
+ frameCountInput.value = frameCountSlider.value;
760
+ updateSpriteInfo();
761
+ createFramePreviews();
762
+ drawFrame();
763
+ });
764
+
765
+ // Listen for row selection changes
766
+ rowSelectInput.addEventListener('change', () => {
767
+ currentRow = parseInt(rowSelectInput.value);
768
+ createFramePreviews();
769
+ drawFrame();
770
+ });
771
+
772
+ // Listen for direction selection changes
773
+ directionSelectInput.addEventListener('change', () => {
774
+ // Update direction - the actual change happens in the animation loop
775
+ direction = directionSelectInput.value;
776
+ // Reset alternating state when changing direction
777
+ isReversing = false;
778
+
779
+ // Create new frame previews if we've changed to a vertical animation
780
+ if (direction === 'ttb' || direction === 'btt') {
781
+ createFramePreviews();
782
+ }
783
+
784
+ // If we're actively playing, restart the animation with the new direction
785
+ if (isPlaying) {
786
+ animate();
787
+ }
788
+
789
+ // Update the sprite info to show the new direction
790
+ if (spriteImage) {
791
+ updateSpriteInfo();
792
+ }
793
+ });
794
+
795
+ // Initial drawing
796
+ drawFrame();
797
+ </script>
798
+ </body>
799
+ </html>