@spark-apps/piclet 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/{Readme.md → README.md} +31 -6
- package/dist/cli.js +3027 -1191
- package/dist/cli.js.map +1 -1
- package/dist/gui/border.html +229 -0
- package/dist/gui/css/theme.css +88 -88
- package/dist/gui/extract-frames.html +156 -0
- package/dist/gui/filter.html +180 -0
- package/dist/gui/iconpack.html +113 -113
- package/dist/gui/js/piclet.js +10 -1
- package/dist/gui/loading.hta +8 -17
- package/dist/gui/piclet.html +1529 -998
- package/dist/gui/recolor.html +243 -0
- package/dist/gui/remove-bg.html +178 -178
- package/dist/gui/storepack.html +179 -179
- package/dist/gui/transform.html +202 -0
- package/package.json +2 -2
package/dist/gui/piclet.html
CHANGED
|
@@ -1,998 +1,1529 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html data-piclet data-width="
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>PicLet</title>
|
|
7
|
-
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
-
<script src="/js/piclet.js"></script>
|
|
9
|
-
<style>
|
|
10
|
-
.app{display:flex;flex-direction:column;height:100%}
|
|
11
|
-
.main{display:flex;flex:1;gap:12px;overflow:hidden}
|
|
12
|
-
|
|
13
|
-
/* Left panel - Preview */
|
|
14
|
-
.preview-panel{flex:1;min-width:200px;display:flex;flex-direction:column;gap:8px}
|
|
15
|
-
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;overflow:auto;position:relative;min-height:180px;transition:border-color .2s}
|
|
16
|
-
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
17
|
-
.preview-area.dragover{border:2px dashed var(--acc);background:rgba(255,204,0,0.05)}
|
|
18
|
-
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1;transform-origin:center;cursor:grab}
|
|
19
|
-
.preview-area img.panning{cursor:grabbing}
|
|
20
|
-
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3);text-align:center;padding:12px}
|
|
21
|
-
.preview-area .mini-sp{width:20px;height:20px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
22
|
-
.preview-info{font-size:10px;color:var(--txt3);text-align:center}
|
|
23
|
-
.preview-zoom{display:flex;align-items:center;justify-content:center;gap:4px}
|
|
24
|
-
.preview-zoom button{width:22px;height:22px;padding:0;font-size:14px;background:var(--bg2);border:1px solid var(--brd);border-radius:4px;color:var(--txt2);cursor:pointer;display:flex;align-items:center;justify-content:center}
|
|
25
|
-
.preview-zoom button:hover{border-color:var(--acc);color:var(--acc)}
|
|
26
|
-
.preview-zoom span{font-size:10px;color:var(--txt3);min-width:40px;text-align:center}
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.tool
|
|
48
|
-
.tool.
|
|
49
|
-
.tool-
|
|
50
|
-
.tool-header
|
|
51
|
-
.tool-header
|
|
52
|
-
.tool
|
|
53
|
-
.tool.active .tool-header
|
|
54
|
-
.tool-
|
|
55
|
-
.tool.active .tool-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.tool-
|
|
63
|
-
.tool-
|
|
64
|
-
.tool-
|
|
65
|
-
.tool-
|
|
66
|
-
.tool-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.tool-
|
|
70
|
-
.tool-row
|
|
71
|
-
.tool-row
|
|
72
|
-
.tool-
|
|
73
|
-
.tool-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.dim-
|
|
88
|
-
.dim-
|
|
89
|
-
.dim-
|
|
90
|
-
.dim-
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.hd
|
|
113
|
-
.hd-
|
|
114
|
-
.hd-
|
|
115
|
-
.hd-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
.
|
|
124
|
-
.
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<
|
|
203
|
-
<span class="
|
|
204
|
-
</div>
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<div class="
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
</
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
$('
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html data-piclet data-width="560" data-height="960">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>PicLet</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
+
<script src="/js/piclet.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
.app{display:flex;flex-direction:column;height:100%}
|
|
11
|
+
.main{display:flex;flex:1;gap:12px;overflow:hidden;margin-top:10px}
|
|
12
|
+
|
|
13
|
+
/* Left panel - Preview */
|
|
14
|
+
.preview-panel{flex:1;min-width:200px;display:flex;flex-direction:column;gap:8px}
|
|
15
|
+
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;overflow:auto;position:relative;min-height:180px;transition:border-color .2s}
|
|
16
|
+
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
17
|
+
.preview-area.dragover{border:2px dashed var(--acc);background:rgba(255,204,0,0.05)}
|
|
18
|
+
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1;transform-origin:center;cursor:grab}
|
|
19
|
+
.preview-area img.panning{cursor:grabbing}
|
|
20
|
+
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3);text-align:center;padding:12px}
|
|
21
|
+
.preview-area .mini-sp{width:20px;height:20px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
22
|
+
.preview-info{font-size:10px;color:var(--txt3);text-align:center}
|
|
23
|
+
.preview-zoom{display:flex;align-items:center;justify-content:center;gap:4px}
|
|
24
|
+
.preview-zoom button{width:22px;height:22px;padding:0;font-size:14px;background:var(--bg2);border:1px solid var(--brd);border-radius:4px;color:var(--txt2);cursor:pointer;display:flex;align-items:center;justify-content:center}
|
|
25
|
+
.preview-zoom button:hover{border-color:var(--acc);color:var(--acc)}
|
|
26
|
+
.preview-zoom span{font-size:10px;color:var(--txt3);min-width:40px;text-align:center}
|
|
27
|
+
.preview-zoom .zoom-sep{width:1px;height:16px;background:var(--brd);margin:0 6px;min-width:1px}
|
|
28
|
+
.play-controls{display:none;align-items:center;gap:4px}
|
|
29
|
+
.play-controls.show{display:flex}
|
|
30
|
+
.play-controls button{width:26px;font-size:10px}
|
|
31
|
+
.play-controls button.playing{background:var(--acc);border-color:var(--acc);color:#000}
|
|
32
|
+
.play-controls select{padding:3px 4px;font-size:10px;background:var(--bg2);color:var(--txt2);border:1px solid var(--brd);border-radius:4px;cursor:pointer}
|
|
33
|
+
.play-controls select:hover{border-color:var(--acc)}
|
|
34
|
+
.load-btn{width:100%;padding:8px;font-size:11px;background:var(--bg2);border:1px dashed var(--brd);border-radius:6px;color:var(--txt3);cursor:pointer;transition:all .2s}
|
|
35
|
+
.load-btn:hover{border-color:var(--acc);color:var(--acc)}
|
|
36
|
+
|
|
37
|
+
/* Resizable divider */
|
|
38
|
+
.divider{width:6px;cursor:col-resize;background:transparent;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin:0 -2px}
|
|
39
|
+
.divider:hover,.divider.dragging{background:var(--bg3)}
|
|
40
|
+
.divider::after{content:'';width:2px;height:40px;background:var(--brd);border-radius:1px;transition:background .2s}
|
|
41
|
+
.divider:hover::after,.divider.dragging::after{background:var(--acc)}
|
|
42
|
+
|
|
43
|
+
/* Right panel - Tools */
|
|
44
|
+
.tools-panel{width:300px;min-width:240px;flex-shrink:0;display:flex;flex-direction:column;gap:6px;overflow-y:auto;overflow-x:hidden;padding-right:4px;align-self:flex-start;max-height:100%;box-sizing:border-box}
|
|
45
|
+
|
|
46
|
+
/* Tool sections */
|
|
47
|
+
.tool{background:var(--bg2);border-radius:6px;border:1px solid var(--brd);overflow:hidden;min-width:0}
|
|
48
|
+
.tool.disabled{opacity:0.35;pointer-events:none}
|
|
49
|
+
.tool.active{border-color:var(--acc)}
|
|
50
|
+
.tool-header{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
|
|
51
|
+
.tool-header:hover{background:rgba(255,255,255,0.02)}
|
|
52
|
+
.tool-header img{width:18px;height:18px;opacity:0.7}
|
|
53
|
+
.tool.active .tool-header img{opacity:1}
|
|
54
|
+
.tool-header .name{flex:1;font-size:12px;color:var(--txt2)}
|
|
55
|
+
.tool.active .tool-header .name{color:var(--txt);font-weight:500}
|
|
56
|
+
.tool-header input[type="checkbox"]{display:none}
|
|
57
|
+
.tool-header .toggle{width:16px;height:16px;border:1.5px solid var(--brd);border-radius:4px;display:flex;align-items:center;justify-content:center}
|
|
58
|
+
.tool-header .toggle svg{width:10px;height:10px;fill:none;stroke:var(--acc);stroke-width:2.5;opacity:0}
|
|
59
|
+
.tool.active .tool-header .toggle{border-color:var(--acc);background:var(--acc)}
|
|
60
|
+
.tool.active .tool-header .toggle svg{opacity:1;stroke:#000}
|
|
61
|
+
.tool-body{display:none;padding:6px 10px 10px;border-top:1px solid var(--brd);min-width:0;overflow:hidden}
|
|
62
|
+
.tool.active .tool-body{display:block}
|
|
63
|
+
#t-removebg .tool-body{min-height:70px}
|
|
64
|
+
#t-scale .tool-body{min-height:95px}
|
|
65
|
+
#t-icons .tool-body{min-height:85px}
|
|
66
|
+
#t-storepack .tool-body{min-height:170px}
|
|
67
|
+
|
|
68
|
+
/* Tool options */
|
|
69
|
+
.tool-opts{display:flex;flex-direction:column;gap:6px}
|
|
70
|
+
.tool-row{display:flex;align-items:center;gap:8px}
|
|
71
|
+
.tool-row label{font-size:10px;color:var(--txt3);width:55px;flex-shrink:0}
|
|
72
|
+
.tool-row input[type="number"]{width:45px;padding:4px 6px;font-size:11px}
|
|
73
|
+
.tool-row input[type="range"]{flex:1;height:4px}
|
|
74
|
+
.tool-row .val{width:45px;font-size:10px;color:var(--acc2);text-align:right}
|
|
75
|
+
.tool-row select{flex:1;padding:6px 8px;font-size:11px;background:var(--bg3);color:var(--txt);border:1px solid var(--brd);border-radius:4px;cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M3 4l3 4 3-4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;padding-right:24px}
|
|
76
|
+
.tool-row select:hover{border-color:var(--acc)}
|
|
77
|
+
.tool-row select:focus{outline:none;border-color:var(--acc)}
|
|
78
|
+
.tool-row select option{background:var(--bg2);color:var(--txt);padding:8px}
|
|
79
|
+
.tool-opts .opt{font-size:10px;padding:2px 0}
|
|
80
|
+
.tool-opts .opt .box{width:12px;height:12px}
|
|
81
|
+
|
|
82
|
+
.platforms{display:flex;gap:4px}
|
|
83
|
+
.platforms .opt{flex:1;padding:6px 8px;background:var(--bg3);border-radius:4px;font-size:10px;white-space:nowrap}
|
|
84
|
+
.platform-info{display:none}
|
|
85
|
+
|
|
86
|
+
/* Dimension list */
|
|
87
|
+
.dim-list{display:flex;flex-wrap:wrap;gap:4px;max-height:80px;overflow-y:auto;margin-bottom:6px}
|
|
88
|
+
.dim-item{display:flex;align-items:center;gap:4px;padding:3px 6px;background:var(--bg3);border-radius:4px;font-size:10px;color:var(--txt2)}
|
|
89
|
+
.dim-item .dim-x{color:var(--txt3);font-size:9px}
|
|
90
|
+
.dim-item .dim-rm{width:12px;height:12px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--txt3);border-radius:2px;margin-left:2px}
|
|
91
|
+
.dim-item .dim-rm:hover{background:rgba(255,100,100,0.2);color:#f66}
|
|
92
|
+
.dim-add{display:flex;align-items:center;gap:4px;margin-bottom:8px}
|
|
93
|
+
.dim-add input{width:50px;padding:4px 6px;font-size:10px;background:var(--bg3);border:1px solid var(--brd);border-radius:4px;color:var(--txt);-moz-appearance:textfield}
|
|
94
|
+
.dim-add input::-webkit-outer-spin-button,.dim-add input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
95
|
+
.dim-add span{color:var(--txt3);font-size:10px}
|
|
96
|
+
.dim-add-btn{width:22px;height:22px;padding:0;font-size:14px;background:var(--acc);border:none;border-radius:4px;color:#000;cursor:pointer;font-weight:bold}
|
|
97
|
+
.dim-add-btn:hover{background:var(--acc2)}
|
|
98
|
+
.preset-actions{display:flex;gap:4px}
|
|
99
|
+
.preset-actions input{flex:1;padding:5px 8px;font-size:10px;background:var(--bg3);border:1px solid var(--brd);border-radius:4px;color:var(--txt)}
|
|
100
|
+
.btn-sm{padding:5px 10px;font-size:10px;background:var(--bg3);border:1px solid var(--brd);border-radius:4px;color:var(--txt);cursor:pointer}
|
|
101
|
+
.btn-sm:hover{border-color:var(--acc);color:var(--acc)}
|
|
102
|
+
.btn-sm.btn-del{color:#a66}
|
|
103
|
+
.btn-sm.btn-del:hover{border-color:#c44;color:#c44}
|
|
104
|
+
|
|
105
|
+
/* Bottom bar */
|
|
106
|
+
.bottom-bar{display:flex;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--brd)}
|
|
107
|
+
.bottom-bar .btn{flex:1}
|
|
108
|
+
.apply-btn{background:var(--acc)!important;color:#000!important;font-weight:600}
|
|
109
|
+
.apply-btn:disabled{opacity:0.5;cursor:not-allowed}
|
|
110
|
+
|
|
111
|
+
/* Header branding */
|
|
112
|
+
.hd{flex-direction:column;align-items:stretch;gap:0;padding-bottom:8px;border-bottom:1px solid var(--brd);position:relative}
|
|
113
|
+
.hd-top{display:flex;align-items:center;gap:10px;width:100%}
|
|
114
|
+
.hd-brand{display:flex;flex-direction:column;gap:2px}
|
|
115
|
+
.hd-title{display:flex;align-items:baseline;gap:8px}
|
|
116
|
+
.hd-title b{font-size:20px;color:#eab308;letter-spacing:-0.5px;font-weight:700}
|
|
117
|
+
.hd-subtitle{font-size:9px;color:var(--acc);font-weight:600;text-transform:uppercase;letter-spacing:1.5px;opacity:0.9}
|
|
118
|
+
.hd-desc{font-size:10px;color:var(--txt3);margin-top:1px}
|
|
119
|
+
.hd-links{margin-left:auto;display:flex;gap:6px;align-items:center}
|
|
120
|
+
.hd-link{font-size:9px;color:var(--acc);text-decoration:none;padding:5px 10px;background:rgba(234,179,8,0.1);border:1px solid rgba(234,179,8,0.3);border-radius:4px;transition:all .2s;font-weight:500}
|
|
121
|
+
.hd-link:hover{color:#000;border-color:var(--acc);background:var(--acc)}
|
|
122
|
+
.hd-coffee{display:flex;align-items:center;gap:4px;font-size:9px;color:#ffdd00;text-decoration:none;padding:5px 8px;background:rgba(255,221,0,0.1);border:1px solid rgba(255,221,0,0.25);border-radius:4px;transition:all .2s;font-weight:500}
|
|
123
|
+
.hd-coffee:hover{color:#000;border-color:#ffdd00;background:#ffdd00}
|
|
124
|
+
.hd-coffee svg{width:12px;height:12px;fill:currentColor}
|
|
125
|
+
.hd-meta{display:flex;align-items:center;gap:12px;font-size:10px;color:var(--txt3);margin-top:8px;padding-top:8px;border-top:1px solid var(--brd);width:100%}
|
|
126
|
+
.hd-meta b{color:var(--txt2);font-weight:500;margin-left:3px}
|
|
127
|
+
|
|
128
|
+
/* States */
|
|
129
|
+
.main.hide{display:none}
|
|
130
|
+
.bottom-bar.hide{display:none}
|
|
131
|
+
|
|
132
|
+
/* Themed alert modal */
|
|
133
|
+
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:100;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
134
|
+
.modal-overlay.on{opacity:1;pointer-events:auto}
|
|
135
|
+
.modal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:16px 20px;min-width:200px;max-width:300px;text-align:center}
|
|
136
|
+
.modal-msg{font-size:12px;color:var(--txt);margin-bottom:12px}
|
|
137
|
+
.modal-btns{display:flex;gap:8px;justify-content:center}
|
|
138
|
+
.modal-btn{padding:6px 20px;font-size:11px;background:var(--acc);border:none;border-radius:4px;color:#000;cursor:pointer;font-weight:500}
|
|
139
|
+
.modal-btn:hover{background:var(--acc2)}
|
|
140
|
+
.modal-btn.danger{background:#c44;color:#fff}
|
|
141
|
+
.modal-btn.danger:hover{background:#a33}
|
|
142
|
+
.modal-btn.secondary{background:var(--bg3);color:var(--txt);border:1px solid var(--brd)}
|
|
143
|
+
.modal-btn.secondary:hover{border-color:var(--acc)}
|
|
144
|
+
|
|
145
|
+
/* GIF Frame Strip - Left vertical panel */
|
|
146
|
+
.frame-panel{display:none;width:80px;min-width:60px;flex-shrink:0;flex-direction:column;gap:6px;background:var(--bg2);border-radius:6px;padding:8px;overflow:hidden}
|
|
147
|
+
.frame-panel.show{display:flex}
|
|
148
|
+
.frame-panel-header{font-size:9px;color:var(--txt3);text-align:center;padding-bottom:6px;border-bottom:1px solid var(--brd)}
|
|
149
|
+
.frame-panel-header b{color:var(--acc2);display:block;font-size:11px}
|
|
150
|
+
.frame-strip-scroll{display:flex;flex-direction:column;gap:4px;overflow-y:auto;flex:1;padding:4px 0;scrollbar-width:thin}
|
|
151
|
+
.frame-strip-scroll::-webkit-scrollbar{width:4px}
|
|
152
|
+
.frame-strip-scroll::-webkit-scrollbar-track{background:var(--bg3);border-radius:2px}
|
|
153
|
+
.frame-strip-scroll::-webkit-scrollbar-thumb{background:var(--brd);border-radius:2px}
|
|
154
|
+
.frame-strip-scroll::-webkit-scrollbar-thumb:hover{background:var(--txt3)}
|
|
155
|
+
.frame-thumb{flex-shrink:0;width:100%;aspect-ratio:1;border:2px solid var(--brd);border-radius:4px;overflow:hidden;cursor:pointer;position:relative;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/8px 8px}
|
|
156
|
+
.frame-thumb:hover{border-color:var(--txt3)}
|
|
157
|
+
.frame-thumb.selected{border-color:var(--acc)}
|
|
158
|
+
.frame-thumb img{width:100%;height:100%;object-fit:contain}
|
|
159
|
+
.frame-thumb .frame-num{position:absolute;bottom:1px;right:2px;font-size:8px;color:#fff;text-shadow:0 0 2px #000,0 0 2px #000}
|
|
160
|
+
.frame-thumb.processing::after{content:'';position:absolute;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center}
|
|
161
|
+
.frame-thumb.processing::before{content:'';position:absolute;top:50%;left:50%;width:12px;height:12px;margin:-6px 0 0 -6px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;z-index:1}
|
|
162
|
+
.gif-export-btn{display:none;width:100%;padding:8px 4px;font-size:10px;background:var(--acc);border:none;border-radius:4px;color:#000;cursor:pointer;font-weight:600;margin-top:6px}
|
|
163
|
+
.gif-export-btn.show{display:block}
|
|
164
|
+
.gif-export-btn:hover{background:var(--acc2)}
|
|
165
|
+
|
|
166
|
+
/* Export modal */
|
|
167
|
+
.export-modal{background:var(--bg2);border:1px solid var(--brd);border-radius:8px;padding:16px 20px;min-width:280px;max-width:320px}
|
|
168
|
+
.export-modal h3{font-size:14px;color:var(--txt);margin:0 0 12px;font-weight:600}
|
|
169
|
+
.export-options{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
|
|
170
|
+
.export-opt{display:flex;align-items:flex-start;gap:10px;padding:10px;background:var(--bg3);border:1px solid var(--brd);border-radius:6px;cursor:pointer;transition:all .2s}
|
|
171
|
+
.export-opt:hover{border-color:var(--txt3)}
|
|
172
|
+
.export-opt.selected{border-color:var(--acc);background:rgba(234,179,8,0.1)}
|
|
173
|
+
.export-opt input{display:none}
|
|
174
|
+
.export-opt .radio{width:16px;height:16px;border:2px solid var(--brd);border-radius:50%;flex-shrink:0;margin-top:2px;position:relative}
|
|
175
|
+
.export-opt.selected .radio{border-color:var(--acc)}
|
|
176
|
+
.export-opt.selected .radio::after{content:'';position:absolute;top:3px;left:3px;width:6px;height:6px;background:var(--acc);border-radius:50%}
|
|
177
|
+
.export-opt-content{flex:1;min-width:0}
|
|
178
|
+
.export-opt-title{font-size:12px;color:var(--txt);font-weight:500;margin-bottom:2px}
|
|
179
|
+
.export-opt-desc{font-size:10px;color:var(--txt3)}
|
|
180
|
+
.export-opt-thumb{width:40px;height:40px;border-radius:4px;overflow:hidden;flex-shrink:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/8px 8px}
|
|
181
|
+
.export-opt-thumb img{width:100%;height:100%;object-fit:contain}
|
|
182
|
+
.export-modal-btns{display:flex;gap:8px;justify-content:flex-end}
|
|
183
|
+
.export-modal-btns button{padding:8px 16px;font-size:11px;border-radius:4px;cursor:pointer;font-weight:500}
|
|
184
|
+
.export-modal-btns .cancel{background:var(--bg3);border:1px solid var(--brd);color:var(--txt)}
|
|
185
|
+
.export-modal-btns .cancel:hover{border-color:var(--acc)}
|
|
186
|
+
.export-modal-btns .confirm{background:var(--acc);border:none;color:#000}
|
|
187
|
+
.export-modal-btns .confirm:hover{background:var(--acc2)}
|
|
188
|
+
/* Left divider for frame panel */
|
|
189
|
+
.divider-left{display:none;width:6px;cursor:col-resize;background:transparent;align-items:center;justify-content:center;flex-shrink:0;margin:0 -2px}
|
|
190
|
+
.divider-left.show{display:flex}
|
|
191
|
+
.divider-left:hover,.divider-left.dragging{background:var(--bg3)}
|
|
192
|
+
.divider-left::after{content:'';width:2px;height:40px;background:var(--brd);border-radius:1px;transition:background .2s}
|
|
193
|
+
.divider-left:hover::after,.divider-left.dragging::after{background:var(--acc)}
|
|
194
|
+
</style>
|
|
195
|
+
</head>
|
|
196
|
+
<body>
|
|
197
|
+
<div class="app">
|
|
198
|
+
<div class="hd">
|
|
199
|
+
<div class="hd-top">
|
|
200
|
+
<div class="hd-brand">
|
|
201
|
+
<div class="hd-title">
|
|
202
|
+
<b>PicLet</b>
|
|
203
|
+
<span class="hd-subtitle">Edit. Scale. Ship.</span>
|
|
204
|
+
</div>
|
|
205
|
+
<span class="hd-desc">Lightweight image tools for content creators</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="hd-links">
|
|
208
|
+
<a href="https://buymeacoffee.com/spark88" class="hd-coffee" onclick="PicLet.openUrl('https://buymeacoffee.com/spark88');return false">
|
|
209
|
+
<svg viewBox="0 0 24 24"><path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364z"/></svg>
|
|
210
|
+
Donate
|
|
211
|
+
</a>
|
|
212
|
+
<a href="https://piclet.app" class="hd-link" onclick="PicLet.openUrl('https://piclet.app');return false">piclet.app</a>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="hd-meta">
|
|
216
|
+
<div>File<b id="fileName">-</b></div>
|
|
217
|
+
<div>Original<b id="origSize">-</b></div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<!-- Main content -->
|
|
222
|
+
<div class="main" id="M">
|
|
223
|
+
<!-- GIF Frame Panel (left) -->
|
|
224
|
+
<div class="frame-panel" id="framePanel">
|
|
225
|
+
<div class="frame-panel-header">
|
|
226
|
+
<b id="frameCount">0</b>
|
|
227
|
+
<span>Frames</span>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="frame-strip-scroll" id="frameScroll"></div>
|
|
230
|
+
<button class="gif-export-btn" id="gifExportBtn" onclick="showExportModal()">Export</button>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- Left divider (for frame panel) -->
|
|
234
|
+
<div class="divider-left" id="dividerLeft"></div>
|
|
235
|
+
|
|
236
|
+
<!-- Preview panel -->
|
|
237
|
+
<div class="preview-panel">
|
|
238
|
+
<div class="preview-area" id="pA">
|
|
239
|
+
<span class="placeholder">Enable tools to preview</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="preview-info" id="pI"></div>
|
|
242
|
+
<div class="preview-zoom">
|
|
243
|
+
<button onclick="zoomOut()" title="Zoom out">−</button>
|
|
244
|
+
<span id="zoomLevel">100%</span>
|
|
245
|
+
<button onclick="zoomIn()" title="Zoom in">+</button>
|
|
246
|
+
<button id="fitBtn" onclick="zoomReset()" title="Reset zoom" style="font-size:10px;width:28px;display:none">Fit</button>
|
|
247
|
+
<span class="zoom-sep"></span>
|
|
248
|
+
<div class="play-controls" id="playControls">
|
|
249
|
+
<button id="playBtn" onclick="togglePlayback()" title="Play/Pause">▶</button>
|
|
250
|
+
<select id="playSpeed" onchange="setPlaySpeed(this.value)" title="Playback speed">
|
|
251
|
+
<option value="200">0.5×</option>
|
|
252
|
+
<option value="100" selected>1×</option>
|
|
253
|
+
<option value="50">2×</option>
|
|
254
|
+
<option value="33">3×</option>
|
|
255
|
+
</select>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<button class="load-btn" onclick="loadNewImage()">Load Different Image...</button>
|
|
259
|
+
<input type="file" id="fileInput" accept=".png,.jpg,.jpeg,.gif,.bmp,.ico" style="display:none" onchange="handleFileSelect(event)">
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<!-- Resizable divider -->
|
|
263
|
+
<div class="divider" id="divider"></div>
|
|
264
|
+
|
|
265
|
+
<!-- Tools panel -->
|
|
266
|
+
<div class="tools-panel">
|
|
267
|
+
<!-- Remove Background -->
|
|
268
|
+
<div class="tool" id="t-removebg" data-tool="removebg" data-ext=".png,.jpg,.jpeg,.ico,.gif">
|
|
269
|
+
<div class="tool-header" onclick="toggleTool('removebg')">
|
|
270
|
+
<img src="/icons/removebg.ico" alt="">
|
|
271
|
+
<span class="name">Remove Background</span>
|
|
272
|
+
<span class="toggle"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="tool-body">
|
|
275
|
+
<div class="tool-opts">
|
|
276
|
+
<div class="tool-row">
|
|
277
|
+
<label>Tolerance</label>
|
|
278
|
+
<input type="range" id="rb-fuzz" min="0" max="100" value="10" oninput="$('rb-fuzzV').textContent=this.value+'%'" onchange="schedulePreview()">
|
|
279
|
+
<span class="val" id="rb-fuzzV">10%</span>
|
|
280
|
+
</div>
|
|
281
|
+
<label class="opt"><input type="checkbox" id="rb-trim" checked onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim edges</label>
|
|
282
|
+
<label class="opt"><input type="checkbox" id="rb-edges" onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Border only (preserve inner)</label>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Scale Image -->
|
|
288
|
+
<div class="tool" id="t-scale" data-tool="scale" data-ext=".png,.jpg,.jpeg,.gif,.bmp,.ico">
|
|
289
|
+
<div class="tool-header" onclick="toggleTool('scale')">
|
|
290
|
+
<img src="/icons/rescale.ico" alt="">
|
|
291
|
+
<span class="name">Scale Image</span>
|
|
292
|
+
<span class="toggle"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="tool-body">
|
|
295
|
+
<div class="tool-opts">
|
|
296
|
+
<div class="tool-row">
|
|
297
|
+
<label>Width</label>
|
|
298
|
+
<input type="range" id="sc-w" min="16" max="512" value="512" oninput="scaleSlide('w')" onchange="schedulePreview()">
|
|
299
|
+
<span class="val" id="sc-wV">512px</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="tool-row" id="sc-hRow">
|
|
302
|
+
<label>Height</label>
|
|
303
|
+
<input type="range" id="sc-h" min="16" max="512" value="512" oninput="scaleSlide('h')" onchange="schedulePreview()">
|
|
304
|
+
<span class="val" id="sc-hV">512px</span>
|
|
305
|
+
</div>
|
|
306
|
+
<label class="opt"><input type="checkbox" id="sc-lock" onchange="toggleRatioLock()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Lock aspect ratio</label>
|
|
307
|
+
<label class="opt"><input type="checkbox" id="sc-sq" onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Square output (add padding)</label>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<!-- Generate Icons (combined ICO + Icon Pack) -->
|
|
313
|
+
<div class="tool" id="t-icons" data-tool="icons" data-ext=".png,.jpg,.jpeg,.ico,.gif">
|
|
314
|
+
<div class="tool-header" onclick="toggleTool('icons')">
|
|
315
|
+
<img src="/icons/iconpack.ico" alt="">
|
|
316
|
+
<span class="name">Generate Icons</span>
|
|
317
|
+
<span class="toggle"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="tool-body">
|
|
320
|
+
<div class="tool-opts">
|
|
321
|
+
<label class="opt"><input type="checkbox" id="ic-trim" checked onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim transparent edges</label>
|
|
322
|
+
<label class="opt"><input type="checkbox" id="ic-sq" checked onchange="schedulePreview()"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Make square before generating</label>
|
|
323
|
+
</div>
|
|
324
|
+
<div style="font-size:9px;color:var(--txt3);margin:6px 0 4px;text-transform:uppercase">Output Formats</div>
|
|
325
|
+
<div class="platforms">
|
|
326
|
+
<label class="opt"><input type="checkbox" id="ic-ico" checked><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>ICO</label>
|
|
327
|
+
<label class="opt"><input type="checkbox" id="ic-web"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Web</label>
|
|
328
|
+
<label class="opt"><input type="checkbox" id="ic-android"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Android</label>
|
|
329
|
+
<label class="opt"><input type="checkbox" id="ic-ios"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>iOS</label>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<!-- Generate Store Assets -->
|
|
335
|
+
<div class="tool" id="t-storepack" data-tool="storepack" data-ext=".png,.jpg,.jpeg,.gif">
|
|
336
|
+
<div class="tool-header" onclick="toggleTool('storepack')">
|
|
337
|
+
<img src="/icons/storepack.ico" alt="">
|
|
338
|
+
<span class="name">Generate Store Assets</span>
|
|
339
|
+
<span class="toggle"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="tool-body">
|
|
342
|
+
<div class="tool-opts">
|
|
343
|
+
<div class="tool-row">
|
|
344
|
+
<label>Preset</label>
|
|
345
|
+
<select id="sp-preset" onchange="onPresetChange()"></select>
|
|
346
|
+
</div>
|
|
347
|
+
<div class="tool-row">
|
|
348
|
+
<label>Scale</label>
|
|
349
|
+
<select id="sp-mode">
|
|
350
|
+
<option value="fit">Fit (letterbox)</option>
|
|
351
|
+
<option value="fill">Fill (crop)</option>
|
|
352
|
+
<option value="stretch">Stretch</option>
|
|
353
|
+
</select>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
<div style="font-size:9px;color:var(--txt3);margin:8px 0 4px;text-transform:uppercase">Output Sizes</div>
|
|
357
|
+
<div class="dim-list" id="sp-dims"></div>
|
|
358
|
+
<div class="dim-add">
|
|
359
|
+
<input type="number" id="sp-newW" placeholder="W" min="1" max="9999">
|
|
360
|
+
<span>×</span>
|
|
361
|
+
<input type="number" id="sp-newH" placeholder="H" min="1" max="9999">
|
|
362
|
+
<button class="dim-add-btn" onclick="addDimension()">+</button>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="preset-actions">
|
|
365
|
+
<input type="text" id="sp-name" placeholder="Preset name...">
|
|
366
|
+
<button class="btn-sm" onclick="saveCurrentPreset()">Save</button>
|
|
367
|
+
<button class="btn-sm btn-del" onclick="deleteCurrentPreset()">Delete</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<!-- Bottom bar -->
|
|
375
|
+
<div class="bottom-bar" id="B">
|
|
376
|
+
<button class="btn btn-g" onclick="tryClose()">Close</button>
|
|
377
|
+
<button class="btn apply-btn" id="applyBtn" onclick="apply()" disabled>Apply</button>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<!-- Loading state -->
|
|
381
|
+
<div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
|
|
382
|
+
<!-- Log -->
|
|
383
|
+
<div class="log" id="G"></div>
|
|
384
|
+
<!-- Done state -->
|
|
385
|
+
<div class="dn" id="D">
|
|
386
|
+
<h4 id="dT"></h4>
|
|
387
|
+
<p id="dM"></p>
|
|
388
|
+
<div class="btns" style="width:100%;margin-top:8px">
|
|
389
|
+
<button class="btn btn-g" onclick="reset()">Back</button>
|
|
390
|
+
<button class="btn" onclick="openOutputFolder()" id="openFolderBtn" style="display:none">Open Folder</button>
|
|
391
|
+
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Themed alert/confirm modal -->
|
|
397
|
+
<div class="modal-overlay" id="modal" onclick="hideModal(event)">
|
|
398
|
+
<div class="modal">
|
|
399
|
+
<div class="modal-msg" id="modalMsg"></div>
|
|
400
|
+
<div class="modal-btns" id="modalBtns">
|
|
401
|
+
<button class="modal-btn" onclick="hideModal()">OK</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<!-- Export modal -->
|
|
407
|
+
<div class="modal-overlay" id="exportModal" onclick="hideExportModal(event)">
|
|
408
|
+
<div class="export-modal" onclick="event.stopPropagation()">
|
|
409
|
+
<h3>Export GIF</h3>
|
|
410
|
+
<div class="export-options">
|
|
411
|
+
<label class="export-opt selected" onclick="selectExportOption('frame')">
|
|
412
|
+
<input type="radio" name="exportType" value="frame" checked>
|
|
413
|
+
<span class="radio"></span>
|
|
414
|
+
<div class="export-opt-content">
|
|
415
|
+
<div class="export-opt-title">Current Frame</div>
|
|
416
|
+
<div class="export-opt-desc">Save frame <span id="exportFrameNum">1</span> as PNG</div>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="export-opt-thumb" id="exportThumb"></div>
|
|
419
|
+
</label>
|
|
420
|
+
<label class="export-opt" onclick="selectExportOption('all-frames')">
|
|
421
|
+
<input type="radio" name="exportType" value="all-frames">
|
|
422
|
+
<span class="radio"></span>
|
|
423
|
+
<div class="export-opt-content">
|
|
424
|
+
<div class="export-opt-title">All Frames (ZIP)</div>
|
|
425
|
+
<div class="export-opt-desc">Save all <span id="exportTotalFrames">0</span> frames as PNG in a ZIP file</div>
|
|
426
|
+
</div>
|
|
427
|
+
</label>
|
|
428
|
+
<label class="export-opt" onclick="selectExportOption('gif')">
|
|
429
|
+
<input type="radio" name="exportType" value="gif">
|
|
430
|
+
<span class="radio"></span>
|
|
431
|
+
<div class="export-opt-content">
|
|
432
|
+
<div class="export-opt-title">Processed GIF</div>
|
|
433
|
+
<div class="export-opt-desc">Save as animated GIF with all effects applied</div>
|
|
434
|
+
</div>
|
|
435
|
+
</label>
|
|
436
|
+
</div>
|
|
437
|
+
<div class="export-modal-btns">
|
|
438
|
+
<button class="cancel" onclick="hideExportModal()">Cancel</button>
|
|
439
|
+
<button class="confirm" onclick="confirmExport()">Export</button>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<script>
|
|
445
|
+
const { $, log, fetchJson, postJson } = PicLet;
|
|
446
|
+
|
|
447
|
+
// Themed alert/confirm
|
|
448
|
+
let modalCallback = null;
|
|
449
|
+
|
|
450
|
+
function showAlert(msg) {
|
|
451
|
+
$('modalMsg').textContent = msg;
|
|
452
|
+
$('modalBtns').innerHTML = '<button class="modal-btn" onclick="hideModal()">OK</button>';
|
|
453
|
+
$('modal').classList.add('on');
|
|
454
|
+
modalCallback = null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function showConfirm(msg, onConfirm, dangerText = 'Delete') {
|
|
458
|
+
$('modalMsg').textContent = msg;
|
|
459
|
+
$('modalBtns').innerHTML = `
|
|
460
|
+
<button class="modal-btn secondary" onclick="hideModal()">Cancel</button>
|
|
461
|
+
<button class="modal-btn danger" onclick="confirmModal()">${dangerText}</button>
|
|
462
|
+
`;
|
|
463
|
+
$('modal').classList.add('on');
|
|
464
|
+
modalCallback = onConfirm;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function hideModal(e) {
|
|
468
|
+
if (!e || e.target === $('modal')) {
|
|
469
|
+
$('modal').classList.remove('on');
|
|
470
|
+
modalCallback = null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function confirmModal() {
|
|
475
|
+
$('modal').classList.remove('on');
|
|
476
|
+
if (modalCallback) modalCallback();
|
|
477
|
+
modalCallback = null;
|
|
478
|
+
}
|
|
479
|
+
const pA = $('pA'), pI = $('pI');
|
|
480
|
+
|
|
481
|
+
// Zoom and pan state
|
|
482
|
+
let zoomScale = 1;
|
|
483
|
+
const ZOOM_MIN = 0.25;
|
|
484
|
+
const ZOOM_MAX = 4;
|
|
485
|
+
const ZOOM_STEP = 0.25;
|
|
486
|
+
|
|
487
|
+
function updateZoom() {
|
|
488
|
+
const img = pA.querySelector('img');
|
|
489
|
+
if (img) {
|
|
490
|
+
img.style.transform = `scale(${zoomScale})`;
|
|
491
|
+
// Remove max constraints when zoomed in
|
|
492
|
+
if (zoomScale > 1) {
|
|
493
|
+
img.style.maxWidth = 'none';
|
|
494
|
+
img.style.maxHeight = 'none';
|
|
495
|
+
} else {
|
|
496
|
+
img.style.maxWidth = '100%';
|
|
497
|
+
img.style.maxHeight = '100%';
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
$('zoomLevel').textContent = Math.round(zoomScale * 100) + '%';
|
|
501
|
+
// Show Fit button only when zoomed in or out
|
|
502
|
+
$('fitBtn').style.display = zoomScale !== 1 ? '' : 'none';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function zoomIn() {
|
|
506
|
+
zoomScale = Math.min(ZOOM_MAX, zoomScale + ZOOM_STEP);
|
|
507
|
+
updateZoom();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function zoomOut() {
|
|
511
|
+
zoomScale = Math.max(ZOOM_MIN, zoomScale - ZOOM_STEP);
|
|
512
|
+
updateZoom();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function zoomReset() {
|
|
516
|
+
zoomScale = 1;
|
|
517
|
+
pA.scrollLeft = 0;
|
|
518
|
+
pA.scrollTop = 0;
|
|
519
|
+
updateZoom();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Mouse wheel zoom (no modifier key needed)
|
|
523
|
+
pA.addEventListener('wheel', (e) => {
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
if (e.deltaY < 0) zoomIn();
|
|
526
|
+
else zoomOut();
|
|
527
|
+
}, { passive: false });
|
|
528
|
+
|
|
529
|
+
// Mouse drag panning
|
|
530
|
+
let isPanning = false;
|
|
531
|
+
let panStart = { x: 0, y: 0 };
|
|
532
|
+
let scrollStart = { x: 0, y: 0 };
|
|
533
|
+
|
|
534
|
+
pA.addEventListener('mousedown', (e) => {
|
|
535
|
+
// Only pan on left click and if there's an image
|
|
536
|
+
if (e.button !== 0 || !pA.querySelector('img')) return;
|
|
537
|
+
isPanning = true;
|
|
538
|
+
panStart = { x: e.clientX, y: e.clientY };
|
|
539
|
+
scrollStart = { x: pA.scrollLeft, y: pA.scrollTop };
|
|
540
|
+
const img = pA.querySelector('img');
|
|
541
|
+
if (img) img.classList.add('panning');
|
|
542
|
+
e.preventDefault();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
document.addEventListener('mousemove', (e) => {
|
|
546
|
+
if (!isPanning) return;
|
|
547
|
+
const dx = e.clientX - panStart.x;
|
|
548
|
+
const dy = e.clientY - panStart.y;
|
|
549
|
+
pA.scrollLeft = scrollStart.x - dx;
|
|
550
|
+
pA.scrollTop = scrollStart.y - dy;
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
document.addEventListener('mouseup', () => {
|
|
554
|
+
if (isPanning) {
|
|
555
|
+
isPanning = false;
|
|
556
|
+
const img = pA.querySelector('img');
|
|
557
|
+
if (img) img.classList.remove('panning');
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
let imageInfo = {};
|
|
562
|
+
let previewTimeout = null;
|
|
563
|
+
let activeTools = new Set();
|
|
564
|
+
let lastOutputSuccess = false;
|
|
565
|
+
let hasChanges = false;
|
|
566
|
+
let presets = [];
|
|
567
|
+
let currentDimensions = []; // Editable dimensions for storepack
|
|
568
|
+
|
|
569
|
+
// GIF frame state
|
|
570
|
+
let isGifFile = false;
|
|
571
|
+
let gifFrames = []; // Array of { index, thumbnail (base64) }
|
|
572
|
+
let selectedFrameIndex = 0;
|
|
573
|
+
let framePreviewCache = {}; // Cache processed frame previews
|
|
574
|
+
|
|
575
|
+
// Toggle tool active state
|
|
576
|
+
function toggleTool(tool) {
|
|
577
|
+
const el = $('t-' + tool);
|
|
578
|
+
if (!el || el.classList.contains('disabled')) return;
|
|
579
|
+
|
|
580
|
+
if (activeTools.has(tool)) {
|
|
581
|
+
activeTools.delete(tool);
|
|
582
|
+
el.classList.remove('active');
|
|
583
|
+
} else {
|
|
584
|
+
activeTools.add(tool);
|
|
585
|
+
el.classList.add('active');
|
|
586
|
+
hasChanges = true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
updateApplyButton();
|
|
590
|
+
schedulePreview();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Update apply button state
|
|
594
|
+
function updateApplyButton() {
|
|
595
|
+
$('applyBtn').disabled = activeTools.size === 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Filter tools by file extension
|
|
599
|
+
function filterTools(ext) {
|
|
600
|
+
document.querySelectorAll('.tool').forEach(el => {
|
|
601
|
+
const exts = el.dataset.ext.split(',');
|
|
602
|
+
el.classList.toggle('disabled', !exts.includes(ext));
|
|
603
|
+
if (!exts.includes(ext)) {
|
|
604
|
+
activeTools.delete(el.dataset.tool);
|
|
605
|
+
el.classList.remove('active');
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Scale slider update with aspect ratio lock
|
|
611
|
+
let aspectRatio = 1;
|
|
612
|
+
let scaleDebounce = null;
|
|
613
|
+
|
|
614
|
+
function scaleSlide(dim) {
|
|
615
|
+
const locked = $('sc-lock').checked;
|
|
616
|
+
const w = $('sc-w'), h = $('sc-h');
|
|
617
|
+
|
|
618
|
+
if (locked) {
|
|
619
|
+
// When locked, only width slider is used - calculate height from ratio
|
|
620
|
+
const newW = +w.value;
|
|
621
|
+
const newH = Math.round(newW / aspectRatio);
|
|
622
|
+
h.value = Math.min(newH, +h.max);
|
|
623
|
+
$('sc-wV').textContent = newW + 'px';
|
|
624
|
+
$('sc-hV').textContent = h.value + 'px';
|
|
625
|
+
} else {
|
|
626
|
+
// Independent sliders
|
|
627
|
+
$('sc-' + dim + 'V').textContent = $('sc-' + dim).value + 'px';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Debounced preview update while sliding
|
|
631
|
+
if (scaleDebounce) clearTimeout(scaleDebounce);
|
|
632
|
+
scaleDebounce = setTimeout(schedulePreview, 150);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function toggleRatioLock() {
|
|
636
|
+
const locked = $('sc-lock').checked;
|
|
637
|
+
$('sc-hRow').style.display = locked ? 'none' : 'flex';
|
|
638
|
+
|
|
639
|
+
if (locked) {
|
|
640
|
+
// Recalculate height based on current width and aspect ratio
|
|
641
|
+
const newH = Math.round(+$('sc-w').value / aspectRatio);
|
|
642
|
+
$('sc-h').value = Math.min(newH, +$('sc-h').max);
|
|
643
|
+
$('sc-hV').textContent = $('sc-h').value + 'px';
|
|
644
|
+
}
|
|
645
|
+
schedulePreview();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Get combined options for all active tools
|
|
649
|
+
function getOptions() {
|
|
650
|
+
const opts = { tools: Array.from(activeTools) };
|
|
651
|
+
|
|
652
|
+
if (activeTools.has('removebg')) {
|
|
653
|
+
opts.removebg = {
|
|
654
|
+
fuzz: +$('rb-fuzz').value,
|
|
655
|
+
trim: $('rb-trim').checked,
|
|
656
|
+
preserveInner: $('rb-edges').checked
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (activeTools.has('scale')) {
|
|
661
|
+
opts.scale = {
|
|
662
|
+
width: +$('sc-w').value,
|
|
663
|
+
height: +$('sc-h').value,
|
|
664
|
+
makeSquare: $('sc-sq').checked
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (activeTools.has('icons')) {
|
|
669
|
+
opts.icons = {
|
|
670
|
+
trim: $('ic-trim').checked,
|
|
671
|
+
makeSquare: $('ic-sq').checked,
|
|
672
|
+
ico: $('ic-ico').checked,
|
|
673
|
+
web: $('ic-web').checked,
|
|
674
|
+
android: $('ic-android').checked,
|
|
675
|
+
ios: $('ic-ios').checked
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (activeTools.has('storepack')) {
|
|
680
|
+
const presetSel = $('sp-preset');
|
|
681
|
+
const presetName = presetSel.value || $('sp-name').value.trim() || 'custom';
|
|
682
|
+
opts.storepack = {
|
|
683
|
+
dimensions: currentDimensions,
|
|
684
|
+
scaleMode: $('sp-mode').value,
|
|
685
|
+
presetName: presetName
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return opts;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Schedule preview
|
|
693
|
+
function schedulePreview() {
|
|
694
|
+
if (previewTimeout) clearTimeout(previewTimeout);
|
|
695
|
+
previewTimeout = setTimeout(generatePreview, 300);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Show original image
|
|
699
|
+
async function showOriginal() {
|
|
700
|
+
// For GIFs with frames loaded, show selected frame directly
|
|
701
|
+
if (isGifFile && gifFrames.length > 0 && gifFrames[selectedFrameIndex]) {
|
|
702
|
+
const frame = gifFrames[selectedFrameIndex];
|
|
703
|
+
pA.innerHTML = '';
|
|
704
|
+
const img = document.createElement('img');
|
|
705
|
+
img.src = frame.thumbnail;
|
|
706
|
+
pA.appendChild(img);
|
|
707
|
+
pI.textContent = `${imageInfo.width} × ${imageInfo.height} (Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount})`;
|
|
708
|
+
updateZoom();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
713
|
+
try {
|
|
714
|
+
const opts = { tools: [], original: true };
|
|
715
|
+
if (isGifFile) {
|
|
716
|
+
opts.frameIndex = selectedFrameIndex;
|
|
717
|
+
}
|
|
718
|
+
const result = await postJson('/api/preview', opts);
|
|
719
|
+
if (result.success && result.imageData) {
|
|
720
|
+
const img = document.createElement('img');
|
|
721
|
+
img.src = result.imageData;
|
|
722
|
+
pA.innerHTML = '';
|
|
723
|
+
pA.appendChild(img);
|
|
724
|
+
const frameInfo = isGifFile && imageInfo.frameCount > 1 ? ` (Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount})` : '';
|
|
725
|
+
pI.textContent = `${imageInfo.width} × ${imageInfo.height}${frameInfo}`;
|
|
726
|
+
updateZoom(); // Apply current zoom
|
|
727
|
+
}
|
|
728
|
+
} catch (e) {
|
|
729
|
+
pA.innerHTML = '<span class="placeholder">Failed to load image</span>';
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Generate preview
|
|
734
|
+
async function generatePreview() {
|
|
735
|
+
if (activeTools.size === 0) {
|
|
736
|
+
showOriginal();
|
|
737
|
+
// Reset frame thumbnails to original if GIF
|
|
738
|
+
if (isGifFile && gifFrames.length > 0) {
|
|
739
|
+
updateFramePreviews();
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Icon pack and store pack don't have meaningful previews (icons does though - shows trimmed/squared source)
|
|
745
|
+
const previewableTools = ['removebg', 'scale', 'icons'];
|
|
746
|
+
const hasPreviewable = Array.from(activeTools).some(t => previewableTools.includes(t));
|
|
747
|
+
|
|
748
|
+
if (!hasPreviewable) {
|
|
749
|
+
showOriginal();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
754
|
+
pI.textContent = '';
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
// For GIFs, preview the selected frame
|
|
758
|
+
const opts = getOptions();
|
|
759
|
+
if (isGifFile) {
|
|
760
|
+
opts.frameIndex = selectedFrameIndex;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const result = await postJson('/api/preview', opts);
|
|
764
|
+
if (result.success && result.imageData) {
|
|
765
|
+
const img = document.createElement('img');
|
|
766
|
+
img.src = result.imageData;
|
|
767
|
+
pA.innerHTML = '';
|
|
768
|
+
pA.appendChild(img);
|
|
769
|
+
if (result.width && result.height) {
|
|
770
|
+
const frameInfo = isGifFile ? ` (Frame ${selectedFrameIndex + 1})` : '';
|
|
771
|
+
pI.textContent = `${result.width} × ${result.height}${frameInfo}`;
|
|
772
|
+
}
|
|
773
|
+
updateZoom(); // Apply current zoom
|
|
774
|
+
} else {
|
|
775
|
+
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Update all frame thumbnails for GIFs (in background)
|
|
779
|
+
if (isGifFile && gifFrames.length > 0) {
|
|
780
|
+
updateFramePreviews();
|
|
781
|
+
}
|
|
782
|
+
} catch (e) {
|
|
783
|
+
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Load new image
|
|
788
|
+
function loadNewImage() {
|
|
789
|
+
$('fileInput').click();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function handleFileSelect(e) {
|
|
793
|
+
const file = e.target.files[0];
|
|
794
|
+
if (!file) return;
|
|
795
|
+
await loadImageFile(file);
|
|
796
|
+
e.target.value = '';
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Load image file (shared by file input and drag/drop)
|
|
800
|
+
async function loadImageFile(file) {
|
|
801
|
+
const reader = new FileReader();
|
|
802
|
+
reader.onload = async () => {
|
|
803
|
+
try {
|
|
804
|
+
const base64 = reader.result.split(',')[1]; // Remove data URL prefix
|
|
805
|
+
const result = await postJson('/api/load', {
|
|
806
|
+
fileName: file.name,
|
|
807
|
+
data: base64,
|
|
808
|
+
mimeType: file.type
|
|
809
|
+
});
|
|
810
|
+
if (result.success) {
|
|
811
|
+
imageInfo = result;
|
|
812
|
+
updateImageInfo();
|
|
813
|
+
zoomReset(); // Reset zoom for new image
|
|
814
|
+
showOriginal();
|
|
815
|
+
} else {
|
|
816
|
+
showAlert('Failed to load image: ' + (result.error || 'Unknown error'));
|
|
817
|
+
}
|
|
818
|
+
} catch (err) {
|
|
819
|
+
showAlert('Failed to load image: ' + err.message);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
reader.readAsDataURL(file);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Drag and drop handlers (only for external file drops)
|
|
826
|
+
function setupDragDrop() {
|
|
827
|
+
const area = pA;
|
|
828
|
+
|
|
829
|
+
area.addEventListener('dragenter', (e) => {
|
|
830
|
+
// Only respond to file drags from outside
|
|
831
|
+
if (e.dataTransfer?.types?.includes('Files')) {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
area.classList.add('dragover');
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
area.addEventListener('dragover', (e) => {
|
|
838
|
+
if (e.dataTransfer?.types?.includes('Files')) {
|
|
839
|
+
e.preventDefault();
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
area.addEventListener('dragleave', (e) => {
|
|
844
|
+
// Only remove if actually leaving the area
|
|
845
|
+
if (!area.contains(e.relatedTarget)) {
|
|
846
|
+
area.classList.remove('dragover');
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
area.addEventListener('drop', (e) => {
|
|
851
|
+
e.preventDefault();
|
|
852
|
+
area.classList.remove('dragover');
|
|
853
|
+
|
|
854
|
+
const files = e.dataTransfer?.files;
|
|
855
|
+
if (files && files.length > 0) {
|
|
856
|
+
const file = files[0];
|
|
857
|
+
// Check if it's an image
|
|
858
|
+
if (file.type.startsWith('image/') || /\.(png|jpg|jpeg|gif|bmp|ico)$/i.test(file.name)) {
|
|
859
|
+
loadImageFile(file);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Update displayed image info
|
|
866
|
+
function updateImageInfo() {
|
|
867
|
+
$('fileName').textContent = imageInfo.fileName;
|
|
868
|
+
$('origSize').textContent = imageInfo.width + '×' + imageInfo.height;
|
|
869
|
+
|
|
870
|
+
const ext = '.' + imageInfo.fileName.split('.').pop().toLowerCase();
|
|
871
|
+
filterTools(ext);
|
|
872
|
+
|
|
873
|
+
// Check if GIF
|
|
874
|
+
isGifFile = ext === '.gif';
|
|
875
|
+
if (isGifFile && imageInfo.frameCount > 1) {
|
|
876
|
+
showFrameStrip();
|
|
877
|
+
} else {
|
|
878
|
+
hideFrameStrip();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Calculate aspect ratio
|
|
882
|
+
aspectRatio = imageInfo.width / imageInfo.height;
|
|
883
|
+
|
|
884
|
+
// Set scale sliders max to image dimensions (can't upscale)
|
|
885
|
+
$('sc-w').max = imageInfo.width;
|
|
886
|
+
$('sc-h').max = imageInfo.height;
|
|
887
|
+
$('sc-w').value = imageInfo.width;
|
|
888
|
+
$('sc-h').value = imageInfo.height;
|
|
889
|
+
$('sc-wV').textContent = imageInfo.width + 'px';
|
|
890
|
+
$('sc-hV').textContent = imageInfo.height + 'px';
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ── GIF Frame Strip Functions ──
|
|
894
|
+
|
|
895
|
+
// Animation playback state
|
|
896
|
+
let isPlaying = false;
|
|
897
|
+
let playInterval = null;
|
|
898
|
+
let playSpeed = 100; // ms per frame
|
|
899
|
+
|
|
900
|
+
function showFrameStrip() {
|
|
901
|
+
$('framePanel').classList.add('show');
|
|
902
|
+
$('dividerLeft').classList.add('show');
|
|
903
|
+
$('playControls').classList.add('show');
|
|
904
|
+
$('gifExportBtn').classList.add('show');
|
|
905
|
+
$('frameCount').textContent = imageInfo.frameCount;
|
|
906
|
+
selectedFrameIndex = 0;
|
|
907
|
+
gifFrames = [];
|
|
908
|
+
framePreviewCache = {};
|
|
909
|
+
loadFrameThumbnails();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function hideFrameStrip() {
|
|
913
|
+
$('framePanel').classList.remove('show');
|
|
914
|
+
$('dividerLeft').classList.remove('show');
|
|
915
|
+
$('playControls').classList.remove('show');
|
|
916
|
+
$('gifExportBtn').classList.remove('show');
|
|
917
|
+
stopPlayback();
|
|
918
|
+
gifFrames = [];
|
|
919
|
+
framePreviewCache = {};
|
|
920
|
+
$('frameScroll').innerHTML = '';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Export modal
|
|
924
|
+
let selectedExportOption = 'frame';
|
|
925
|
+
|
|
926
|
+
function showExportModal() {
|
|
927
|
+
stopPlayback();
|
|
928
|
+
|
|
929
|
+
// Update modal with current frame info
|
|
930
|
+
$('exportFrameNum').textContent = selectedFrameIndex + 1;
|
|
931
|
+
$('exportTotalFrames').textContent = imageInfo.frameCount;
|
|
932
|
+
|
|
933
|
+
// Set thumbnail preview
|
|
934
|
+
const thumbContainer = $('exportThumb');
|
|
935
|
+
thumbContainer.innerHTML = '';
|
|
936
|
+
if (gifFrames[selectedFrameIndex] && gifFrames[selectedFrameIndex].thumbnail) {
|
|
937
|
+
const img = document.createElement('img');
|
|
938
|
+
img.src = gifFrames[selectedFrameIndex].thumbnail;
|
|
939
|
+
thumbContainer.appendChild(img);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Reset selection to first option
|
|
943
|
+
selectedExportOption = 'frame';
|
|
944
|
+
document.querySelectorAll('.export-opt').forEach(opt => {
|
|
945
|
+
const input = opt.querySelector('input');
|
|
946
|
+
opt.classList.toggle('selected', input.value === 'frame');
|
|
947
|
+
input.checked = input.value === 'frame';
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
$('exportModal').classList.add('on');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function hideExportModal(e) {
|
|
954
|
+
if (!e || e.target === $('exportModal')) {
|
|
955
|
+
$('exportModal').classList.remove('on');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function selectExportOption(value) {
|
|
960
|
+
selectedExportOption = value;
|
|
961
|
+
document.querySelectorAll('.export-opt').forEach(opt => {
|
|
962
|
+
const input = opt.querySelector('input');
|
|
963
|
+
opt.classList.toggle('selected', input.value === value);
|
|
964
|
+
input.checked = input.value === value;
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function confirmExport() {
|
|
969
|
+
hideExportModal();
|
|
970
|
+
|
|
971
|
+
switch (selectedExportOption) {
|
|
972
|
+
case 'frame':
|
|
973
|
+
exportSelectedFrame();
|
|
974
|
+
break;
|
|
975
|
+
case 'all-frames':
|
|
976
|
+
exportAllFrames();
|
|
977
|
+
break;
|
|
978
|
+
case 'gif':
|
|
979
|
+
exportAsGif();
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function togglePlayback() {
|
|
985
|
+
if (isPlaying) {
|
|
986
|
+
stopPlayback();
|
|
987
|
+
} else {
|
|
988
|
+
startPlayback();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function startPlayback() {
|
|
993
|
+
if (gifFrames.length < 2) return;
|
|
994
|
+
isPlaying = true;
|
|
995
|
+
$('playBtn').textContent = '⏸';
|
|
996
|
+
$('playBtn').classList.add('playing');
|
|
997
|
+
|
|
998
|
+
playInterval = setInterval(() => {
|
|
999
|
+
selectedFrameIndex = (selectedFrameIndex + 1) % gifFrames.length;
|
|
1000
|
+
updatePlaybackFrame();
|
|
1001
|
+
}, playSpeed);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function stopPlayback() {
|
|
1005
|
+
isPlaying = false;
|
|
1006
|
+
if (playInterval) {
|
|
1007
|
+
clearInterval(playInterval);
|
|
1008
|
+
playInterval = null;
|
|
1009
|
+
}
|
|
1010
|
+
$('playBtn').textContent = '▶';
|
|
1011
|
+
$('playBtn').classList.remove('playing');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function setPlaySpeed(ms) {
|
|
1015
|
+
playSpeed = parseInt(ms, 10);
|
|
1016
|
+
if (isPlaying) {
|
|
1017
|
+
stopPlayback();
|
|
1018
|
+
startPlayback();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function updatePlaybackFrame() {
|
|
1023
|
+
// Update thumbnail selection (no scrolling during playback - it's distracting)
|
|
1024
|
+
document.querySelectorAll('.frame-thumb').forEach((thumb, i) => {
|
|
1025
|
+
thumb.classList.toggle('selected', i === selectedFrameIndex);
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Update main preview
|
|
1029
|
+
const frame = gifFrames[selectedFrameIndex];
|
|
1030
|
+
if (frame && frame.thumbnail) {
|
|
1031
|
+
const img = pA.querySelector('img');
|
|
1032
|
+
if (img) {
|
|
1033
|
+
img.src = frame.thumbnail;
|
|
1034
|
+
} else {
|
|
1035
|
+
pA.innerHTML = '';
|
|
1036
|
+
const newImg = document.createElement('img');
|
|
1037
|
+
newImg.src = frame.thumbnail;
|
|
1038
|
+
pA.appendChild(newImg);
|
|
1039
|
+
updateZoom();
|
|
1040
|
+
}
|
|
1041
|
+
pI.textContent = `Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount}`;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function loadFrameThumbnails() {
|
|
1046
|
+
const scroll = $('frameScroll');
|
|
1047
|
+
scroll.innerHTML = '';
|
|
1048
|
+
|
|
1049
|
+
// Create placeholder thumbnails first
|
|
1050
|
+
for (let i = 0; i < imageInfo.frameCount; i++) {
|
|
1051
|
+
const thumb = document.createElement('div');
|
|
1052
|
+
thumb.className = 'frame-thumb' + (i === 0 ? ' selected' : '');
|
|
1053
|
+
thumb.dataset.index = i;
|
|
1054
|
+
thumb.innerHTML = `<span class="frame-num">${i + 1}</span>`;
|
|
1055
|
+
thumb.onclick = () => selectFrame(i);
|
|
1056
|
+
scroll.appendChild(thumb);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Load thumbnails in batches
|
|
1060
|
+
for (let i = 0; i < imageInfo.frameCount; i++) {
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await postJson('/api/frame-thumbnail', { frameIndex: i });
|
|
1063
|
+
if (result.success && result.imageData) {
|
|
1064
|
+
const thumb = scroll.children[i];
|
|
1065
|
+
const img = document.createElement('img');
|
|
1066
|
+
img.src = result.imageData;
|
|
1067
|
+
thumb.insertBefore(img, thumb.firstChild);
|
|
1068
|
+
gifFrames[i] = { index: i, thumbnail: result.imageData };
|
|
1069
|
+
}
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
console.error('Failed to load frame', i, e);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function selectFrame(index) {
|
|
1077
|
+
// Stop playback on manual selection
|
|
1078
|
+
stopPlayback();
|
|
1079
|
+
|
|
1080
|
+
selectedFrameIndex = index;
|
|
1081
|
+
|
|
1082
|
+
// Update selection UI
|
|
1083
|
+
document.querySelectorAll('.frame-thumb').forEach((thumb, i) => {
|
|
1084
|
+
thumb.classList.toggle('selected', i === index);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// Show selected frame in main preview
|
|
1088
|
+
showSelectedFrame();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function showSelectedFrame() {
|
|
1092
|
+
if (activeTools.size === 0) {
|
|
1093
|
+
// Show original frame
|
|
1094
|
+
const frame = gifFrames[selectedFrameIndex];
|
|
1095
|
+
if (frame && frame.thumbnail) {
|
|
1096
|
+
pA.innerHTML = '';
|
|
1097
|
+
const img = document.createElement('img');
|
|
1098
|
+
img.src = frame.thumbnail;
|
|
1099
|
+
pA.appendChild(img);
|
|
1100
|
+
pI.textContent = `Frame ${selectedFrameIndex + 1} of ${imageInfo.frameCount}`;
|
|
1101
|
+
updateZoom();
|
|
1102
|
+
}
|
|
1103
|
+
} else {
|
|
1104
|
+
// Regenerate preview for selected frame
|
|
1105
|
+
generatePreview();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Update frame thumbnails with processed preview
|
|
1110
|
+
async function updateFramePreviews() {
|
|
1111
|
+
if (!isGifFile || gifFrames.length === 0) return;
|
|
1112
|
+
|
|
1113
|
+
const opts = getOptions();
|
|
1114
|
+
if (activeTools.size === 0) {
|
|
1115
|
+
// Reset to original thumbnails
|
|
1116
|
+
document.querySelectorAll('.frame-thumb').forEach((thumb, i) => {
|
|
1117
|
+
thumb.classList.remove('processing');
|
|
1118
|
+
const img = thumb.querySelector('img');
|
|
1119
|
+
if (img && gifFrames[i]) {
|
|
1120
|
+
img.src = gifFrames[i].thumbnail;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Show processing state on all thumbnails
|
|
1127
|
+
document.querySelectorAll('.frame-thumb').forEach(thumb => {
|
|
1128
|
+
thumb.classList.add('processing');
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
// Process frames (limit concurrent requests)
|
|
1132
|
+
for (let i = 0; i < gifFrames.length; i++) {
|
|
1133
|
+
try {
|
|
1134
|
+
const result = await postJson('/api/frame-preview', {
|
|
1135
|
+
frameIndex: i,
|
|
1136
|
+
...opts
|
|
1137
|
+
});
|
|
1138
|
+
const thumb = document.querySelector(`.frame-thumb[data-index="${i}"]`);
|
|
1139
|
+
if (thumb) {
|
|
1140
|
+
thumb.classList.remove('processing');
|
|
1141
|
+
if (result.success && result.imageData) {
|
|
1142
|
+
let img = thumb.querySelector('img');
|
|
1143
|
+
if (!img) {
|
|
1144
|
+
img = document.createElement('img');
|
|
1145
|
+
thumb.insertBefore(img, thumb.firstChild);
|
|
1146
|
+
}
|
|
1147
|
+
img.src = result.imageData;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
const thumb = document.querySelector(`.frame-thumb[data-index="${i}"]`);
|
|
1152
|
+
if (thumb) thumb.classList.remove('processing');
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Export functions
|
|
1158
|
+
async function exportSelectedFrame() {
|
|
1159
|
+
const opts = getOptions();
|
|
1160
|
+
opts.exportMode = 'frame';
|
|
1161
|
+
opts.frameIndex = selectedFrameIndex;
|
|
1162
|
+
await runExport(opts, `Exporting frame ${selectedFrameIndex + 1}...`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function exportAllFrames() {
|
|
1166
|
+
const opts = getOptions();
|
|
1167
|
+
opts.exportMode = 'all-frames';
|
|
1168
|
+
await runExport(opts, 'Exporting all frames...');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function exportAsGif() {
|
|
1172
|
+
const opts = getOptions();
|
|
1173
|
+
opts.exportMode = 'gif';
|
|
1174
|
+
await runExport(opts, 'Processing and exporting GIF...');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async function runExport(opts, message) {
|
|
1178
|
+
$('M').classList.add('hide');
|
|
1179
|
+
$('B').classList.add('hide');
|
|
1180
|
+
$('L').classList.add('on');
|
|
1181
|
+
$('G').classList.add('on');
|
|
1182
|
+
$('G').innerHTML = '';
|
|
1183
|
+
$('lT').textContent = message;
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
const result = await postJson('/api/process', opts);
|
|
1187
|
+
if (result.logs) {
|
|
1188
|
+
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
1189
|
+
}
|
|
1190
|
+
showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
log('G', 'e', e.message);
|
|
1193
|
+
showDone(false, 'Error', e.message);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ── Store Assets Preset Management ──
|
|
1198
|
+
|
|
1199
|
+
// Handle preset selection change
|
|
1200
|
+
function onPresetChange() {
|
|
1201
|
+
const sel = $('sp-preset');
|
|
1202
|
+
const preset = presets.find(p => p.id === sel.value);
|
|
1203
|
+
console.log('Selected preset:', sel.value, preset);
|
|
1204
|
+
if (preset && preset.icons && preset.icons.length > 0) {
|
|
1205
|
+
currentDimensions = preset.icons.map(i => ({ width: i.width, height: i.height, filename: i.filename }));
|
|
1206
|
+
$('sp-name').value = preset.name;
|
|
1207
|
+
console.log('Loaded dimensions:', currentDimensions);
|
|
1208
|
+
} else {
|
|
1209
|
+
currentDimensions = [];
|
|
1210
|
+
$('sp-name').value = sel.value ? (preset?.name || '') : '';
|
|
1211
|
+
}
|
|
1212
|
+
// Hide delete button for unsaved custom presets
|
|
1213
|
+
document.querySelector('.btn-del').style.display = sel.value ? '' : 'none';
|
|
1214
|
+
renderDimensions();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Render dimension chips
|
|
1218
|
+
function renderDimensions() {
|
|
1219
|
+
const container = $('sp-dims');
|
|
1220
|
+
container.innerHTML = currentDimensions.map((d, i) =>
|
|
1221
|
+
`<div class="dim-item"><span>${d.width}</span><span class="dim-x">×</span><span>${d.height}</span><span class="dim-rm" onclick="removeDimension(${i})">×</span></div>`
|
|
1222
|
+
).join('');
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Add a new dimension
|
|
1226
|
+
function addDimension() {
|
|
1227
|
+
const w = +$('sp-newW').value;
|
|
1228
|
+
const h = +$('sp-newH').value;
|
|
1229
|
+
console.log('Adding dimension:', w, h);
|
|
1230
|
+
if (w > 0 && h > 0) {
|
|
1231
|
+
// Avoid duplicates
|
|
1232
|
+
if (!currentDimensions.some(d => d.width === w && d.height === h)) {
|
|
1233
|
+
currentDimensions.push({ width: w, height: h, filename: `${w}x${h}.png` });
|
|
1234
|
+
console.log('Current dimensions:', currentDimensions);
|
|
1235
|
+
renderDimensions();
|
|
1236
|
+
}
|
|
1237
|
+
$('sp-newW').value = '';
|
|
1238
|
+
$('sp-newH').value = '';
|
|
1239
|
+
} else {
|
|
1240
|
+
console.log('Invalid dimensions - w or h not > 0');
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Remove a dimension
|
|
1245
|
+
function removeDimension(idx) {
|
|
1246
|
+
currentDimensions.splice(idx, 1);
|
|
1247
|
+
renderDimensions();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Save current dimensions as preset
|
|
1251
|
+
async function saveCurrentPreset() {
|
|
1252
|
+
console.log('Saving preset, currentDimensions:', currentDimensions);
|
|
1253
|
+
const name = $('sp-name').value.trim();
|
|
1254
|
+
if (!name) {
|
|
1255
|
+
showAlert('Please enter a preset name');
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (currentDimensions.length === 0) {
|
|
1259
|
+
showAlert('Please add at least one dimension');
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Use selected preset ID if editing, otherwise generate from name
|
|
1264
|
+
const selectedId = $('sp-preset').value;
|
|
1265
|
+
const id = selectedId || name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
1266
|
+
const preset = {
|
|
1267
|
+
id,
|
|
1268
|
+
name,
|
|
1269
|
+
description: 'Custom preset',
|
|
1270
|
+
icons: currentDimensions.map(d => ({
|
|
1271
|
+
filename: d.filename || `${d.width}x${d.height}.png`,
|
|
1272
|
+
width: d.width,
|
|
1273
|
+
height: d.height
|
|
1274
|
+
}))
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
const btn = document.querySelector('.preset-actions .btn-sm');
|
|
1278
|
+
btn.disabled = true;
|
|
1279
|
+
btn.textContent = 'Saving...';
|
|
1280
|
+
|
|
1281
|
+
try {
|
|
1282
|
+
const result = await postJson('/api/save-preset', preset);
|
|
1283
|
+
if (result.success) {
|
|
1284
|
+
// Update presets list and select the new one
|
|
1285
|
+
const existing = presets.findIndex(p => p.id === id);
|
|
1286
|
+
if (existing >= 0) {
|
|
1287
|
+
presets[existing] = preset;
|
|
1288
|
+
} else {
|
|
1289
|
+
presets.push(preset);
|
|
1290
|
+
const opt = document.createElement('option');
|
|
1291
|
+
opt.value = id;
|
|
1292
|
+
opt.textContent = name;
|
|
1293
|
+
$('sp-preset').appendChild(opt);
|
|
1294
|
+
}
|
|
1295
|
+
$('sp-preset').value = id;
|
|
1296
|
+
// Show delete button now that preset is saved
|
|
1297
|
+
document.querySelector('.btn-del').style.display = '';
|
|
1298
|
+
|
|
1299
|
+
// Show success feedback
|
|
1300
|
+
btn.textContent = 'Saved!';
|
|
1301
|
+
btn.style.borderColor = '#4c6';
|
|
1302
|
+
btn.style.color = '#4c6';
|
|
1303
|
+
setTimeout(() => {
|
|
1304
|
+
btn.textContent = 'Save Preset';
|
|
1305
|
+
btn.style.borderColor = '';
|
|
1306
|
+
btn.style.color = '';
|
|
1307
|
+
btn.disabled = false;
|
|
1308
|
+
}, 1500);
|
|
1309
|
+
} else {
|
|
1310
|
+
btn.textContent = 'Failed';
|
|
1311
|
+
btn.style.borderColor = '#f66';
|
|
1312
|
+
btn.style.color = '#f66';
|
|
1313
|
+
setTimeout(() => {
|
|
1314
|
+
btn.textContent = 'Save Preset';
|
|
1315
|
+
btn.style.borderColor = '';
|
|
1316
|
+
btn.style.color = '';
|
|
1317
|
+
btn.disabled = false;
|
|
1318
|
+
}, 1500);
|
|
1319
|
+
}
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
btn.textContent = 'Error';
|
|
1322
|
+
btn.style.borderColor = '#f66';
|
|
1323
|
+
btn.style.color = '#f66';
|
|
1324
|
+
setTimeout(() => {
|
|
1325
|
+
btn.textContent = 'Save Preset';
|
|
1326
|
+
btn.style.borderColor = '';
|
|
1327
|
+
btn.style.color = '';
|
|
1328
|
+
btn.disabled = false;
|
|
1329
|
+
}, 1500);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Delete current preset
|
|
1334
|
+
function deleteCurrentPreset() {
|
|
1335
|
+
const selectedId = $('sp-preset').value;
|
|
1336
|
+
if (!selectedId) {
|
|
1337
|
+
showAlert('No preset selected to delete');
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const preset = presets.find(p => p.id === selectedId);
|
|
1342
|
+
const presetName = preset?.name || selectedId;
|
|
1343
|
+
|
|
1344
|
+
showConfirm(`Delete preset "${presetName}"?\n\nThis cannot be undone.`, async () => {
|
|
1345
|
+
try {
|
|
1346
|
+
const result = await postJson('/api/delete-preset', { id: selectedId });
|
|
1347
|
+
if (result.success) {
|
|
1348
|
+
// Remove from local presets array
|
|
1349
|
+
const idx = presets.findIndex(p => p.id === selectedId);
|
|
1350
|
+
if (idx >= 0) presets.splice(idx, 1);
|
|
1351
|
+
|
|
1352
|
+
// Remove from dropdown and reset
|
|
1353
|
+
const sel = $('sp-preset');
|
|
1354
|
+
const opt = sel.querySelector(`option[value="${selectedId}"]`);
|
|
1355
|
+
if (opt) opt.remove();
|
|
1356
|
+
sel.value = '';
|
|
1357
|
+
onPresetChange();
|
|
1358
|
+
|
|
1359
|
+
showAlert('Preset deleted');
|
|
1360
|
+
} else {
|
|
1361
|
+
showAlert('Failed to delete: ' + (result.error || 'Unknown error'));
|
|
1362
|
+
}
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
showAlert('Failed to delete: ' + e.message);
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Apply all selected tools
|
|
1370
|
+
async function apply() {
|
|
1371
|
+
if (activeTools.size === 0) return;
|
|
1372
|
+
|
|
1373
|
+
$('M').classList.add('hide');
|
|
1374
|
+
$('B').classList.add('hide');
|
|
1375
|
+
$('L').classList.add('on');
|
|
1376
|
+
$('G').classList.add('on');
|
|
1377
|
+
$('G').innerHTML = '';
|
|
1378
|
+
|
|
1379
|
+
const toolNames = Array.from(activeTools).map(t => {
|
|
1380
|
+
const names = { removebg: 'Remove BG', scale: 'Scale', icons: 'Icons', storepack: 'Store Assets' };
|
|
1381
|
+
return names[t] || t;
|
|
1382
|
+
});
|
|
1383
|
+
$('lT').textContent = toolNames.join(' → ') + '...';
|
|
1384
|
+
|
|
1385
|
+
try {
|
|
1386
|
+
const result = await postJson('/api/process', getOptions());
|
|
1387
|
+
|
|
1388
|
+
if (result.logs) {
|
|
1389
|
+
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
showDone(result.success, result.success ? 'Done' : 'Failed', result.success ? result.output : result.error);
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
log('G', 'e', e.message);
|
|
1395
|
+
showDone(false, 'Error', e.message);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Reset to main view
|
|
1400
|
+
function reset() {
|
|
1401
|
+
$('D').classList.remove('on', 'ok', 'err');
|
|
1402
|
+
$('G').classList.remove('on');
|
|
1403
|
+
$('M').classList.remove('hide');
|
|
1404
|
+
$('B').classList.remove('hide');
|
|
1405
|
+
$('openFolderBtn').style.display = 'none';
|
|
1406
|
+
lastOutputSuccess = false;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Close with unsaved changes warning
|
|
1410
|
+
function tryClose() {
|
|
1411
|
+
if (hasChanges && activeTools.size > 0) {
|
|
1412
|
+
showConfirm('You have unsaved changes. Close anyway?', () => PicLet.close(), 'Close');
|
|
1413
|
+
} else {
|
|
1414
|
+
PicLet.close();
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Open output folder in Explorer
|
|
1419
|
+
function openOutputFolder() {
|
|
1420
|
+
postJson('/api/open-folder', {});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Show done state with optional folder button
|
|
1424
|
+
function showDone(success, title, message) {
|
|
1425
|
+
$('L').classList.remove('on');
|
|
1426
|
+
$('D').classList.add('on', success ? 'ok' : 'err');
|
|
1427
|
+
$('dT').textContent = title;
|
|
1428
|
+
$('dM').textContent = message;
|
|
1429
|
+
lastOutputSuccess = success;
|
|
1430
|
+
$('openFolderBtn').style.display = success ? '' : 'none';
|
|
1431
|
+
if (success) hasChanges = false;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Init
|
|
1435
|
+
setupDragDrop();
|
|
1436
|
+
setupDivider();
|
|
1437
|
+
|
|
1438
|
+
// Resizable dividers
|
|
1439
|
+
function setupDivider() {
|
|
1440
|
+
const divider = $('divider');
|
|
1441
|
+
const dividerLeft = $('dividerLeft');
|
|
1442
|
+
const toolsPanel = document.querySelector('.tools-panel');
|
|
1443
|
+
const framePanel = $('framePanel');
|
|
1444
|
+
const main = $('M');
|
|
1445
|
+
let isDraggingRight = false;
|
|
1446
|
+
let isDraggingLeft = false;
|
|
1447
|
+
|
|
1448
|
+
// Right divider (tools panel)
|
|
1449
|
+
divider.addEventListener('mousedown', (e) => {
|
|
1450
|
+
isDraggingRight = true;
|
|
1451
|
+
divider.classList.add('dragging');
|
|
1452
|
+
document.body.style.cursor = 'col-resize';
|
|
1453
|
+
document.body.style.userSelect = 'none';
|
|
1454
|
+
e.preventDefault();
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
// Left divider (frame panel)
|
|
1458
|
+
dividerLeft.addEventListener('mousedown', (e) => {
|
|
1459
|
+
isDraggingLeft = true;
|
|
1460
|
+
dividerLeft.classList.add('dragging');
|
|
1461
|
+
document.body.style.cursor = 'col-resize';
|
|
1462
|
+
document.body.style.userSelect = 'none';
|
|
1463
|
+
e.preventDefault();
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
document.addEventListener('mousemove', (e) => {
|
|
1467
|
+
const mainRect = main.getBoundingClientRect();
|
|
1468
|
+
|
|
1469
|
+
if (isDraggingRight) {
|
|
1470
|
+
// Tools width = distance from cursor to right edge
|
|
1471
|
+
const toolsWidth = mainRect.right - e.clientX;
|
|
1472
|
+
// Clamp: min 240px for tools, leave at least 150px for preview
|
|
1473
|
+
const maxTools = mainRect.width - 150;
|
|
1474
|
+
const clampedWidth = Math.max(240, Math.min(maxTools, toolsWidth));
|
|
1475
|
+
toolsPanel.style.width = clampedWidth + 'px';
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (isDraggingLeft) {
|
|
1479
|
+
// Frame panel width = distance from left edge to cursor
|
|
1480
|
+
const frameWidth = e.clientX - mainRect.left;
|
|
1481
|
+
// Clamp: min 60px, max 150px for frame panel
|
|
1482
|
+
const clampedWidth = Math.max(60, Math.min(150, frameWidth));
|
|
1483
|
+
framePanel.style.width = clampedWidth + 'px';
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
document.addEventListener('mouseup', () => {
|
|
1488
|
+
if (isDraggingRight) {
|
|
1489
|
+
isDraggingRight = false;
|
|
1490
|
+
divider.classList.remove('dragging');
|
|
1491
|
+
document.body.style.cursor = '';
|
|
1492
|
+
document.body.style.userSelect = '';
|
|
1493
|
+
}
|
|
1494
|
+
if (isDraggingLeft) {
|
|
1495
|
+
isDraggingLeft = false;
|
|
1496
|
+
dividerLeft.classList.remove('dragging');
|
|
1497
|
+
document.body.style.cursor = '';
|
|
1498
|
+
document.body.style.userSelect = '';
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
fetchJson('/api/info').then(d => {
|
|
1504
|
+
imageInfo = d;
|
|
1505
|
+
updateImageInfo();
|
|
1506
|
+
|
|
1507
|
+
// Load presets for store pack (full preset data)
|
|
1508
|
+
const presetData = d.defaults?.presets || d.presets || [];
|
|
1509
|
+
console.log('Loaded presets:', presetData);
|
|
1510
|
+
if (presetData.length > 0) {
|
|
1511
|
+
presets = presetData;
|
|
1512
|
+
const sel = $('sp-preset');
|
|
1513
|
+
sel.innerHTML = '<option value="">Custom...</option>';
|
|
1514
|
+
presets.forEach(p => {
|
|
1515
|
+
const opt = document.createElement('option');
|
|
1516
|
+
opt.value = p.id;
|
|
1517
|
+
opt.textContent = p.name;
|
|
1518
|
+
sel.appendChild(opt);
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
// Hide delete button initially (Custom... selected)
|
|
1522
|
+
document.querySelector('.btn-del').style.display = 'none';
|
|
1523
|
+
|
|
1524
|
+
// Show original image immediately
|
|
1525
|
+
showOriginal();
|
|
1526
|
+
});
|
|
1527
|
+
</script>
|
|
1528
|
+
</body>
|
|
1529
|
+
</html>
|