Ezequield commited on
Commit
fdbee22
·
verified ·
1 Parent(s): 6c21727

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +806 -869
index.html CHANGED
@@ -3,1046 +3,983 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SORA 2 Image to Video BETA</title>
7
- <!-- Import Icons -->
 
 
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- <!-- Import Google Fonts -->
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
11
-
12
  <style>
13
  :root {
14
- --bg-dark: #09090b;
15
- --bg-panel: #18181b;
16
- --bg-input: #27272a;
17
- --primary: #8b5cf6; /* Violet */
18
- --primary-hover: #7c3aed;
19
- --accent: #06b6d4; /* Cyan */
20
- --text-main: #f4f4f5;
21
- --text-muted: #a1a1aa;
22
- --border: #3f3f46;
23
- --radius: 12px;
24
- --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
25
- --gradient: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%);
26
  }
27
 
28
  * {
29
  box-sizing: border-box;
30
  margin: 0;
31
  padding: 0;
32
- outline: none;
33
  }
34
 
35
  body {
36
- font-family: 'Inter', sans-serif;
37
- background-color: var(--bg-dark);
38
- color: var(--text-main);
39
- height: 100vh;
40
- overflow: hidden;
41
  display: flex;
42
  flex-direction: column;
 
 
43
  }
44
 
45
  /* --- Header --- */
46
  header {
47
- height: 60px;
48
- background-color: rgba(24, 24, 27, 0.8);
49
- backdrop-filter: blur(12px);
50
- border-bottom: 1px solid var(--border);
51
  display: flex;
52
- align-items: center;
53
  justify-content: space-between;
54
- padding: 0 24px;
55
- z-index: 100;
56
- }
57
-
58
- .logo {
59
- font-weight: 700;
60
- font-size: 1.25rem;
61
- background: var(--gradient);
62
- -webkit-background-clip: text;
63
- -webkit-text-fill-color: transparent;
64
- letter-spacing: -0.025em;
65
- display: flex;
66
  align-items: center;
67
- gap: 10px;
68
- }
69
-
70
- .logo i {
71
- -webkit-text-fill-color: var(--primary);
72
  }
73
 
74
- .nav-links {
75
  display: flex;
76
- gap: 20px;
77
  }
78
 
79
- .nav-links a {
80
- color: var(--text-muted);
81
- text-decoration: none;
82
- font-size: 0.9rem;
83
- transition: color 0.2s;
 
 
 
84
  }
85
 
86
- .nav-links a:hover {
87
- color: var(--text-main);
 
 
 
 
88
  }
89
 
90
- .credit-link {
91
- color: var(--text-muted);
92
- font-size: 0.8rem;
93
  text-decoration: none;
94
- transition: color 0.3s;
95
- display: flex;
96
- align-items: center;
97
- gap: 6px;
 
98
  }
99
 
100
- .credit-link:hover {
101
- color: var(--primary);
 
 
 
102
  }
103
 
104
  /* --- Main Layout --- */
105
  main {
106
  flex: 1;
107
  display: grid;
108
- grid-template-columns: 320px 1fr 300px; /* Sidebar | Canvas | History */
109
- height: calc(100vh - 60px);
110
- overflow: hidden;
111
- }
112
-
113
- /* --- Left Sidebar (Controls) --- */
114
- .sidebar-left {
115
- background-color: var(--bg-panel);
116
- border-right: 1px solid var(--border);
117
- padding: 24px;
118
- overflow-y: auto;
119
- display: flex;
120
- flex-direction: column;
121
- gap: 24px;
122
  }
123
 
124
- h2 {
125
- font-size: 0.85rem;
126
- text-transform: uppercase;
127
- letter-spacing: 0.05em;
128
- color: var(--text-muted);
129
- margin-bottom: 12px;
130
- font-weight: 600;
131
  }
132
 
133
- .control-group {
 
 
 
 
134
  display: flex;
135
  flex-direction: column;
136
- gap: 12px;
137
- }
138
-
139
- label {
140
- font-size: 0.85rem;
141
- color: var(--text-main);
142
- display: flex;
143
- justify-content: space-between;
144
- }
145
-
146
- .value-display {
147
- color: var(--primary);
148
- font-family: 'JetBrains Mono', monospace;
149
- font-size: 0.8rem;
150
- }
151
-
152
- /* Inputs */
153
- input[type="text"],
154
- textarea,
155
- select {
156
- background-color: var(--bg-input);
157
- border: 1px solid var(--border);
158
- border-radius: var(--radius);
159
- padding: 10px 12px;
160
- color: var(--text-main);
161
- font-family: inherit;
162
- font-size: 0.9rem;
163
- transition: border-color 0.2s;
164
- width: 100%;
165
- }
166
-
167
- textarea {
168
- resize: vertical;
169
- min-height: 80px;
170
- }
171
-
172
- input:focus, textarea:focus, select:focus {
173
- border-color: var(--primary);
174
- }
175
-
176
- /* Range Slider */
177
- input[type="range"] {
178
- -webkit-appearance: none;
179
- width: 100%;
180
- background: transparent;
181
  }
182
 
183
- input[type="range"]::-webkit-slider-thumb {
184
- -webkit-appearance: none;
185
- height: 16px;
186
- width: 16px;
187
- border-radius: 50%;
188
- background: var(--primary);
189
- cursor: pointer;
190
- margin-top: -6px;
191
- box-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
192
  }
193
 
194
- input[type="range"]::-webkit-slider-runnable-track {
195
- width: 100%;
196
- height: 4px;
197
- background: var(--bg-input);
198
- border-radius: 2px;
 
199
  }
200
 
201
- /* Generate Button */
202
- .btn-generate {
203
- background: var(--gradient);
204
  color: white;
205
- border: none;
206
- padding: 14px;
207
- border-radius: var(--radius);
208
- font-weight: 600;
209
- font-size: 1rem;
210
  cursor: pointer;
211
- transition: transform 0.1s, opacity 0.2s;
 
212
  display: flex;
213
  align-items: center;
214
- justify-content: center;
215
  gap: 8px;
216
- box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
217
  }
218
 
219
- .btn-generate:hover {
220
- opacity: 0.9;
 
221
  }
222
 
223
- .btn-generate:active {
224
- transform: scale(0.98);
 
 
225
  }
226
 
227
- .btn-generate:disabled {
228
- background: var(--bg-input);
229
- color: var(--text-muted);
230
- cursor: not-allowed;
231
- box-shadow: none;
232
  }
233
 
234
- /* --- Center Stage (Canvas/Preview) --- */
235
- .stage {
 
 
 
236
  display: flex;
237
  flex-direction: column;
238
- align-items: center;
239
- justify-content: center;
240
- padding: 24px;
241
- background-color: #000;
242
- background-image: radial-gradient(#1f1f22 1px, transparent 1px);
243
- background-size: 20px 20px;
244
- position: relative;
245
- }
246
-
247
- .canvas-container {
248
- width: 100%;
249
- max-width: 800px;
250
- aspect-ratio: 16/9;
251
- background-color: var(--bg-panel);
252
- border-radius: var(--radius);
253
- border: 1px solid var(--border);
254
- position: relative;
255
  overflow: hidden;
256
- box-shadow: 0 20px 50px rgba(0,0,0,0.5);
257
- display: flex;
258
- align-items: center;
259
- justify-content: center;
260
- transition: border-color 0.3s;
261
- }
262
-
263
- .canvas-container.drag-over {
264
- border-color: var(--accent);
265
- border-style: dashed;
266
- }
267
-
268
- /* Empty State */
269
- .empty-state {
270
- text-align: center;
271
- pointer-events: none;
272
- }
273
-
274
- .empty-state i {
275
- font-size: 3rem;
276
- color: var(--text-muted);
277
- margin-bottom: 16px;
278
  }
279
 
280
- .empty-state p {
281
- color: var(--text-muted);
282
- font-size: 1rem;
 
 
 
283
  }
 
284
 
285
- .upload-hint {
286
- margin-top: 8px;
287
- font-size: 0.8rem;
288
- color: var(--primary);
 
289
  cursor: pointer;
290
- pointer-events: auto;
291
- text-decoration: underline;
292
- }
293
-
294
- /* Image Preview & Video Simulation */
295
- #preview-image {
296
- width: 100%;
297
- height: 100%;
298
- object-fit: contain;
299
- display: none;
300
- z-index: 1;
301
  }
302
 
303
- #video-overlay {
304
- position: absolute;
305
- top: 0;
306
- left: 0;
307
- width: 100%;
308
- height: 100%;
309
- z-index: 2;
310
- pointer-events: none; /* Let clicks pass to canvas for drawing */
311
  }
312
 
313
- /* Motion Brush Canvas */
314
- #motion-canvas {
315
- position: absolute;
316
- top: 0;
317
- left: 0;
318
- width: 100%;
319
- height: 100%;
320
- z-index: 3;
321
- cursor: crosshair;
322
- opacity: 0.7;
323
  }
324
 
325
- /* Toolbar for Brush */
326
- .brush-toolbar {
327
- position: absolute;
328
- bottom: 16px;
329
- left: 50%;
330
- transform: translateX(-50%);
331
- background: rgba(0,0,0,0.8);
332
- backdrop-filter: blur(8px);
333
- padding: 8px 16px;
334
- border-radius: 20px;
335
- display: flex;
336
- gap: 12px;
337
- align-items: center;
338
- z-index: 10;
339
- border: 1px solid var(--border);
340
- opacity: 0;
341
- transition: opacity 0.3s;
342
- pointer-events: none;
343
- }
344
-
345
- .brush-toolbar.active {
346
- opacity: 1;
347
- pointer-events: auto;
348
  }
