Minimal sketch — auto-render mode
import algorithmic.typography.*;
AlgorithmicTypography at;
void setup() {
size(800, 800);
at = new AlgorithmicTypography(this);
at.loadConfiguration("config.json");
at.initialize();
}
void draw() {
// auto-renders — nothing to add
}
Programmatic config (no JSON file)
Configuration cfg = new Configuration.Builder()
.initialTiles(12, 12)
.changedTiles(6, 6)
.changeTime(5000)
.character("@")
.build();
at.setConfiguration(cfg);
at.initialize();
Restart & keyboard
void keyPressed() {
if (key == 'r') at.restart();
if (key == 's') at.toggleFrameSaving();
}
| Method | Returns | Description |
|---|---|---|
| loadConfiguration(file) | this | Load JSON config |
| setConfiguration(cfg) | this | Set Configuration object |
| getConfiguration() | Configuration | Get current config |
| initialize() | this | Build grid & start |
| render() | void | Draw full-window grid |
| renderAt(x,y,w,h) | void | Draw into rectangle |
| restart() | void | Reset animation timer |
| setAutoRender(bool) | this | Enable/disable auto draw |
| isAutoRender() | boolean | Check auto-render state |
| isRunning() | boolean | Animation still playing? |
| getProgress() | float | 0.0 → 1.0 progress |
| getFrameCount() | int | Current frame index |
| getTotalFrames() | int | Total frames in anim |
| toggleFrameSaving() | this | Toggle frame export |
| setFrameSaving(bool) | this | Set frame export on/off |
| getFramesDirectory() | String | Output folder path |
| dispose() | void | Clean up resources |
{
"canvas": { "width": 1080, "height": 1080 },
"animation": {
"duration": 18,
"fps": 30,
"changeTime": 6000,
"secondChangeTime": 12000,
"fadeDuration": 2000,
"character": "A", // or a word: "TYPE"
"textScale": 0.8,
"waveSpeed": 1.0, "waveAngle": 45,
"waveType": "SINE",
"waveMultiplierMin": 0.0, "waveMultiplierMax": 2.0,
"saveFrames": false
},
"grid": {
"initialTilesX": 16, "initialTilesY": 16,
"changedTilesX": 8, "changedTilesY": 8,
"finalTilesX": 4, "finalTilesY": 4
},
"colors": {
"hueMin": 0, "hueMax": 0,
"saturationMin": 0, "saturationMax": 0,
"brightnessMin": 50, "brightnessMax": 255,
"waveAmplitudeMin": -200, "waveAmplitudeMax": 200,
"backgroundR": 0, "backgroundG": 0, "backgroundB": 0
},
"cellBorder": {
"sides": 0,
"r": 255, "g": 255, "b": 255,
"weight": 1.0, "colorMode": 0
},
"cellMotion": {
"style": "perlin",
"radius": 12,
"speed": 1.0
},
"glyphOutline": {
"style": "none",
"r": 255, "g": 255, "b": 255,
"weight": 1.5,
"dashLength": 8.0,
"gapLength": 4.0
},
"gridStripMotion": { // v0.3.0
"axis": "row", // "row" · "column" · "both"
"amplitude": 0.3,
"phaseStep": 0.4,
"rowWaveType": "SINE",
"columnWaveType": "SINE",
"rowSpeed": 1.0,
"columnSpeed": 1.0,
"enabled": true
}
}
Wave presets
at.setWaveType(WavePresets.Type.SINE);
// SINE · TANGENT · SQUARE · TRIANGLE · SAWTOOTH
at.setWaveType("SQUARE"); // string overload
// or via config.json: "waveType": "TANGENT"
Custom wave function (lambda)
at.setWaveFunction(
(x, y, time, angle) -> {
return (float)Math.sin(
x * 0.1 + time
);
}
);
Design system presets
at.useDesignSystem(
DesignSystem.SystemType.SWISS
);
// SWISS · BAUHAUS · JAPANESE
// BRUTALIST · MINIMAL
Vibe coding — natural language
at.setVibe("calm ocean waves");
at.setVibe("chaos energy");
at.setVibe("geometric minimal");
Glyph motion (v0.2.1 – v0.2.4)
// Perlin noise wander
cfg.setCellMotion(new PerlinMotion(12, 1.0f));
// Circular orbital (CW / CCW)
cfg.setCellMotion(new CircularMotion(12, 1.0f, true));
// Lissajous figure-8 (1:2) or knot (3:2)
cfg.setCellMotion(new LissajousMotion(10, 1.0f));
// Spring-damped glyphs
cfg.setCellMotion(new SpringMotion(0.35f, 0.12f));
// Gravity + bounce (kick() for impulse)
cfg.setCellMotion(new GravityMotion(0.4f, 0.7f));
// Mouse repel/attract field
cfg.setCellMotion(new MagneticMotion(this));
// ── v0.2.3 ──────────────────────────────────
// Ripple: click-triggered concentric rings
RippleMotion r = new RippleMotion(14, 1.0f);
r.setTileGrid(width, height, tilesX, tilesY);
r.trigger(mouseX, mouseY); // in mousePressed()
// FlowField: Perlin vector field
cfg.setCellMotion(new FlowFieldMotion(12, 1.0f));
// Orbital: constellation orbit patterns
cfg.setCellMotion(new OrbitalMotion(10, 1.0f));
// Disable
cfg.setCellMotion(null);
// ── JSON alternative (v0.2.4) ────────────────
// Any style above can be set in config.json:
// "cellMotion": { "style": "gravity",
// "radius": 14, "speed": 1.0,
// "gravity": 0.5, "restitution": 0.8 }
Three-stage grid progression
| Stage | Grid Size | Trigger | Default |
|---|---|---|---|
| 1 Initial | initialTilesX × Y | 0 ms | 16 × 16 |
| 2 Changed | changedTilesX × Y | changeTime | 8 × 8 @ 6 s |
| 3 Final | finalTilesX × Y | secondChangeTime | 4 × 4 @ 12 s |
fadeDuration Cross-fade duration between stages (default 2000 ms)
secondChangeTime Set to 0 to
disable the third stage
Reading grid state at runtime
Configuration cfg = at.getConfiguration();
cfg.getInitialTilesX(); // stage 1 cols
cfg.getInitialTilesY(); // stage 1 rows
cfg.getChangedTilesX(); // stage 2 cols
cfg.getChangedTilesY(); // stage 2 rows
cfg.getFinalTilesX(); // stage 3 cols
cfg.getFinalTilesY(); // stage 3 rows
cfg.getChangeTime(); // ms → stage 2
cfg.getSecondChangeTime();// ms → stage 3
cfg.getFadeDuration(); // crossfade ms
Determining current stage manually
int elapsed = millis() - startTime;
int cols, rows;
if (elapsed > cfg.getSecondChangeTime()) {
cols = cfg.getFinalTilesX();
rows = cfg.getFinalTilesY();
} else if (elapsed > cfg.getChangeTime()) {
cols = cfg.getChangedTilesX();
rows = cfg.getChangedTilesY();
} else {
cols = cfg.getInitialTilesX();
rows = cfg.getInitialTilesY();
}
| Method | Type | Default |
|---|---|---|
| getCanvasWidth() | int | 1080 |
| getCanvasHeight() | int | 1080 |
| getAnimationDuration() | int | 18 |
| getAnimationFPS() | int | 30 |
| getChangeTime() | int | 6000 |
| getSecondChangeTime() | int | 12000 |
| getFadeDuration() | int | 2000 |
| getCharacter() | String | "A" |
| getTextScale() | float | 0.8 |
| getWaveSpeed() | float | 1.0 |
| getWaveAngle() | float | 45.0 |
| getWaveType() | String | "SINE" |
| getWaveMultiplierMin() | float | 0.0 |
| getWaveMultiplierMax() | float | 2.0 |
| getInitialTilesX/Y() | int | 16 |
| getChangedTilesX/Y() | int | 8 |
| getFinalTilesX/Y() | int | 4 |
| getHueMin/Max() | float | 0 / 0 |
| getSaturationMin/Max() | float | 0 / 0 |
| getBrightnessMin/Max() | float | 50 / 255 |
| getWaveAmplitudeMin/Max() | float | -200 / 200 |
| getBackgroundRed/Green/Blue() | int | 0 |
| isSaveFrames() | boolean | false |
| getCellBorderSides() | int | 0 |
| getCellBorderRed/Green/Blue() | int | 255 |
| getCellBorderWeight() | float | 1.0 |
| getCellBorderColorMode() | int | 0 = static, 1 = wave |
| getGlyphOutlineStyle() | int | 0=none, 1=solid, 2=dashed, 3=dashedOnly |
| getGlyphOutlineRed/Green/Blue() | int | 255 |
| getGlyphOutlineWeight() | float | 1.5 |
| getGlyphOutlineDashLength() | float | 8.0 (dashed only) |
| getGlyphOutlineGapLength() | float | 4.0 (dashed only) |
Auto Default — library clears & draws each frame
at.initialize();
// draw() is empty — auto-render handles it
Manual You control what goes on the canvas
at.setAutoRender(false);
at.initialize();
void draw() {
background(0);
// … your drawing code …
at.renderAt(0, 0, width, height);
}
Multi Several instances side by side
AlgorithmicTypography a, b;
void setup() {
size(1600, 800);
a = new AlgorithmicTypography(this);
b = new AlgorithmicTypography(this);
a.setAutoRender(false); b.setAutoRender(false);
a.loadConfiguration("a.json"); b.loadConfiguration("b.json");
a.initialize(); b.initialize();
}
void draw() {
a.renderAt(0,0,width/2,height);
b.renderAt(width/2,0,width/2,height);
}
Save frames as PNG sequence
// Enable in config.json
"saveFrames": true
// Or toggle at runtime
at.toggleFrameSaving();
at.setFrameSaving(true);
// Check output directory
println(at.getFramesDirectory());
HSB Colours render correctly in PDF &
SVG — the library switches colorMode(HSB) before
every draw call and restores RGB after. Hue-driven
palettes look identical on screen and in export.
Save vector SVG (correct artboard)
import processing.svg.*;
import algorithmic.typography.render.VectorExporter;
void setup() {
pixelDensity(1); // no Retina scale() in SVG
/* ... */
}
void keyPressed() {
if (key == 's') {
String f = "output.svg";
PGraphics svg = createGraphics(
width, height, SVG, f);
svg.beginDraw();
svg.background(0);
renderGrid(svg); // draw into buffer
svg.endDraw();
svg.dispose();
// fix pt units & viewBox for Affinity /
// Illustrator / Inkscape artboard
VectorExporter.fixArtboardDimensions(
this, f, width, height);
println("SVG saved!");
}
}
Common keyboard shortcuts
| Key | Action |
|---|---|
| R | Restart animation |
| S | Toggle frame saving / save SVG |
glyphOutline
JSON block. O cycles 4 styles: None → Solid → Dashed → Dashed Only
(fill suppressed); left/right arrows cycle 16 test chars; S saves
PNG. (v0.2.5)
interpolateTo(): large central
morph, five-thumbnail timeline strip, four styles (Outline / Filled
/ Dots / Dual), mouse-X scrub (v0.2.3).
GridStripMotion.
1/2/3 cycle ROW/COLUMN/BOTH; W cycles 5 wave types (row+column
sync); ↑/↓ amplitude; ←/→ phase step; SPACE pause; R reset. (v0.3.0)
GlyphCurvatureField modulates wave amplitude by
letterform curvature. C/c cycle chars; +/− adjust intensity; [/]
adjust Gaussian falloff; SPACE toggle outline; S saves. (v0.3.0)
CounterpointEngine: outer form and counter-forms on
independent waves. C/c cycle chars (O B R P D g e @); 1/2 main
speed; 3/4 counter speed; A/a, Z/z adjust angles; S saves. (v0.3.0)
setRhythmFromFont(char) derives waveSpeed from typeface
geometry. ←/→ cycle chars; R toggles auto/manual; [/] manual speed;
+/− rhythmScale; live speed bar HUD. (v0.3.0)
setContent("TEXT") fills tiles L→R top→bottom with
successive chars. ↑/↓ cycle presets; type a custom word + Return;
SPACE toggle word/single mode; H hue preset; S saves. (v0.3.0)
Designer-oriented glyph geometry access
import algorithmic.typography.render.GlyphExtractor;
import java.util.List;
GlyphExtractor g = new GlyphExtractor(this, "Helvetica", 72);
g.setFlatness(0.3); // curve resolution
// Fluent GlyphBuilder (v0.2.3)
GlyphExtractor.of('A', 400)
.fillPoints(500)
.hatch(45, 8)
.colourByPosition()
.hueRange(180, 270)
.draw(this, x, y);
| Method | Returns | Notes |
|---|---|---|
| extractChar(ch, size) | PShape | filled shape |
| extractDeformed(ch, sz, …) | PShape | wave-distorted |
| getContours(ch, size) | List<PVector[]> | all contours |
| getOuterContour(ch, size) | PVector[] | largest area |
| getInnerContours(ch, size) | List<PVector[]> | counter-forms |
| fillWithPoints(ch, size, n) | PVector[] | interior scatter |
| distributeAlongOutline(ch, sz, n) | PVector[] | arc-length spaced |
| getBounds(ch, size) | float[4] | x,y,w,h |
| fillWithLines(ch, sz, ang, gap) | float[][] | hatch segments |
| offsetOutline(ch, size, dist) | PVector[] | inline / outline |
| interpolateTo(c1, c2, sz, t) | PVector[] | morph at t∈[0,1] |
| getMedialAxis(ch, size, n) | PVector[] | spine approx. |
| sampleAlongPath(ch, size, t) | PVector | point at arc t |
| getBoundingContour(chs[], sz, f) | PVector[] | union outline |
| of(ch, size) → GlyphBuilder | GlyphBuilder | fluent API |
| centerOf(str, sz, cx, cy) | PVector | word centering offset |
| getDashedOutline(str, sz, dash, gap) | float[][] | word dashed outline |
v0.2.3 rows
highlighted in accent colour.
See Page 6 for v0.2.5 additions: boolean ops,
path utilities & Type DNA.
Bottom two rows: v0.2.5 word-support String overloads — pass a
word wherever a char was accepted.
| Package | Key classes |
|---|---|
| algorithmic.typography | AlgorithmicTypography, Configuration, HeadlessRenderer |
| …typography.core | WaveEngine, WavePresets, CellMotion, all Motion types, TemporalTrail |
| …typography.render | GridRenderer, GlyphExtractor, GlyphBuilder, TypeDNAProfile, GlyphPhysics, ExportController, VectorExporter |
| …typography.audio | AudioBridge (FFT, beat detection) |
| …typography.style | ColorPalette, Serendipity |
| …typography.system | DesignSystem, VibePreset |
| …typography.text | RitaBridge (RiTa integration) |
| …typography.ui | ControlPanel, Slider, ProgressBar, OSCBridge |
| …typography.ml | AIWaveFunction |
| …typography.net | WebSocketServer |
CellMotion Types
| Class | Since | Description |
|---|---|---|
| CircularMotion | 0.2.1 | CW / CCW orbital |
| PerlinMotion | 0.2.1 | noise wandering |
| LissajousMotion | 0.2.1 | figure-8 / knot |
| SpringMotion | 0.2.1 | spring-damped chase |
| GravityMotion | 0.2.1 | fall + bounce + kick() |
| MagneticMotion | 0.2.2 | mouse repel / attract |
| RippleMotion | 0.2.3 | concentric click rings |
| FlowFieldMotion | 0.2.3 | Perlin vector field |
| OrbitalMotion | 0.2.3 | constellation orbit |
AWT Area boolean algebra on native font outlines.
Both PShape (draw with shape()) and
PVector[] (physics / custom render) return variants.
import algorithmic.typography.render.*;
GlyphExtractor g = new GlyphExtractor(this, "Helvetica", 72);
// PShape overloads — ready to draw with shape()
PShape u = g.union('O', 'C', 400);
PShape i = g.intersect('B', 'E', 400);
PShape s = g.subtract('A', 'V', 400);
shape(u, x, y);
// PVector[] overloads — for physics / custom draw
PVector[] uc = g.getUnionContour('O', 'C', 400);
PVector[] ic = g.getIntersectContour('B', 'E', 400);
PVector[] sc = g.getSubtractContour('A', 'V', 400);
// Arc-length-resampled dashed outline
float[][] dash = g.getDashedOutline('O', 400, 10, 5);
for (float[] seg : dash)
line(seg[0], seg[1], seg[2], seg[3]);
| Method | Returns | Description |
|---|---|---|
| union(c1,c2,sz) | PShape | merged outline |
| getUnionContour(c1,c2,sz) | PVector[] | merged contour pts |
| intersect(c1,c2,sz) | PShape | overlap region |
| getIntersectContour(c1,c2,sz) | PVector[] | overlap contour pts |
| subtract(c1,c2,sz) | PShape | c1 minus c2 |
| getSubtractContour(c1,c2,sz) | PVector[] | knockout contour |
| getDashedOutline(ch,sz,dash,gap) | float[][] | {x1,y1,x2,y2}/seg; per-contour |
| getDashedOutline(str,sz,dash,gap) | float[][] | word variant; per-contour |
Config-driven: add
"glyphOutline": {"style":"dashedOnly", …} to
config.json — AT renders outlines automatically. Four
styles: OUTLINE_NONE, OUTLINE_SOLID,
OUTLINE_DASHED,
OUTLINE_DASHED_ONLY (fill suppressed). See
GlyphOutline: O cycles all 4; ←/→=char;
S=save.
// Unit tangent direction at arc-length t ∈ [0,1]
PVector tan = g.getTangent('A', 400, 0.25f);
float angle = atan2(tan.y, tan.x);
// Orient 48 ornaments along an outline
PVector[] path = g.getOuterContour('O', 600);
for (int k = 0; k < 48; k++) {
float t = (float) k / 48;
PVector pos = /* samplePath(path, t) */;
PVector dir = g.getTangent('O', 600, t);
pushMatrix(); translate(pos.x, pos.y);
rotate(atan2(dir.y, dir.x));
text("›", 0, 0); popMatrix();
}
// Extra tessellation density for deformation
PVector[] dense = g.subdivide('O', 400, 6);
// Lay a string along any contour
PVector[] outer = g.getOuterContour('O', 600);
g.textOnPath("Around the letterform", outer, 24);
| Method | Returns | Notes |
|---|---|---|
| getTangent(ch, size, t) | PVector | unit dir at arc t∈[0,1] |
| subdivide(ch, size, n) | PVector[] | n pts per segment |
| textOnPath(text, path, sz) | PShape | glyph group on contour |
| textOnPath(text, path, sz, spacing) | PShape | + extra px between chars |
See TextOnPath: mode 1 text on outer contour; mode 2 tangent-oriented ornaments; SPACE cycles chars, T cycles text, S saves.
Font outlines drive the animation. Switch typefaces and the animation changes character automatically.
import algorithmic.typography.render.*;
GlyphExtractor g =
new GlyphExtractor(this, "Garamond", 600);
// Individual metrics
float axis = g.getStressAxis('A', 600); // °
PVector centre = g.getOpticalCentroid('A', 600);
float ratio = g.getCounterRatio('O', 600); // 0–1
float weight = g.getStrokeWeight('A', 600); // px
// Aggregate into a reusable profile
TypeDNAProfile dna =
g.buildTypeDNAProfile('A', 600);
// One-call preset — returns this for chaining
at.applyTypeDNA(dna).initialize();
// JSON round-trip — save & share presets
saveJSONObject(dna.toJSON(), "garamond.json");
TypeDNAProfile p =
TypeDNAProfile.fromJSON(
loadJSONObject("garamond.json"));
at.applyTypeDNA(p).initialize();
applyTypeDNA maps the profile:
| DNA field | Config parameter set |
|---|---|
| stressAxis | waveAngle |
| counterRatio | waveAmplitudeRange |
| strokeWeight | brightnessRange |
| Method | Returns | Use as… |
|---|---|---|
| getStressAxis(ch,sz) | float ° | waveAngle |
| getOpticalCentroid(ch,sz) | PVector | orbit / pivot origin |
| getCounterRatio(ch,sz) | float 0–1 | amplitude / breathing |
| getStrokeWeight(ch,sz) | float px | brightness / mass |
| buildTypeDNAProfile(ch,sz) | TypeDNAProfile | full fingerprint |
Shifts entire rows or columns as rigid strips driven by a wave.
Composes with CellMotion — strip slides + per-glyph
orbits work simultaneously.
// Via config.json (recommended)
// "gridStripMotion": { "axis": "row",
// "amplitude": 0.3, "phaseStep": 0.4 }
// Or programmatically
GridStripMotion strip = new GridStripMotion();
strip.setAxis(GridStripMotion.ROW); // ROW · COLUMN · BOTH
strip.setAmplitude(0.3f); // fraction of tile size
strip.setPhaseStep(0.4f); // radians between strips
strip.setRowWaveType("SINE"); // per-axis wave override
strip.setColumnWaveType("PERLIN");
strip.setRowSpeed(1.0f);
strip.setColumnSpeed(-1.0f); // negative = reverse
at.setGridStripMotion(strip);
| Method | Notes |
|---|---|
| setAxis(int) | ROW=0, COLUMN=1, BOTH=2 |
| setAmplitude(float) | 0–1 fraction of tile size |
| setPhaseStep(float) | radians; more = more cycles visible |
| setRowWaveType(String) | SINE · SQUARE · TRIANGLE · |
| setColumnWaveType(String) | SAWTOOTH · TANGENT · PERLIN |
| setRowSpeed(float) | negative reverses direction |
| setColumnSpeed(float) | counter-travel when opposite sign |
| setEnabled(boolean) | toggle without destroying state |
| at.setGridStripMotion(gsm) | attach to AlgorithmicTypography |
Displaces each glyph outline vertex with independent X/Y Perlin
fields. Works on arrays from GlyphExtractor.
GlyphExtractor ge = new GlyphExtractor(this, "Helvetica", 72);
PVector[] outline = ge.getOuterContour('A', 200);
PerlinVertexMotion pvm = new PerlinVertexMotion(this);
pvm.setAmplitude(6.0f); // px displacement
pvm.setSpatialScale(0.03f); // field frequency
pvm.setTimeSpeed(0.015f); // evolution rate/frame
// In draw():
PVector[] warped = pvm.deform(outline, frameCount);
beginShape();
for (PVector v : warped) vertex(v.x, v.y);
endShape(CLOSE);
// Multi-contour shortcut
PVector[][] all = ge.getContours('B', 200);
PVector[][] w = pvm.deformContours(all, frameCount);
Preset ranges of note:
| Feel | amplitude | spatialScale | timeSpeed |
|---|---|---|---|
| Subtle | 2.0 | 0.05 | 0.008 |
| Organic | 6.0 | 0.03 | 0.015 |
| Chaotic | 14.0 | 0.012 | 0.04 |
Replace raw parameter tables with a single
setPreset(int) call.
// MagneticMotion — 4 intensity levels
MagneticMotion mag = new MagneticMotion(this);
mag.setPreset(MagneticMotion.GENTLE); // drift away
mag.setPreset(MagneticMotion.MODERATE); // balanced
mag.setPreset(MagneticMotion.STRONG); // forceful
mag.setPreset(MagneticMotion.SNAPPING); // instant snap
// Normalised strength (0 = off, 1 = preset max)
mag.setStrengthNormalized(0.6f);
// RippleMotion — 3 intensity levels
RippleMotion rip = new RippleMotion(this);
rip.setPreset(RippleMotion.GENTLE);
rip.setPreset(RippleMotion.MODERATE);
rip.setPreset(RippleMotion.STRONG);
// Normalised speed (0 = frozen, 1 = preset max)
rip.setExpandSpeedNormalized(0.5f);
Three named intensity constants replace manual min/max ranges. Each band has pre-tuned standard ranges.
AudioBridge audio = new AudioBridge(this);
audio.initialize();
// Semantic mapping — intensity constants:
// SUBTLE (0) · EXPRESSIVE (1) · FULL (2)
audio.mapBassTo(
config::setWaveSpeed, AudioBridge.EXPRESSIVE);
// bass SUBTLE=[0.8,1.5] EXP=[0.3,3.0] FULL=[0,6]
audio.mapMidTo(
config::setSaturationMax, AudioBridge.SUBTLE);
// mid SUBTLE=[70,140] EXP=[30,200] FULL=[0,255]
audio.mapTrebleTo(
config::setBrightnessMax, AudioBridge.FULL);
// treb SUBTLE=[150,220] EXP=[80,240] FULL=[50,255]
audio.mapOverallTo(
config::setWaveMultiplierMax, AudioBridge.EXPRESSIVE);
// ovrl SUBTLE=[0.5,1.5] EXP=[0.2,3.0] FULL=[0,6]
// In draw():
audio.update();
Add Minim to build.gradle or drop
minim.jar in code/ to enable audio.
| Method | Notes |
|---|---|
| new AudioBridge(PApplet) | pass this |
| initialize() | start audio engine; call once in setup() |
| update() | advance FFT — call every draw() |
| mapBassTo(fn, int) | 20–250 Hz; int = SUBTLE/EXPRESSIVE/FULL |
| mapMidTo(fn, int) | 250 Hz – 4 kHz |
| mapTrebleTo(fn, int) | 4 – 16 kHz |
| mapOverallTo(fn, int) | full-spectrum RMS |
| mapBassTo(fn, float, float) | custom min/max override |
| clearMappings() | remove all band → config bindings |
| getBass() / getMid() | raw 0–1 band energy |
| getTreble() / getOverall() | raw 0–1 band energy |
Pre-tuned output ranges
| Constant | Bass [speed] | Mid [sat] | Treble [bri] | Overall [mult] |
|---|---|---|---|---|
| SUBTLE | 0.8 – 1.5 | 70 – 140 | 150 – 220 | 0.5 – 1.5 |
| EXPRESSIVE | 0.3 – 3.0 | 30 – 200 | 80 – 240 | 0.2 – 3.0 |
| FULL | 0 – 6 | 0 – 255 | 50 – 255 | 0 – 6 |
Samples letterform outline curvature as a Gaussian scalar field that modulates the spatial phase spread of the wave per grid cell. Cells over tight corners and bowls push neighbouring cells into different wave phases — they ripple and flutter independently. Cells over straight stems collapse all nearby cells to the same phase — they pulse together in unison, no ripple.
// Build the field from a single character
GlyphExtractor ge = new GlyphExtractor(this, "Helvetica", 72);
GlyphCurvatureField field =
GlyphCurvatureField.from(ge, 'B', 800f);
// Optional tuning
field.setIntensity(1.0f); // 0–1 phase spread strength
field.setFalloff(0.10f); // Gaussian σ — lower = sharper hotspots
// Attach to the AlgorithmicTypography instance
at.setCurvatureField(field);
// Or attach to WaveEngine directly
waveEngine.setAmplitudeField(field);
// Multi-character version (averages curvature)
GlyphCurvatureField multi =
GlyphCurvatureField.from(ge,
new char[]{ 'B', 'R', 'O' }, 800f);
// Remove
at.setCurvatureField(null);
| Method | Notes |
|---|---|
| from(ge, char, size) | factory — single character |
| from(ge, char[], size) | factory — averaged over set |
| setIntensity(float) | 0 = no effect, 1 = full field |
| setFalloff(float) | Gaussian σ in normalised coords |
| getSampleCount() | number of curvature samples |
| sampleAt(nx, ny) | raw field value at 0–1 coords |
| at.setCurvatureField(f) | attach / null to remove |
| at.getCurvatureField() | returns current field or null |
Two independent WaveEngine instances animate the
outer letterform and inner counter-forms separately. Characters
with enclosed counter-forms (O, B, P, R, D, g, e, a …) show the
strongest effect.
// Configure two wave engines independently
Configuration mainCfg = new Configuration();
mainCfg.setWaveSpeed(0.9f).setWaveAngle(45f)
.setWaveType("SINE");
Configuration cntrCfg = new Configuration();
cntrCfg.setWaveSpeed(1.7f).setWaveAngle(135f)
.setWaveType("TRIANGLE")
.setHueRange(0, 60); // warm for holes
WaveEngine main = new WaveEngine(mainCfg);
WaveEngine cntr = new WaveEngine(cntrCfg);
CounterpointEngine cp =
new CounterpointEngine(main, cntr);
at.setCounterpointEngine(cp);
// Access wave engines for live tweaks
cp.getMainWave().getConfig().setWaveSpeed(1.2f);
cp.getCounterWave().getConfig().setWaveAngle(90f);
| Method | Notes |
|---|---|
| new CounterpointEngine(m, c) | both args non-null required |
| getMainWave() | outer-form WaveEngine |
| getCounterWave() | counter-form WaveEngine |
| update(frameCount) | called automatically by AT |
| at.setCounterpointEngine(e) | attach / null to remove |
| # | Tip |
|---|---|
| 1 |
Always call initialize() after loading or setting
a configuration.
|
| 2 |
Use setAutoRender(false) whenever you draw
backgrounds or composite.
|
| 3 |
renderAt(x, y, w, h) places the grid anywhere —
split layouts, insets, etc.
|
| 4 |
Track your own startTime = millis() — the
library's start time is private.
|
| 5 |
Call restart() + reset
startTime together when restarting.
|
| 6 |
Set secondChangeTime to 0 in JSON to
use only two grid stages.
|
| # | Tip |
|---|---|
| 7 | Colours are HSB: hue 0–360, saturation 0–255, brightness 0–255. |
| 8 |
Place images and JSON files in the sketch's
data/ folder.
|
| 9 |
Builder pattern:
new Configuration.Builder().initialTiles(8,8).build()
|
| 10 |
Javadoc is in reference/ — open
index.html in a browser.
|
| 11 |
RippleMotion.trigger(x,y) must be called from
mousePressed(); call
setTileGrid() once in setup().
|
| 12 |
All GlyphExtractor methods have String overloads
— pass a word (e.g. "TYPE") wherever a single
char was accepted.
|
Derives waveSpeed from the typeface's own geometry —
stroke weight and counter ratio — so each letterform breathes at
its own natural visual pace.
// Automatically set waveSpeed from font geometry
at.setRhythmFromFont('O'); // large counter → faster pulse
at.setRhythmFromFont('I'); // no counter → slower pulse
// Scale the derived frequency up/down
config.setRhythmScale(1.5f); // persist in JSON too
// Formula (for reference):
// freq = (0.5 + counterRatio)
// / (1.0 + strokeWeight / 80.0)
// * rhythmScale
// Read back the computed speed
float spd = config.getWaveSpeed();
// config.json — persisted field:
// "animation": { "rhythmScale": 1.2 }
| Method | Notes |
|---|---|
| at.setRhythmFromFont(char) | sets config.waveSpeed |
| config.setRhythmScale(float) | multiplier; ignored if ≤ 0 |
| config.getRhythmScale() | returns current scale |
Each tile picks its character from a string, left-to-right top-to-bottom, wrapping by string length. Full wave, motion, and physics control is preserved on every character.
// Enable word mode
at.setContent("TYPOGRAPHY");
// or via configuration:
config.setContent("THE GRID");
// Disable (back to single-char mode)
at.setContent(null);
config.setContent("");
// config.json — persisted field:
// "text": { "character": "A",
// "content": "TYPOGRAPHY" }
// Live swap in draw() is safe
if (frameCount % 120 == 0) {
at.setContent(nextWord());
}
| Method | Notes |
|---|---|
| at.setContent(String) | null/"" = single-char mode |
| config.setContent(String) | same; JSON: text.content |
| config.getContent() | returns null when inactive |
Per-character PShape cache is built lazily on the
first frame and reused each draw call — no rebuild cost per frame.