-
Notifications
You must be signed in to change notification settings - Fork 8
/
script.js
1393 lines (1223 loc) · 60.3 KB
/
script.js
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
const options = {
// Note: When adding more options, only add to the bottom in order not to
// break the prefixing used to shorten option keys in presets and shared
// links. Option keys should use only a-z, and obviously there shouldn't be
// any duplicate keys (not like that would work). The keys shouldn't be
// changed. The defaults should be zero values, effectively no-ops, or,
// where that doesn't make sense, sensible defaults since newly added
// options must not modify the behavior of existing presets or shared links.
shape: {letter: "𐡎", description: "shape (1: circle, 2: square, 3: triangle, 4: line)", min: 1, max: 4, step: 1, default: 1},
radius: {letter: "𐤕", description: "radius of shape <i>in pixels</i>", min: 0, max: 3000, step: 10, default: 500},
rotationspeed: {letter: "𐤒", description: "rotation speed <i>in degrees per iteration</i>", min: -5, max: 5, step: 0.05, default: 0},
rotationoriginhori: {letter: "𐡄", description: "horizontal origin of rotation <i>as a fraction of the canvas width</i>", min: 0, max: 1, step: 0.01, default: 0.5},
rotationoriginverti: {letter: "𐤊", description: "vertical origin of rotation <i>as a fraction of the canvas height</i>", min: 0, max: 1, step: 0.01, default: 0.5},
expansionhori: {letter: "𐤗", description: "horizontal rate of expansion or contraction per iteration", min: 0.95, max: 1.05, step: 0.001, default: 1},
expansionverti: {letter: "𐤓", description: "vertical rate of expansion or contraction per iteration", min: 0.95, max: 1.05, step: 0.001, default: 1},
thickness: {letter: "𐤇", description: "line thiccness <i>in pixels</i>", min: 0.1, max: 4, step: 0.1, default: 1},
segments: {letter: "𐡔", description: "number of line segments the shape is comprised of", min: 100, max: 20000, step: 100, default: 1000},
skipchance: {letter: "𐡛", description: "chance each line segment will be skipped during drawing in each iteration", min: 0, max: 1, step: 0.01, default: 0},
iterations: {letter: "𐡘", description: "iterations before stopping", min: 10, max: 2000, step: 1, default: 500},
width: {letter: "𐤑", description: "canvas width <i>in pixels</i>", min: 500, max: 2560, step: 1, default: 1024},
height: {letter: "𐡉", description: "canvas height <i>in pixels</i>", min: 500, max: 2560, step: 1, default: 1024},
horicenter: {letter: "𐤔", description: "horizontal center <i>as a fraction of the canvas width</i>", min: 0, max: 1, step: 0.01, default: 0.5},
vericenter: {letter: "𐤘", description: "vertical center <i>as a fraction of the canvas height</i>", min: 0, max: 1, step: 0.01, default: 0.5},
canvasred: {letter: "𐤏", description: "red component of background color", min: 0, max: 255, step: 1, default: 255, class: "red"},
canvasgreen: {letter: "𐤏", description: "green component of background color", min: 0, max: 255, step: 1, default: 255, class: "green"},
canvasblue: {letter: "𐤏", description: "blue component of background color", min: 0, max: 255, step: 1, default: 255, class: "blue"},
canvasopacity: {letter: "𐡈", description: "opacity of background color", min: 0, max: 1, step: 0.01, default: 1, class: "halftransparent"},
linered: {letter: "𐤟", description: "red component of line color", min: 0, max: 255, step: 1, default: 0, class: "red"},
linegreen: {letter: "𐤟", description: "green component of line color", min: 0, max: 255, step: 1, default: 0, class: "green"},
lineblue: {letter: "𐤟", description: "blue component of line color", min: 0, max: 255, step: 1, default: 0, class: "blue"},
lineopacity: {letter: "𐡈", description: "opacity of line segments", min: 0, max: 1, step: 0.01, default: 1, class: "halftransparent"},
blendmode: {letter: "𐤀", description: "blend mode used during line drawing (0: source-over, 1: multiply, 2: screen, 3: overlay, 4: darken, 5: lighten, 6: color-dodge, 7: color-burn, 8: hard-light, 9: soft-light, 10: difference, 11: exclusion)", min: 0, max: 11, step: 1, default: 0},
fadeoutspeed: {letter: "𐡞", description: "rate at which line segments randomly disappear (-1 to disable) <i>in iterations</i>", min: -1, max: 1000, step: 1, default: -1},
initialrotation: {letter: "𐤈", description: "initial rotation of shape <i>in degrees</i>", min: 0, max: 359, step: 1, default: 0},
revealspeed: {letter: "𐡗", description: "rate at which line segments are added along the shape's edge (-1 to disable) <i>in line segments per iteration</i>", min: -1, max: 500, step: 1, default: -1},
translationhori: {letter: "𐡝", description: "horizontal rate of linear movement per iteration <i>in pixels</i>", min: -10, max: 10, step: 0.1, default: 0},
translationverti: {letter: "𐡍", description: "vertical rate of linear movement per iteration <i>in pixels</i>", min: -10, max: 10, step: 0.1, default: 0},
rotationperiod: {letter: "𐤙", description: "period of sinusoidal rotation variance (-1 to disable) <i>in iterations</i>", min: -1, max: 1000, step: 1, default: -1},
wavinessphori: {letter: "𐤉", description: "period of horizontal sinusoidal expansion variance (-1 to disable) <i>in line segments</i>", min: -1, max: 10000, step: 1, default: -1},
wavinesspverti: {letter: "𐡇", description: "period of vertical sinusoidal expansion variance (-1 to disable) <i>in line segments</i>", min: -1, max: 10000, step: 1, default: -1},
wavinessahori: {letter: "𐡒", description: "amplitude of horizontal sinusoidal expansion variance (note that it adds up across iterations) <i>in pixels</i>", min: 0, max: 10, step: 0.1, default: 1},
wavinessaverti: {letter: "𐡅", description: "amplitude of vertical sinusoidal expansion variance (note that it adds up across iterations) <i>in pixels</i>", min: 0, max: 10, step: 0.1, default: 1},
jitter: {letter: "𐡕", description: "jitter added to the shape in each iteration (large values coupled with lots of line segments can make things slow)", min: 0, max: 10, step: 0.1, default: 1},
rotationuntil: {letter: "𐡐", description: "until which iteration should the rotation fade to zero? (-1 to disable)", min: -1, max: 2000, step: 1, default: -1},
fadeoutstart: {letter: "𐡂", description: "iteration where line segments begin disappearing", min: 0, max: 1000, step: 1, default: 0},
sawtoothfadeoutsize: {letter: "𐡚", description: "size of \"saw teeth\" that disappear (-1 to disable) <i>in line segments</i>", min: -1, max: 5000, step: 10, default: -1},
sawtoothfadeoutstart: {letter: "𐤋", description: "iteration where line segments begin disappearing, saw-tooth-style", min: 0, max: 1000, step: 1, default: 0},
expansionhoriexp: {letter: "𐡜", description: "exponential factor added to horizontal rate of expansion or contraction", min: -100, max: 300, step: 1, default: 0},
expansionvertiexp: {letter: "𐡙", description: "exponential factor added to vertical rate of expansion or contraction", min: -100, max: 300, step: 1, default: 0},
canvasnoise: {letter: "𐡀", description: "intensity of salt-and-pepper noise applied to canvas", min: 0, max: 1, step: 0.01, default: 0},
shadowblur: {letter: "𐤎", description: "extent of blurry shadow applied to line segments (non-zero values might not play well with some blend modes & negative values use the inverted line color for the shadow) <i>in pixels</i>", min: -10, max: 50, step: 0.1, default: 0},
linecap: {letter: "𐤂", description: "line cap (1: butt, 2: round, 3: square)", min: 1, max: 3, step: 1, default: 1},
fadeinspeed: {letter: "𐤁", description: "rate at which line segments randomly appear <i>in iterations</i>", min: 0, max: 200, step: 1, default: 0},
hueshiftspeed: {letter:"𐡌", description: "hue shift speed <i>in degrees per iteration</i>", min: -10, max: 10, step: 0.1, default: 0, class: "hueshifty"},
segmentrotation: {letter:"𐤌", description: "rotation of individual line segments <i>in degrees</i>", min: 0, max: 179, step: 1, default: 0},
segmentlengthening: {letter:"𐡑", description: "length of individual line segments <i>in % of their nominal length</i>", min: 10, max: 500, step: 10, default: 100},
rotationspeedup: {letter:"𐤄", description: "change of the rotation speed per iteration (negative values will eventually turn things the other way 'round) <i>in %</i>", min: -0.05, max: 0.05, step: 0.001, default: 0},
lineswappiness: {letter:"𐡁", description: "chaotic line swappiness (how many randomly picked pairs of line segments, on average, should be swapped, creating long \"strands\", during initial line generation?)", min: 0, max: 100, step: 1, default: 0},
// See note above when adding more.
};
let optionShorts = {};
let shortsOptions = {};
Object.keys(options).forEach(n => {
for (let i = 1; i < n.length; i++) {
let prefix = n.substring(0, i);
if (!Object.values(optionShorts).includes(prefix)) {
optionShorts[n] = prefix;
shortsOptions[prefix] = n;
break;
}
}
// could handle case where all prefixes already exist, but that's not gonna happen here, so whatever
});
const optionSections = {
"": ["shape", "radius", "horicenter", "vericenter", "iterations"],
"expansion": ["expansionhori", "expansionverti", "expansionhoriexp", "expansionvertiexp"],
"rotation": ["initialrotation", "rotationspeed", "rotationoriginhori", "rotationoriginverti", "rotationspeedup", "rotationperiod", "rotationuntil", "segmentrotation"],
"movement": ["translationhori", "translationverti"],
"waviness": ["jitter", "wavinessphori", "wavinessahori", "wavinesspverti", "wavinessaverti"],
"fade": ["revealspeed", "fadeinspeed", "fadeoutspeed", "fadeoutstart", "sawtoothfadeoutsize", "sawtoothfadeoutstart"],
"line": ["segments", "skipchance", "thickness", "segmentlengthening", "lineswappiness", "linecap", "linered", "linegreen", "lineblue", "lineopacity", "hueshiftspeed", "blendmode", "shadowblur"],
"canvas": ["width", "height", "canvasred", "canvasgreen", "canvasblue", "canvasopacity", "canvasnoise"],
};
const defaults = Object.fromEntries(Object.entries(options).map(([n, o]) => [n, o.default]));
let optionValues = JSON.parse(JSON.stringify(defaults));
function setupOptions() {
let rendered = "";
let tindex = 1;
Object.keys(optionSections).forEach(s => {
rendered += `<hr><h2>${s}</h2>`;
optionSections[s].forEach(n => {
const o = options[n];
let c = "";
if (o.hasOwnProperty("class")) {
c = o.class;
}
rendered += `<label class="${c}" name="${n}"><div class="letter">${o.letter}</div><input type="range" min="${o.min}" max="${o.max}" step="${o.step}" value="${o.default}" name="${n}" class="slider" oninput="handleOptionInput(this)" onchange="handleOptionCommit(this)"><input type="text" value="${o.default}" name="${n}" class="value" oninput="handleOptionValueInput(this)" onchange="handleOptionValueCommit(this)" tabindex="${tindex}"><div class="description">${o.description}</div></label>`;
});
});
document.querySelector(".options").innerHTML = rendered;
}
function handleOptionInput(e) {
divergePreset();
const v = parseFloat(e.value);
optionValues[e.name] = v;
refreshRenderedOptionValue(e.name);
refreshShareSheetUrl();
refreshFilesizeEstimate();
restartRendering(optionValues);
historize({modifiedOption: e.name, method: "drag"});
updateStorage();
}
function handleOptionCommit(e) {
// if the value hasn't changed since the last input event (which *should*
// always be the case, but you never know), skip redrawing and all that
if (parseFloat(e.value) != optionValues[e.name]) {
handleOptionInput(e);
}
historize({modifiedOption: e.name, method: "drag", commit: true});
}
function handleOptionValueInput(e, commit=false) {
divergePreset();
const v = parseFloat(e.value);
if (isNaN(v)) return; // this can occur when the value is, say, -1, and the user presses backspace to replace the "1" with a "2" – just "-" counts as NaN
optionValues[e.name] = v;
refreshRenderedOptionValue(e.name);
e.parentElement.querySelector(".slider").value = v;
refreshShareSheetUrl();
refreshFilesizeEstimate();
restartRendering(optionValues);
historize({modifiedOption: e.name, method: "typing"});
updateStorage();
}
function handleOptionValueCommit(e) {
// if the value hasn't changed since the last input event (which *should*
// always be the case, but you never know), skip redrawing and all that
if (parseFloat(e.value) != optionValues[e.name]) {
handleOptionValueInput(e);
}
historize({modifiedOption: e.name, method: "typing", commit: true});
}
function refreshRenderedOptionValue(name) {
const e = document.querySelector(`.options .value[name=${name}]`);
const v = optionValues[name];
// avoid possibly making the cursor jump to the end
if (v != e.value) {
e.value = v;
}
if (v != Math.min(Math.max(v, options[e.name].min), options[e.name].max)) {
e.classList.add("out-of-bounds");
} else {
e.classList.remove("out-of-bounds");
}
}
function refreshRenderedOption(name) {
const e = document.querySelector(`.options .slider[name=${name}]`);
const v = optionValues[name];
e.value = v;
refreshRenderedOptionValue(name);
}
function refreshAllRenderedOptions() {
Object.keys(optionValues).forEach(refreshRenderedOption);
}
let hoveredOption = null;
function handleOptionHover(e) {
const optionLabel = e.target.closest("label");
// reset if not hovering over a label (or not even over .options)
if (!optionLabel || !e.target.closest(".options")) {
hoveredOption = null;
return;
}
hoveredOption = optionLabel.getAttribute("name");
}
function handleKeyboardIncrement(e) {
// disregard the key press we're not hovering over an option, or if the
// focus is on the value input field, or if a modifier other than shift is
// pressed
if (!hoveredOption || e.target.matches("input.value") || e.ctrlKey || e.altKey || e.metaKey) {
return;
}
let increment = options[hoveredOption].step;
if (e.shiftKey) increment *= 10;
switch (e.key) {
case "ArrowRight":
case "ArrowUp":
case "+":
case "*": // "shift +" on the German keyboard layout i use
e.preventDefault();
incrementOption(hoveredOption, increment);
break;
case "ArrowLeft":
case "ArrowDown":
case "-":
case "_": // "shift -" on the German keyboard layout i use
e.preventDefault();
incrementOption(hoveredOption, -increment);
break;
}
}
function incrementOption(name, increment) {
divergePreset();
let v = optionValues[name] + increment;
// clean up float addition artifacts by determining how many decimal places
// it's supposed to have, then stringifying it accordingly, than parsing it
// again – this is terrible but it works
const step = options[name].step.toString();
const decimalPlaces = step.substring(step.indexOf(".") + 1).length;
v = parseFloat(v.toFixed(decimalPlaces));
// make sure the new value is within bounds
v = Math.min(Math.max(v, options[name].min), options[name].max);
optionValues[name] = v;
refreshRenderedOptionValue(name);
document.querySelector(`.options .slider[name=${name}]`).value = v;
refreshShareSheetUrl();
refreshFilesizeEstimate();
restartRendering(optionValues);
historize({modifiedOption: name, method: "keyboard"});
updateStorage();
}
const presets = {
"ⵋ": "s4r320ro-0.2ex1.003t0.5se5000i450v0.13c223ca216can168l201li96lin34line0.25b10f711re100tra1.1rotat100wav2740wavi0.8wavin0.1sa460saw168",
"ⴼ": "s2r1250ro0.025e0.994ex0.994t0.5se8000sk0.74i1388w2560h2560line0.35wav1300",
"ⵛ": "s3r1560ro-0.4e0.988ex0.988t0.2se8000i297c170ca181can180l232li255lin222b3tr-0.9wa239wav95wavi0.3wavin2j4.7",
"ⵍ": "s4r460ro0.05rot0.68rota1e0.999ex1.05t2se3000sk0.5i201v0.94c0ca34can28l238li169lin54line0.75b3f196rotat9j0.2",
"ⵟ": "r170ro-0.4e1.013ex1.01t1.8se9000i192w2560h2560c16ca12can26l159li157lin161b3f153j2fa77sa170saw58rotatio-0.02",
"ⵥ": "s4r1080ro-0.9t0.5se5600sk0.31i201w2560h2560c44ca55can78l163li183lin201b6f201re172tr0.9wav2200hu0.4",
"ⵣ": "s2r820ro-0.55e0.996ex0.996t0.8sk0.47i192w2095h2104ho0.46v0.58c0ca0can0l200li233lin255in21re18wav336j1.5sa70saw72exp-10expa58canva0.4seg90",
"ⵠ": "r590ro-0.5e0.995ex0.995t0.8se4000i134w1659h1381c81ca91can103l255li255lin255line0.8f129in150wa913wav1586wavi0.2wavin0.3j0.5fa14sa70saw34exp15expa183sh20",
"ⵒ": "s3r1820ro5rot0.46rota0.22e0.96ex0.965t1.5se4000i297w2560h2560ho0.77v0.52c0ca0can0l233li243lin255b8f384rotat192wa432wav143wavi1.2wavin0.5j0.1",
"ⴱ": "s2r320ro0.15e0.997ex0.997se10510i1474line0.02f1000re37canva0.04",
"ⴶ": "r1080ro-0.9e0.999ex0.997t0.5se5600sk0.31i201w2560h2560c44ca55can78l192li183lin201b6f201re172tr0.9wav2200",
"ⵅ": "s2r100ro-1e1.01ex1.01t0.3se10000sk0.5i259ho0.56v0.46c13ca13can51l238li243lin230f302tr1tra1rotat500j0.1",
"ⵙ": "r10ro2.55rot0.51rota0e1.05ex1.05t0.2se5000i100w1342h1342ho1v0c177ca63can32l227li173lin99rotat100wa47wavi8j0.1canva0.08",
"ⵢ": "r2550ro-3.2rot0.19rota0.53e0.97ex0.97t0.4se5700sk0.27i48w2300h2218ho0.89v0.43c194ca248can190canv0l254li218lin66line0.8tr0.5tra2j8sa0exp48expa-13hu10",
"ⵉ": "s2r300ro-0.2e1.002ex0.995t0.5se5000i460c42ca47can72l158li180lin212line0.25b3tra0.9rotat100wav576wavin0.1j1.3sa100saw53canva0.04",
"ⵚ": "s4r1500ro-0.1t4se10sk0.26w2000h2000v0.12c221ca223can242li15lin26line0.67b3f300tr0.5tra3.5j3.5fa200canva0.1sh7hu100seg49",
"ⴳ": "s4r850ro1.5rot0.16rota0.16e1.008ex1.014t3.4se21sk0.1w2560h1966ho0.47v0.19c33ca40can49l213li226lin205line0.8b8in359re1tr2.6tra10rotat52wa3wav3wavi0.7j1.4exp-17expa-23canva0.13hu1",
"ⵞ": "r160ro-0.65e1.005ex1.006t0.5i402w2560h2560c0ca0can17l255li244lin204line0.05f1000wa300j10",
"ⵓ": "s4r390ro0.15e0.994se5000i565w857h1372v0.04c53ca39can48l255li255lin255line0.24b3f803tra1.8wa6000wav6000wavi0.1wavin0.4j0.1",
"ⵘ": "s4ro4e0.99ex0.983t0.8i278v0.01c16ca26can27l173li179lin147b8in183tr-3.7wa50wav40j0.2",
"ⵆ": "s3r650ro5t2se400sk0.25i77w1995h1995c20ca23can39l227li235lin255b8in41j0sh5",
"ⵐ": "s2r1010ro-0.95rot0.65rota0.2e0.977ex0.969i58w1401h1401ho0.33v0.31f384re35wa95wav47wavi1.2wavin2j0.1",
"ⵖ": "r400ro-0.4rota0.64e1.01ex1.01se10000i192h2560v0.64c229ca210can136l172li102lin194b7f71tra-1.3rotat201wa1874wav3557wavi1.2wavin0.9j0.2fa82",
"ⵤ": "s4r330ro-0.45ex1.022t0.3se4000sk0.2i316w1679h2164ho0.29v0.97c66ca69can71l218li223lin218line0.7in3re66tr2.5tra-5rotat33wa1634wav864wavi0.1wavin0.2j0.4rotati220sa120saw183expa-6canva0.05sh10",
"ⴵ": "s4r100ro-2.5e0.98ex1.01i1005ho0.22v0.22c7ca10can16l196li219lin255line0.16f1000rotat384wa287wav576wavi3wavin3j0.2",
"ⴻ": "s3r550ro-0.2e0.999ex0.985t0.5se2900i335ho0.36v0.42c26ca22can47l204li181lin145line0.66f336in238tra-3wa150wav24wavi0.1wavin0.1j0.7",
"ⵡ": "s3r360ro-0.95e0.995ex0.997t1.5i460v0.46c33ca33can89l194li202lin255b8f105j0.2fa34sa120saw10exp17expa-25",
"ⴸ": "s3r1600ro0.2e0.99ex0.998se6400i402w1114h1580v0c5ca23can37l175li141lin140line0.27b8f403wav747j3.4sa410",
"ⴲ": "r1850ro0.7rot0.25rota0.25e0.994ex0.994t0.5se11000sk0.74i603w2560h2560ho0.52ca238can239l172li168lin168line0.04b10rotat170wav1300j8",
"ⵌ": "r200ro4e1.05ex0.958se5000sk0.8i508w1580h1580ho0.51v0.52c44ca55can96l126li164lin240b2tr10tra10j0.2lines5",
"ⴷ": "r10ro-1e1.037ex1.037t2se1300sk0.1i220w946h946ho0.43c11ca15can20l159li204lin148re13rotat60wa358wav415wavi0.4wavin0.3j0canva0.1sh30linec2",
"ⵎ": "s4r630ro0.1t0.3se4000i1206v0c244ca242can222l53lin12line0.3tr-0.1tra1rotat403wav3749wavin0.1j0.4",
"ⴿ": "r30ro2.75e1.05ex1.05t0.2se2000sk0.5i58w2005c16ca21can60l130li163lin199b6wa432wav47wavi5wavin5j10canva0.3sh7.5",
"ⵄ": "r900ro-1e0.99ex0.99t4se20sk0.8i150w2560h2560c31ca32can47l255li255lin255line0.66j0canva0.1sh40seg90segm210",
"ⵇ": "s4r980ro-4.65e0.995ex0.995t4se100sk0.5i240w2372h1708c89ca107can72l255li255lin255line0.66b3rotat730j0canva0.07linec3",
"ⴾ": "r200ro0.3rota0.4e1.007ex1.007t0.8se8000i508w2560ho0.14c4ca4can12l196li174lin211line0.8b6f658in357tr4rotat66wa1970wav2643wavi0.2wavin0.2j0.5sh15fad50hu-5",
"ⴴ": "s2r1280ro-0.35rot0.12rota0.13e0.989ex0.989t4se100sk0.67i450w2560h2560c38ca18can10l188li72b6f1000rotat600wa18wav47wavi0.1wavin0.1seg30segm500",
"ⵁ": "r200e1.002ex1.002t0.5se10000sk0.5i470w1920h1280ho0.3canv0li10lin66line0.1f33tr0.6j0.5fa420",
"ⵃ": "s2r130ro-0.1t0.4i259w1698c44ca44can44l179li179lin179b6f225re25wa816wav336wavi3.2sa140saw48canva0.05",
"⌘": "",
};
function setupPresets() {
let rendered = "";
Object.keys(presets).forEach(n => {
const p = presets[n];
rendered += `<button name="${n}" onclick="handlePresetClick(this)">${n}</button>`;
});
document.querySelector(".presets").innerHTML = rendered;
}
function handlePresetClick(e) {
unselectPreset();
applyPreset(presets[e.name]);
historize({method: "preset", selectedPreset: e.name});
updateStorage();
e.classList.add("selected");
}
function applyPreset(preset) {
const opts = parseShareHash(preset);
applyOptions(opts);
}
function applyOptions(opts) {
// first reset everything to defaults...
optionValues = Object.assign(optionValues, defaults);
// then apply the new, possibly-incomplete options
optionValues = Object.assign(optionValues, opts);
refreshAllRenderedOptions();
refreshShareSheetUrl();
refreshFilesizeEstimate();
restartRendering(optionValues);
// historization and adding-to-local-storage should have been done by the
// caller if desired
}
function applyRandomPreset() {
// exclude last preset since it's empty on purpose
const i = Math.floor(Math.random() * (Object.keys(presets).length - 1));
const l = Object.keys(presets)[i];
const p = presets[l];
applyPreset(p);
historize({method: "preset", selectedPreset: l});
const e = document.querySelector(`.presets button[name=${l}]`)
e.classList.add("selected");
}
function unselectPreset() {
const selectedPreset = document.querySelector(".presets .selected");
if (selectedPreset) {
selectedPreset.classList.remove("selected");
}
const divergedPreset = document.querySelector(".presets .diverged");
if (divergedPreset) {
divergedPreset.classList.remove("diverged");
}
}
function divergePreset() {
const selectedPreset = document.querySelector(".presets .selected");
if (selectedPreset) {
selectedPreset.classList.remove("selected");
selectedPreset.classList.add("diverged");
}
}
let history = [];
let historyPosition = -1;
let preliminaryHistoryFrame = null;
function changedOptions(optionValues1, optionValues2) {
let changedNames = [];
Object.keys(optionValues1).forEach(name => {
if (optionValues1[name] !== optionValues2[name]) {
changedNames.push(name);
}
});
return changedNames;
}
function commitHistoryFrame(historyFrame) {
// if the user has performed some undos before changing an option, overwrite
// the undone part of history (it'd be neat to keep this around and display
// it as a sort of tree, but that's a whole different project!)
history = history.slice(0, historyPosition + 1);
document.querySelector(".redo").setAttribute("disabled", "");
// store the new history frame
history.push(historyFrame);
historyPosition++;
}
function historize(newHistoryFrame) {
// assemble new history frame...
let historyFrame = {
optionValues: JSON.parse(JSON.stringify(optionValues)), // may not change after passing to this function, hence making a copy for this default value
modifiedOption: null, // which *single* option/slider (its name, not letter) has been manipulated? leave null for preset applys etc.
method: null, // mandatory: by which method has this change been performed (drag, typing, keyboard, preset, url)?
selectedPreset: null, // if method = preset, which preset (its letter) has been selected?
divergedPreset: null, // leave as null, filled automatically below based on previous history
commit: false, // whether to directly commit the change to history, bypassing preliminary history frame creation. used when letting go of a slider, for example, and implicit for actions where there isn't a single modified option
};
historyFrame = Object.assign(historyFrame, newHistoryFrame);
// ...and, if relevant, propagate preset from preliminary or previous
// history frame
if (historyPosition > -1 && historyFrame.selectedPreset == null) {
let previousHistoryFrame = history[historyPosition];
if (preliminaryHistoryFrame) {
previousHistoryFrame = preliminaryHistoryFrame;
}
if (previousHistoryFrame.selectedPreset != null) {
historyFrame.divergedPreset = previousHistoryFrame.selectedPreset;
} else if (previousHistoryFrame.divergedPreset != null) {
historyFrame.divergedPreset = previousHistoryFrame.divergedPreset;
}
}
// make the implicit explicit
if (!historyFrame.modifiedOption) {
historyFrame.commit = true;
}
// if the user has performed some undos before changing an option, disable
// the redo button (but do not delete that part of history yet since, say,
// the relevant slider may yet be dragged back to its original position)
document.querySelector(".redo").setAttribute("disabled", "");
// if we're gonna commit the new history frame...
if (historyFrame.commit) {
// ...first commit any uncommitted preliminary history frame (slider
// drag and direct field changes do usually commit, but might not if
// something weird happens, but importantly, pressing the left/right
// arrow keys to adjust an option has no way of sending a commit) if
// it's describing an option modification that's unrelated to the new
// history frame (i.e. don't implicitly commit a preliminary history
// frame if we're explicitly committing it right now)
if (preliminaryHistoryFrame) {
const differentModifiedOption = !historyFrame.modifiedOption || historyFrame.modifiedOption != preliminaryHistoryFrame.modifiedOption;
const differentMethod = historyFrame.method != preliminaryHistoryFrame.method;
if (differentModifiedOption || differentMethod) {
commitHistoryFrame(preliminaryHistoryFrame);
}
}
// if this is the first history frame to be added this session, do so
// unconditionally – otherwise, check if the options differ at all from
// the most recent history frame and only commit if they do (this should
// not strictly be required since grabbing a slider but returning to the
// original value before letting go does not send an onchange event (and
// ditto for text inputs), but you never know) or the method differs –
// as a side effect, this only creates one history frame when repeatedly
// clicking the same preset
if (historyPosition == -1 || (historyPosition > -1 && (changedOptions(historyFrame.optionValues, history[historyPosition].optionValues).length) || historyFrame.method != history[historyPosition].method)) {
commitHistoryFrame(historyFrame);
}
// any preliminary history frame is now moot
preliminaryHistoryFrame = null;
} else {
if (preliminaryHistoryFrame) {
// similar logic as above: if there's a preliminary history frame
// but we're modifying a different option (or the same by a
// different method), commit it before setting our new history frame
// as the preliminary one (i think it's best to leave this explicit
// instead of abstracting to deduplicate the code with the bit
// above)
const differentModifiedOption = !historyFrame.modifiedOption || historyFrame.modifiedOption != preliminaryHistoryFrame.modifiedOption;
const differentMethod = historyFrame.method != preliminaryHistoryFrame.method;
if (differentModifiedOption || differentMethod) {
commitHistoryFrame(preliminaryHistoryFrame);
preliminaryHistoryFrame = historyFrame;
} else {
// if the preliminary and new history frame are indeed
// describing the same option and method, but the value hasn't
// changed from the most recent commit (i.e. the user had
// dragged a slider but returned it to its original position
// before letting go, or ditto for tabbing out of direct option
// input), discard it
if (!changedOptions(historyFrame.optionValues, history[historyPosition].optionValues).length) {
preliminaryHistoryFrame = null;
if (historyPosition == 0) {
document.querySelector(".undo").setAttribute("disabled", "");
}
// show the redo button again if it was previously visible
// before changing this option and subsequently returning it
// to the previous value
if (historyPosition < history.length - 1) {
document.querySelector(".redo").removeAttribute("disabled");
}
} else {
// else, simply update the preliminary history frame
preliminaryHistoryFrame = historyFrame;
}
}
} else {
// if this isn't a commit but also no preliminary history frame
// exists yet, set it!
preliminaryHistoryFrame = historyFrame;
}
}
// finally, enable undo button if this wasn't the first action of the
// session (or if it is but there's a change "in progres")
if (historyPosition == 1 || (historyPosition == 0 && preliminaryHistoryFrame)) {
document.querySelector(".undo").removeAttribute("disabled");
}
}
function applyHistory(i) {
// clear .flashed class from potential previous undos/redos
document.querySelectorAll(`.flashed`).forEach(e => {
e.classList.remove("flashed");
});
// wait until the .flashed class has been cleared before potentially adding
// it again to make sure the visual behavior is as expected (when
// undoing/redoing quickly, browsers, especially firefix, tend to forget
// about the flashing entirely, oh well)
setTimeout(() => {
// flash changed options
const changedNames = changedOptions(optionValues, history[i].optionValues);
changedNames.forEach(name => {
const e = document.querySelector(`.options label[name=${name}]`);
e.classList.add("flashed");
});
// actually visually change the sliders etc. and update local storage
applyOptions(history[i].optionValues);
updateStorage();
// (un/re)select relevant preset (whether diverged or not)
unselectPreset();
if (history[i].selectedPreset != null) {
const e = document.querySelector(`.presets button[name=${history[i].selectedPreset}]`);
e.classList.add("selected");
} else if (history[i].divergedPreset != null) {
const e = document.querySelector(`.presets button[name=${history[i].divergedPreset}]`);
e.classList.add("diverged");
}
}, 1);
}
function undo() {
// if preliminary frame exists and the value of the relevant option isn't
// identical to the most recently committed history frame, first commit it,
// only then undo
if (preliminaryHistoryFrame) {
if (changedOptions(preliminaryHistoryFrame.optionValues, history[historyPosition].optionValues).length) {
commitHistoryFrame(preliminaryHistoryFrame);
}
// reset it either way
preliminaryHistoryFrame = null;
}
applyHistory(--historyPosition);
document.querySelector(".redo").removeAttribute("disabled");
if (historyPosition == 0) {
document.querySelector(".undo").setAttribute("disabled", "");
}
}
function redo() {
applyHistory(++historyPosition);
document.querySelector(".undo").removeAttribute("disabled");
if (historyPosition == history.length - 1) {
document.querySelector(".redo").setAttribute("disabled", "");
}
}
// i believ using .code instead of .key would be correct here for alternate
// keyboard layouts such as dvorak, but considering that on german keyboards, y
// and z are switched, and i'm german, i can sleep soundly after using key .key
// here (trying to detect the locale or other shenanigans are franky too much
// work)
function handleKeyboardUndoRedo(e) {
// disregard the key press the focus is on the value input field or neither
// ctrl nor cmd are pressed
if (e.target.matches("input.value") || !(e.metaKey || e.ctrlKey)) {
return;
}
// knowing that either ctrl nor cmd are pressed, there's no need to check
// for that anymore
if (e.key == "y" || (e.shiftKey && e.key == "z")) {
e.preventDefault();
if (historyPosition < history.length - 1) {
redo();
}
} else if (e.key == "z") {
e.preventDefault();
if (historyPosition > 0) {
undo();
}
}
}
function setupRestoreDialogEtc() {
const stored = window.localStorage.getItem("uji-stored");
if (stored) {
const opts = parseShareHash(stored);
if (opts && Object.keys(opts).length) {
// so, if there seems to be something to restore, show the dialog
document.querySelector(".restore-dialog").style.display = "block";
// make sure the options are complete even if not all sliders had
// been changed
let completedOpts = Object.assign({}, defaults);
completedOpts = Object.assign(completedOpts, opts);
// "populate" undo button with it
historize({optionValues: completedOpts, method: "restore"})
}
}
}
function hideRestoreDialog() {
document.querySelector(".restore-dialog").style.display = "none";
}
// this function should be called whenever anything changes, i.e., whenever
// rendering is kicked off (except on random preset selection or share url
// extraction on load – since it seems bad to overwrite the user's previous work
// without any interaction on their part – hence no call of this function in
// applyRandomPreset() and the load event handler)
function updateStorage() {
hideRestoreDialog();
window.localStorage.setItem("uji-stored", generateShareHash(optionValues));
}
function closeSheets() {
document.querySelector(".share").classList.remove("active");
document.querySelector(".share-sheet").style.display = "none";
document.querySelector(".export").classList.remove("active");
document.querySelector(".export-sheet").style.display = "none";
}
function downloadFile(dataUrl, filename) {
// poor man's async
setTimeout(() => {
const a = document.createElement("a");
a.href = dataUrl;
a.setAttribute("download", filename);
document.body.appendChild(a);
a.click();
a.outerHTML = "";
}, 1);
}
function generateFilename(extension) {
const date = new Date().toISOString().replace(/\:/g, ".");
const hash = generateShareHash(optionValues);
const filename = `uji_${date}_${hash}.${extension}`;
return filename;
}
function downloadPNG() {
const dataUrl = canvas.toDataURL();
const filename = generateFilename("png");
downloadFile(dataUrl, filename);
}
function downloadJPEG() {
const dataUrl = canvas.toDataURL("image/jpeg", 0.9);
const filename = generateFilename("jpg");
downloadFile(dataUrl, filename);
}
function clipSvgDataToCanvasBounds(svgData) {
// cohen-sutherland algorithm, based on (but heavily customized):
// https://en.wikipedia.org/wiki/Cohen–Sutherland_algorithm
// https://github.com/mapbox/lineclip/blob/master/index.js
const INSIDE = 0;
const LEFT = 1;
const RIGHT = 2;
const BOTTOM = 4;
const TOP = 8;
// bit code computation
const code = p => {
let code = INSIDE;
if (p.x < 0) code |= LEFT;
else if (p.x > svgData.width - 1) code |= RIGHT;
if (p.y < 0) code |= TOP;
else if (p.y > svgData.height - 1) code |= BOTTOM;
return code;
};
// returns the type-less point at which the line between the two input
// points intersect the specified edge
const intersect = (a, b, edge) => {
if (edge & BOTTOM) return {x: a.x + (b.x - a.x) * (svgData.height - 1 - a.y) / (b.y - a.y), y: svgData.height - 1};
if (edge & TOP) return {x: a.x + (b.x - a.x) * (0 - a.y) / (b.y - a.y), y: 0};
if (edge & RIGHT) return {x: svgData.width - 1, y: a.y + (b.y - a.y) * (svgData.width - 1 - a.x) / (b.x - a.x)};
if (edge & LEFT) return {x: 0, y: a.y + (b.y - a.y) * (0 - a.x) / (b.x - a.x)};
return null;
};
const cohenSutherland = path => {
let a = null;
let codeA = code(path[0]);
let b = null;
let codeB = null;
let lastCode = null;
let result = [];
for (let i = 1; i < path.length; i++) {
a = path[i - 1];
b = path[i];
codeB = code(b);
lastCode = codeB;
while (true) {
if (!(codeA | codeB)) {
// if at least part of the line between a (which we know is
// inside from information gathered in the previous
// iteration) and b is inside, accept a...
result.push(a);
// ...and if b has been recalculated (in the bottom-most
// case of this if-elseif-else construct) or we've reached
// the end of the line, accept b (if it *hasn't* been
// recalculated, it'll be a in the next iteration)
if (codeB !== lastCode || i === path.length - 1) {
result.push(b);
}
// advance to the next iteration
break;
} else if (codeA & codeB) {
// both outside on the same side, so that's a trivial reject
break;
} else if (codeA) {
// if a is outside, compute a new in-bounds variant of it by
// computing the intersection point of the line between a
// and b with the relevant clip edge
let tempA = intersect(a, b, codeA);
a = {type: "M", x: tempA.x, y: tempA.y};
codeA = code(a);
} else {
// ditto if b is outside
let tempB = intersect(a, b, codeB);
b = {type: b.type, x: tempB.x, y: tempB.y};
codeB = code(b);
}
}
codeA = lastCode;
}
return result;
};
// preserve separation of iterations
const clippedPaths = svgData.paths.map(cohenSutherland);
return {
paths: clippedPaths,
width: svgData.width,
height: svgData.height,
strokeWidth: svgData.strokeWidth
};
}
function cullRedundantSvgDataMoves(svgData) {
const culledPaths = svgData.paths.map(path => {
let lastMove = path[0];
let result = [];
for (let i = 1; i < path.length; i++) {
a = path[i - 1];
b = path[i];
if (b.type == "L") {
// if we're dealing with a line end point, push the most recent
// move and the line end point
if (lastMove) {
result.push(lastMove);
lastMove = null;
}
result.push(b);
} else {
// else, update the most recent move
lastMove = b;
}
}
return result;
});
return {
paths: culledPaths,
width: svgData.width,
height: svgData.height,
strokeWidth: svgData.strokeWidth
};
}
function downloadSVG() {
const clippedSvgData = clipSvgDataToCanvasBounds(svgData);
const culledSvgData = cullRedundantSvgDataMoves(clippedSvgData);
const processedSvgData = culledSvgData;
const paths = processedSvgData.paths.map(path => {
// round the coordinates to two decimal places to reduce file size
// (100ths of pixels are, visually, plenty accurate!)
const formattedPath = path.map(p => `${p.type}${p.x.toFixed(2)},${p.y.toFixed(2)}`).join("");
return `<path d="${formattedPath}" />`;
}).join("\n");
const shareUrl = generateShareURL(optionValues);
let svg = `<?xml version="1.0" standalone="no"?>
<svg width="${processedSvgData.width}px" height="${processedSvgData.height}px" style="stroke: black; stroke-width: ${processedSvgData.strokeWidth}px; fill: none;" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 ${processedSvgData.width - 1} ${processedSvgData.height - 1}">
<g>
${paths}
</g>
<metadata>${shareUrl}</metadata>
</svg>`;
// turn the svg into a data url, this approach is ~6x faster than the
// semantically-equivalent ...utf-8,${encodeURIComponent(svg)}...
const dataUrl = `data:image/svg+xml;base64,${btoa(svg)}`;
const filename = generateFilename("svg");
downloadFile(dataUrl, filename);
}
function refreshFilesizeEstimate() {
const svgFilesizeEstimate = document.querySelector(".svg-filesize-estimate");
const jsonFilesizeEstimate = document.querySelector(".json-filesize-estimate");
const estimate = parseInt((optionValues.segments * optionValues.iterations * ((optionValues.segmentrotation != 0 || optionValues.segmentlengthening != 100) ? 2 : 1) * 15) / (1000 * 1000));
if (estimate < 5) {
svgFilesizeEstimate.innerHTML = "";
jsonFilesizeEstimate.innerHTML = "";
} else {
svgFilesizeEstimate.innerHTML = `<em>Note:</em> Based on the configured number of line segments and iterations, it looks like <strong>the SVG file will weigh in at up to ~${estimate} MB</strong> (it might be substantially less if much of the geometry is outside the bounds of the canvas, or if there's a lot of skipped line segments). The export <strong>might take a couple of seconds</strong>.`;
jsonFilesizeEstimate.innerHTML = `<em>Note:</em> It'll be about as large as the SVG variant.`;
}
}
function downloadJSON() {
// same preprocessing as for svg
const clippedSvgData = clipSvgDataToCanvasBounds(svgData);
const culledSvgData = cullRedundantSvgDataMoves(clippedSvgData);
const processedSvgData = culledSvgData;
let processedJsonData = [];
let currentLine = [];
processedSvgData.paths.flat().forEach(p => {
if (p.type == "M") {
if (currentLine.length > 1) { // > 1 instead of 0 to account for potential multiple subsequent moves
processedJsonData.push(currentLine);
}
currentLine = [];
}
currentLine.push([parseFloat(p.x.toFixed(2)), parseFloat(p.y.toFixed(2))]);
});
if (currentLine.length > 1) {
processedJsonData.push(currentLine);
}
const dataUrl = `data:application/json;base64,${btoa(JSON.stringify(processedJsonData))}`;
const filename = generateFilename("json");
downloadFile(dataUrl, filename);
}
function exportDrawing(e) { // not just "export" because that's a keyword
const exportButton = document.querySelector(".export");
const exportSheet = document.querySelector(".export-sheet");
const exportSheetWasOpen = exportSheet.style.display == "block";
// download png directly if alt key pressed
if (e.altKey) {
downloadPNG();
return;
}
closeSheets();
if (!exportSheetWasOpen) {
refreshFilesizeEstimate();
exportSheet.style.display = "block";
exportButton.classList.add("active");
}
}
document.querySelector(".export").addEventListener("click", exportDrawing);
function generateShareHash(opts) {
let hash = "";
Object.keys(opts).forEach(n => {
// omit values identical to defaults
if (opts[n] === defaults[n]) {
return;
}
const l = optionShorts[n];
const v = opts[n];
hash += `${l}${v}`;
});
return hash;
}
function generateShareURL(opts) {
const baseUrl = window.location.href.replace(window.location.hash, "");
const hash = generateShareHash(opts);
const url = `${baseUrl}#${hash}`;
return url;
}
function parseShareHash(hash) {
//const matches = [...hash.matchAll(/([a-z]+)([0-9.\-]+)/g)];
let matches = [];
const regex = /([a-z]+)([0-9.\-]+)/g;
let temp;
while ((temp = regex.exec(hash)) !== null) {
matches.push(temp);
}
if (!matches) return false;
let opts = {};
matches.forEach(m => {
const n = shortsOptions[m[1]];
if (!n) return false;
const v = parseFloat(m[2]);
if (v === undefined) return false; // important not to use !v here since !0 => true
opts[n] = v;
})
return opts;
}
function parseShareURL(url) {
const hash = url.split("#")[1];
if (!hash) return false;
return parseShareHash(hash);
}
function tryExtractingOptionsFromUrl() {
return parseShareURL(window.location.href);
}
function showShareStatus(text) {
const shareStatus = document.querySelector(".share-status");
shareStatus.innerText = text;
shareStatus.style.display = "block";
}
function clearShareStatus() {
document.querySelector(".share-status").style.display = "none";
}
function copyShareLink() {
const caring = document.querySelector(".caring");
caring.focus();
caring.select();
let successful = false;
let error = "";
try {
successful = document.execCommand("copy");
} catch (err) {
error = err;
}
if (successful) {
showShareStatus("Success!");
} else {
let text = "Couldn't copy"
if (error) text += ` (${err})`
text += " – try copying the URL manually."
showShareStatus(text);
}
}
function openShareLink() {
const caring = document.querySelector(".caring");
window.location = caring.value;
}
async function shareShareLink() {
const url = document.querySelector(".caring").value;
try {
await navigator.share({url: url});
} catch(err) {
if (err.name != "AbortError") {
showShareStatus(`Couldn't share (${err}) – try copying the URL manually.`);
}
}
}
function refreshShareSheetUrl() {
const shareUrl = generateShareURL(optionValues);
document.querySelector(".caring").value = shareUrl;
clearShareStatus();
}
function share(e) {
const shareButton = document.querySelector(".share");
const shareSheet = document.querySelector(".share-sheet");
const shareSheetWasOpen = shareSheet.style.display == "block";
// copy directly if alt key pressed
if (e.altKey) {
refreshShareSheetUrl();
clearShareStatus();
// since the text of invisible elements can't be copied, we need to do
// silly stuff like this – the browser won't actually redraw during this
// time, so the user won't notice
shareSheet.style.display = "block";