349
 
350
- .tool-btn {
351
- background: none;
352
- border: none;
353
- color: var(--text-muted);
354
- cursor: pointer;
355
  font-size: 1.1rem;
356
- transition: color 0.2s;
 
 
357
  }
358
 
359
- .tool-btn.active, .tool-btn:hover {
360
- color: var(--primary);
 
 
361
  }
362
 
363
- /* Loading Overlay */
364
- .loading-overlay {
365
- position: absolute;
366
- top: 0;
367
- left: 0;
368
- width: 100%;
369
- height: 100%;
370
- background: rgba(0,0,0,0.85);
371
- z-index: 20;
372
  display: flex;
373
- flex-direction: column;
374
  align-items: center;
375
  justify-content: center;
376
- backdrop-filter: blur(5px);
377
- display: none;
 
378
  }
379
 
380
- .spinner {
381
- width: 50px;
382
- height: 50px;
383
- border: 3px solid rgba(255,255,255,0.1);
384
- border-radius: 50%;
385
- border-top-color: var(--primary);
386
- animation: spin 1s ease-in-out infinite;
387
- margin-bottom: 16px;
388
  }
389
 
390
- @keyframes spin {
391
- to { transform: rotate(360deg); }
 
 
392
  }
393
 
394
- .loading-text {
395
- font-family: 'JetBrains Mono', monospace;
396
- color: var(--primary);
397
- font-size: 0.9rem;
398
  }
399
 
400
- /* --- Right Sidebar (History) --- */
401
- .sidebar-right {
402
- background-color: var(--bg-panel);
403
- border-left: 1px solid var(--border);
404
- padding: 24px;
405
- overflow-y: auto;
 
 
 
 
 
 
 
406
  }
407
 
408
- .history-grid {
409
- display: grid;
410
- grid-template-columns: 1fr;
411
- gap: 16px;
412
  }
413
 
414
- .history-item {
415
- background-color: var(--bg-input);
416
- border-radius: 8px;
417
- overflow: hidden;
418
  cursor: pointer;
419
- border: 1px solid transparent;
420
- transition: all 0.2s;
421
  }
422
 
423
- .history-item:hover {
424
- border-color: var(--primary);
425
- transform: translateY(-2px);
426
  }
427
 
428
- .history-thumb {
429
- width: 100%;
430
- aspect-ratio: 16/9;
431
- object-fit: cover;
432
- background-color: #000;
433
  }
434
 
435
- .history-info {
436
- padding: 8px 12px;
437
- display: flex;
438
- justify-content: space-between;
439
- align-items: center;
 
 
440
  }
441
 
442
- .history-prompt {
443
- font-size: 0.75rem;
444
- color: var(--text-muted);
445
- white-space: nowrap;
446
- overflow: hidden;
447
- text-overflow: ellipsis;
448
- max-width: 180px;
449
  }
450
 
451
- .video-badge {
452
- font-size: 0.7rem;
453
- background: var(--primary);
454
- color: white;
455
- padding: 2px 6px;
456
- border-radius: 4px;
457
  }
458
 
459
- /* --- Toast Notification --- */
460
- .toast-container {
461
- position: fixed;
462
- bottom: 24px;
463
- right: 24px;
464
- z-index: 1000;
465
- display: flex;
466
- flex-direction: column;
467
- gap: 10px;
468
  }
