Repository navigation
Expand file tree
/
Copy pathGenerativeLineArt.pde
More file actions
3926 lines (3694 loc) · 164 KB
/
Copy pathGenerativeLineArt.pde
File metadata and controls
3926 lines (3694 loc) · 164 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
// Generative Line Art - pen plotter edition
// ------------------------------------------------------------------
// Processing 4 (Java mode). No extra libraries needed.
//
// * 35 patterns (incl. image-, data-, map-, SVG-, audio- and text-driven), up
// to 4 independent pen layers (each: pattern, seed, pen colour, pen width,
// scale / rotate / offset, mask, field source)
// * live preview while you drag the sliders, undo / redo
// * export as SVG (millimetres, one Inkscape layer per pen), G-code or HPGL,
// with path clean-up (simplify, merge, dedupe), pen-travel optimisation,
// plot-time estimate, registration marks, pen test swatches, batch export
// * text / image masks: keep a layer only inside or outside a shape
// (text in any font installed on the system)
// * presets: save / load the complete setup as a .json file
// * external sources (Sources tab): photos, CSV / JSON data, GPX / GeoJSON
// routes, OpenStreetMap streets, terrain, weather, SVG, audio, text, a
// watched folder, AI-generated designs, and a command-line mode (Cli tab)
//
// Keys: S quick-export into the "exports" folder next to this sketch
// P quick-save preset into "presets" L load preset...
// N / Space new seed R randomize the current pattern
// 1-4 select layer Left / Right previous / next pattern
// Ctrl+Z undo Ctrl+Y / Ctrl+Shift+Z redo
// Mouse wheel over a slider = fine adjustment, elsewhere in the panel = scroll.
// Right-click a pattern parameter or the seed to lock it (Randomize,
// New seed and batch export leave locked values alone).
import java.util.*;
import java.io.File;
// ---------- constants -----------------------------------------------------
final int PANEL_W = 390;
final float PAD = 16;
final float GAP = 5;
final String[] PATTERN_NAMES = {
"Flow field", "Wave lines", "Spirograph", "Noise rings", "Lissajous",
"Contour map", "Truchet tiles", "Hatch shading", "Strange attractor", "Superformula",
"Nested polygons", "Spiral", "Moire circles", "Circle packing", "Harmonograph",
"Hilbert curve", "Sunburst", "Maze", "L-system", "Guilloche", "Maurer rose",
"Phyllotaxis", "Subdivision", "Warped grid", "Fractal tree", "Voronoi",
"Image", "Hex Truchet", "Penrose", "Hex maze",
"Route", "Data lines", "SVG import", "Audio", "Text"
};
final String[] FORMAT_NAMES = { "SVG", "G-code", "HPGL" };
final String[] FORMAT_EXT = { "svg", "gcode", "hpgl" };
final String[] IMAGE_STYLES = { "TSP line", "Stipple", "Hatch", "Spiral", "Squiggle" };
final String[] PAPER_NAMES = { "A5", "A4", "A3", "Square" };
final float[][] PAPER_MM = { {148, 210}, {210, 297}, {297, 420}, {250, 250} };
final String[] PEN_NAMES = { "Black", "Red", "Blue", "Green", "Orange", "Purple", "Teal", "Brown", "White", "Gold", "Silver" };
final int[] PEN_COLORS = { 0xFF111111, 0xFFD62828, 0xFF1D4ED8, 0xFF15803D, 0xFFEA7A00, 0xFF7E22CE, 0xFF0E9AA7, 0xFF7C4A21, 0xFFFAFAFA, 0xFFD4A537, 0xFFB4B8BF };
// paper colours for the preview, image and simulation (plot files stay transparent)
final String[] PAPER_COLOUR_NAMES = { "White", "Cream", "Kraft", "Grey", "Black", "Navy" };
final int[] PAPER_COLOURS = { 0xFFFFFFFF, 0xFFF5EEDC, 0xFFC9A57B, 0xFF8E9196, 0xFF1B1B1D, 0xFF1E2A47 };
final int NUM_LAYERS = 4;
final String[] LSYS_NAMES = { "Snowflake", "Dragon", "Gosper", "Sierpinski", "Plant" };
final String[] FIELD_NAMES = { "Noise", "Photo", "Grid" }; // what drives the field-based patterns
final int C_PANEL = 0xFF1E1F22;
final int C_CANVAS = 0xFF2B2D31;
final int C_FIELD = 0xFF2E3035;
final int C_TRACK = 0xFF44474D;
final int C_TEXT = 0xFFD8DADF;
final int C_DIM = 0xFF8A8E96;
final int C_ACCENT = 0xFF3EC9B0;
final int C_HI = 0xFFA5F3E4;
// ---------- state ---------------------------------------------------------
Random ui = new Random(); // UI randomness (pattern generation uses randomSeed)
Param pPaper, pPaperCol, pLandscape, pMargin, pOptimize, pSeparate;
Param pSpeedDown, pSpeedUp, pLiftTime; // plotter (time estimate, G-code, HPGL)
Param pSimplify, pMerge, pMinLen, pDedupe; // path clean-up
Param pMarks, pPenTest; // extras
Param pMaskSrc, pMaskSize, pMaskThr, pMaskInv; // mask
Param pFormat, pLift, pFlipY, pZUp, pZDown, pServoUp, pServoDown, pBatch; // export
Param pRadius, pStreets, pZoom, pWatch, pAutoExport; // external sources (Sources tab)
ArrayList<Param> globals = new ArrayList<Param>(); // all page-level params (saved in presets)
ArrayList<Param> allParams = new ArrayList<Param>(); // fixed order snapshot used by undo / redo
Layer[] layers = new Layer[NUM_LAYERS];
int curLayer = 0;
Layer gen; // layer that is currently being generated
ArrayList<Widget> panelFlat = new ArrayList<Widget>();
Widget activeW = null;
Param activeParam = null;
boolean ddOpen = false; // pattern dropdown open?
int panelTab = 0; // 0 = Design, 1 = Sources, 2 = Output
float panelScroll = 0, maxScroll = 0;
float clipTop = 0, clipBottom = 0; // visible band of the scrolling part of the panel
TextVal focusText = null; // text field that has keyboard focus (null = none)
int lastChangeMs = -100000; // last time any parameter changed (undo grouping)
ArrayList<float[]> undoStack = new ArrayList<float[]>(), redoStack = new ArrayList<float[]>();
float[] committed;
// mask (text or image), rasterised at MASK_K pixels per mm
final float MASK_K = 4;
TextVal tvMask = new TextVal("PLOT", 60);
PImage maskImg; PShape maskSvg; String maskPath = "";
int maskVersion = 0;
boolean[] maskBits; int maskW, maskH; String maskKey = "";
PFont maskFont, uiFont;
final String DEFAULT_MASK_FONT = "SansSerif.bold";
String maskFontName = DEFAULT_MASK_FONT;
String[] fontNames; // every font installed on this system (PFont.list())
PFont fontPreview; // the chosen font at panel size, for the picker
boolean fontOpen = false; // font list open?
String fontFilter = ""; // typed while the font list is open
int fontScroll = 0; // first visible entry of the filtered font list
// source photo for the Image pattern
PImage photo; String photoPath = "";
float[] photoLum; int photoW, photoH, photoVersion = 0;
float imgX, imgY, imgW, imgH; // where the photo lands on the page (mm)
boolean previewStale = true; // geometry changed -> redraw preview image
float genW = 210, genH = 297; // page size in mm
float xmin, xmax, ymin, ymax; // drawing area inside the margin
PGraphics pg;
File pendingExport, pendingPresetSave, pendingPresetLoad, pendingPhoto, pendingMask;
boolean pendingBatch = false;
String status = "";
int statusMs = -100000;
// ---------- setup / draw --------------------------------------------------
void setup() {
size(1400, 900);
surface.setTitle("Generative Line Art");
surface.setResizable(true);
uiFont = createFont("SansSerif", 13);
textFont(uiFont);
pPaper = new Param(null, "Paper", PAPER_NAMES, 1);
pPaperCol = new Param(null, "Paper colour", PAPER_COLOUR_NAMES, 0);
pPaperCol.geometry = false;
pLandscape = new Param(null, "Landscape", false);
pMargin = new Param(null, "Margin (mm)", 0, 60, 15, false);
pOptimize = new Param(null, "Optimize travel", true);
pSeparate = new Param(null, "File per layer", false);
pSeparate.geometry = false;
pSpeedDown = new Param(null, "Pen-down speed (mm/s)", 1, 150, 25, false);
pSpeedUp = new Param(null, "Travel speed (mm/s)", 5, 300, 80, false);
pLiftTime = new Param(null, "Pen lift (s)", 0, 1, 0.15, false);
pSpeedDown.geometry = false; pSpeedUp.geometry = false; pLiftTime.geometry = false;
pSimplify = new Param(null, "Simplify (mm)", 0, 0.5, 0.05, false);
pMerge = new Param(null, "Merge gap (mm)", 0, 1, 0.1, false);
pMinLen = new Param(null, "Min length (mm)", 0, 3, 0.2, false);
pDedupe = new Param(null, "Remove duplicates", true);
pMarks = new Param(null, "Registration marks", false);
pPenTest = new Param(null, "Pen test swatch", false);
pMaskSrc = new Param(null, "Mask source", new String[] { "Text", "Image" }, 0);
pMaskSize = new Param(null, "Text size (mm)", 10, 250, 90, false);
pMaskThr = new Param(null, "Threshold", 0.05, 0.95, 0.5, false);
pMaskInv = new Param(null, "Invert", false);
pMaskSrc.maskOnly = true; pMaskSize.maskOnly = true; pMaskThr.maskOnly = true; pMaskInv.maskOnly = true;
pFormat = new Param(null, "Export format", FORMAT_NAMES, 0);
pLift = new Param(null, "Pen lift", new String[] { "Z axis", "Servo M3" }, 0);
pFlipY = new Param(null, "Origin bottom-left", true);
pZUp = new Param(null, "Z up (mm)", 0, 20, 5, false);
pZDown = new Param(null, "Z down (mm)", -5, 5, 0, false);
pServoUp = new Param(null, "Servo up (S)", 0, 1000, 50, true);
pServoDown = new Param(null, "Servo down (S)", 0, 1000, 30, true);
pBatch = new Param(null, "Batch count", 2, 50, 10, true);
for (Param p : new Param[] { pFormat, pLift, pFlipY, pZUp, pZDown, pServoUp, pServoDown, pBatch }) p.geometry = false;
pLift.wide = false;
pRadius = new Param(null, "Radius (m)", 200, 5000, 1200, true);
pStreets = new Param(null, "Streets", STREET_TYPES, 0);
pZoom = new Param(null, "Terrain zoom", 6, 14, 11, true);
pWatch = new Param(null, "Watch", false);
pAutoExport = new Param(null, "Auto-export", true);
for (Param p : new Param[] { pRadius, pStreets, pZoom, pWatch, pAutoExport }) p.geometry = false;
Collections.addAll(globals, pPaper, pPaperCol, pLandscape, pMargin, pOptimize, pSeparate, pSpeedDown, pSpeedUp, pLiftTime,
pSimplify, pMerge, pMinLen, pDedupe, pMarks, pPenTest, pMaskSrc, pMaskSize, pMaskThr, pMaskInv,
pFormat, pLift, pFlipY, pZUp, pZDown, pServoUp, pServoDown, pBatch,
pRadius, pStreets, pZoom, pWatch, pAutoExport);
layers[0] = new Layer(0, 0, 0, true, 1234);
layers[1] = new Layer(1, 3, 1, false, 42);
layers[2] = new Layer(2, 2, 2, false, 7);
layers[3] = new Layer(3, 4, 3, false, 99);
allParams.addAll(globals);
for (Layer L : layers) {
Collections.addAll(allParams, L.pEnabled, L.pColor, L.pPen, L.pMode, L.pSeed, L.pScale, L.pRot, L.pOffX, L.pOffY, L.pMask, L.pField);
for (ArrayList<Param> ps : L.modeParams) allParams.addAll(ps);
}
fontNames = PFont.list();
if (!Arrays.asList(fontNames).contains(DEFAULT_MASK_FONT)) fontNames = splice(fontNames, DEFAULT_MASK_FONT, 0);
setMaskFont(DEFAULT_MASK_FONT);
loadHersheyFonts();
cliSetup();
if (!cliOneShot) setupDragDrop();
if (!cliMode) restoreSession();
}
void draw() {
background(C_CANVAS);
if (pendingExport != null) { File f = pendingExport; pendingExport = null; writeExport(f); }
if (pendingPresetSave != null) { File f = pendingPresetSave; pendingPresetSave = null; savePreset(f); }
if (pendingPresetLoad != null) { File f = pendingPresetLoad; pendingPresetLoad = null; loadPreset(f); }
if (pendingPhoto != null) { File f = pendingPhoto; pendingPhoto = null; loadPhoto(f); }
if (pendingMask != null) { File f = pendingMask; pendingMask = null; loadMaskImage(f); }
if (pendingShare != null) { File f = pendingShare; pendingShare = null; writeShareImage(f); }
runMainQueue();
pollWatchFolder();
updatePage();
if (pendingBatch) { pendingBatch = false; batchExport(); }
if (exploreOn && layers[exploreLayer].dirty) newExploreBatch(); // the layer changed: start over
regenerate();
if (pendingAutoExport != null) { File f = pendingAutoExport; pendingAutoExport = null; writeExport(f); }
if (cliStep()) return;
buildPanel();
if (simOn) drawSimulation();
else if (exploreOn) { exploreStep(); drawExplore(); }
else drawPaper();
drawPanel();
drawStatusBar();
drawPalette();
drawDropOverlay();
trackUndo();
autosaveSession();
}
void regenerate() {
for (Layer L : layers) {
if (L.dirty && L.pEnabled.on()) { L.dirty = false; generateLayer(L); previewStale = true; }
}
}
void updatePage() {
float[] sz = PAPER_MM[pPaper.i()];
float a = min(sz[0], sz[1]), b = max(sz[0], sz[1]);
genW = pLandscape.on() ? b : a;
genH = pLandscape.on() ? a : b;
float m = min(pMargin.val, min(genW, genH) / 2 - 1);
xmin = m; ymin = m; xmax = genW - m; ymax = genH - m;
}
// ---------- parameters / layers -------------------------------------------
class Param {
Layer owner; // null = global (page) parameter
String label;
float min, max, val, def;
boolean isInt;
boolean geometry = true; // false: only changes how it looks/exports, not the paths
boolean wide = false; // takes a whole row in the panel
boolean maskOnly = false; // global mask setting: only layers that use the mask regenerate
boolean locked = false; // left alone by Randomize, New seed and batch export
int type; // 0 slider, 1 toggle, 2 choice
String[] opts;
Param(Layer owner, String label, float min, float max, float def, boolean isInt) {
this.owner = owner; this.label = label; this.min = min; this.max = max;
this.val = def; this.def = def; this.isInt = isInt; this.type = 0;
}
Param(Layer owner, String label, boolean def) {
this.owner = owner; this.label = label; this.min = 0; this.max = 1;
this.val = def ? 1 : 0; this.def = this.val; this.isInt = true; this.type = 1;
}
Param(Layer owner, String label, String[] opts, int def) {
this.owner = owner; this.label = label; this.opts = opts; this.min = 0; this.max = opts.length - 1;
this.val = def; this.def = def; this.isInt = true; this.type = 2; this.wide = true;
}
void set(float v) {
v = constrain(v, min, max);
if (isInt) v = round(v);
if (v == val) return;
val = v;
lastChangeMs = millis();
if (!geometry) previewStale = true;
else if (owner == null) { for (Layer L : layers) if (!maskOnly || L.pMask.i() > 0) L.dirty = true; }
else owner.dirty = true;
}
int i() { return round(val); }
boolean on() { return val > 0.5; }
}
class Layer {
int id;
Param pEnabled, pColor, pPen, pMode, pSeed, pScale, pRot, pOffX, pOffY, pMask;
Param pField; // Noise / Photo / Grid for the field-based patterns
TextVal text; // what the Text pattern writes
ArrayList<ArrayList<Param>> modeParams = new ArrayList<ArrayList<Param>>();
ArrayList<ArrayList<PVector>> paths = new ArrayList<ArrayList<PVector>>(); // final plot order
boolean dirty = true;
int nPaths, nPoints;
float length, travel; // pen-down and pen-up distance (mm)
Layer(int id, int mode, int colorIdx, boolean enabled, int seed) {
this.id = id;
pEnabled = new Param(this, "Enabled", enabled);
pColor = new Param(this, "Pen colour", PEN_NAMES, colorIdx);
pPen = new Param(this, "Pen width (mm)", 0.1, 2, 0.4, false);
pMode = new Param(this, "Pattern", PATTERN_NAMES, mode);
pSeed = new Param(this, "Seed", 0, 9999, seed, true);
pScale = new Param(this, "Scale", 0.1, 2, 1, false);
pRot = new Param(this, "Rotate (deg)", -180, 180, 0, false);
pOffX = new Param(this, "Offset X (mm)", -150, 150, 0, false);
pOffY = new Param(this, "Offset Y (mm)", -150, 150, 0, false);
pMask = new Param(this, "Mask", new String[] { "Off", "Inside", "Outside" }, 0);
pField = new Param(this, "Field", FIELD_NAMES, 0);
text = new TextVal("PlotterGraph|single-line text", 200);
text.layer = this;
pEnabled.geometry = false; pColor.geometry = false; pPen.geometry = false;
for (int m = 0; m < PATTERN_NAMES.length; m++) modeParams.add(makeParams(this, m));
}
ArrayList<Param> params() { return modeParams.get(pMode.i()); }
int penColor() { return PEN_COLORS[pColor.i()]; }
float seconds() { // estimated plot time
return length / pSpeedDown.val + travel / pSpeedUp.val + nPaths * 2 * pLiftTime.val;
}
}
Param P(Layer L, String label, float min, float max, float def, boolean isInt) {
return new Param(L, label, min, max, def, isInt);
}
ArrayList<Param> makeParams(Layer L, int mode) {
ArrayList<Param> l = new ArrayList<Param>();
switch (mode) {
case 0: // Flow field
l.add(P(L, "Attempts", 50, 3000, 800, true));
l.add(P(L, "Noise scale", 0.2, 8, 2, false));
l.add(P(L, "Max steps", 10, 500, 150, true));
l.add(P(L, "Step (mm)", 0.2, 4, 0.8, false));
l.add(P(L, "Turbulence", 0.1, 3, 0.5, false));
l.add(P(L, "Octaves", 1, 6, 2, true));
l.add(P(L, "Base angle", 0, 360, 0, false));
l.add(P(L, "Spacing (0=off)", 0, 6, 1.2, false));
break;
case 1: // Wave lines
l.add(P(L, "Lines", 5, 150, 55, true));
l.add(P(L, "Resolution", 50, 800, 300, true));
l.add(P(L, "Amplitude", 0, 80, 22, false));
l.add(P(L, "Noise scale", 0.3, 15, 4, false));
l.add(P(L, "Drift", 0, 1.5, 0.25, false));
l.add(P(L, "Center focus", 0, 1, 0.75, false));
l.add(new Param(L, "Hide overlap", true));
break;
case 2: // Spirograph
l.add(P(L, "Ring teeth", 30, 150, 105, true));
l.add(P(L, "Wheel teeth", 5, 140, 44, true));
l.add(P(L, "Pen offset", 0.1, 1.5, 0.85, false));
l.add(P(L, "Copies", 1, 12, 1, true));
l.add(P(L, "Rotate/copy", 0, 60, 4, false));
l.add(P(L, "Scale/copy", 0.5, 1, 0.92, false));
break;
case 3: // Noise rings
l.add(P(L, "Rings", 5, 150, 60, true));
l.add(P(L, "Resolution", 60, 1000, 400, true));
l.add(P(L, "Distortion", 0, 50, 14, false));
l.add(P(L, "Noise scale", 0.1, 4, 1, false));
l.add(P(L, "Drift", 0, 1.5, 0.25, false));
l.add(P(L, "Inner radius", 0, 0.9, 0.05, false));
break;
case 4: // Lissajous
l.add(P(L, "Freq X", 1, 12, 3, true));
l.add(P(L, "Freq Y", 1, 12, 4, true));
l.add(P(L, "Phase", 0, 360, 90, false));
l.add(P(L, "Phase/copy", 0, 20, 1.5, false));
l.add(P(L, "Copies", 1, 200, 40, true));
l.add(P(L, "Resolution", 100, 3000, 800, true));
l.add(P(L, "Shrink", 0, 0.9, 0, false));
break;
case 5: // Contour map
l.add(P(L, "Levels", 3, 80, 28, true));
l.add(P(L, "Resolution", 40, 300, 160, true));
l.add(P(L, "Noise scale", 0.3, 10, 1.6, false));
l.add(P(L, "Octaves", 1, 6, 3, true));
l.add(P(L, "Warp", 0, 1, 0.3, false));
l.add(P(L, "Island", 0, 1, 0, false));
l.add(new Param(L, "Ridged", false));
break;
case 6: // Truchet tiles
l.add(P(L, "Tiles", 3, 60, 14, true));
l.add(new Param(L, "Style", new String[] { "Arcs", "Diagonals", "Fans" }, 0));
l.add(P(L, "Lines/tile", 1, 8, 2, true));
l.add(P(L, "Bias", 0, 1, 0.5, false));
l.add(P(L, "Inset", 0, 0.8, 0.1, false));
break;
case 7: // Hatch shading
l.add(P(L, "Spacing (mm)", 0.6, 8, 1.6, false));
l.add(P(L, "Angle", 0, 180, 45, false));
l.add(P(L, "Passes", 1, 4, 3, true));
l.add(P(L, "Angle step", 0, 180, 60, false));
l.add(P(L, "Noise scale", 0.3, 10, 2.5, false));
l.add(P(L, "Coverage", 0.3, 0.9, 0.7, false));
l.add(P(L, "Vignette", 0, 1, 0, false));
break;
case 8: // Strange attractor
l.add(new Param(L, "Type", new String[] { "Clifford", "De Jong" }, 0));
l.add(P(L, "a", -3, 3, -1.4, false));
l.add(P(L, "b", -3, 3, 1.6, false));
l.add(P(L, "c", -3, 3, 1.0, false));
l.add(P(L, "d", -3, 3, 0.7, false));
l.add(P(L, "Iterations", 200, 100000, 2500, true));
l.add(P(L, "Strands", 1, 40, 1, true));
break;
case 9: // Superformula
l.add(P(L, "Shapes", 1, 120, 40, true));
l.add(P(L, "Lobes m", 1, 24, 6, true));
l.add(P(L, "n1", 0.1, 10, 0.7, false));
l.add(P(L, "n2", 0.1, 10, 1.6, false));
l.add(P(L, "n3", 0.1, 10, 1.6, false));
l.add(P(L, "Inner size", 0, 0.95, 0.05, false));
l.add(P(L, "Twist", -180, 180, 30, false));
l.add(P(L, "Resolution", 200, 2000, 600, true));
break;
case 10: // Nested polygons
l.add(P(L, "Sides", 3, 12, 4, true));
l.add(P(L, "Layers", 2, 200, 45, true));
l.add(P(L, "Twist/step", 0, 20, 4.0, false));
l.add(P(L, "Shrink/step", 0.8, 0.995, 0.93, false));
l.add(P(L, "Drift X", -4, 4, 0, false));
l.add(P(L, "Drift Y", -4, 4, 0, false));
break;
case 11: // Spiral
l.add(P(L, "Turns", 1, 150, 45, true));
l.add(P(L, "Arms", 1, 12, 1, true));
l.add(P(L, "Points/turn", 30, 400, 120, true));
l.add(P(L, "Wobble (mm)", 0, 20, 3, false));
l.add(P(L, "Wobble scale", 0.2, 8, 1.5, false));
l.add(P(L, "Page fit", 0, 1, 0.6, false));
l.add(P(L, "Hole", 0, 0.9, 0.02, false));
break;
case 12: // Moire circles
l.add(P(L, "Circle sets", 2, 5, 2, true));
l.add(P(L, "Rings", 10, 250, 55, true));
l.add(P(L, "Spacing (mm)", 0.5, 5, 2.4, false));
l.add(P(L, "Spread (mm)", 0, 150, 18, false));
l.add(P(L, "Rotation", 0, 360, 0, false));
l.add(P(L, "Resolution", 100, 800, 360, true));
break;
case 13: // Circle packing
l.add(P(L, "Attempts", 500, 20000, 6000, true));
l.add(P(L, "Min radius", 0.8, 10, 2, false));
l.add(P(L, "Max radius", 5, 100, 40, false));
l.add(P(L, "Padding", 0, 5, 0.8, false));
l.add(P(L, "Ring gap (0=off)", 0, 5, 1.2, false));
break;
case 14: // Harmonograph
l.add(P(L, "Freq X", 1, 8, 2, true));
l.add(P(L, "Freq Y", 1, 8, 3, true));
l.add(P(L, "Detune X", -0.05, 0.05, 0.003, false));
l.add(P(L, "Detune Y", -0.05, 0.05, -0.002, false));
l.add(P(L, "Phase", 0, 360, 90, false));
l.add(P(L, "Damping", 0.0005, 0.03, 0.004, false));
l.add(P(L, "Duration", 10, 400, 100, false));
l.add(P(L, "Steps", 2000, 60000, 15000, true));
break;
case 15: // Hilbert curve
l.add(P(L, "Order", 1, 8, 5, true));
l.add(P(L, "Smooth", 0, 3, 0, true));
l.add(P(L, "Jitter", 0, 1, 0, false));
l.add(new Param(L, "Stretch to page", false));
break;
case 16: // Sunburst
l.add(P(L, "Rays", 30, 1500, 300, true));
l.add(P(L, "Inner radius", 0, 0.9, 0.15, false));
l.add(P(L, "Length var", 0, 1, 0.6, false));
l.add(P(L, "Noise scale", 0.2, 8, 2, false));
l.add(P(L, "Twist", -180, 180, 40, false));
l.add(P(L, "Inner var", 0, 1, 0, false));
break;
case 17: // Maze
l.add(P(L, "Cells", 4, 100, 24, true));
l.add(new Param(L, "Style", new String[] { "Walls", "Path" }, 0));
l.add(P(L, "Straightness", 0, 1, 0.3, false));
l.add(P(L, "Loops", 0, 1, 0, false));
l.add(new Param(L, "Entrance/exit", true));
break;
case 18: // L-system
l.add(new Param(L, "Type", LSYS_NAMES, 0));
l.add(P(L, "Iterations", 1, 16, 4, true));
l.add(P(L, "Angle tweak", -15, 15, 0, false));
l.add(P(L, "Jitter", 0, 0.5, 0, false));
l.add(P(L, "Smooth", 0, 3, 0, true));
break;
case 19: // Guilloche
l.add(P(L, "Lines", 1, 60, 10, true));
l.add(P(L, "Oscillations", 3, 100, 28, true));
l.add(P(L, "Outer radius", 0.2, 1, 0.95, false));
l.add(P(L, "Inner radius", 0, 0.95, 0.35, false));
l.add(P(L, "Outer lobes", 0, 24, 12, true));
l.add(P(L, "Inner lobes", 0, 24, 6, true));
l.add(P(L, "Lobe depth", 0, 0.5, 0.12, false));
l.add(P(L, "Resolution", 300, 6000, 2000, true));
break;
case 20: // Maurer rose
l.add(P(L, "Petals n", 1, 12, 6, true));
l.add(P(L, "Step d (deg)", 1, 359, 71, true));
l.add(P(L, "Points", 60, 3600, 361, true));
l.add(P(L, "Copies", 1, 8, 1, true));
l.add(P(L, "d / copy", 1, 30, 1, true));
l.add(new Param(L, "Draw rose", true));
break;
case 21: // Phyllotaxis
l.add(P(L, "Points", 50, 4000, 900, true));
l.add(P(L, "Angle tweak", -3, 3, 0, false));
l.add(new Param(L, "Mark", new String[] { "Circles", "Dashes", "Spirals" }, 0));
l.add(P(L, "Dot size (mm)", 0.3, 8, 2.4, false));
l.add(P(L, "Size growth", 0, 1, 0.7, false));
l.add(P(L, "Rings / dashes", 1, 5, 1, true));
l.add(P(L, "Spiral family", 3, 9, 6, true));
break;
case 22: // Subdivision
l.add(P(L, "Depth", 1, 14, 8, true));
l.add(P(L, "Min size (mm)", 2, 60, 8, false));
l.add(P(L, "Split variance", 0, 0.45, 0.3, false));
l.add(P(L, "Stop chance", 0, 0.7, 0.2, false));
l.add(P(L, "Fill amount", 0, 1, 0.5, false));
l.add(P(L, "Hatch (mm)", 0.4, 6, 1.2, false));
l.add(P(L, "Gap (mm)", 0, 6, 1.5, false));
break;
case 23: // Warped grid
l.add(P(L, "Lines", 5, 250, 70, true));
l.add(new Param(L, "Direction", new String[] { "Horizontal", "Vertical", "Grid" }, 0));
l.add(P(L, "Bulges", 1, 8, 1, true));
l.add(P(L, "Strength", -1, 1.5, 0.9, false));
l.add(P(L, "Radius", 0.05, 1, 0.35, false));
l.add(P(L, "Noise warp (mm)", 0, 20, 0, false));
l.add(P(L, "Resolution", 50, 1000, 400, true));
break;
case 24: // Fractal tree
l.add(P(L, "Depth", 2, 14, 10, true));
l.add(P(L, "Branches", 2, 4, 2, true));
l.add(P(L, "Branch angle", 5, 90, 25, false));
l.add(P(L, "Angle var", 0, 1, 0.35, false));
l.add(P(L, "Length ratio", 0.5, 0.9, 0.72, false));
l.add(P(L, "Length var", 0, 0.5, 0.2, false));
l.add(P(L, "Bend", -40, 40, 0, false));
break;
case 25: // Voronoi
l.add(P(L, "Cells", 5, 800, 150, true));
l.add(P(L, "Relax", 0, 10, 2, true));
l.add(P(L, "Center bias", 0, 1, 0, false));
l.add(P(L, "Inset rings", 0, 12, 3, true));
l.add(P(L, "Ring gap (mm)", 0.4, 6, 1.2, false));
l.add(P(L, "Smooth", 0, 4, 2, true));
l.add(new Param(L, "Borders", true));
break;
case 26: // Image
l.add(new Param(L, "Style", IMAGE_STYLES, 0));
l.add(P(L, "Points", 200, 12000, 5000, true));
l.add(P(L, "Lines / turns", 10, 250, 70, true));
l.add(P(L, "Contrast", 0.2, 3, 1.2, false));
l.add(P(L, "Brightness", -0.5, 0.5, 0, false));
l.add(P(L, "Angle", 0, 180, 45, false));
l.add(P(L, "Amplitude", 0.1, 1.5, 0.9, false));
l.add(P(L, "Frequency", 0.1, 3, 0.8, false));
l.add(P(L, "Dot size (mm)", 0.2, 3, 0.6, false));
l.add(new Param(L, "Invert", false));
break;
case 27: // Hex Truchet
l.add(P(L, "Tiles", 3, 40, 12, true));
l.add(new Param(L, "Style", new String[] { "Arcs", "Mixed", "Random" }, 0));
l.add(P(L, "Lines/tile", 1, 5, 1, true));
l.add(P(L, "Bias", 0, 1, 0.5, false));
l.add(P(L, "Spread", 0, 0.9, 0.6, false));
l.add(new Param(L, "Hex outline", false));
break;
case 28: // Penrose
l.add(P(L, "Divisions", 1, 8, 5, true));
l.add(new Param(L, "Style", new String[] { "Rhombs", "Arcs", "Both" }, 0));
l.add(P(L, "Zoom", 0.3, 4, 1, false));
l.add(P(L, "Rotation", 0, 72, 0, false));
break;
case 29: // Hex maze
l.add(P(L, "Cells", 4, 60, 18, true));
l.add(new Param(L, "Style", new String[] { "Walls", "Path" }, 0));
l.add(P(L, "Loops", 0, 1, 0, false));
l.add(new Param(L, "Entrance/exit", true));
break;
case 30: // Route (GPX / GeoJSON / OpenStreetMap)
l.add(P(L, "Smooth", 0, 3, 0, true));
l.add(P(L, "Copies", 1, 10, 1, true));
l.add(P(L, "Copy offset (mm)", -3, 3, 0.6, false));
l.add(new Param(L, "Frame", new String[] { "None", "Rectangle", "Circle" }, 0));
l.add(new Param(L, "Fit", new String[] { "Show all", "Fill page" }, 0));
break;
case 31: // Data lines (CSV / JSON series)
l.add(new Param(L, "Style", new String[] { "Ridgeline", "Radial", "Spiral" }, 0));
l.add(P(L, "Row length (0=auto)", 0, 500, 0, true));
l.add(P(L, "Amplitude (mm)", 1, 80, 18, false));
l.add(P(L, "Smoothing", 0, 20, 2, true));
l.add(P(L, "Resolution", 20, 1000, 300, true));
l.add(P(L, "Inner radius", 0, 0.9, 0.2, false));
l.add(new Param(L, "Hide overlap", true));
l.add(new Param(L, "Scale per row", false));
break;
case 32: // SVG import
l.add(new Param(L, "Outline", true));
l.add(P(L, "Hatch (mm, 0=off)", 0, 6, 0, false));
l.add(P(L, "Hatch angle", 0, 180, 45, false));
l.add(new Param(L, "Cross-hatch", false));
break;
case 33: // Audio
l.add(new Param(L, "Style", new String[] { "Waveform", "Spiral", "Spectrum" }, 0));
l.add(P(L, "Lines / turns", 1, 200, 40, true));
l.add(P(L, "Amplitude (mm)", 1, 60, 10, false));
l.add(P(L, "Resolution", 50, 2000, 400, true));
l.add(P(L, "Bands", 8, 256, 64, true));
l.add(P(L, "Gain", 0.2, 5, 1, false));
l.add(new Param(L, "Hide overlap", true));
break;
case 34: // Text (single-line Hershey fonts)
l.add(new Param(L, "Font", HERSHEY_NAMES, 0));
l.add(P(L, "Size (mm)", 3, 80, 14, false));
l.add(P(L, "Line spacing", 0.8, 3, 1.6, false));
l.add(P(L, "Letter spacing", -2, 5, 0, false));
l.add(new Param(L, "Align", new String[] { "Left", "Center", "Right" }, 1));
l.add(new Param(L, "Position", new String[] { "Top", "Middle", "Bottom" }, 1));
l.add(P(L, "Weight", 1, 4, 1, true));
l.add(new Param(L, "Fit to page", false));
break;
}
return l;
}
float pv(int i) { return gen.params().get(i).val; }
void randomizeLayer(Layer L) { randomizeLayer(L, ui); }
void randomizeLayer(Layer L, Random r) {
if (!L.pSeed.locked) L.pSeed.set(r.nextInt(10000));
for (Param p : L.params()) {
if (p.type == 1 || p.locked) continue;
float lo = p.min + 0.08 * (p.max - p.min);
float hi = p.min + 0.75 * (p.max - p.min);
p.set(lo + r.nextFloat() * (hi - lo));
}
}
void newSeed(Layer L) {
if (L.pSeed.locked) setStatus("Seed is locked (right-click it to unlock)");
else L.pSeed.set(ui.nextInt(10000));
}
void resetLayer(Layer L) {
for (Param p : L.params()) p.set(p.def);
}
// Parameters that can be locked: the current pattern's parameters and the seed.
boolean lockable(Param p) {
if (p == null || p.owner == null) return false;
return p == p.owner.pSeed || p.owner.params().contains(p);
}
// ---------- undo / redo ---------------------------------------------------
// The state is the value of every parameter. Changes are committed once the
// mouse is released and nothing has changed for a moment, so one slider drag
// or a burst of mouse-wheel ticks becomes a single undo step.
float[] captureState() {
float[] s = new float[allParams.size()];
for (int i = 0; i < s.length; i++) s[i] = allParams.get(i).val;
return s;
}
void applyState(float[] s) {
for (int i = 0; i < s.length; i++) allParams.get(i).set(s[i]);
}
void commitPending() {
float[] s = captureState();
if (committed == null) { committed = s; return; }
if (Arrays.equals(s, committed)) return;
undoStack.add(committed);
if (undoStack.size() > 200) undoStack.remove(0);
redoStack.clear();
committed = s;
}
void trackUndo() {
if (activeW != null || millis() - lastChangeMs < 300) return;
commitPending();
}
void undo() {
if (activeW != null) return;
commitPending();
if (undoStack.isEmpty()) { setStatus("Nothing to undo"); return; }
redoStack.add(committed);
committed = undoStack.remove(undoStack.size() - 1);
applyState(committed);
setStatus("Undo (" + undoStack.size() + " more)");
}
void redo() {
if (activeW != null) return;
commitPending();
if (redoStack.isEmpty()) { setStatus("Nothing to redo"); return; }
undoStack.add(committed);
committed = redoStack.remove(redoStack.size() - 1);
applyState(committed);
setStatus("Redo (" + redoStack.size() + " more)");
}
// ---------- actions -------------------------------------------------------
void action(int id) {
Layer L = layers[curLayer];
switch (id) {
case 0: newSeed(L); break;
case 1: randomizeLayer(L); break;
case 2: resetLayer(L); break;
case 3: requestExport(); break;
case 4: requestPresetSave(); break;
case 5: requestPresetLoad(); break;
case 6: undo(); break;
case 7: redo(); break;
case 8: pendingBatch = true; setStatus("Batch export running..."); break;
case 9: selectInput("Choose a photo / image", "photoSelected"); break;
case 10: selectInput("Choose a mask image (PNG, JPG, GIF or SVG)", "maskSelected"); break;
case 11: maskImg = null; maskSvg = null; maskPath = ""; maskChanged(); break;
case 12: photo = null; photoLum = null; photoPath = ""; photoChanged(); break;
case 13: selectInput("Choose a GPX or GeoJSON file", "geoSelected"); break;
case 14: selectInput("Choose a CSV or JSON data file", "dataSelected"); break;
case 15: selectInput("Choose an SVG file", "svgSelected"); break;
case 16: selectInput("Choose an audio file (WAV, AIFF or AU)", "audioSelected"); break;
case 17: selectInput("Choose a grid: CSV matrix, ESRI .asc or grey-scale heightmap image", "gridSelected"); break;
case 18: fetchStreets(); break;
case 19: fetchTerrain(); break;
case 20: fetchWeather(); break;
case 21: findLocation(); break;
case 22: fetchUrl(tvUrl.s); break;
case 23: selectFolder("Choose a folder to watch", "watchSelected"); break;
case 24: seedFromText(L, tvSeedText.s); break;
case 25: designFromText(L, tvSeedText.s); break;
case 26: aiGenerate(tvPrompt.s); break;
case 27: clearSources(); break;
case 28: toggleExplore(); break;
case 29: toggleSimulation(); break;
case 30: requestShareImage(); break;
}
}
void keyPressed() {
if (paletteOn) { paletteKey(); return; }
if (fontOpen) { typeFontFilter(); return; }
if (focusText != null) { typeText(focusText); return; }
if (exploreKey()) return;
if (simKey()) return;
Layer L = layers[curLayer];
boolean ctrl = keyEvent != null && (keyEvent.isControlDown() || keyEvent.isMetaDown());
if (ctrl) {
boolean shift = keyEvent.isShiftDown();
if (keyCode == 'Z') { if (shift) redo(); else undo(); }
else if (keyCode == 'Y') redo();
else if (keyCode == 'N') startFresh();
else if (keyCode == 'K') openPalette();
return;
}
if (key == CODED) {
if (keyCode == RIGHT) L.pMode.set((L.pMode.i() + 1) % PATTERN_NAMES.length);
else if (keyCode == LEFT) L.pMode.set((L.pMode.i() + PATTERN_NAMES.length - 1) % PATTERN_NAMES.length);
return;
}
if (key == 's' || key == 'S') quickSave();
else if (key == 'p' || key == 'P') quickSavePreset();
else if (key == 'l' || key == 'L') requestPresetLoad();
else if (key == 'n' || key == 'N' || key == ' ') newSeed(L);
else if (key == 'r' || key == 'R') randomizeLayer(L);
else if (key >= '1' && key <= '0' + NUM_LAYERS) curLayer = key - '1';
else if (key == '/' || key == '?') openPalette();
else if (key == 'e' || key == 'E') toggleExplore();
else if (key == 'g' || key == 'G') toggleSimulation();
else if (key == 'i' || key == 'I') requestShareImage();
else if (key == '0') resetView();
else if (key == '+' || key == '=') zoomAt(PANEL_W + areaW() / 2, areaH() / 2, 1.5);
else if (key == '-') zoomAt(PANEL_W + areaW() / 2, areaH() / 2, 1 / 1.5);
}
// A piece of text edited in a TextFieldW (mask text, URL, prompt, ...).
class TextVal {
String s; int maxLen;
Layer layer; // set for a layer's Text pattern text
TextVal(String s, int maxLen) { this.s = s; this.maxLen = maxLen; }
void set(String v) {
if (v.length() > maxLen) v = v.substring(0, maxLen);
if (!v.equals(s)) { s = v; textEdited(this); }
}
}
void textEdited(TextVal t) {
if (t == tvMask) maskChanged();
else if (t.layer != null && t.layer.pMode.i() == 34) t.layer.dirty = true;
}
// Enter in a field runs the button next to it.
void textSubmitted(TextVal t) {
if (t == tvLocation) findLocation();
else if (t == tvUrl) fetchUrl(t.s);
else if (t == tvSeedText) seedFromText(layers[curLayer], t.s);
else if (t == tvPrompt) aiGenerate(t.s);
}
// Keyboard input while a text field has focus. Ctrl+V (Cmd+V) pastes.
void typeText(TextVal t) {
if (key == ESC) { key = 0; focusText = null; return; } // don't let Esc quit the sketch
if (t == tvNum && numKey()) return;
boolean ctrl = keyEvent != null && (keyEvent.isControlDown() || keyEvent.isMetaDown());
if (ctrl) {
if (keyCode == 'V') { String c = clipboardText(); if (c != null) t.set(t.s + c.replaceAll("[\\r\\n\\t]+", " ")); }
return;
}
if (key == CODED) return;
if (key == ENTER || key == RETURN) { focusText = null; textSubmitted(t); return; }
if (key == TAB) { focusText = null; return; }
if (key == BACKSPACE) {
if (t.s.length() > 0) t.set(t.s.substring(0, t.s.length() - 1));
} else if (key >= 32 && key != DELETE) {
t.set(t.s + key);
}
}
String clipboardText() {
try {
return (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().getData(java.awt.datatransfer.DataFlavor.stringFlavor);
} catch (Exception e) { return null; }
}
// Keyboard input while the font list is open: typing filters the list.
void typeFontFilter() {
if (key == ESC) { key = 0; fontOpen = false; return; }
if (key == CODED) {
if (keyCode == UP) fontScroll--;
else if (keyCode == DOWN) fontScroll++;
return;
}
if (key == ENTER || key == RETURN) { // pick the top visible match
ArrayList<String> f = filteredFonts();
if (f.size() > 0) setMaskFont(f.get(constrain(fontScroll, 0, f.size() - 1)));
fontOpen = false;
} else if (key == BACKSPACE) {
if (fontFilter.length() > 0) fontFilter = fontFilter.substring(0, fontFilter.length() - 1);
fontScroll = 0;
} else if (key >= 32 && key != DELETE && fontFilter.length() < 40) {
fontFilter += key;
fontScroll = 0;
}
}
// ---------- GUI widgets ---------------------------------------------------
abstract class Widget {
float x, y, w, h;
Param p; // parameter this widget edits (if any)
boolean scrolls = false; // part of the scrolling middle of the panel
abstract void display();
void press() {}
void drag() {}
boolean hit() {
if (scrolls && (mouseY < clipTop || mouseY > clipBottom)) return false;
return mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h;
}
}
// Small padlock drawn next to the label of a locked parameter.
void drawLock(float x, float y) {
noFill(); stroke(C_HI); strokeWeight(1.4);
arc(x + 4, y + 5, 6, 7, PI, TWO_PI);
noStroke(); fill(C_HI); rect(x, y + 5, 8, 6, 1);
}
class InfoW extends Widget {
String t;
InfoW(String t) { this.t = t; h = 15 * (split(t, '\n').length) + 4; }
void display() {
fill(C_DIM); textSize(11); textAlign(LEFT, TOP);
textLeading(15);
text(t, x, y + 2);
}
}
class TextFieldW extends Widget {
String label; TextVal tv;
TextFieldW(String label, TextVal tv) { this.label = label; this.tv = tv; h = 40; }
void display() {
boolean focus = focusText == tv;
textSize(11); fill(C_DIM); textAlign(LEFT, TOP); text(label.toUpperCase(), x, y);
stroke(focus ? C_ACCENT : C_TRACK); strokeWeight(1); fill(C_FIELD);
rect(x, y + 16, w, 24, 4);
noStroke(); fill(C_TEXT); textSize(12); textAlign(LEFT, CENTER);
String t = tv.s;
if (focus) { while (t.length() > 0 && textWidth(t) > w - 20) t = t.substring(1); }
else if (textWidth(t) > w - 20) { while (t.length() > 0 && textWidth(t + "...") > w - 20) t = t.substring(0, t.length() - 1); t += "..."; }
text(t, x + 8, y + 27);
if (focus && (millis() / 500) % 2 == 0) { stroke(C_ACCENT); line(x + 9 + textWidth(t), y + 20, x + 9 + textWidth(t), y + 35); }
}
void press() { focusText = tv; }
}
class TabsW extends Widget {
String[] labels = { "Design", "Sources", "Output" };
TabsW() { h = 26; }
void display() {
float bw = w / labels.length;
for (int i = 0; i < labels.length; i++) {
boolean sel = (i == panelTab);
noStroke(); fill(sel ? C_ACCENT : C_FIELD); rect(x + i * bw, y, bw - 3, h, 5);
fill(sel ? C_PANEL : C_TEXT); textSize(12); textAlign(CENTER, CENTER);
text(labels[i], x + i * bw + (bw - 3) / 2, y + h / 2 - 1);
}
}
void press() {
int t = constrain(floor((mouseX - x) / (w / labels.length)), 0, labels.length - 1);
if (t != panelTab) { panelTab = t; panelScroll = 0; ddOpen = false; fontOpen = false; }
}
}
class Header extends Widget {
String t;
Header(String t) { this.t = t; h = 24; }
void display() {
fill(C_DIM); textSize(11); textAlign(LEFT, BOTTOM);
text(t, x, y + h - 5);
stroke(C_TRACK); strokeWeight(1); line(x, y + h - 1, x + w, y + h - 1);
}
}
class SliderW extends Widget {
SliderW(Param p) { this.p = p; h = 32; }
void display() {
textSize(12);
fill(C_TEXT); textAlign(LEFT, TOP); text(p.label, x, y);
if (p.locked) drawLock(x + textWidth(p.label) + 6, y + 1);
if (focusText == tvNum && numParam == p) drawNumField(this);
else { fill(C_ACCENT); textAlign(RIGHT, TOP); text(fmtParam(p), x + w, y); }
float ty = y + 25, x0 = x + 6, x1 = x + w - 6;
strokeCap(ROUND); stroke(C_TRACK); strokeWeight(3); line(x0, ty, x1, ty);
float k = map(p.val, p.min, p.max, x0, x1);
stroke(C_ACCENT); line(x0, ty, k, ty);
noStroke(); fill((activeParam == p || (activeParam == null && hit())) ? C_HI : C_ACCENT);
ellipse(k, ty, 13, 13);
}
void press() { if (sliderPress(this)) { activeW = null; activeParam = null; return; } drag(); }
void drag() {
if (activeW != this) return;
float x0 = x + 6, x1 = x + w - 6;
p.set(map(constrain(mouseX, x0, x1), x0, x1, p.min, p.max));
}
}
class ToggleW extends Widget {
ToggleW(Param p) { this.p = p; h = 22; }
void display() {
stroke(C_TRACK); strokeWeight(1);
fill(p.on() ? C_ACCENT : C_FIELD);
rect(x, y + 3, 15, 15, 3);
if (p.on()) { stroke(C_PANEL); strokeWeight(2); line(x + 3, y + 10, x + 6.5, y + 14); line(x + 6.5, y + 14, x + 12, y + 6); }
noStroke(); fill(C_TEXT); textSize(12); textAlign(LEFT, CENTER);
text(p.label, x + 24, y + 10);
if (p.locked) drawLock(x + 30 + textWidth(p.label), y + 4);
}
void press() { p.set(p.on() ? 0 : 1); }
}
class ChoiceW extends Widget {
int perRow, rows;
Param extra; // optional toggle drawn at the right of the label line
ChoiceW(Param p, int perRow, Param extra) {
this.p = p; this.perRow = perRow; this.extra = extra;
rows = ceil(p.opts.length / (float) perRow);
h = 16 + rows * 26;
}
boolean overExtra() { return extra != null && mouseY < y + 16 && mouseX > x + w - 90; }
void display() {
textSize(11); fill(C_DIM); textAlign(LEFT, TOP); text(p.label.toUpperCase(), x, y);
if (p.locked) drawLock(x + textWidth(p.label.toUpperCase()) + 6, y);
if (extra != null) {
float ex = x + w - 88;
stroke(C_TRACK); strokeWeight(1); fill(extra.on() ? C_ACCENT : C_FIELD); rect(ex, y, 12, 12, 2);
noStroke(); fill(C_TEXT); textAlign(LEFT, TOP); text(extra.label, ex + 18, y);
}
float sw = (w - (perRow - 1) * 3) / perRow;
for (int i = 0; i < p.opts.length; i++) {
float bx = x + (i % perRow) * (sw + 3), by = y + 16 + (i / perRow) * 26;
boolean sel = (i == p.i());
boolean over = mouseX >= bx && mouseX <= bx + sw && mouseY >= by && mouseY <= by + 23;
noStroke(); fill(sel ? C_ACCENT : (over ? C_TRACK : C_FIELD));
rect(bx, by, sw, 23, 4);
fill(sel ? C_PANEL : C_TEXT); textSize(12); textAlign(CENTER, CENTER);
text(p.opts[i], bx + sw / 2, by + 11);
}
}
void press() {
if (overExtra()) { extra.set(extra.on() ? 0 : 1); return; }
float sw = (w - (perRow - 1) * 3) / perRow;
for (int i = 0; i < p.opts.length; i++) {
float bx = x + (i % perRow) * (sw + 3), by = y + 16 + (i / perRow) * 26;
if (mouseX >= bx && mouseX <= bx + sw && mouseY >= by && mouseY <= by + 23) p.set(i);
}
}