Repository navigation
Expand file tree
/
Copy pathapp.js
More file actions
8516 lines (7448 loc) · 343 KB
/
Copy pathapp.js
File metadata and controls
8516 lines (7448 loc) · 343 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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
204
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
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
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
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
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
999
1000
// State management
const state = {
screenshots: [],
selectedIndex: 0,
transferTarget: null, // Index of screenshot waiting to receive style transfer
outputDevice: 'iphone-6.9',
currentLanguage: 'en', // Global current language for all text
projectLanguages: ['en'], // Languages available in this project
customWidth: 1290,
customHeight: 2796,
// Default settings applied to new screenshots
defaults: {
background: {
type: 'gradient',
gradient: {
angle: 135,
stops: [
{ color: '#667eea', position: 0 },
{ color: '#764ba2', position: 100 }
]
},
solid: '#1a1a2e',
image: null,
imageFit: 'cover',
imageBlur: 0,
overlayColor: '#000000',
overlayOpacity: 0,
noise: false,
noiseIntensity: 10
},
screenshot: {
scale: 70,
y: 60,
x: 50,
rotation: 0,
perspective: 0,
cornerRadius: 24,
use3D: false,
device3D: 'iphone',
rotation3D: { x: 0, y: 0, z: 0 },
shadow: {
enabled: true,
color: '#000000',
blur: 40,
opacity: 30,
x: 0,
y: 20
},
frame: {
enabled: false,
color: '#1d1d1f',
width: 12,
opacity: 100
}
},
text: {
headlineEnabled: true,
headlines: { en: '' },
headlineLanguages: ['en'],
currentHeadlineLang: 'en',
headlineFont: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'",
headlineSize: 100,
headlineWeight: '600',
headlineItalic: false,
headlineUnderline: false,
headlineStrikethrough: false,
headlineColor: '#ffffff',
perLanguageLayout: false,
languageSettings: {
en: {
headlineSize: 100,
subheadlineSize: 50,
position: 'top',
offsetY: 12,
lineHeight: 110
}
},
currentLayoutLang: 'en',
position: 'top',
offsetY: 12,
lineHeight: 110,
subheadlineEnabled: false,
subheadlines: { en: '' },
subheadlineLanguages: ['en'],
currentSubheadlineLang: 'en',
subheadlineFont: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'",
subheadlineSize: 50,
subheadlineWeight: '400',
subheadlineItalic: false,
subheadlineUnderline: false,
subheadlineStrikethrough: false,
subheadlineColor: '#ffffff',
subheadlineOpacity: 70
},
elements: [],
popouts: []
}
};
const baseTextDefaults = JSON.parse(JSON.stringify(state.defaults.text));
// Runtime-only state (not persisted)
let selectedElementId = null;
let selectedPopoutId = null;
let draggingElement = null;
// Preload laurel SVG images for element frames
const laurelImages = {};
['laurel-simple-left', 'laurel-detailed-left'].forEach(name => {
const img = new Image();
img.src = `img/${name}.svg`;
laurelImages[name] = img;
});
// Helper functions to get/set current screenshot settings
function getCurrentScreenshot() {
if (state.screenshots.length === 0) return null;
return state.screenshots[state.selectedIndex];
}
function getBackground() {
const screenshot = getCurrentScreenshot();
return screenshot ? screenshot.background : state.defaults.background;
}
function getScreenshotSettings() {
const screenshot = getCurrentScreenshot();
return screenshot ? screenshot.screenshot : state.defaults.screenshot;
}
function getText() {
const screenshot = getCurrentScreenshot();
if (screenshot) {
screenshot.text = normalizeTextSettings(screenshot.text);
return screenshot.text;
}
state.defaults.text = normalizeTextSettings(state.defaults.text);
return state.defaults.text;
}
function getTextLayoutLanguage(text) {
if (text.currentLayoutLang) return text.currentLayoutLang;
if (text.headlineEnabled !== false) return text.currentHeadlineLang || 'en';
if (text.subheadlineEnabled) return text.currentSubheadlineLang || 'en';
return text.currentHeadlineLang || text.currentSubheadlineLang || 'en';
}
function getTextLanguageSettings(text, lang) {
if (!text.languageSettings) text.languageSettings = {};
if (!text.languageSettings[lang]) {
const sourceLang = text.currentLayoutLang || text.currentHeadlineLang || text.currentSubheadlineLang || 'en';
const sourceSettings = text.languageSettings[sourceLang];
text.languageSettings[lang] = {
headlineSize: sourceSettings ? sourceSettings.headlineSize : (text.headlineSize || 100),
subheadlineSize: sourceSettings ? sourceSettings.subheadlineSize : (text.subheadlineSize || 50),
position: sourceSettings ? sourceSettings.position : (text.position || 'top'),
offsetY: sourceSettings ? sourceSettings.offsetY : (typeof text.offsetY === 'number' ? text.offsetY : 12),
lineHeight: sourceSettings ? sourceSettings.lineHeight : (text.lineHeight || 110)
};
}
return text.languageSettings[lang];
}
function getEffectiveLayout(text, lang) {
if (!text.perLanguageLayout) {
return {
headlineSize: text.headlineSize || 100,
subheadlineSize: text.subheadlineSize || 50,
position: text.position || 'top',
offsetY: typeof text.offsetY === 'number' ? text.offsetY : 12,
lineHeight: text.lineHeight || 110
};
}
return getTextLanguageSettings(text, lang);
}
function normalizeTextSettings(text) {
const merged = JSON.parse(JSON.stringify(baseTextDefaults));
if (text) {
Object.assign(merged, text);
if (text.languageSettings) {
merged.languageSettings = JSON.parse(JSON.stringify(text.languageSettings));
}
}
merged.headlines = merged.headlines || { en: '' };
merged.headlineLanguages = merged.headlineLanguages || ['en'];
merged.currentHeadlineLang = merged.currentHeadlineLang || merged.headlineLanguages[0] || 'en';
merged.currentLayoutLang = merged.currentLayoutLang || merged.currentHeadlineLang || 'en';
merged.subheadlines = merged.subheadlines || { en: '' };
merged.subheadlineLanguages = merged.subheadlineLanguages || ['en'];
merged.currentSubheadlineLang = merged.currentSubheadlineLang || merged.subheadlineLanguages[0] || 'en';
if (!merged.languageSettings) merged.languageSettings = {};
const languages = new Set([...merged.headlineLanguages, ...merged.subheadlineLanguages]);
if (languages.size === 0) languages.add('en');
languages.forEach((lang) => {
getTextLanguageSettings(merged, lang);
});
return merged;
}
function getElements() {
const screenshot = getCurrentScreenshot();
return screenshot ? (screenshot.elements || []) : [];
}
function getSelectedElement() {
if (!selectedElementId) return null;
return getElements().find(el => el.id === selectedElementId) || null;
}
function getElementText(el) {
if (el.texts) {
return el.texts[state.currentLanguage]
|| el.texts['en']
|| Object.values(el.texts).find(v => v)
|| el.text || '';
}
return el.text || '';
}
function setElementProperty(id, key, value) {
const elements = getElements();
const el = elements.find(e => e.id === id);
if (el) {
el[key] = value;
updateCanvas();
updateElementsList();
}
}
// ===== Popout accessors =====
function getPopouts() {
const screenshot = getCurrentScreenshot();
return screenshot ? (screenshot.popouts || []) : [];
}
function getSelectedPopout() {
if (!selectedPopoutId) return null;
return getPopouts().find(p => p.id === selectedPopoutId) || null;
}
function setPopoutProperty(id, key, value) {
const popouts = getPopouts();
const p = popouts.find(po => po.id === id);
if (p) {
if (key.includes('.')) {
const parts = key.split('.');
let obj = p;
for (let i = 0; i < parts.length - 1; i++) {
obj = obj[parts[i]];
}
obj[parts[parts.length - 1]] = value;
} else {
p[key] = value;
}
updateCanvas();
updatePopoutProperties();
}
}
function addPopout() {
const screenshot = getCurrentScreenshot();
if (!screenshot) return;
const img = getScreenshotImage(screenshot);
if (!img) return;
if (!screenshot.popouts) screenshot.popouts = [];
const p = {
id: crypto.randomUUID(),
cropX: 25, cropY: 25, cropWidth: 30, cropHeight: 30,
x: 70, y: 30,
width: 30,
rotation: 0, opacity: 100, cornerRadius: 12,
shadow: { enabled: true, color: '#000000', blur: 30, opacity: 40, x: 0, y: 15 },
border: { enabled: true, color: '#ffffff', width: 3, opacity: 100 }
};
screenshot.popouts.push(p);
selectedPopoutId = p.id;
updateCanvas();
updatePopoutsList();
updatePopoutProperties();
}
function deletePopout(id) {
const screenshot = getCurrentScreenshot();
if (!screenshot || !screenshot.popouts) return;
screenshot.popouts = screenshot.popouts.filter(p => p.id !== id);
if (selectedPopoutId === id) selectedPopoutId = null;
updateCanvas();
updatePopoutsList();
updatePopoutProperties();
}
function movePopout(id, direction) {
const screenshot = getCurrentScreenshot();
if (!screenshot || !screenshot.popouts) return;
const idx = screenshot.popouts.findIndex(p => p.id === id);
if (idx === -1) return;
if (direction === 'up' && idx < screenshot.popouts.length - 1) {
[screenshot.popouts[idx], screenshot.popouts[idx + 1]] = [screenshot.popouts[idx + 1], screenshot.popouts[idx]];
} else if (direction === 'down' && idx > 0) {
[screenshot.popouts[idx], screenshot.popouts[idx - 1]] = [screenshot.popouts[idx - 1], screenshot.popouts[idx]];
}
updateCanvas();
updatePopoutsList();
}
function addGraphicElement(img, src, name) {
const screenshot = getCurrentScreenshot();
if (!screenshot) return;
if (!screenshot.elements) screenshot.elements = [];
const el = {
id: crypto.randomUUID(),
type: 'graphic',
x: 50, y: 50,
width: 20,
rotation: 0,
opacity: 100,
layer: 'above-text',
image: img,
src: src,
name: name || 'Graphic',
text: '',
font: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'",
fontSize: 60,
fontWeight: '600',
fontColor: '#ffffff',
italic: false,
frame: 'none',
frameColor: '#ffffff',
frameScale: 100
};
screenshot.elements.push(el);
selectedElementId = el.id;
updateCanvas();
updateElementsList();
updateElementProperties();
}
function addTextElement() {
const screenshot = getCurrentScreenshot();
if (!screenshot) return;
if (!screenshot.elements) screenshot.elements = [];
const el = {
id: crypto.randomUUID(),
type: 'text',
x: 50, y: 50,
width: 40,
rotation: 0,
opacity: 100,
layer: 'above-text',
image: null,
src: null,
name: 'Text',
text: 'Your Text',
texts: { [state.currentLanguage]: 'Your Text' },
font: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'",
fontSize: 60,
fontWeight: '600',
fontColor: '#ffffff',
italic: false,
frame: 'none',
frameColor: '#ffffff',
frameScale: 100
};
screenshot.elements.push(el);
selectedElementId = el.id;
updateCanvas();
updateElementsList();
updateElementProperties();
}
// ===== Lucide SVG loading & caching =====
const lucideSVGCache = new Map(); // name -> raw SVG text
async function fetchLucideSVG(name) {
if (lucideSVGCache.has(name)) return lucideSVGCache.get(name);
const url = `https://unpkg.com/lucide-static@latest/icons/${name}.svg`;
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Failed to fetch icon: ${name}`);
const svgText = await resp.text();
lucideSVGCache.set(name, svgText);
return svgText;
}
function colorizeLucideSVG(svgText, color, strokeWidth) {
return svgText
.replace(/stroke="currentColor"/g, `stroke="${color}"`)
.replace(/stroke-width="[^"]*"/g, `stroke-width="${strokeWidth}"`);
}
async function getLucideImage(name, color, strokeWidth) {
const rawSVG = await fetchLucideSVG(name);
const colorized = colorizeLucideSVG(rawSVG, color, strokeWidth);
const blob = new Blob([colorized], { type: 'image/svg+xml' });
const blobURL = URL.createObjectURL(blob);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = blobURL;
});
}
async function updateIconImage(el) {
if (el.type !== 'icon') return;
try {
el.image = await getLucideImage(el.iconName, el.iconColor, el.iconStrokeWidth);
updateCanvas();
} catch (e) {
console.error('Failed to update icon image:', e);
}
}
function addEmojiElement(emoji, name) {
const screenshot = getCurrentScreenshot();
if (!screenshot) return;
if (!screenshot.elements) screenshot.elements = [];
const el = {
id: crypto.randomUUID(),
type: 'emoji',
x: 50, y: 50,
width: 15,
rotation: 0,
opacity: 100,
layer: 'above-text',
emoji: emoji,
name: name || 'Emoji',
image: null,
src: null,
text: '',
font: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'",
fontSize: 60,
fontWeight: '600',
fontColor: '#ffffff',
italic: false,
frame: 'none',
frameColor: '#ffffff',
frameScale: 100
};
screenshot.elements.push(el);
selectedElementId = el.id;
updateCanvas();
updateElementsList();
updateElementProperties();
}
async function addIconElement(iconName) {
const screenshot = getCurrentScreenshot();
if (!screenshot) return;
if (!screenshot.elements) screenshot.elements = [];
const el = {
id: crypto.randomUUID(),
type: 'icon',
x: 50, y: 50,
width: 15,
rotation: 0,
opacity: 100,
layer: 'above-text',
iconName: iconName,
iconColor: '#ffffff',
iconStrokeWidth: 2,
iconShadow: { enabled: false, color: '#000000', blur: 20, opacity: 40, x: 0, y: 10 },
image: null,
src: null,
name: iconName,
text: '',
font: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'",
fontSize: 60,
fontWeight: '600',
fontColor: '#ffffff',
italic: false,
frame: 'none',
frameColor: '#ffffff',
frameScale: 100
};
screenshot.elements.push(el);
selectedElementId = el.id;
updateElementsList();
updateElementProperties();
// Async: fetch icon SVG
try {
el.image = await getLucideImage(iconName, el.iconColor, el.iconStrokeWidth);
updateCanvas();
} catch (e) {
console.error('Failed to load icon:', e);
}
updateCanvas();
}
function deleteElement(id) {
const screenshot = getCurrentScreenshot();
if (!screenshot || !screenshot.elements) return;
screenshot.elements = screenshot.elements.filter(e => e.id !== id);
if (selectedElementId === id) selectedElementId = null;
updateCanvas();
updateElementsList();
updateElementProperties();
}
function moveElementLayer(id, direction) {
const screenshot = getCurrentScreenshot();
if (!screenshot || !screenshot.elements) return;
const idx = screenshot.elements.findIndex(e => e.id === id);
if (idx === -1) return;
if (direction === 'up' && idx < screenshot.elements.length - 1) {
[screenshot.elements[idx], screenshot.elements[idx + 1]] = [screenshot.elements[idx + 1], screenshot.elements[idx]];
} else if (direction === 'down' && idx > 0) {
[screenshot.elements[idx], screenshot.elements[idx - 1]] = [screenshot.elements[idx - 1], screenshot.elements[idx]];
}
updateCanvas();
updateElementsList();
}
// Add reset buttons to all slider control rows
function setupSliderResetButtons() {
document.querySelectorAll('.control-row input[type="range"]').forEach(slider => {
const row = slider.closest('.control-row');
if (!row || row.querySelector('.slider-reset-btn')) return;
const btn = document.createElement('button');
btn.className = 'slider-reset-btn';
btn.title = 'Reset to default';
btn.type = 'button';
btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 1 3 6.75"/><polyline points="3 16 3 10 9 10"/></svg>';
btn.addEventListener('click', () => {
slider.value = slider.defaultValue;
slider.dispatchEvent(new Event('input', { bubbles: true }));
});
row.appendChild(btn);
});
}
// Format number to at most 1 decimal place
function formatValue(num) {
const rounded = Math.round(num * 10) / 10;
return Number.isInteger(rounded) ? rounded.toString() : rounded.toFixed(1);
}
function setBackground(key, value) {
const screenshot = getCurrentScreenshot();
if (screenshot) {
if (key.includes('.')) {
const parts = key.split('.');
let obj = screenshot.background;
for (let i = 0; i < parts.length - 1; i++) {
obj = obj[parts[i]];
}
obj[parts[parts.length - 1]] = value;
} else {
screenshot.background[key] = value;
}
}
}
function setScreenshotSetting(key, value) {
const screenshot = getCurrentScreenshot();
if (screenshot) {
if (key.includes('.')) {
const parts = key.split('.');
let obj = screenshot.screenshot;
for (let i = 0; i < parts.length - 1; i++) {
obj = obj[parts[i]];
}
obj[parts[parts.length - 1]] = value;
} else {
screenshot.screenshot[key] = value;
}
}
}
function setTextSetting(key, value) {
const screenshot = getCurrentScreenshot();
if (screenshot) {
screenshot.text[key] = value;
}
}
function setCurrentScreenshotAsDefault() {
const screenshot = getCurrentScreenshot();
if (screenshot) {
state.defaults.background = JSON.parse(JSON.stringify(screenshot.background));
state.defaults.screenshot = JSON.parse(JSON.stringify(screenshot.screenshot));
state.defaults.text = JSON.parse(JSON.stringify(screenshot.text));
}
}
// Language flags mapping
const languageFlags = {
'en': '🇺🇸', 'en-gb': '🇬🇧', 'de': '🇩🇪', 'fr': '🇫🇷', 'es': '🇪🇸',
'it': '🇮🇹', 'pt': '🇵🇹', 'pt-br': '🇧🇷', 'nl': '🇳🇱', 'ru': '🇷🇺',
'ja': '🇯🇵', 'ko': '🇰🇷', 'zh': '🇨🇳', 'zh-tw': '🇹🇼', 'ar': '🇸🇦',
'hi': '🇮🇳', 'tr': '🇹🇷', 'pl': '🇵🇱', 'sv': '🇸🇪', 'da': '🇩🇰',
'no': '🇳🇴', 'fi': '🇫🇮', 'th': '🇹🇭', 'vi': '🇻🇳', 'id': '🇮🇩',
'uk': '🇺🇦'
};
// Google Fonts configuration
const googleFonts = {
loaded: new Set(),
loading: new Set(),
// Popular fonts that are commonly used for marketing/app store
popular: [
'Inter', 'Poppins', 'Roboto', 'Open Sans', 'Montserrat', 'Lato', 'Raleway',
'Nunito', 'Playfair Display', 'Oswald', 'Merriweather', 'Source Sans Pro',
'PT Sans', 'Ubuntu', 'Rubik', 'Work Sans', 'Quicksand', 'Mulish', 'Barlow',
'DM Sans', 'Manrope', 'Space Grotesk', 'Plus Jakarta Sans', 'Outfit', 'Sora',
'Lexend', 'Figtree', 'Albert Sans', 'Urbanist', 'Satoshi', 'General Sans',
'Bebas Neue', 'Anton', 'Archivo', 'Bitter', 'Cabin', 'Crimson Text',
'Dancing Script', 'Fira Sans', 'Heebo', 'IBM Plex Sans', 'Josefin Sans',
'Karla', 'Libre Franklin', 'Lora', 'Noto Sans', 'Nunito Sans', 'Pacifico',
'Permanent Marker', 'Roboto Condensed', 'Roboto Mono', 'Roboto Slab',
'Shadows Into Light', 'Signika', 'Slabo 27px', 'Source Code Pro', 'Titillium Web',
'Varela Round', 'Zilla Slab', 'Arimo', 'Barlow Condensed', 'Catamaran',
'Comfortaa', 'Cormorant Garamond', 'Dosis', 'EB Garamond', 'Exo 2',
'Fira Code', 'Hind', 'Inconsolata', 'Indie Flower', 'Jost', 'Kanit',
'Libre Baskerville', 'Maven Pro', 'Mukta', 'Nanum Gothic', 'Noticia Text',
'Oxygen', 'Philosopher', 'Play', 'Prompt', 'Rajdhani', 'Red Hat Display',
'Righteous', 'Saira', 'Sen', 'Spectral', 'Teko', 'Vollkorn', 'Yanone Kaffeesatz',
'Zeyada', 'Amatic SC', 'Archivo Black', 'Asap', 'Assistant', 'Bangers',
'BioRhyme', 'Cairo', 'Cardo', 'Chivo', 'Concert One', 'Cormorant',
'Cousine', 'DM Serif Display', 'DM Serif Text', 'Dela Gothic One',
'El Messiri', 'Encode Sans', 'Eczar', 'Fahkwang', 'Gelasio'
],
// System fonts that don't need loading
system: [
{ name: 'SF Pro Display', value: "-apple-system, BlinkMacSystemFont, 'SF Pro Display'" },
{ name: 'SF Pro Rounded', value: "'SF Pro Rounded', -apple-system" },
{ name: 'Helvetica Neue', value: "'Helvetica Neue', Helvetica" },
{ name: 'Avenir Next', value: "'Avenir Next', Avenir" },
{ name: 'Georgia', value: "Georgia, serif" },
{ name: 'Arial', value: "Arial, sans-serif" },
{ name: 'Times New Roman', value: "'Times New Roman', serif" },
{ name: 'Courier New', value: "'Courier New', monospace" },
{ name: 'Verdana', value: "Verdana, sans-serif" },
{ name: 'Trebuchet MS', value: "'Trebuchet MS', sans-serif" }
],
// Cache for all Google Fonts (loaded on demand)
allFonts: null
};
// Load a Google Font dynamically
async function loadGoogleFont(fontName) {
// Check if it's a system font
const isSystem = googleFonts.system.some(f => f.name === fontName);
if (isSystem) return;
// If already loaded, just ensure the current weight is available
if (googleFonts.loaded.has(fontName)) {
const text = getTextSettings();
const weight = text.headlineWeight || '600';
try {
await document.fonts.load(`${weight} 16px "${fontName}"`);
} catch (e) {
// Font already loaded, weight might not exist but that's ok
}
return;
}
// If currently loading, wait for it
if (googleFonts.loading.has(fontName)) {
// Wait a bit and check again
await new Promise(resolve => setTimeout(resolve, 100));
if (googleFonts.loading.has(fontName)) {
await new Promise(resolve => setTimeout(resolve, 500));
}
return;
}
googleFonts.loading.add(fontName);
try {
const link = document.createElement('link');
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontName)}:wght@300;400;500;600;700;800;900&display=swap`;
link.rel = 'stylesheet';
// Wait for stylesheet to load first
await new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
// Wait for the font to actually load with the required weights
const text = getTextSettings();
const headlineWeight = text.headlineWeight || '600';
const subheadlineWeight = text.subheadlineWeight || '400';
// Load all weights we might need
await Promise.all([
document.fonts.load(`400 16px "${fontName}"`),
document.fonts.load(`${headlineWeight} 16px "${fontName}"`),
document.fonts.load(`${subheadlineWeight} 16px "${fontName}"`)
]);
googleFonts.loaded.add(fontName);
googleFonts.loading.delete(fontName);
} catch (error) {
console.warn(`Failed to load font: ${fontName}`, error);
googleFonts.loading.delete(fontName);
}
}
// Fetch all Google Fonts from the API (cached)
async function fetchAllGoogleFonts() {
if (googleFonts.allFonts) {
return googleFonts.allFonts;
}
try {
// Try to fetch from Google Fonts API v2
// API key is optional - the API works without it but has lower rate limits
const apiKey = state.settings?.googleFontsApiKey || '';
const url = new URL('https://www.googleapis.com/webfonts/v1/webfonts');
url.searchParams.set('sort', 'popularity');
if (apiKey) {
url.searchParams.set('key', apiKey);
}
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
if (data.items && data.items.length > 0) {
// Extract font family names from API response
googleFonts.allFonts = data.items.map(font => font.family);
console.log(`Loaded ${googleFonts.allFonts.length} fonts from Google Fonts API`);
return googleFonts.allFonts;
}
} else if (response.status === 429) {
console.warn('Google Fonts API rate limit reached, using fallback font list');
} else {
console.warn(`Google Fonts API returned status ${response.status}, using fallback font list`);
}
} catch (apiError) {
console.warn('Failed to fetch from Google Fonts API, using fallback font list:', apiError);
}
// Fallback to curated list of 1000+ popular fonts
// This list covers the most commonly used fonts on Google Fonts
googleFonts.allFonts = [
...googleFonts.popular,
'ABeeZee', 'Abel', 'Abhaya Libre', 'Abril Fatface', 'Aclonica', 'Acme',
'Actor', 'Adamina', 'Advent Pro', 'Aguafina Script', 'Akronim', 'Aladin',
'Aldrich', 'Alef', 'Alegreya', 'Alegreya Sans', 'Alegreya Sans SC', 'Alex Brush',
'Alfa Slab One', 'Alice', 'Alike', 'Alike Angular', 'Allan', 'Allerta',
'Allison', 'Allura', 'Almendra', 'Amaranth', 'Amatic SC', 'Amethysta',
'Amiko', 'Amiri', 'Amita', 'Anaheim', 'Andada', 'Andika', 'Angkor',
'Annie Use Your Telescope', 'Anonymous Pro', 'Antic', 'Antic Didone',
'Antonio', 'Arapey', 'Arbutus', 'Arbutus Slab', 'Architects Daughter',
'Archivo Narrow', 'Aref Ruqaa', 'Arima Madurai', 'Arvo', 'Asap Condensed',
'Asar', 'Asset', 'Astloch', 'Asul', 'Athiti', 'Atkinson Hyperlegible',
'Atomic Age', 'Aubrey', 'Audiowide', 'Autour One', 'Average', 'Average Sans',
'Averia Gruesa Libre', 'Averia Libre', 'Averia Sans Libre', 'Averia Serif Libre',
'B612', 'B612 Mono', 'Bad Script', 'Bahiana', 'Bahianita', 'Bai Jamjuree',
'Baloo', 'Baloo 2', 'Balsamiq Sans', 'Balthazar', 'Baskervville',
'Battambang', 'Baumans', 'Bellefair', 'Belleza', 'Bellota', 'Bellota Text',
'BenchNine', 'Bentham', 'Berkshire Swash', 'Beth Ellen', 'Bevan',
'Big Shoulders Display', 'Big Shoulders Text', 'Bigelow Rules', 'Bigshot One',
'Bilbo', 'Bilbo Swash Caps', 'Blinker', 'Bodoni Moda', 'Bokor', 'Bonbon',
'Boogaloo', 'Bowlby One', 'Bowlby One SC', 'Brawler', 'Bree Serif',
'Brygada 1918', 'Bubblegum Sans', 'Bubbler One', 'Buda', 'Buenard',
'Bungee', 'Bungee Hairline', 'Bungee Inline', 'Bungee Outline', 'Bungee Shade',
'Butcherman', 'Butterfly Kids', 'Cabin Condensed', 'Cabin Sketch', 'Caesar Dressing',
'Cagliostro', 'Caladea', 'Calistoga', 'Calligraffitti', 'Cambay', 'Cambo',
'Candal', 'Cantarell', 'Cantata One', 'Cantora One', 'Capriola', 'Cardo',
'Carme', 'Carrois Gothic', 'Carrois Gothic SC', 'Carter One', 'Castoro',
'Caveat', 'Caveat Brush', 'Cedarville Cursive', 'Ceviche One', 'Chakra Petch',
'Changa', 'Changa One', 'Chango', 'Charm', 'Charmonman', 'Chathura',
'Chau Philomene One', 'Chela One', 'Chelsea Market', 'Chenla', 'Cherry Cream Soda',
'Cherry Swash', 'Chewy', 'Chicle', 'Chilanka', 'Chonburi', 'Cinzel',
'Cinzel Decorative', 'Clicker Script', 'Coda', 'Coda Caption', 'Codystar',
'Coiny', 'Combo', 'Comforter', 'Comforter Brush', 'Comic Neue', 'Coming Soon',
'Commissioner', 'Condiment', 'Content', 'Contrail One', 'Convergence',
'Cookie', 'Copse', 'Corben', 'Corinthia', 'Cormorant Infant', 'Cormorant SC',
'Cormorant Unicase', 'Cormorant Upright', 'Courgette', 'Courier Prime',
'Covered By Your Grace', 'Crafty Girls', 'Creepster', 'Crete Round',
'Crimson Pro', 'Croissant One', 'Crushed', 'Cuprum', 'Cute Font',
'Cutive', 'Cutive Mono', 'Damion', 'Dangrek', 'Darker Grotesque',
'David Libre', 'Dawning of a New Day', 'Days One', 'Dekko', 'Delius',
'Delius Swash Caps', 'Delius Unicase', 'Della Respira', 'Denk One',
'Devonshire', 'Dhurjati', 'Didact Gothic', 'Diplomata', 'Diplomata SC',
'Do Hyeon', 'Dokdo', 'Domine', 'Donegal One', 'Dongle', 'Doppio One',
'Dorsa', 'Droid Sans', 'Droid Sans Mono', 'Droid Serif', 'Duru Sans',
'Dynalight', 'Eagle Lake', 'East Sea Dokdo', 'Eater', 'Economica',
'Eczar', 'Edu NSW ACT Foundation', 'Edu QLD Beginner', 'Edu SA Beginner',
'Edu TAS Beginner', 'Edu VIC WA NT Beginner', 'Electrolize', 'Elsie',
'Elsie Swash Caps', 'Emblema One', 'Emilys Candy', 'Encode Sans Condensed',
'Encode Sans Expanded', 'Encode Sans Semi Condensed', 'Encode Sans Semi Expanded',
'Engagement', 'Englebert', 'Enriqueta', 'Ephesis', 'Epilogue', 'Erica One',
'Esteban', 'Estonia', 'Euphoria Script', 'Ewert', 'Exo', 'Expletus Sans',
'Explora', 'Fahkwang', 'Fanwood Text', 'Farro', 'Farsan', 'Fascinate',
'Fascinate Inline', 'Faster One', 'Fasthand', 'Fauna One', 'Faustina',
'Federant', 'Federo', 'Felipa', 'Fenix', 'Festive', 'Finger Paint',
'Fira Sans Condensed', 'Fira Sans Extra Condensed', 'Fjalla One', 'Fjord One',
'Flamenco', 'Flavors', 'Fleur De Leah', 'Flow Block', 'Flow Circular',
'Flow Rounded', 'Fondamento', 'Fontdiner Swanky', 'Forum', 'Francois One',
'Frank Ruhl Libre', 'Fraunces', 'Freckle Face', 'Fredericka the Great',
'Fredoka', 'Fredoka One', 'Freehand', 'Fresca', 'Frijole', 'Fruktur',
'Fugaz One', 'Fuggles', 'Fuzzy Bubbles', 'GFS Didot', 'GFS Neohellenic',
'Gabriela', 'Gaegu', 'Gafata', 'Galada', 'Galdeano', 'Galindo', 'Gamja Flower',
'Gayathri', 'Gelasio', 'Gemunu Libre', 'Genos', 'Gentium Basic', 'Gentium Book Basic',
'Gentium Book Plus', 'Gentium Plus', 'Geo', 'Georama', 'Geostar', 'Geostar Fill',
'Germania One', 'Gideon Roman', 'Gidugu', 'Gilda Display', 'Girassol',
'Give You Glory', 'Glass Antiqua', 'Glegoo', 'Gloria Hallelujah', 'Glory',
'Gluten', 'Goblin One', 'Gochi Hand', 'Goldman', 'Gorditas', 'Gothic A1',
'Gotu', 'Goudy Bookletter 1911', 'Gowun Batang', 'Gowun Dodum', 'Graduate',
'Grand Hotel', 'Grandstander', 'Grape Nuts', 'Gravitas One', 'Great Vibes',
'Grechen Fuemen', 'Grenze', 'Grenze Gotisch', 'Grey Qo', 'Griffy', 'Gruppo',
'Gudea', 'Gugi', 'Gupter', 'Gurajada', 'Gwendolyn', 'Habibi', 'Hachi Maru Pop',
'Hahmlet', 'Halant', 'Hammersmith One', 'Hanalei', 'Hanalei Fill', 'Handlee',
'Hanuman', 'Happy Monkey', 'Harmattan', 'Headland One', 'Hepta Slab',
'Herr Von Muellerhoff', 'Hi Melody', 'Hina Mincho', 'Hind Guntur', 'Hind Madurai',
'Hind Siliguri', 'Hind Vadodara', 'Holtwood One SC', 'Homemade Apple', 'Homenaje',
'Hubballi', 'Hurricane', 'IBM Plex Mono', 'IBM Plex Sans Condensed', 'IBM Plex Serif',
'IM Fell DW Pica', 'IM Fell DW Pica SC', 'IM Fell Double Pica', 'IM Fell Double Pica SC',
'IM Fell English', 'IM Fell English SC', 'IM Fell French Canon', 'IM Fell French Canon SC',
'IM Fell Great Primer', 'IM Fell Great Primer SC', 'Ibarra Real Nova', 'Iceberg',
'Iceland', 'Imbue', 'Imperial Script', 'Imprima', 'Inconsolata', 'Inder', 'Ingrid Darling',
'Inika', 'Inknut Antiqua', 'Inria Sans', 'Inria Serif', 'Inspiration', 'Inter Tight',
'Irish Grover', 'Island Moments', 'Istok Web', 'Italiana', 'Italianno', 'Itim',
'Jacques Francois', 'Jacques Francois Shadow', 'Jaldi', 'JetBrains Mono', 'Jim Nightshade',
'Joan', 'Jockey One', 'Jolly Lodger', 'Jomhuria', 'Jomolhari', 'Josefin Slab',
'Joti One', 'Jua', 'Judson', 'Julee', 'Julius Sans One', 'Junge', 'Jura',
'Just Another Hand', 'Just Me Again Down Here', 'K2D', 'Kadwa', 'Kaisei Decol',
'Kaisei HarunoUmi', 'Kaisei Opti', 'Kaisei Tokumin', 'Kalam', 'Kameron', 'Kanit',
'Kantumruy', 'Kantumruy Pro', 'Karantina', 'Karla', 'Karma', 'Katibeh', 'Kaushan Script',
'Kavivanar', 'Kavoon', 'Kdam Thmor Pro', 'Keania One', 'Kelly Slab', 'Kenia',
'Khand', 'Khmer', 'Khula', 'Kings', 'Kirang Haerang', 'Kite One', 'Kiwi Maru',
'Klee One', 'Knewave', 'KoHo', 'Kodchasan', 'Koh Santepheap', 'Kolker Brush',
'Kosugi', 'Kosugi Maru', 'Kotta One', 'Koulen', 'Kranky', 'Kreon', 'Kristi',
'Krona One', 'Krub', 'Kufam', 'Kulim Park', 'Kumar One', 'Kumar One Outline',
'Kumbh Sans', 'Kurale', 'La Belle Aurore', 'Lacquer', 'Laila', 'Lakki Reddy',
'Lalezar', 'Lancelot', 'Langar', 'Lateef', 'League Gothic', 'League Script',
'League Spartan', 'Leckerli One', 'Ledger', 'Lekton', 'Lemon', 'Lemonada',
'Lexend Deca', 'Lexend Exa', 'Lexend Giga', 'Lexend Mega', 'Lexend Peta',
'Lexend Tera', 'Lexend Zetta', 'Libre Barcode 128', 'Libre Barcode 128 Text',
'Libre Barcode 39', 'Libre Barcode 39 Extended', 'Libre Barcode 39 Extended Text',
'Libre Barcode 39 Text', 'Libre Barcode EAN13 Text', 'Libre Bodoni', 'Libre Caslon Display',
'Libre Caslon Text', 'Life Savers', 'Lilita One', 'Lily Script One', 'Limelight',
'Linden Hill', 'Literata', 'Liu Jian Mao Cao', 'Livvic', 'Lobster', 'Lobster Two',
'Londrina Outline', 'Londrina Shadow', 'Londrina Sketch', 'Londrina Solid',
'Long Cang', 'Lora', 'Love Light', 'Love Ya Like A Sister', 'Loved by the King',
'Lovers Quarrel', 'Luckiest Guy', 'Lusitana', 'Lustria', 'Luxurious Roman',
'Luxurious Script', 'M PLUS 1', 'M PLUS 1 Code', 'M PLUS 1p', 'M PLUS 2',
'M PLUS Code Latin', 'M PLUS Rounded 1c', 'Ma Shan Zheng', 'Macondo', 'Macondo Swash Caps',
'Mada', 'Magra', 'Maiden Orange', 'Maitree', 'Major Mono Display', 'Mako', 'Mali',
'Mallanna', 'Mandali', 'Manjari', 'Mansalva', 'Manuale', 'Marcellus', 'Marcellus SC',
'Marck Script', 'Margarine', 'Markazi Text', 'Marko One', 'Marmelad', 'Martel',
'Martel Sans', 'Marvel', 'Mate', 'Mate SC', 'Material Icons', 'Material Icons Outlined',
'Material Icons Round', 'Material Icons Sharp', 'Material Icons Two Tone', 'Material Symbols Outlined',
'Material Symbols Rounded', 'Material Symbols Sharp', 'Maven Pro', 'McLaren', 'Mea Culpa',
'Meddon', 'MedievalSharp', 'Medula One', 'Meera Inimai', 'Megrim', 'Meie Script',
'Meow Script', 'Merienda', 'Merienda One', 'Merriweather Sans', 'Metal', 'Metal Mania',
'Metamorphous', 'Metrophobic', 'Michroma', 'Milonga', 'Miltonian', 'Miltonian Tattoo',
'Mina', 'Miniver', 'Miriam Libre', 'Mirza', 'Miss Fajardose', 'Mitr', 'Mochiy Pop One',
'Mochiy Pop P One', 'Modak', 'Modern Antiqua', 'Mogra', 'Mohave', 'Molengo', 'Molle',
'Monda', 'Monofett', 'Monoton', 'Monsieur La Doulaise', 'Montaga', 'Montagu Slab',
'MonteCarlo', 'Montez', 'Montserrat Alternates', 'Montserrat Subrayada', 'Moo Lah Lah',
'Moon Dance', 'Moul', 'Moulpali', 'Mountains of Christmas', 'Mouse Memoirs', 'Mr Bedfort',
'Mr Dafoe', 'Mr De Haviland', 'Mrs Saint Delafield', 'Mrs Sheppards', 'Ms Madi', 'Mukta Mahee',
'Mukta Malar', 'Mukta Vaani', 'Muli', 'Murecho', 'MuseoModerno', 'My Soul', 'Mystery Quest',
'NTR', 'Nanum Brush Script', 'Nanum Gothic Coding', 'Nanum Myeongjo', 'Nanum Pen Script',
'Neonderthaw', 'Nerko One', 'Neucha', 'Neuton', 'New Rocker', 'New Tegomin', 'News Cycle',
'Newsreader', 'Niconne', 'Niramit', 'Nixie One', 'Nobile', 'Nokora', 'Norican', 'Nosifer',
'Notable', 'Nothing You Could Do', 'Noticia Text', 'Noto Color Emoji', 'Noto Emoji',
'Noto Kufi Arabic', 'Noto Music', 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', 'Noto Rashi Hebrew',
'Noto Sans Arabic', 'Noto Sans Bengali', 'Noto Sans Devanagari', 'Noto Sans Display',
'Noto Sans Georgian', 'Noto Sans Hebrew', 'Noto Sans HK', 'Noto Sans JP', 'Noto Sans KR',
'Noto Sans Mono', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans Thai', 'Noto Serif',
'Noto Serif Bengali', 'Noto Serif Devanagari', 'Noto Serif Display', 'Noto Serif Georgian',
'Noto Serif Hebrew', 'Noto Serif JP', 'Noto Serif KR', 'Noto Serif SC', 'Noto Serif TC',
'Noto Serif Thai', 'Nova Cut', 'Nova Flat', 'Nova Mono', 'Nova Oval', 'Nova Round',
'Nova Script', 'Nova Slim', 'Nova Square', 'Numans', 'Nunito', 'Nunito Sans', 'Nuosu SIL',
'Odibee Sans', 'Odor Mean Chey', 'Offside', 'Oi', 'Old Standard TT', 'Oldenburg', 'Ole',
'Oleo Script', 'Oleo Script Swash Caps', 'Oooh Baby', 'Open Sans Condensed', 'Oranienbaum',
'Orbit', 'Orbitron', 'Oregano', 'Orelega One', 'Orienta', 'Original Surfer', 'Oswald',
'Otomanopee One', 'Outfit', 'Over the Rainbow', 'Overlock', 'Overlock SC', 'Overpass',
'Overpass Mono', 'Ovo', 'Oxanium', 'Oxygen Mono', 'PT Mono', 'PT Sans Caption',
'PT Sans Narrow', 'PT Serif', 'PT Serif Caption', 'Pacifico', 'Padauk', 'Padyakke Expanded One',
'Palanquin', 'Palanquin Dark', 'Palette Mosaic', 'Pangolin', 'Paprika', 'Parisienne',
'Passero One', 'Passion One', 'Passions Conflict', 'Pathway Gothic One', 'Patrick Hand',
'Patrick Hand SC', 'Pattaya', 'Patua One', 'Pavanam', 'Paytone One', 'Peddana',
'Peralta', 'Permanent Marker', 'Petemoss', 'Petit Formal Script', 'Petrona', 'Phetsarath',
'Philosopher', 'Piazzolla', 'Piedra', 'Pinyon Script', 'Pirata One', 'Plaster', 'Play',
'Playball', 'Playfair Display SC', 'Podkova', 'Poiret One', 'Poller One', 'Poly', 'Pompiere',
'Pontano Sans', 'Poor Story', 'Poppins', 'Port Lligat Sans', 'Port Lligat Slab', 'Potta One',
'Pragati Narrow', 'Praise', 'Prata', 'Preahvihear', 'Press Start 2P', 'Pridi', 'Princess Sofia',
'Prociono', 'Prompt', 'Prosto One', 'Proza Libre', 'Public Sans', 'Puppies Play', 'Puritan',
'Purple Purse', 'Qahiri', 'Quando', 'Quantico', 'Quattrocento', 'Quattrocento Sans', 'Questrial',
'Quicksand', 'Quintessential', 'Qwigley', 'Qwitcher Grypen', 'Racing Sans One', 'Radio Canada',
'Radley', 'Rajdhani', 'Rakkas', 'Raleway Dots', 'Ramabhadra', 'Ramaraja', 'Rambla', 'Rammetto One',
'Rampart One', 'Ranchers', 'Rancho', 'Ranga', 'Rasa', 'Rationale', 'Ravi Prakash', 'Readex Pro',
'Recursive', 'Red Hat Mono', 'Red Hat Text', 'Red Rose', 'Redacted', 'Redacted Script', 'Redressed',
'Reem Kufi', 'Reenie Beanie', 'Reggae One', 'Revalia', 'Rhodium Libre', 'Ribeye', 'Ribeye Marrow',
'Righteous', 'Risque', 'Road Rage', 'Roboto Flex', 'Rochester', 'Rock Salt', 'RocknRoll One',
'Rokkitt', 'Romanesco', 'Ropa Sans', 'Rosario', 'Rosarivo', 'Rouge Script', 'Rowdies', 'Rozha One',
'Rubik Beastly', 'Rubik Bubbles', 'Rubik Burned', 'Rubik Dirt', 'Rubik Distressed', 'Rubik Glitch',
'Rubik Marker Hatch', 'Rubik Maze', 'Rubik Microbe', 'Rubik Mono One', 'Rubik Moonrocks',
'Rubik Puddles', 'Rubik Wet Paint', 'Ruda', 'Rufina', 'Ruge Boogie', 'Ruluko', 'Rum Raisin',
'Ruslan Display', 'Russo One', 'Ruthie', 'Rye', 'STIX Two Math', 'STIX Two Text', 'Sacramento',
'Sahitya', 'Sail', 'Saira Condensed', 'Saira Extra Condensed', 'Saira Semi Condensed', 'Saira Stencil One',
'Salsa', 'Sanchez', 'Sancreek', 'Sansita', 'Sansita Swashed', 'Sarabun', 'Sarala', 'Sarina', 'Sarpanch',
'Sassy Frass', 'Satisfy', 'Sawarabi Gothic', 'Sawarabi Mincho', 'Scada', 'Scheherazade New', 'Schoolbell',
'Scope One', 'Seaweed Script', 'Secular One', 'Sedgwick Ave', 'Sedgwick Ave Display', 'Sen',
'Send Flowers', 'Sevillana', 'Seymour One', 'Shadows Into Light Two', 'Shalimar', 'Shanti',
'Share', 'Share Tech', 'Share Tech Mono', 'Shippori Antique', 'Shippori Antique B1', 'Shippori Mincho',
'Shippori Mincho B1', 'Shizuru', 'Shojumaru', 'Short Stack', 'Shrikhand', 'Siemreap', 'Sigmar One',
'Signika Negative', 'Silkscreen', 'Simonetta', 'Single Day', 'Sintony', 'Sirin Stencil', 'Six Caps',
'Skranji', 'Slabo 13px', 'Slackey', 'Smokum', 'Smooch', 'Smooch Sans', 'Smythe', 'Sniglet',
'Snippet', 'Snowburst One', 'Sofadi One', 'Sofia', 'Sofia Sans', 'Sofia Sans Condensed',
'Sofia Sans Extra Condensed', 'Sofia Sans Semi Condensed', 'Solitreo', 'Solway', 'Song Myung',
'Sophia', 'Sora', 'Sorts Mill Goudy', 'Source Code Pro', 'Source Sans 3', 'Source Serif 4',
'Source Serif Pro', 'Space Mono', 'Spartan', 'Special Elite', 'Spectral SC', 'Spicy Rice',
'Spinnaker', 'Spirax', 'Splash', 'Spline Sans', 'Spline Sans Mono', 'Squada One', 'Square Peg',
'Sree Krushnadevaraya', 'Sriracha', 'Srisakdi', 'Staatliches', 'Stalemate', 'Stalinist One',
'Stardos Stencil', 'Stick', 'Stick No Bills', 'Stint Ultra Condensed', 'Stint Ultra Expanded',
'Stoke', 'Strait', 'Style Script', 'Stylish', 'Sue Ellen Francisco', 'Suez One', 'Sulphur Point',
'Sumana', 'Sunflower', 'Sunshiney', 'Supermercado One', 'Sura', 'Suranna', 'Suravaram', 'Suwannaphum',
'Swanky and Moo Moo', 'Syncopate', 'Syne', 'Syne Mono', 'Syne Tactile', 'Tajawal', 'Tangerine',
'Tapestry', 'Taprom', 'Tauri', 'Taviraj', 'Teko', 'Telex', 'Tenali Ramakrishna', 'Tenor Sans',
'Text Me One', 'Texturina', 'Thasadith', 'The Girl Next Door', 'The Nautigal', 'Tienne', 'Tillana',
'Tilt Neon', 'Tilt Prism', 'Tilt Warp', 'Timmana', 'Tinos', 'Tiro Bangla', 'Tiro Devanagari Hindi',
'Tiro Devanagari Marathi', 'Tiro Devanagari Sanskrit', 'Tiro Gurmukhi', 'Tiro Kannada', 'Tiro Tamil',
'Tiro Telugu', 'Titan One', 'Trade Winds', 'Train One', 'Trirong', 'Trispace', 'Trocchi',
'Trochut', 'Truculenta', 'Trykker', 'Tulpen One', 'Turret Road', 'Twinkle Star', 'Ubuntu Condensed',
'Ubuntu Mono', 'Uchen', 'Ultra', 'Uncial Antiqua', 'Underdog', 'Unica One', 'UnifrakturCook',
'UnifrakturMaguntia', 'Unkempt', 'Unlock', 'Unna', 'Updock', 'Urbanist', 'Varta', 'Vast Shadow',
'Vazirmatn', 'Vesper Libre', 'Viaoda Libre', 'Vibes', 'Vibur', 'Vidaloka', 'Viga', 'Voces',
'Volkhov', 'Vollkorn SC', 'Voltaire', 'Vujahday Script', 'Waiting for the Sunrise', 'Wallpoet',
'Walter Turncoat', 'Warnes', 'Water Brush', 'Waterfall', 'Wellfleet', 'Wendy One', 'Whisper',
'WindSong', 'Wire One', 'Wix Madefor Display', 'Wix Madefor Text', 'Work Sans', 'Xanh Mono',
'Yaldevi', 'Yanone Kaffeesatz', 'Yantramanav', 'Yatra One', 'Yellowtail', 'Yeon Sung', 'Yeseva One',
'Yesteryear', 'Yomogi', 'Yrsa', 'Ysabeau', 'Ysabeau Infant', 'Ysabeau Office', 'Ysabeau SC',
'Yuji Boku', 'Yuji Hentaigana Akari', 'Yuji Hentaigana Akebono', 'Yuji Mai', 'Yuji Syuku',
'Yusei Magic', 'ZCOOL KuaiLe', 'ZCOOL QingKe HuangYou', 'ZCOOL XiaoWei', 'Zen Antique',
'Zen Antique Soft', 'Zen Dots', 'Zen Kaku Gothic Antique', 'Zen Kaku Gothic New', 'Zen Kurenaido',
'Zen Loop', 'Zen Maru Gothic', 'Zen Old Mincho', 'Zen Tokyo Zoo', 'Zeyada', 'Zhi Mang Xing',
'Zilla Slab Highlight'
];
// Remove duplicates
googleFonts.allFonts = [...new Set(googleFonts.allFonts)].sort();
return googleFonts.allFonts;
} catch (error) {
console.error('Failed to load font list:', error);
return googleFonts.popular;
}
}
// Font picker state - separate state for each picker
const fontPickerState = {
headline: { category: 'popular', search: '' },
subheadline: { category: 'popular', search: '' },
element: { category: 'popular', search: '' }
};
// Initialize all font pickers
function initFontPicker() {
initSingleFontPicker('headline', {
picker: 'font-picker',
trigger: 'font-picker-trigger',
dropdown: 'font-picker-dropdown',
search: 'font-search',
list: 'font-picker-list',
preview: 'font-picker-preview',
hidden: 'headline-font',
stateKey: 'headlineFont'
});
initSingleFontPicker('subheadline', {
picker: 'subheadline-font-picker',
trigger: 'subheadline-font-picker-trigger',
dropdown: 'subheadline-font-picker-dropdown',
search: 'subheadline-font-search',
list: 'subheadline-font-picker-list',
preview: 'subheadline-font-picker-preview',
hidden: 'subheadline-font',
stateKey: 'subheadlineFont'
});
initSingleFontPicker('element', {
picker: 'element-font-picker',
trigger: 'element-font-picker-trigger',
dropdown: 'element-font-picker-dropdown',
search: 'element-font-search',
list: 'element-font-picker-list',
preview: 'element-font-picker-preview',
hidden: 'element-font',
stateKey: 'font',
getFont: () => { const el = getSelectedElement(); return el ? el.font : ''; },
setFont: (value) => { if (selectedElementId) setElementProperty(selectedElementId, 'font', value); }
});
}
// Initialize a single font picker instance
function initSingleFontPicker(pickerId, ids) {
const trigger = document.getElementById(ids.trigger);