469
 
 
470
  .toast {
471
- background-color: var(--bg-panel);
472
- border: 1px solid var(--border);
473
- color: var(--text-main);
474
- padding: 12px 16px;
475
- border-radius: 8px;
476
- box-shadow: var(--shadow);
477
- display: flex;
478
- align-items: center;
479
- gap: 10px;
480
- min-width: 250px;
481
- animation: slideIn 0.3s ease-out;
482
- }
483
-
484
- .toast.success i { color: #10b981; }
485
- .toast.error i { color: #ef4444; }
486
- .toast.info i { color: var(--accent); }
487
-
488
- @keyframes slideIn {
489
- from { transform: translateX(100%); opacity: 0; }
490
- to { transform: translateX(0); opacity: 1; }
491
  }
492
-
493
- /* --- Responsive --- */
494
- @media (max-width: 1024px) {
495
- main {
496
- grid-template-columns: 280px 1fr;
497
- }
498
- .sidebar-right {
499
- display: none; /* Hide history on medium screens */
500
- }
501
  }
502
 
503
- @media (max-width: 768px) {
504
- main {
505
- grid-template-columns: 1fr;
506
- grid-template-rows: auto 1fr;
507
- overflow-y: auto;
508
- height: auto;
509
- }
510
- .sidebar-left {
511
- order: 2;
512
- border-right: none;
513
- border-top: 1px solid var(--border);
514
- }
515
- .stage {
516
- order: 1;
517
- height: 50vh;
518
- }
519
- }
520
  </style>
521
  </head>
522
  <body>
523
 
524
- <!-- Header -->
525
  <header>
526
- <div class="logo">
527
- <i class="fa-solid fa-film"></i>
528
- SORA 2 <span style="font-weight: 300; font-size: 0.9em; color: var(--text-muted);">BETA</span>
529
- </div>
530
- <div class="nav-links">
531
- <a href="#">Models</a>
532
- <a href="#">Pricing</a>
533
- <a href="#">Docs</a>
534
  </div>
535
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="credit-link">
536
- <i class="fa-solid fa-code"></i> Built with anycoder
537
  </a>
538
  </header>
539
 
540
- <!-- Main Application -->
541
  <main>
542
-
543
- <!-- Left Sidebar: Controls -->
544
- <aside class="sidebar-left">
545
- <div class="control-group">
546
- <h2>Generation Settings</h2>
547
-
548
- <label>
549
- Aspect Ratio
550
- <span class="value-display">16:9</span>
551
- </label>
552
- <select id="aspect-ratio">
553
- <option value="16:9">16:9 (Landscape)</option>
554
- <option value="9:16">9:16 (Portrait)</option>
555
- <option value="1:1">1:1 (Square)</option>
556
- <option value="2.35:1">2.35:1 (Cinematic)</option>
557
- </select>
558
- </div>
559
-
560
- <div class="control-group">
561
- <label>
562
- Motion Strength
563
- <span class="value-display" id="motion-val">5</span>
564
- </label>
565
- <input type="range" id="motion-strength" min="1" max="10" value="5">
566
- <p style="font-size: 0.75rem; color: var(--text-muted);">Controls how much the image moves.</p>
567
- </div>
568
-
569
- <div class="control-group">
570
- <label>
571
- Duration (sec)
572
- <span class="value-display" id="duration-val">5</span>
573
- </label>
574
- <input type="range" id="duration" min="2" max="15" value="5">
575
- </div>
576
-
577
- <div class="control-group">
578
- <label>Motion Brush</label>
579
- <div style="display: flex; gap: 8px;">
580
- <button class="tool-btn active" id="btn-brush-horizontal" title="Horizontal Motion"><i class="fa-solid fa-arrows-left-right"></i></button>
581
- <button class="tool-btn" id="btn-brush-vertical" title="Vertical Motion"><i class="fa-solid fa-arrows-up-down"></i></button>
582
- <button class="tool-btn" id="btn-brush-zoom" title="Zoom Motion"><i class="fa-solid fa-magnifying-glass-plus"></i></button>
583
- <button class="tool-btn" id="btn-clear" title="Clear Brush" style="margin-left: auto; color: #ef4444;"><i class="fa-solid fa-trash"></i></button>
584
- </div>
585
- <p style="font-size: 0.75rem; color: var(--text-muted);">Draw on image to indicate motion areas.</p>
586
  </div>
 
587
 
588
- <div class="control-group">
589
- <label>Prompt (Optional)</label>
590
- <textarea id="prompt-input" placeholder="Describe the motion (e.g., 'The clouds move slowly', 'Water flows down')..."></textarea>
 
591
  </div>
592
 
593
- <button class="btn-generate" id="generate-btn">
594
- <i class="fa-solid fa-wand-magic-sparkles"></i> Generate Video
595
- </button>
596
- </aside>
597
-
598
- <!-- Center Stage: Canvas -->
599
- <section class="stage">
600
- <input type="file" id="file-input" accept="image/*" style="display: none;">
601
-
602
- <div class="canvas-container" id="drop-zone">
603
- <!-- Empty State -->
604
- <div class="empty-state" id="empty-state">
605
- <i class="fa-solid fa-cloud-arrow-up"></i>
606
- <p>Drag & Drop Image or <span class="upload-hint" onclick="document.getElementById('file-input').click()">Browse</span></p>
607
- </div>
608
-
609
- <!-- Image Preview -->
610
- <img id="preview-image" alt="Source Image">
611
-
612
- <!-- Canvas for Motion Brush -->
613
- <canvas id="motion-canvas"></canvas>
614
-
615
- <!-- Simulated Video Overlay (for playing result) -->
616
- <div id="video-overlay" style="display: none;"></div>
617
-
618
- <!-- Loading Overlay -->
619
- <div class="loading-overlay" id="loading-overlay">
620
- <div class="spinner"></div>
621
- <div class="loading-text" id="loading-text">Initializing SORA 2...</div>
622
  </div>
623
 
624
- <!-- Brush Toolbar -->
625
- <div class="brush-toolbar" id="brush-toolbar">
626
- <span style="color: white; font-size: 0.8rem; font-weight: 600;">Brush Mode</span>
627
- <div style="width: 1px; height: 16px; background: rgba(255,255,255,0.2);"></div>
628
- <button class="tool-btn" onclick="undoStroke()"><i class="fa-solid fa-rotate-left"></i></button>
629
- </div>
630
- </div>
631
-
632
- <!-- Playback Controls (Only visible after generation) -->
633
- <div id="playback-controls" style="margin-top: 20px; display: none; gap: 16px; align-items: center;">
634
- <button class="tool-btn" id="play-pause-btn" style="font-size: 1.5rem; color: white;"><i class="fa-solid fa-play"></i></button>
635
- <div style="width: 200px; height: 4px; background: var(--bg-input); border-radius: 2px;">
636
- <div id="progress-bar" style="width: 0%; height: 100%; background: var(--primary); border-radius: 2px;"></div>
637
- </div>
638
- <span style="font-family: 'JetBrains Mono'; font-size: 0.8rem; color: var(--text-muted);">0:00 / 0:05</span>
639
- </div>
640
- </section>
641
-
642
- <!-- Right Sidebar: History -->
643
- <aside class="sidebar-right">
644
- <h2>Recent Generations</h2>
645
- <div class="history-grid" id="history-grid">
646
- <!-- History Items Injected via JS -->
647
- <div class="history-item" onclick="loadDemo()">
648
- <img src="https://picsum.photos/seed/sora1/300/169" class="history-thumb" alt="Demo">
649
- <div class="history-info">
650
- <span class="history-prompt">Cyberpunk City Rain...</span>
651
- <span class="video-badge">MP4</span>
652
- </div>
653
- </div>
654
- <div class="history-item">
655
- <img src="https://picsum.photos/seed/sora2/300/169" class="history-thumb" alt="Demo">
656
- <div class="history-info">
657
- <span class="history-prompt">Astronaut in space...</span>
658
- <span class="video-badge">MP4</span>
659
- </div>
660
- </div>
661
- <div class="history-item">
662
- <img src="https://picsum.photos/seed/sora3/300/169" class="history-thumb" alt="Demo">
663
- <div class="history-info">
664
- <span class="history-prompt">Underwater coral reef...</span>
665
- <span class="video-badge">MP4</span>
666
  </div>
667
  </div>
668
  </div>
669
- </aside>
670
-
671
  </main>
672
 
673
- <!-- Toast Container -->
674
- <div class="toast-container" id="toast-container"></div>
 
 
 
675
 
676
  <script>
677
- /* --- State & Elements --- */
678
- const state = {
679
- imageLoaded: false,
680
- isGenerating: false,
681
- isPlaying: false,
682
- brushMode: 'horizontal', // horizontal, vertical, zoom
683
- strokes: [], // Array of stroke paths
684
- isDrawing: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  };
686
 
687
- const elements = {
688
- dropZone: document.getElementById('drop-zone'),
689
- fileInput: document.getElementById('file-input'),
690
- emptyState: document.getElementById('empty-state'),
691
- previewImage: document.getElementById('preview-image'),
692
- canvas: document.getElementById('motion-canvas'),
693
- brushToolbar: document.getElementById('brush-toolbar'),
694
- generateBtn: document.getElementById('generate-btn'),
695
- loadingOverlay: document.getElementById('loading-overlay'),
696
- loadingText: document.getElementById('loading-text'),
697
- videoOverlay: document.getElementById('video-overlay'),
698
- playbackControls: document.getElementById('playback-controls'),
699
- motionStrength: document.getElementById('motion-strength'),
700
- motionVal: document.getElementById('motion-val'),
701
- duration: document.getElementById('duration'),
702
- durationVal: document.getElementById('duration-val'),
703
- playBtn: document.getElementById('play-pause-btn'),
704
- progressBar: document.getElementById('progress-bar'),
705
- toastContainer: document.getElementById('toast-container'),
706
- brushBtns: {
707
- horizontal: document.getElementById('btn-brush-horizontal'),
708
- vertical: document.getElementById('btn-brush-vertical'),
709
- zoom: document.getElementById('btn-brush-zoom'),
710
- }
711
  };
712
 
713
- const ctx = elements.canvas.getContext('2d');
714
 
715
- /* --- Event Listeners --- */
716
-
717
- // File Upload
718
- elements.dropZone.addEventListener('dragover', (e) => {
719
- e.preventDefault();
720
- elements.dropZone.classList.add('drag-over');
721
- });
722
-
723
- elements.dropZone.addEventListener('dragleave', () => {
724
- elements.dropZone.classList.remove('drag-over');
725
- });
726
-
727
- elements.dropZone.addEventListener('drop', (e) => {
728
- e.preventDefault();
729
- elements.dropZone.classList.remove('drag-over');
730
- const file = e.dataTransfer.files[0];
731
- handleFile(file);
732
- });
733
-
734
- elements.fileInput.addEventListener('change', (e) => {
735
- const file = e.target.files[0];
736
- handleFile(file);
737
- });
738
-
739
- // Sliders
740
- elements.motionStrength.addEventListener('input', (e) => {
741
- elements.motionVal.textContent = e.target.value;
742
- });
743
-
744
- elements.duration.addEventListener('input', (e) => {
745
- elements.durationVal.textContent = e.target.value;
746
- });
747
-
748
- // Brush Mode Toggles
749
- Object.keys(elements.brushBtns).forEach(mode => {
750
- elements.brushBtns[mode].addEventListener('click', () => {
751
- state.brushMode = mode;
752
- // Update UI
753
- document.querySelectorAll('.sidebar-left .tool-btn').forEach(btn => btn.classList.remove('active'));
754
- elements.brushBtns[mode].classList.add('active');
755
- showToast(`Switched to ${mode} brush`, 'info');
756
- });
757
- });
758
-
759
- document.getElementById('btn-clear').addEventListener('click', () => {
760
- state.strokes = [];
761
- redrawCanvas();
762
- });
763
-
764
- // Generate
765
- elements.generateBtn.addEventListener('click', startGeneration);
766
-
767
- // Playback
768
- elements.playBtn.addEventListener('click', togglePlayback);
769
 
770
- /* --- Functions --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
 
772
- function handleFile(file) {
773
- if (file && file.type.startsWith('image/')) {
774
- const reader = new FileReader();
775
- reader.onload = (e) => {
776
- elements.previewImage.src = e.target.result;
777
- elements.previewImage.style.display = 'block';
778
- elements.emptyState.style.display = 'none';
779
- state.imageLoaded = true;
780
 
781
- // Reset states
782
- elements.videoOverlay.style.display = 'none';
783
- elements.playbackControls.style.display = 'none';
784
- elements.brushToolbar.classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
 
786
- // Resize canvas to match image once loaded
787
- elements.previewImage.onload = () => {
788
- resizeCanvas();
789
- showToast('Image uploaded successfully', 'success');
790
- };
791
- };
792
- reader.readAsDataURL(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  } else {
794
- showToast('Please upload a valid image file', 'error');
795
  }
796
- }
797
 
798
- function resizeCanvas() {
799
- elements.canvas.width = elements.dropZone.clientWidth;
800
- elements.canvas.height = elements.dropZone.clientHeight;
801
- redrawCanvas();
802
- }
803
 
804
- window.addEventListener('resize', resizeCanvas);
805
-
806
- /* --- Canvas Drawing Logic --- */
807
- let currentPath = [];
808
-
809
- elements.canvas.addEventListener('mousedown', startDrawing);
810
- elements.canvas.addEventListener('mousemove', draw);
811
- elements.canvas.addEventListener('mouseup', stopDrawing);
812
- elements.canvas.addEventListener('mouseleave', stopDrawing);
813
-
814
- // Touch support
815
- elements.canvas.addEventListener('touchstart', (e) => {
816
- e.preventDefault();
817
- startDrawing(e.touches[0]);
818
- });
819
- elements.canvas.addEventListener('touchmove', (e) => {
820
- e.preventDefault();
821
- draw(e.touches[0]);
822
- });
823
- elements.canvas.addEventListener('touchend', stopDrawing);
824
-
825
- function startDrawing(e) {
826
- if (!state.imageLoaded) return;
827
- state.isDrawing = true;
828
- const rect = elements.canvas.getBoundingClientRect();
829
- const x = e.clientX - rect.left;
830
- const y = e.clientY - rect.top;
831
- currentPath = [{x, y, mode: state.brushMode}];
832
- }
833
 
834
- function draw(e) {
835
- if (!state.isDrawing) return;
836
- const rect = elements.canvas.getBoundingClientRect();
837
- const x = e.clientX - rect.left;
838
- const y = e.clientY - rect.top;
839
- currentPath.push({x, y, mode: state.brushMode});
840
- redrawCanvas();
841
- }
842
 
843
- function stopDrawing() {
844
- if (!state.isDrawing) return;
845
- state.isDrawing = false;
846
- if (currentPath.length > 0) {
847
- state.strokes.push([...currentPath]);
848
  }
849
- currentPath = [];
850
- }
851
 
852
- function undoStroke() {
853
- state.strokes.pop();
854
- redrawCanvas();
 
855
  }
856
 
857
- function redrawCanvas() {
858
- ctx.clearRect(0, 0, elements.canvas.width, elements.canvas.height);
859
-
860
- // Helper to draw a single path
861
- const drawPath = (path) => {
862
- if (path.length < 2) return;
863
- ctx.beginPath();
864
- ctx.moveTo(path[0].x, path[0].y);
865
- for (let i = 1; i < path.length; i++) {
866
- ctx.lineTo(path[i].x, path[i].y);
867
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
 
869
- // Style based on mode
870
- if (path[0].mode === 'horizontal') {
871
- ctx.strokeStyle = 'rgba(255, 0, 0, 0.6)'; // Red
872
- ctx.lineWidth = 10;
873
- ctx.lineCap = 'round';
874
- // Add arrow hints
875
- ctx.setLineDash([10, 10]);
876
- } else if (path[0].mode === 'vertical') {
877
- ctx.strokeStyle = 'rgba(0, 255, 0, 0.6)'; // Green
878
- ctx.lineWidth = 10;
879
- ctx.lineCap = 'round';
880
- ctx.setLineDash([10, 10]);
 
 
 
 
 
 
881
  } else {
882
- ctx.strokeStyle = 'rgba(0, 100, 255, 0.6)'; // Blue
883
- ctx.lineWidth = 15;
884
- ctx.lineCap = 'round';
885
- ctx.setLineDash([]);
886
  }
887
-
888
- ctx.stroke();
889
- ctx.setLineDash([]); // Reset
890
- };
891
 
892
- // Draw saved strokes
893
- state.strokes.forEach(stroke => drawPath(stroke));
894
-
895
- // Draw current active path
896
- if (state.isDrawing) drawPath(currentPath);
897
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
 
899
- /* --- Simulation Logic --- */
 
 
900
 
901
- function startGeneration() {
902
- if (!state.imageLoaded) {
903
- showToast('Please upload an image first', 'error');
904
- return;
 
 
 
 
 
 
 
 
 
905
  }
906
 
907
- state.isGenerating = true;
908
- elements.generateBtn.disabled = true;
909
- elements.loadingOverlay.style.display = 'flex';
910
- elements.brushToolbar.classList.remove('active');
911
-
912
- const steps = [
913
- "Analyzing image composition...",
914
- "Processing motion vectors...",
915
- "Generating latent diffusion frames...",
916
- "Upscaling output...",
917
- "Finalizing render..."
918
- ];
919
-
920
- let stepIndex = 0;
921
- const interval = setInterval(() => {
922
- elements.loadingText.textContent = steps[stepIndex];
923
- stepIndex++;
924
- if (stepIndex >= steps.length) {
925
- clearInterval(interval);
926
- finishGeneration();
 
 
 
 
 
 
 
 
 
927
  }
928
- }, 800);
929
- }
930
-
931
- function finishGeneration() {
932
- state.isGenerating = false;
933
- elements.loadingOverlay.style.display = 'none';
934
- elements.generateBtn.disabled = false;
935
- elements.videoOverlay.style.display = 'block';
936
- elements.playbackControls.style.display = 'flex';
937
-
938
- // Hide canvas strokes so we can see the "video"
939
- ctx.clearRect(0, 0, elements.canvas.width, elements.canvas.height);
940
-
941
- showToast('Video generated successfully!', 'success');
942
- addToHistory();
943
- }
944
-
945
- function togglePlayback() {
946
- state.isPlaying = !state.isPlaying;
947
- elements.playBtn.innerHTML = state.isPlaying ? '<i class="fa-solid fa-pause"></i>' : '<i class="fa-solid fa-play"></i>';
948
-
949
- if (state.isPlaying) {
950
- // Simulate playing by animating the image transform
951
- const strength = elements.motionStrength.value / 2;
952
- const duration = elements.duration.value * 1000;
953
-
954
- // Simple CSS animation simulation on the image
955
- // We apply a transform based on the dominant brush mode if any
956
- let transform = '';
957
- const hasHorizontal = state.strokes.some(s => s[0].mode === 'horizontal');
958
- const hasVertical = state.strokes.some(s => s[0].mode === 'vertical');
959
- const hasZoom = state.strokes.some(s => s[0].mode === 'zoom');
960
-
961
- if (hasZoom) transform = `scale(1.${strength})`;
962
- if (hasHorizontal) transform += ` translateX(${strength * 20}px)`;
963
- if (hasVertical) transform += ` translateY(${strength * 10}px)`;
964
-
965
- // Default subtle motion if no brush
966
- if (transform === '') transform = `scale(1.05) translateX(10px)`;
967
-
968
- elements.previewImage.style.transition = `transform ${duration}ms ease-in-out alternate`;
969
- elements.previewImage.style.transform = transform;
970
-
971
- // Simulate progress bar
972
- elements.progressBar.style.transition = `width ${duration}ms linear`;
973
- elements.progressBar.style.width = '100%';
974
-
975
- setTimeout(() => {
976
- if(state.isPlaying) togglePlayback(); // Auto pause at end
977
- }, duration);
978
-
979
- } else {
980
- // Reset
981
- elements.previewImage.style.transform = 'none';
982
- elements.progressBar.style.transition = 'none';
983
- elements.progressBar.style.width = '0%';
984
  }
985
- }
986
 
987
- function addToHistory() {
988
- const historyGrid = document.getElementById('history-grid');
989
- const newItem = document.createElement('div');
990
- newItem.className = 'history-item';
991
-
992
- // Use current image source
993
- const thumbUrl = elements.previewImage.src;
994
- const prompt = document.getElementById('prompt-input').value || "Untitled Generation";
995
-
996
- newItem.innerHTML = `
997
- <img src="${thumbUrl}" class="history-thumb" alt="History">
998
- <div class="history-info">
999
- <span class="history-prompt">${prompt}</span>
1000
- <span class="video-badge">NEW</span>
1001
- </div>
1002
- `;
1003
-
1004
- // Add to top
1005
- historyGrid.insertBefore(newItem, historyGrid.firstChild);
1006
  }
1007
 
1008
- /* --- Toast System --- */
1009
- function showToast(message, type = 'info') {
1010
- const toast = document.createElement('div');
1011
- toast.className = `toast ${type}`;
1012
-
1013
- let icon = 'fa-info-circle';
1014
- if (type === 'success') icon = 'fa-check-circle';
1015
- if (type === 'error') icon = 'fa-exclamation-circle';
1016
-
1017
- toast.innerHTML = `<i class="fa-solid ${icon}"></i> <span>${message}</span>`;
1018
-
1019
- elements.toastContainer.appendChild(toast);
1020
-
1021
- // Remove after 3 seconds
1022
- setTimeout(() => {
1023
- toast.style.opacity = '0';
1024
- toast.style.transform = 'translateX(100%)';
1025
- setTimeout(() => toast.remove(), 300);
1026
- }, 3000);
1027
- }
1028
-
1029
- // Demo loader
1030
- window.loadDemo = function() {
1031
- // Load a demo image
1032
- const demoImg = "https://picsum.photos/seed/sora1/800/450";
1033
- elements.previewImage.src = demoImg;
1034
- elements.previewImage.style.display = 'block';
1035
- elements.emptyState.style.display = 'none';
1036
- state.imageLoaded = true;
1037
-
1038
- elements.previewImage.onload = () => {
1039
- resizeCanvas();
1040
- // Pre-fill prompt
1041
- document.getElementById('prompt-input').value = "Cyberpunk city streets in the rain, neon lights reflecting, cinematic 4k";
1042
- showToast('Demo project loaded', 'info');
1043
- }
1044
- };
1045
-
1046
- </script>
1047
- </body>
1048
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sprunki Custom Maker OC Phase 5</title>
7
+ <!-- Import Google Font for a Creepy/Digital look -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Creepster&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
9
+ <!-- Import FontAwesome for Icons -->
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
+
 
 
12
  <style>
13
  :root {
14
+ --bg-color: #0f0f11;
15
+ --panel-bg: #1a1a1e;
16
+ --accent-color: #76ff03; /* Toxic Green */
17
+ --accent-hover: #64dd17;
18
+ --text-color: #e0e0e0;
19
+ --border-color: #333;
20
+ --danger-color: #ff1744;
21
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
22
+ --font-display: 'Creepster', cursive;
23
+ --font-body: 'Roboto', sans-serif;
 
 
24
  }
25
 
26
  * {
27
  box-sizing: border-box;
28
  margin: 0;
29
  padding: 0;
30
+ -webkit-tap-highlight-color: transparent;
31
  }
32
 
33
  body {
34
+ background-color: var(--bg-color);
35
+ color: var(--text-color);
36
+ font-family: var(--font-body);
 
 
37
  display: flex;
38
  flex-direction: column;
39
+ min-height: 100vh;
40
+ overflow-x: hidden;
41
  }
42
 
43
  /* --- Header --- */
44
  header {
45
+ background: linear-gradient(180deg, #000000 0%, #1a1a1e 100%);
46
+ padding: 1rem 2rem;
 
 
47
  display: flex;
 
48
  justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
 
 
49
  align-items: center;
50
+ border-bottom: 2px solid var(--accent-color);
51
+ box-shadow: 0 0 15px rgba(118, 255, 3, 0.2);
52
+ position: relative;
53
+ z-index: 10;
 
54
  }
55
 
56
+ .brand {
57
  display: flex;
58
+ flex-direction: column;
59
  }
60
 
61
+ h1 {
62
+ font-family: var(--font-display);
63
+ font-size: 2.5rem;
64
+ color: var(--accent-color);
65
+ text-shadow: 2px 2px 0px #ff0000;
66
+ letter-spacing: 2px;
67
+ margin: 0;
68
+ line-height: 1;
69
  }
70
 
71
+ .subtitle {
72
+ font-size: 0.9rem;
73
+ color: #888;
74
+ margin-top: 5px;
75
+ text-transform: uppercase;
76
+ letter-spacing: 1px;
77
  }
78
 
79
+ .anycoder-link {
80
+ font-size: 0.85rem;
81
+ color: #fff;
82
  text-decoration: none;
83
+ background: #333;
84
+ padding: 5px 10px;
85
+ border-radius: 4px;
86
+ transition: all 0.3s ease;
87
+ border: 1px solid #555;
88
  }
89
 
90
+ .anycoder-link:hover {
91
+ background: var(--accent-color);
92
+ color: #000;
93
+ border-color: var(--accent-color);
94
+ box-shadow: 0 0 10px var(--accent-color);
95
  }
96
 
97
  /* --- Main Layout --- */
98
  main {
99
  flex: 1;
100
  display: grid;
101
+ grid-template-columns: 1fr 400px;
102
+ gap: 2rem;
103
+ padding: 2rem;
104
+ max-width: 1400px;
105
+ margin: 0 auto;
106
+ width: 100%;
 
 
 
 
 
 
 
 
107
  }
108
 
109
+ @media (max-width: 900px) {
110
+ main {
111
+ grid-template-columns: 1fr;
112
+ padding: 1rem;
113
+ }
114
+ h1 { font-size: 2rem; }
 
115
  }
116
 
117
+ /* --- Preview Section (Left) --- */
118
+ .preview-area {
119
+ background: var(--panel-bg);
120
+ border-radius: 12px;
121
+ border: 1px solid var(--border-color);
122
  display: flex;
123
  flex-direction: column;
124
+ align-items: center;
125
+ justify-content: center;
126
+ position: relative;
127
+ padding: 2rem;
128
+ box-shadow: var(--shadow);
129
+ min-height: 500px;
130
+ background-image: radial-gradient(circle at center, #2a2a30 0%, #1a1a1e 70%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  }
132
 
133
+ canvas {
134
+ max-width: 100%;
135
+ max-height: 500px;
136
+ filter: drop-shadow(0 0 20px rgba(0,0,0,0.5));
137
+ transition: transform 0.2s ease;
 
 
 
 
138
  }
139
 
140
+ .canvas-controls {
141
+ margin-top: 2rem;
142
+ display: flex;
143
+ gap: 1rem;
144
+ flex-wrap: wrap;
145
+ justify-content: center;
146
  }
147
 
148
+ .action-btn {
149
+ background: #333;
 
150
  color: white;
151
+ border: 1px solid var(--border-color);
152
+ padding: 10px 20px;
153
+ border-radius: 6px;
 
 
154
  cursor: pointer;
155
+ font-family: var(--font-body);
156
+ font-weight: bold;
157
  display: flex;
158
  align-items: center;
 
159
  gap: 8px;
160
+ transition: all 0.2s;
161
  }
162
 
163
+ .action-btn:hover {
164
+ background: #444;
165
+ transform: translateY(-2px);
166
  }
167
 
168
+ .action-btn.primary {
169
+ background: var(--accent-color);
170
+ color: #000;
171
+ border-color: var(--accent-color);
172
  }
173
 
174
+ .action-btn.primary:hover {
175
+ background: var(--accent-hover);
176
+ box-shadow: 0 0 15px rgba(118, 255, 3, 0.4);
 
 
177
  }
178
 
179
+ /* --- Customizer Section (Right) --- */
180
+ .customizer-panel {
181
+ background: var(--panel-bg);
182
+ border-radius: 12px;
183
+ border: 1px solid var(--border-color);
184
  display: flex;
185
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  overflow: hidden;
187
+ box-shadow: var(--shadow);
188
+ height: fit-content;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
 
191
+ /* Tabs */
192
+ .tabs {
193
+ display: flex;
194
+ background: #111;
195
+ overflow-x: auto;
196
+ scrollbar-width: none;
197
  }
198
+ .tabs::-webkit-scrollbar { display: none; }
199
 
200
+ .tab-btn {
201
+ background: transparent;
202
+ border: none;
203
+ color: #888;
204
+ padding: 1rem;
205
  cursor: pointer;
206
+ font-weight: bold;
207
+ text-transform: uppercase;
208
+ font-size: 0.85rem;
209
+ border-bottom: 3px solid transparent;
210
+ transition: all 0.3s;
211
+ white-space: nowrap;
 
 
 
 
 
212
  }
213
 
214
+ .tab-btn:hover {
215
+ color: #fff;
216
+ background: rgba(255,255,255,0.05);
 
 
 
 
 
217
  }
218
 
219
+ .tab-btn.active {
220
+ color: var(--accent-color);
221
+ border-bottom-color: var(--accent-color);
222
+ background: rgba(118, 255, 3, 0.05);
 
 
 
 
 
 
223
  }
224
 
225
+ /* Items Grid */
226
+ .items-container {
227
+ padding: 1.5rem;
228
+ flex: 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  }
230
 
231
+ .category-title {
232
+ margin-bottom: 1rem;
 
 
 
233
  font-size: 1.1rem;
234
+ color: #fff;
235
+ border-left: 4px solid var(--accent-color);
236
+ padding-left: 10px;
237
  }
238
 
239
+ .grid {
240
+ display: grid;
241
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
242
+ gap: 10px;
243
  }
244
 
245
+ .grid-item {
246
+ aspect-ratio: 1;
247
+ background: #25252a;
248
+ border: 2px solid #333;
249
+ border-radius: 8px;
250
+ cursor: pointer;
 
 
 
251
  display: flex;
 
252
  align-items: center;
253
  justify-content: center;
254
+ transition: all 0.2s;
255
+ position: relative;
256
+ overflow: hidden;
257
  }
258
 
259
+ .grid-item:hover {
260
+ border-color: #666;
261
+ transform: scale(1.05);
 
 
 
 
 
262
  }
263
 
264
+ .grid-item.selected {
265
+ border-color: var(--accent-color);
266
+ box-shadow: 0 0 10px rgba(118, 255, 3, 0.2);
267
+ background: #2a2a30;
268
  }
269
 
270
+ .grid-item canvas {
271
+ max-width: 90%;
272
+ max-height: 90%;
273
+ filter: none; /* Remove drop shadow for thumbnails */
274
  }
275
 
276
+ /* Color Picker */
277
+ .color-picker-container {
278
+ margin-top: 1rem;
279
+ padding: 1rem;
280
+ background: #25252a;
281
+ border-radius: 8px;
282
+ }
283
+
284
+ .color-label {
285
+ display: block;
286
+ margin-bottom: 0.5rem;
287
+ font-size: 0.9rem;
288
+ color: #aaa;
289
  }
290
 
291
+ .color-options {
292
+ display: flex;
293
+ gap: 10px;
294
+ flex-wrap: wrap;
295
  }
296
 
297
+ .color-swatch {
298
+ width: 30px;
299
+ height: 30px;
300
+ border-radius: 50%;
301
  cursor: pointer;
302
+ border: 2px solid transparent;
303
+ transition: transform 0.2s;
304
  }
305
 
306
+ .color-swatch:hover {
307
+ transform: scale(1.2);
 
308
  }
309
 
310
+ .color-swatch.active {
311
+ border-color: #fff;
312
+ box-shadow: 0 0 0 2px var(--accent-color);
 
 
313
  }
314
 
315
+ /* --- Footer --- */
316
+ footer {
317
+ text-align: center;
318
+ padding: 1.5rem;
319
+ color: #555;
320
+ font-size: 0.8rem;
321
+ margin-top: auto;
322
  }
323
 
324
+ /* --- Animations --- */
325
+ @keyframes float {
326
+ 0% { transform: translateY(0px); }
327
+ 50% { transform: translateY(-5px); }
328
+ 100% { transform: translateY(0px); }
 
 
329
  }
330
 
331
+ @keyframes glitch {
332
+ 0% { text-shadow: 2px 2px 0px #ff0000; }
333
+ 25% { text-shadow: -2px -2px 0px #00ffff; }
334
+ 50% { text-shadow: 2px -2px 0px #ff0000; }
335
+ 75% { text-shadow: -2px 2px 0px #00ffff; }
336
+ 100% { text-shadow: 2px 2px 0px #ff0000; }
337
  }
338
 
339
+ .glitch-hover:hover {
340
+ animation: glitch 0.3s infinite;
 
 
 
 
 
 
 
341
  }
342
 
343
+ /* Notification Toast */
344
  .toast {
345
+ position: fixed;
346
+ bottom: 20px;
347
+ right: 20px;
348
+ background: var(--accent-color);
349
+ color: #000;
350
+ padding: 10px 20px;
351
+ border-radius: 4px;
352
+ font-weight: bold;
353
+ transform: translateY(100px);
354
+ opacity: 0;
355
+ transition: all 0.3s ease;
356
+ z-index: 100;
357
+ box-shadow: 0 5px 15px rgba(0,0,0,0.5);
 
 
 
 
 
 
 
358
  }
359
+ .toast.show {
360
+ transform: translateY(0);
361
+ opacity: 1;
 
 
 
 
 
 
362
  }
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </style>
365
  </head>
366
  <body>
367
 
 
368
  <header>
369
+ <div class="brand">
370
+ <h1 class="glitch-hover">SPRUNKI MAKER</h1>
371
+ <span class="subtitle">OC Phase 5 Edition</span>
 
 
 
 
 
372
  </div>
373
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
374
+ Built with anycoder <i class="fas fa-external-link-alt"></i>
375
  </a>
376
  </header>
377
 
 
378
  <main>
379
+ <!-- Left Column: Preview -->
380
+ <section class="preview-area">
381
+ <canvas id="mainCanvas" width="500" height="500"></canvas>
382
+
383
+ <div class="canvas-controls">
384
+ <button class="action-btn" id="randomizeBtn">
385
+ <i class="fas fa-dice"></i> Randomize
386
+ </button>
387
+ <button class="action-btn" id="clearBtn">
388
+ <i class="fas fa-trash"></i> Clear
389
+ </button>
390
+ <button class="action-btn primary" id="downloadBtn">
391
+ <i class="fas fa-download"></i> Save OC
392
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  </div>
394
+ </section>
395
 
396
+ <!-- Right Column: Customizer -->
397
+ <section class="customizer-panel">
398
+ <div class="tabs" id="tabsContainer">
399
+ <!-- Tabs injected by JS -->
400
  </div>
401
 
402
+ <div class="items-container">
403
+ <h3 class="category-title" id="categoryTitle">Select a Category</h3>
404
+ <div class="grid" id="itemsGrid">
405
+ <!-- Items injected by JS -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  </div>
407
 
408
+ <!-- Color Picker (Only shows for specific categories) -->
409
+ <div class="color-picker-container" id="colorPickerContainer" style="display:none;">
410
+ <span class="color-label" id="colorLabel">Skin Tone</span>
411
+ <div class="color-options" id="colorOptions">
412
+ <!-- Colors injected by JS -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  </div>
414
  </div>
415
  </div>
416
+ </section>
 
417
  </main>
418
 
419
+ <footer>
420
+ <p>&copy; 2023 Sprunki Fan Tool. Not affiliated with Incredibox original creators.</p>
421
+ </footer>
422
+
423
+ <div class="toast" id="toast">Image Saved!</div>
424
 
425
  <script>
426
+ /**
427
+ * SPRUNKI MAKER ENGINE
428
+ * Procedural drawing logic to create Sprunki-style characters without external assets.
429
+ */
430
+
431
+ const canvas = document.getElementById('mainCanvas');
432
+ const ctx = canvas.getContext('2d');
433
+ const dpr = window.devicePixelRatio || 1;
434
+
435
+ // Fix for high DPI screens
436
+ canvas.width = 500 * dpr;
437
+ canvas.height = 500 * dpr;
438
+ ctx.scale(dpr, dpr);
439
+ canvas.style.width = '500px';
440
+ canvas.style.height = '500px';
441
+
442
+ // --- Configuration & State ---
443
+ const CATEGORIES = ['Base', 'Eyes', 'Mouth', 'Hat', 'Effect', 'Background'];
444
+
445
+ // Colors palette
446
+ const COLORS = {
447
+ skin: ['#e0ac69', '#8d5524', '#c68642', '#3d2314', '#9e9e9e', '#2c3e50', '#4a235a', '#212121'],
448
+ eyes: ['#ffffff', '#ff0000', '#00ff00', '#ffff00', '#000000', '#76ff03'],
449
+ misc: ['#ffffff', '#ff0000', '#000000', '#333333', '#76ff03']
450
  };
451
 
452
+ // Character State
453
+ let currentCharacter = {
454
+ Base: { id: 0, color: COLORS.skin[0] },
455
+ Eyes: { id: 0, color: COLORS.eyes[0] },
456
+ Mouth: { id: 0, color: '#000000' },
457
+ Hat: { id: -1, color: '#333333' }, // -1 means none
458
+ Effect: { id: -1, color: '#ffffff' },
459
+ Background: { id: 0, color: '#1a1a1e' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  };
461
 
462
+ let activeTab = 'Base';
463
 
464
+ // --- Drawing Functions (Procedural Assets) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
+ const Helpers = {
467
+ drawCircle: (x, y, r, color) => {
468
+ ctx.fillStyle = color;
469
+ ctx.beginPath();
470
+ ctx.arc(x, y, r, 0, Math.PI * 2);
471
+ ctx.fill();
472
+ },
473
+ drawRect: (x, y, w, h, color, rotation = 0) => {
474
+ ctx.save();
475
+ ctx.translate(x, y);
476
+ ctx.rotate(rotation);
477
+ ctx.fillStyle = color;
478
+ ctx.fillRect(-w / 2, -h / 2, w, h);
479
+ ctx.restore();
480
+ },
481
+ drawTriangle: (x, y, size, color, direction = 'up') => {
482
+ ctx.fillStyle = color;
483
+ ctx.beginPath();
484
+ if (direction === 'up') {
485
+ ctx.moveTo(x, y - size);
486
+ ctx.lineTo(x + size, y + size);
487
+ ctx.lineTo(x - size, y + size);
488
+ } else {
489
+ ctx.moveTo(x, y + size);
490
+ ctx.lineTo(x + size, y - size);
491
+ ctx.lineTo(x - size, y - size);
492
+ }
493
+ ctx.closePath();
494
+ ctx.fill();
495
+ }
496
+ };
497
 
498
+ const Drawers = {
499
+ Base: {
500
+ count: 5,
501
+ draw: (id, color) => {
502
+ // Standard Sprunki shape is usually a pear/blob
503
+ ctx.fillStyle = color;
504
+ ctx.beginPath();
 
505
 
506
+ if (id === 0) { // Normal
507
+ ctx.moveTo(250, 100); // Top head
508
+ ctx.bezierCurveTo(350, 100, 380, 200, 380, 250); // Right shoulder
509
+ ctx.bezierCurveTo(380, 350, 300, 420, 250, 420); // Bottom right
510
+ ctx.bezierCurveTo(200, 420, 120, 350, 120, 250); // Bottom left
511
+ ctx.bezierCurveTo(120, 200, 150, 100, 250, 100); // Left shoulder
512
+ } else if (id === 1) { // Round
513
+ ctx.arc(250, 250, 130, 0, Math.PI * 2);
514
+ } else if (id === 2) { // Tall
515
+ ctx.moveTo(250, 80);
516
+ ctx.bezierCurveTo(340, 80, 360, 200, 360, 250);
517
+ ctx.bezierCurveTo(360, 380, 300, 430, 250, 430);
518
+ ctx.bezierCurveTo(200, 430, 140, 380, 140, 250);
519
+ ctx.bezierCurveTo(140, 200, 160, 80, 250, 80);
520
+ } else if (id === 3) { // Wide
521
+ ctx.moveTo(250, 120);
522
+ ctx.bezierCurveTo(400, 120, 420, 220, 420, 250);
523
+ ctx.bezierCurveTo(420, 320, 300, 380, 250, 380);
524
+ ctx.bezierCurveTo(200, 380, 80, 320, 80, 250);
525
+ ctx.bezierCurveTo(80, 220, 100, 120, 250, 120);
526
+ } else if (id === 4) { // Sharp/Pointy
527
+ ctx.moveTo(250, 60);
528
+ ctx.lineTo(350, 250);
529
+ ctx.bezierCurveTo(350, 350, 300, 420, 250, 420);
530
+ ctx.bezierCurveTo(200, 420, 150, 350, 150, 250);
531
+ ctx.lineTo(250, 60);
532
+ }
533
 
534
+ ctx.fill();
535
+ // Add subtle shading
536
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
537
+ ctx.beginPath();
538
+ ctx.arc(280, 250, 100, 0, Math.PI * 2);
539
+ ctx.fill();
540
+ }
541
+ },
542
+ Eyes: {
543
+ count: 8,
544
+ draw: (id, color) => {
545
+ ctx.fillStyle = color;
546
+ ctx.strokeStyle = '#000';
547
+ ctx.lineWidth = 2;
548
+
549
+ const leftX = 210;
550
+ const rightX = 290;
551
+ const y = 220;
552
+
553
+ if (id === 0) { // Dots
554
+ Helpers.drawCircle(leftX, y, 10, color);
555
+ Helpers.drawCircle(rightX, y, 10, color);
556
+ } else if (id === 1) { // Hollow
557
+ ctx.beginPath(); ctx.arc(leftX, y, 12, 0, Math.PI*2); ctx.stroke();
558
+ ctx.beginPath(); ctx.arc(rightX, y, 12, 0, Math.PI*2); ctx.stroke();
559
+ } else if (id === 2) { // X eyes (Horror)
560
+ ctx.lineWidth = 4;
561
+ ctx.beginPath(); ctx.moveTo(leftX-10, y-10); ctx.lineTo(leftX+10, y+10); ctx.stroke();
562
+ ctx.beginPath(); ctx.moveTo(leftX+10, y-10); ctx.lineTo(leftX-10, y+10); ctx.stroke();
563
+ ctx.beginPath(); ctx.moveTo(rightX-10, y-10); ctx.lineTo(rightX+10, y+10); ctx.stroke();
564
+ ctx.beginPath(); ctx.moveTo(rightX+10, y-10); ctx.lineTo(rightX-10, y+10); ctx.stroke();
565
+ } else if (id === 3) { // Wide/Crazy
566
+ ctx.beginPath();
567
+ ctx.ellipse(leftX, y, 18, 8, 0, 0, Math.PI*2);
568
+ ctx.ellipse(rightX, y, 18, 8, 0, 0, Math.PI*2);
569
+ ctx.fill();
570
+ // Pupils
571
+ ctx.fillStyle = 'black';
572
+ Helpers.drawCircle(leftX, y, 3, 'black');
573
+ Helpers.drawCircle(rightX, y, 3, 'black');
574
+ } else if (id === 4) { // One Eye (Cyclops)
575
+ Helpers.drawCircle(250, y, 20, color);
576
+ ctx.fillStyle = 'black';
577
+ Helpers.drawCircle(250, y, 5, 'black');
578
+ } else if (id === 5) { // Visor
579
+ ctx.fillStyle = color;
580
+ ctx.fillRect(190, y-10, 120, 20);
581
+ // Glint
582
+ ctx.fillStyle = 'rgba(255,255,255,0.8)';
583
+ ctx.fillRect(200, y-5, 40, 5);
584
+ } else if (id === 6) { // Spiral
585
+ // Simple spiral approximation
586
+ ctx.strokeStyle = color;
587
+ [leftX, rightX].forEach(x => {
588
+ ctx.beginPath();
589
+ ctx.arc(x, y, 15, 0, Math.PI*2);
590
+ ctx.stroke();
591
+ ctx.beginPath();
592
+ ctx.arc(x, y, 8, 0, Math.PI*2);
593
+ ctx.stroke();
594
+ ctx.fillStyle = color;
595
+ Helpers.drawCircle(x, y, 2, color);
596
+ });
597
+ } else if (id === 7) { // Happy Arcs
598
+ ctx.strokeStyle = color;
599
+ ctx.lineWidth = 4;
600
+ ctx.beginPath(); ctx.arc(leftX, y, 10, Math.PI, 0); ctx.stroke();
601
+ ctx.beginPath(); ctx.arc(rightX, y, 10, Math.PI, 0); ctx.stroke();
602
+ }
603
+ }
604
+ },
605
+ Mouth: {
606
+ count: 7,
607
+ draw: (id, color) => {
608
+ ctx.fillStyle = color;
609
+ ctx.strokeStyle = color;
610
+ const x = 250;
611
+ const y = 290;
612
+
613
+ if (id === 0) { // Smile
614
+ ctx.beginPath();
615
+ ctx.arc(x, y, 20, 0, Math.PI);
616
+ ctx.fill();
617
+ } else if (id === 1) // Straight line
618
+ {
619
+ ctx.fillRect(220, y, 60, 5);
620
+ } else if (id === 2) // Open O
621
+ {
622
+ ctx.beginPath();
623
+ ctx.arc(x, y, 15, 0, Math.PI*2);
624
+ ctx.fill();
625
+ ctx.fillStyle = '#fff'; // Teeth hint
626
+ ctx.fillRect(240, y-2, 20, 4);
627
+ } else if (id === 3) // ZigZag (Horror)
628
+ {
629
+ ctx.beginPath();
630
+ ctx.moveTo(220, y);
631
+ ctx.lineTo(230, y+10);
632
+ ctx.lineTo(240, y-5);
633
+ ctx.lineTo(250, y+10);
634
+ ctx.lineTo(260, y-5);
635
+ ctx.lineTo(270, y+10);
636
+ ctx.lineTo(280, y);
637
+ ctx.lineWidth = 3;
638
+ ctx.stroke();
639
+ } else if (id === 4) // Frown
640
+ {
641
+ ctx.beginPath();
642
+ ctx.arc(x, y+20, 20, Math.PI, 0);
643
+ ctx.fill();
644
+ } else if (id === 5) // Stitches
645
+ {
646
+ ctx.strokeStyle = color;
647
+ ctx.lineWidth = 2;
648
+ ctx.beginPath();
649
+ ctx.moveTo(220, y+10); ctx.lineTo(280, y+10);
650
+ ctx.stroke();
651
+ // Vertical stitches
652
+ for(let i=0; i<5; i++) {
653
+ ctx.beginPath();
654
+ ctx.moveTo(230 + (i*12), y+5);
655
+ ctx.lineTo(230 + (i*12), y+15);
656
+ ctx.stroke();
657
+ }
658
+ } else if (id === 6) // Tongue out
659
+ {
660
+ ctx.beginPath();
661
+ ctx.arc(x, y, 10, 0, Math.PI);
662
+ ctx.fill();
663
+ ctx.fillStyle = '#d32f2f'; // Red tongue
664
+ ctx.beginPath();
665
+ ctx.ellipse(x, y+15, 8, 12, 0, 0, Math.PI*2);
666
+ ctx.fill();
667
+ }
668
+ }
669
+ },
670
+ Hat: {
671
+ count: 5,
672
+ draw: (id, color) => {
673
+ ctx.fillStyle = color;
674
+ const cx = 250;
675
+
676
+ if (id === 0) { // Top Hat
677
+ ctx.fillRect(cx-40, 60, 80, 80);
678
+ ctx.fillRect(cx-60, 140, 120, 15);
679
+ } else if (id === 1) { // Horns
680
+ ctx.beginPath();
681
+ ctx.moveTo(180, 120);
682
+ ctx.lineTo(150, 40);
683
+ ctx.lineTo(130, 120);
684
+ ctx.fill();
685
+ ctx.beginPath();
686
+ ctx.moveTo(320, 120);
687
+ ctx.lineTo(350, 40);
688
+ ctx.lineTo(370, 120);
689
+ ctx.fill();
690
+ } else if (id === 2) { // Crown
691
+ ctx.beginPath();
692
+ ctx.moveTo(cx-50, 150);
693
+ ctx.lineTo(cx-50, 80);
694
+ ctx.lineTo(cx-20, 110);
695
+ ctx.lineTo(cx, 60);
696
+ ctx.lineTo(cx+20, 110);
697
+ ctx.lineTo(cx+50, 80);
698
+ ctx.lineTo(cx+50, 150);
699
+ ctx.fill();
700
+ } else if (id === 3) { // Headphones
701
+ ctx.strokeStyle = color;
702
+ ctx.lineWidth = 15;
703
+ ctx.beginPath(); ctx.arc(cx, 220, 90, Math.PI, 0); ctx.stroke();
704
+ ctx.fillStyle = color;
705
+ ctx.fillRect(cx-105, 180, 20, 60); // Left cup
706
+ ctx.fillRect(cx+85, 180, 20, 60); // Right cup
707
+ } else if (id === 4) { // Halo
708
+ ctx.strokeStyle = '#ffff00';
709
+ ctx.lineWidth = 5;
710
+ ctx.beginPath();
711
+ ctx.arc(cx, 80, 40, 0.5, Math.PI - 0.5);
712
+ ctx.stroke();
713
+ }
714
+ }
715
+ },
716
+ Effect: {
717
+ count: 4,
718
+ draw: (id, color) => {
719
+ ctx.globalCompositeOperation = 'source-over';
720
+ ctx.fillStyle = color;
721
+ ctx.strokeStyle = color;
722
+
723
+ if (id === 0) { // Goop (Drips)
724
+ ctx.globalAlpha = 0.8;
725
+ ctx.beginPath();
726
+ ctx.moveTo(150, 400);
727
+ ctx.lineTo(140, 480); ctx.lineTo(150, 480); ctx.lineTo(160, 400);
728
+ ctx.moveTo(350, 400);
729
+ ctx.lineTo(340, 460); ctx.lineTo(350, 460); ctx.lineTo(360, 400);
730
+ ctx.fill();
731
+ } else if (id === 1) { // Glow/Aura
732
+ ctx.globalCompositeOperation = 'screen';
733
+ ctx.globalAlpha = 0.3;
734
+ const grad = ctx.createRadialGradient(250, 250, 100, 250, 250, 250);
735
+ grad.addColorStop(0, color);
736
+ grad.addColorStop(1, 'transparent');
737
+ ctx.fillStyle = grad;
738
+ ctx.fillRect(0,0,500,500);
739
+ } else if (id === 2) { // Static/Noise (Simulated with dots)
740
+ ctx.globalAlpha = 0.5;
741
+ for(let i=0; i<50; i++) {
742
+ const rx = Math.random() * 500;
743
+ const ry = Math.random() * 500;
744
+ ctx.fillRect(rx, ry, 2, 2);
745
+ }
746
+ } else if (id === 3) { // Scar
747
+ ctx.strokeStyle = '#a00';
748
+ ctx.lineWidth = 3;
749
+ ctx.beginPath();
750
+ ctx.moveTo(300, 200);
751
+ ctx.lineTo(330, 250);
752
+ ctx.lineTo(310, 280);
753
+ ctx.stroke();
754
+ }
755
+ ctx.globalAlpha = 1.0;
756
+ }
757
+ },
758
+ Background: {
759
+ count: 3,
760
+ draw: (id, color) => {
761
+ // Background is drawn first in render loop, but this defines the pattern
762
+ if (id === 0) { // Solid
763
+ // Handled by clearRect/fillRect in main render
764
+ } else if (id === 1) { // Radial Gradient
765
+ const grad = ctx.createRadialGradient(250, 250, 50, 250, 250, 400);
766
+ grad.addColorStop(0, '#2a2a30');
767
+ grad.addColorStop(1, '#000000');
768
+ ctx.fillStyle = grad;
769
+ ctx.fillRect(0,0,500,500);
770
+ } else if (id === 2) { // Grid
771
+ ctx.fillStyle = color;
772
+ ctx.fillRect(0,0,500,500);
773
+ ctx.strokeStyle = 'rgba(255,255,255,0.05)';
774
+ ctx.lineWidth = 1;
775
+ for(let i=0; i<500; i+=40) {
776
+ ctx.beginPath(); ctx.moveTo(i,0); ctx.lineTo(i,500); ctx.stroke();
777
+ ctx.beginPath(); ctx.moveTo(0,i); ctx.lineTo(500,i); ctx.stroke();
778
+ }
779
+ }
780
+ }
781
+ }
782
+ };
783
+
784
+ // --- Core Application Logic ---
785
+
786
+ function render() {
787
+ // 1. Draw Background
788
+ const bgState = currentCharacter.Background;
789
+ if (bgState.id === 0) {
790
+ ctx.fillStyle = bgState.color;
791
+ ctx.fillRect(0, 0, 500, 500);
792
  } else {
793
+ Drawers.Background.draw(bgState.id, bgState.color);
794
  }
 
795
 
796
+ // 2. Draw Base
797
+ Drawers.Base.draw(currentCharacter.Base.id, currentCharacter.Base.color);
 
 
 
798
 
799
+ // 3. Draw Eyes
800
+ Drawers.Eyes.draw(currentCharacter.Eyes.id, currentCharacter.Eyes.color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
 
802
+ // 4. Draw Mouth
803
+ Drawers.Mouth.draw(currentCharacter.Mouth.id, currentCharacter.Mouth.color);
 
 
 
 
 
 
804
 
805
+ // 5. Draw Hat
806
+ if (currentCharacter.Hat.id !== -1) {
807
+ Drawers.Hat.draw(currentCharacter.Hat.id, currentCharacter.Hat.color);
 
 
808
  }
 
 
809
 
810
+ // 6. Draw Effect
811
+ if (currentCharacter.Effect.id !== -1) {
812
+ Drawers.Effect.draw(currentCharacter.Effect.id, currentCharacter.Effect.color);
813
+ }
814
  }
815
 
816
+ function init() {
817
+ render();
818
+ setupUI();
819
+ }
820
+
821
+ function setupUI() {
822
+ const tabsContainer = document.getElementById('tabsContainer');
823
+ const grid = document.getElementById('itemsGrid');
824
+ const title = document.getElementById('categoryTitle');
825
+ const colorContainer = document.getElementById('colorPickerContainer');
826
+ const colorOptions = document.getElementById('colorOptions');
827
+ const colorLabel = document.getElementById('colorLabel');
828
+
829
+ // 1. Create Tabs
830
+ CATEGORIES.forEach(cat => {
831
+ const btn = document.createElement('button');
832
+ btn.className = `tab-btn ${cat === activeTab ? 'active' : ''}`;
833
+ btn.textContent = cat;
834
+ btn.onclick = () => {
835
+ activeTab = cat;
836
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
837
+ btn.classList.add('active');
838
+ refreshGrid();
839
+ };
840
+ tabsContainer.appendChild(btn);
841
+ });
842
+
843
+ // 2. Refresh Grid Logic
844
+ function refreshGrid() {
845
+ grid.innerHTML = '';
846
+ title.textContent = `Customize ${activeTab}`;
847
 
848
+ // Determine Drawer and Count
849
+ const drawer = Drawers[activeTab];
850
+ const count = drawer ? drawer.count : 0;
851
+ const currentId = currentCharacter[activeTab].id;
852
+
853
+ // Show Color Picker if applicable
854
+ if (activeTab === 'Base') {
855
+ colorContainer.style.display = 'block';
856
+ colorLabel.textContent = "Skin Tone";
857
+ renderColors(COLORS.skin, 'Base');
858
+ } else if (activeTab === 'Eyes') {
859
+ colorContainer.style.display = 'block';
860
+ colorLabel.textContent = "Eye Color";
861
+ renderColors(COLORS.eyes, 'Eyes');
862
+ } else if (activeTab === 'Hat') {
863
+ colorContainer.style.display = 'block';
864
+ colorLabel.textContent = "Hat Color";
865
+ renderColors(COLORS.misc, 'Hat');
866
  } else {
867
+ colorContainer.style.display = 'none';
 
 
 
868
  }
 
 
 
 
869
 
870
+ // Generate Grid Items
871
+ for (let i = 0; i < count; i++) {
872
+ const itemDiv = document.createElement('div');
873
+ itemDiv.className = `grid-item ${currentId === i ? 'selected' : ''}`;
874
+
875
+ // Create mini canvas for thumbnail
876
+ const miniCanvas = document.createElement('canvas');
877
+ miniCanvas.width = 80;
878
+ miniCanvas.height = 80;
879
+ const mCtx = miniCanvas.getContext('2d');
880
+
881
+ // Draw preview on mini canvas
882
+ mCtx.save();
883
+ mCtx.translate(40, 40); // Center
884
+ mCtx.scale(0.15, 0.15); // Scale down to fit thumbnail
885
+ mCtx.translate(-250, -250); // Offset back to center of main canvas coords
886
+
887
+ // For thumbnails, we usually only draw the part itself, not the whole body
888
+ // But to keep it simple, we just render the specific drawer
889
+ // However, hats need to be positioned correctly.
890
+ // Simplified: We just draw the item relative to center 0,0 in local space
891
+ // Actually, let's just reuse the drawer but with a simplified background
892
+
893
+ // Reset transform for simpler drawing in thumbnail
894
+ mCtx.restore();
895
+ mCtx.clearRect(0,0,80,80);
896
+ mCtx.save();
897
+ mCtx.scale(0.2, 0.2);
898
+
899
+ // Draw a dummy background for visibility
900
+ if(activeTab === 'Eyes' || activeTab === 'Mouth') {
901
+ mCtx.fillStyle = '#ccc';
902
+ mCtx.fillRect(0,0,400,400);
903
+ }
904
+
905
+ // We need to adjust drawer calls to work in small canvas.
906
+ // Since drawers are hardcoded to 500x500 coords, we scale context
907
+ mCtx.translate(0,0);
908
+
909
+ // Re-implement simplified drawing for thumbnails to avoid offset issues
910
+ // Or just use the main drawer but shift context
911
+ mCtx.scale(1,1);
912
+ // Hack: Just draw a simple shape representation for thumbnail
913
+ drawThumbnailIcon(mCtx, activeTab, i);
914
+
915
+ itemDiv.appendChild(miniCanvas);
916
+
917
+ itemDiv.onclick = () => {
918
+ currentCharacter[activeTab].id = i;
919
+ render();
920
+ refreshGrid(); // Update selection state
921
+ };
922
 
923
+ grid.appendChild(itemDiv);
924
+ }
925
+ }
926
 
927
+ function renderColors(palette, category) {
928
+ colorOptions.innerHTML = '';
929
+ palette.forEach(color => {
930
+ const swatch = document.createElement('div');
931
+ swatch.className = `color-swatch ${currentCharacter[category].color === color ? 'active' : ''}`;
932
+ swatch.style.backgroundColor = color;
933
+ swatch.onclick = () => {
934
+ currentCharacter[category].color = color;
935
+ render();
936
+ renderColors(palette, category); // Update active state
937
+ };
938
+ colorOptions.appendChild(swatch);
939
+ });
940
  }
941
 
942
+ // Helper for thumbnail icons
943
+ function drawThumbnailIcon(ctx, cat, id) {
944
+ const cx = 100; // Center of 200x200 space (since scale is 0.2 of 500)
945
+ const cy = 100;
946
+ ctx.fillStyle = '#fff';
947
+ ctx.strokeStyle = '#fff';
948
+
949
+ if (cat === 'Base') {
950
+ ctx.fillStyle = '#555';
951
+ ctx.beginPath(); ctx.arc(cx, cy, 50, 0, Math.PI*2); ctx.fill();
952
+ } else if (cat === 'Eyes') {
953
+ if (id===0) { ctx.beginPath(); ctx.arc(cx-15, cy, 5, 0, Math.PI*2); ctx.arc(cx+15, cy, 5, 0, Math.PI*2); ctx.fill(); }
954
+ else if (id===2) { // X
955
+ ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(cx-20,cy-10); ctx.lineTo(cx-10,cy+10); ctx.stroke();
956
+ ctx.beginPath(); ctx.moveTo(cx-10,cy-10); ctx.lineTo(cx-20,cy+10); ctx.stroke();
957
+ ctx.beginPath(); ctx.moveTo(cx+20,cy-10); ctx.lineTo(cx+10,cy+10); ctx.stroke();
958
+ ctx.beginPath(); ctx.moveTo(cx+10,cy-10); ctx.lineTo(cx+20,cy+10); ctx.stroke();
959
+ } else {
960
+ ctx.beginPath(); ctx.arc(cx-15, cy, 8, 0, Math.PI*2); ctx.arc(cx+15, cy, 8, 0, Math.PI*2); ctx.stroke();
961
+ }
962
+ } else if (cat === 'Mouth') {
963
+ ctx.beginPath(); ctx.arc(cx, cy+10, 15, 0, Math.PI); ctx.fill();
964
+ } else if (cat === 'Hat') {
965
+ ctx.fillRect(cx-20, cy-30, 40, 30);
966
+ } else if (cat === 'Effect') {
967
+ ctx.fillStyle = 'red'; ctx.globalAlpha=0.5; ctx.fillRect(cx-10, cy, 20, 20);
968
+ } else if (cat === 'Background') {
969
+ ctx.fillStyle = id === 0 ? '#000' : '#222';
970
+ ctx.fillRect(0,0,200,200);
971
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972
  }
 
973
 
974
+ // Initial call
975
+ refreshGrid();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
  }
977
 
978
+ // --- Event Listeners for Buttons ---
979
+
980
+ document.getElementById('randomizeBtn').addEventListener('click', () => {
981
+ CATEGORIES.forEach(cat => {
982
+ const count = Drawers[cat].count;
983
+ // Random ID
984
+ const randId = Math.floor(Math.random() * count);
985
+ currentCharacter[cat].id