diff --git a/GenerativeLineArt/GenerativeLineArt.pde b/GenerativeLineArt/GenerativeLineArt.pde
index 093c45e..f6eb402 100644
--- a/GenerativeLineArt/GenerativeLineArt.pde
+++ b/GenerativeLineArt/GenerativeLineArt.pde
@@ -9,6 +9,7 @@
// 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
//
// Keys: S quick-export into the "exports" folder next to this sketch
@@ -89,7 +90,14 @@ String maskText = "PLOT";
PImage maskImg; PShape maskSvg; String maskPath = "";
int maskVersion = 0;
boolean[] maskBits; int maskW, maskH; String maskKey = "";
-PFont maskFont;
+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 = "";
@@ -111,7 +119,8 @@ void setup() {
size(1400, 900);
surface.setTitle("Generative Line Art");
surface.setResizable(true);
- textFont(createFont("SansSerif", 13));
+ uiFont = createFont("SansSerif", 13);
+ textFont(uiFont);
pPaper = new Param(null, "Paper", PAPER_NAMES, 1);
pLandscape = new Param(null, "Landscape", false);
pMargin = new Param(null, "Margin (mm)", 0, 60, 15, false);
@@ -163,7 +172,9 @@ void setup() {
Collections.addAll(allParams, L.pEnabled, L.pColor, L.pPen, L.pMode, L.pSeed, L.pScale, L.pRot, L.pOffX, L.pOffY, L.pMask);
for (ArrayList ps : L.modeParams) allParams.addAll(ps);
}
- maskFont = createFont("SansSerif.bold", 96);
+ fontNames = PFont.list();
+ if (!Arrays.asList(fontNames).contains(DEFAULT_MASK_FONT)) fontNames = splice(fontNames, DEFAULT_MASK_FONT, 0);
+ setMaskFont(DEFAULT_MASK_FONT);
}
void draw() {
@@ -630,6 +641,7 @@ void action(int id) {
}
void keyPressed() {
+ if (fontOpen) { typeFontFilter(); return; }
if (textFocus) { typeMaskText(); return; }
Layer L = layers[curLayer];
boolean ctrl = keyEvent != null && (keyEvent.isControlDown() || keyEvent.isMetaDown());
@@ -665,6 +677,27 @@ void typeMaskText() {
}
}
+// 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 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;
@@ -726,7 +759,7 @@ class TabsW extends Widget {
}
void press() {
int t = constrain(floor((mouseX - x) / (w / labels.length)), 0, labels.length - 1);
- if (t != panelTab) { panelTab = t; panelScroll = 0; ddOpen = false; }
+ if (t != panelTab) { panelTab = t; panelScroll = 0; ddOpen = false; fontOpen = false; }
}
}
@@ -897,6 +930,73 @@ class DropdownW extends Widget {
}
}
+// Font picker for the text mask: shows the chosen font in its own typeface and
+// opens a scrollable, type-to-filter list of every font on the system.
+class FontW extends Widget {
+ FontW() { h = 40; }
+ float itemH() { return 22; }
+ int visibleRows() {
+ float below = height - PAD - (y + 42) - 30, above = y - 46 - 30;
+ return constrain(floor(max(below, above) / itemH()), 3, 18);
+ }
+ boolean opensUp() { return height - PAD - (y + 42) < y - 46 && height - PAD - (y + 42) < visibleRows() * itemH() + 30; }
+ float listY() { return opensUp() ? y - 4 - (visibleRows() * itemH() + 30) : y + 42; }
+ void display() {
+ textSize(11); fill(C_DIM); textAlign(LEFT, TOP); text("FONT", x, y);
+ noStroke(); fill(fontOpen ? C_TRACK : C_FIELD); rect(x, y + 16, w, 24, 4);
+ fill(C_TEXT); textAlign(LEFT, CENTER);
+ textFont(fontPreview); textSize(14);
+ String t = maskFontName;
+ while (t.length() > 1 && textWidth(t) > w - 40) t = t.substring(0, t.length() - 1);
+ text(t, x + 10, y + 27);
+ textFont(uiFont); textSize(12);
+ fill(C_ACCENT); triangle(x + w - 20, y + 24, x + w - 10, y + 24, x + w - 15, y + 32);
+ }
+ void overlay() {
+ if (!fontOpen) return;
+ ArrayList f = filteredFonts();
+ int rowsN = visibleRows();
+ fontScroll = constrain(fontScroll, 0, max(0, f.size() - rowsN));
+ float lx = x, ly = listY(), ih = itemH(), lh = rowsN * ih + 30;
+ noStroke(); fill(0, 120); rect(lx + 3, ly + 4, w, lh, 6);
+ fill(C_FIELD); stroke(C_TRACK); strokeWeight(1); rect(lx, ly, w, lh, 6);
+ noStroke(); textSize(11); textAlign(LEFT, CENTER);
+ fill(fontFilter.isEmpty() ? C_DIM : C_TEXT);
+ text(fontFilter.isEmpty() ? "Type to filter (" + f.size() + " fonts)" : "Filter: " + fontFilter + " (" + f.size() + ")", lx + 10, ly + 13);
+ stroke(C_TRACK); line(lx + 6, ly + 25, lx + w - 6, ly + 25); noStroke();
+ for (int r = 0; r < rowsN && fontScroll + r < f.size(); r++) {
+ String name = f.get(fontScroll + r);
+ float iy = ly + 28 + r * ih;
+ boolean over = mouseX >= lx && mouseX <= lx + w - 10 && mouseY >= iy && mouseY <= iy + ih;
+ boolean sel = name.equals(maskFontName);
+ fill(sel ? C_ACCENT : (over ? C_TRACK : C_FIELD)); rect(lx + 3, iy, w - 14, ih - 2, 4);
+ fill(sel ? C_PANEL : C_TEXT); textSize(12); textAlign(LEFT, CENTER); text(name, lx + 12, iy + ih / 2 - 1);
+ }
+ if (f.size() > rowsN) { // scroll bar
+ float track = rowsN * ih, vis = (float) rowsN / f.size();
+ fill(C_TRACK); rect(lx + w - 8, ly + 28 + (track - track * vis) * fontScroll / max(1, f.size() - rowsN), 3, track * vis, 2);
+ }
+ }
+ boolean overList() {
+ float ly = listY();
+ return mouseX >= x && mouseX <= x + w && mouseY >= ly && mouseY <= ly + visibleRows() * itemH() + 30;
+ }
+ void press() { fontOpen = true; fontFilter = ""; scrollToCurrent(); }
+ void scrollToCurrent() {
+ int i = filteredFonts().indexOf(maskFontName);
+ fontScroll = max(0, i - visibleRows() / 2);
+ }
+ // Returns true when the click landed inside the list (a font or the filter line).
+ boolean clickItem() {
+ if (!overList()) return false;
+ ArrayList f = filteredFonts();
+ float ly = listY();
+ int r = floor((mouseY - ly - 28) / itemH());
+ if (mouseY >= ly + 28 && r < visibleRows() && fontScroll + r < f.size()) { setMaskFont(f.get(fontScroll + r)); fontOpen = false; }
+ return true;
+ }
+}
+
class ButtonRow extends Widget {
String[] labels; int[] ids;
ButtonRow(String[] labels, int[] ids) { this.labels = labels; this.ids = ids; h = 30; }
@@ -1020,6 +1120,7 @@ void buildOutputTab(ArrayList top) {
top.add(new ChoiceW(pMaskSrc, 2, pMaskInv));
if (pMaskSrc.i() == 0) {
top.add(row(new TextFieldW("Text ( | = new line)"), new SliderW(pMaskSize)));
+ top.add(new FontW());
} else {
top.add(new ButtonRow(new String[] { "Load mask image...", "Clear" }, new int[] { 10, 11 }));
top.add(row(new SliderW(pMaskThr), new InfoW(maskPath.isEmpty() ? "No image loaded" : new File(maskPath).getName())));
@@ -1081,6 +1182,7 @@ void drawPanel() {
text("S export | P save preset | L load | N seed | R randomize\nCtrl+Z undo | Ctrl+Y redo | 1-4 layer | arrows = pattern", PAD, clipBottom + 36);
for (Widget w : panelFlat) if (w instanceof DropdownW) ((DropdownW) w).overlay();
+ for (Widget w : panelFlat) if (w instanceof FontW) ((FontW) w).overlay();
}
String fmtParam(Param p) {
@@ -1091,6 +1193,12 @@ String fmtParam(Param p) {
void mousePressed() {
textFocus = false;
+ if (fontOpen) {
+ boolean inList = false;
+ for (Widget w : panelFlat) if (w instanceof FontW) inList |= ((FontW) w).clickItem();
+ if (!inList) fontOpen = false;
+ return;
+ }
if (ddOpen) {
for (Widget w : panelFlat) if (w instanceof DropdownW) ((DropdownW) w).clickItem();
ddOpen = false;
@@ -1115,6 +1223,7 @@ void mouseDragged() { if (activeW != null) activeW.drag(); }
void mouseReleased() { activeW = null; activeParam = null; }
void mouseWheel(MouseEvent event) {
+ if (fontOpen) { fontScroll += event.getCount() * 3; return; }
if (ddOpen) return;
for (Widget w : panelFlat) {
if (w instanceof SliderW && w.hit()) {
@@ -1456,6 +1565,28 @@ void maskChanged() {
for (Layer L : layers) if (L.pMask.i() > 0) L.dirty = true;
}
+// Font names matching the typed filter (every word must appear, any order).
+ArrayList filteredFonts() {
+ ArrayList out = new ArrayList();
+ String[] words = splitTokens(fontFilter.toLowerCase());
+ for (String n : fontNames) {
+ String ln = n.toLowerCase();
+ boolean ok = true;
+ for (String wd : words) if (!ln.contains(wd)) { ok = false; break; }
+ if (ok) out.add(n);
+ }
+ return out;
+}
+
+void setMaskFont(String name) {
+ PFont f = null;
+ try { f = createFont(name, 96); } catch (Exception e) {}
+ if (f == null) { setStatus("Font not available: " + name); return; }
+ maskFont = f;
+ fontPreview = createFont(name, 14);
+ if (!name.equals(maskFontName)) { maskFontName = name; setStatus("Mask font: " + name); maskChanged(); }
+}
+
void maskSelected(File f) { if (f != null) pendingMask = f; }
void loadMaskImage(File f) {
@@ -3443,6 +3574,7 @@ void savePreset(File f) {
for (Param p : globals) st.setFloat(p.label, p.val);
root.setJSONObject("settings", st);
root.setString("maskText", maskText);
+ root.setString("maskFont", maskFontName);
root.setString("maskImage", maskPath);
root.setString("photo", photoPath);
JSONArray arr = new JSONArray();
@@ -3500,6 +3632,11 @@ void loadPreset(File f) {
}
String mt = root.getString("maskText", maskText);
if (!mt.equals(maskText)) { maskText = mt; maskChanged(); }
+ String mf = root.getString("maskFont", DEFAULT_MASK_FONT);
+ if (!mf.equals(maskFontName)) {
+ if (Arrays.asList(fontNames).contains(mf)) setMaskFont(mf);
+ else setStatus("Preset font \"" + mf + "\" is not installed; keeping " + maskFontName);
+ }
String mimg = root.getString("maskImage", "");
if (!mimg.isEmpty() && !mimg.equals(maskPath) && new File(mimg).exists()) loadMaskImage(new File(mimg));
String ph = root.getString("photo", "");
diff --git a/README.md b/README.md
index cd6105b..f13aa42 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ Generative line art with a live-adjustable GUI, built as a single [Processing](h
- **Live preview** — drag sliders, pick paper size (A5/A4/A3/Square, portrait or landscape), and see the result update immediately.
- **Undo / redo** — every parameter change (a whole slider drag counts as one step), up to 200 steps.
- **Parameter locks** — right-click a pattern parameter or the seed to lock it; Randomize, New seed and batch export leave locked values alone.
-- **Masks** — keep a layer only inside or outside a shape: typed text (use `|` for a new line) or a loaded PNG/JPG/GIF/SVG silhouette, with threshold and invert.
+- **Masks** — keep a layer only inside or outside a shape: typed text (use `|` for a new line) in any font installed on your system, or a loaded PNG/JPG/GIF/SVG silhouette, with threshold and invert.
- **Plot time estimate** — pen-down distance, pen-up travel and number of pen lifts per layer, turned into a plot time from your plotter's speeds.
- **Path clean-up** — simplification (Ramer-Douglas-Peucker), joining paths whose ends nearly touch, dropping tiny fragments and duplicate paths, and nearest-neighbour travel optimisation. The preview, statistics and export all use the cleaned-up paths.
- **Registration marks and pen test swatches** — optional corner crosshairs drawn by every pen (to line the paper up after a pen change) and a small test patch per pen, plotted first, in the bottom margin.
@@ -46,6 +46,7 @@ Open [`GenerativeLineArt/GenerativeLineArt.pde`](GenerativeLineArt/GenerativeLin
- **New seed / Randomize / Reset** buttons (or `N`/`Space`, `R`) regenerate the current layer.
- **Undo / Redo** buttons, or `Ctrl+Z` and `Ctrl+Y` / `Ctrl+Shift+Z` (`Cmd` on macOS).
- **Mask** (Design tab, per layer): Off / Inside / Outside. Pick the text or image on the Output tab.
+- **Font** (Output tab, text mask): click to open a list of every installed font. Type to filter it (e.g. `bold`), scroll with the mouse wheel or Up/Down, then click a font or press Enter. The font is saved in presets; if a preset's font isn't installed, the current font stays.
- **Export ...** (or `S`) opens a save dialog in the chosen format; a quick-save also writes into `GenerativeLineArt/exports/`.
- **Save preset... / Load preset...** (or `P`, `L`) store/restore the whole setup as JSON in `GenerativeLineArt/presets/`.