Overview
SWCharacter represents a single typographic symbol — a letter,
digit, or punctuation mark — drawn on a SWGrid.
It is the central class of the SWCharacter Saga, a sequence of
p5.js sketch stages that explore progressively richer animation systems
built on top of the SketchWaveJS class library.
Each SWCharacter instance manages three independently-togglable
animated features — Breathe (size oscillation),
Spin (rotation around centroid), and
CycleHue (fill color cycling) — as well as its visual
presentation (font, size, fill, stroke).
Position is delegated to an embedded SWBug instance
so that wandering, noise-driven, or user-directed movement can be layered on
top without the character class caring how it got there.
SWCharacter
Owns all display and animation logic for one symbol.
- Source:
shapeClasses/swCharacter.js - Entry:
new SWCharacter(sym, bug, opts) - Loop:
ch.update(dt)+ch.draw(grid)
SWBug (position)
Holds the character's (x, y) in grid user coordinates.
Also maintains the polyline trail for visual feedback.
bug.x/bug.y— current positionbug.origin— home positionbug.distanceFromOriginal— distance from origin
SWGrid (rendering)
Converts user-unit coordinates to screen pixels at draw time, handling all canvas-resize math automatically.
grid.userToScreen(x,y)— position transformgrid.xScale— pixels per user unit (for font sizing)
Coordinate System
SWCharacter stores all positions and sizes in
SWGrid user units — a logical coordinate space defined by the grid's
upper-left and lower-right corners (grid.UL and grid.LR).
This means a character with baseSize = 18 occupies 18 user units of
height regardless of whether the canvas is 400 px or 1200 px wide.
The conversion to screen pixels happens entirely inside draw(grid):
Because grid.userToScreen() and grid.xScale are recomputed
whenever the canvas resizes (via grid.updateScreenBounds()), all
characters automatically scale and reposition correctly with no extra sketch code.
bug.x / bug.y in user
units, not screen pixels. Use grid.userToScreen() or
grid.screenToUser() when you need to cross the boundary —
for example, when mapping a mouse click back to user space.
Constructor
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
symbol |
string |
'A' |
Single character to display. If a longer string is passed, only the first character is used. |
bug |
SWBug |
required | Position manager in SWGrid user coordinates. bug.x / bug.y determine where the character is drawn each frame. |
opts.fontFamily |
string |
'Georgia, serif' |
CSS font-family string passed directly to p5.js textFont(). |
opts.baseSize |
number |
18 |
Base font size in SWGrid user units. Converted to screen pixels via grid.xScale at draw time. The Breathe feature oscillates around this value. |
opts.fillColor |
SWColor|null |
null |
Character fill color. The constructor calls SWColor.copy() so external references cannot alias in and cause unexpected changes. |
opts.strokeColor |
SWColor|null |
null |
Character outline color. Pass null to disable the stroke (noStroke() is called). |
opts.strokeWt |
number |
2 |
Stroke weight in screen pixels (not user units). |
opts.shouldBreathe |
boolean |
false |
Enable Breathe animation. See Breathe. |
opts.breatheAmount |
number |
5 |
Breathe amplitude in user units. The size oscillates between baseSize − breatheAmount and baseSize + breatheAmount. |
opts.breatheSpeed |
number |
1 |
Breathe frequency in cycles per second. |
opts.shouldSpin |
boolean |
false |
Enable Spin animation. See Spin. |
opts.spinSpeed |
number |
1.0 |
Spin rate in radians per second. Negative values spin clockwise (in p5.js's screen coordinate system). |
opts.shouldCycleHue |
boolean |
false |
Enable CycleHue animation. See CycleHue. |
opts.hueCycleSpeed |
number |
30 |
Hue advancement rate in degrees per second. At 30°/s, the full color wheel completes in 12 seconds. |
Animated Features
All three animated features are driven by the internal elapsed-time accumulator
this._etime, which is advanced by update(dt) each frame.
Each feature is independently toggled and independently parameterized.
BREATHE Size Oscillation
When shouldBreathe = true, the rendered font size oscillates
around baseSize using an inline sinusoid:
At rest (t = 0)
sin(0) = 0 → size equals exactly baseSize.
This is always the starting state, since _etime
begins at zero.
Peak expansion
sin = +1 → size is baseSize + breatheAmount.
Reached at quarter-period (t = 0.25 / breatheSpeed s).
Unison breathe
To synchronize a group of characters, reset
ch._etime = 0 and equalize their
breatheSpeed values. Used by the Assemble
feature in Stage 5a.
SPIN Rotation Around Centroid
When shouldSpin = true, an accumulated angle is incremented each
frame and applied via rotate() before the character is drawn.
Because the translate moves the canvas origin to the character's screen
position before rotate is called, the character spins
around its own centroid rather than around the canvas origin.
Both push() and pop() bracket all rendering so the
transformation does not leak into the next draw call.
CYCLEHUE Fill Hue Cycling
When shouldCycleHue = true, the fill color's hue value advances
each update() call, cycling through the full HSB color wheel.
Note: SWColor instances wrap a p5.js color object that is
created at construction time. Changing only the .h field on an existing
instance would not update the internal p5 object, so update() reconstructs
the entire SWColor each frame when hue cycling is active.
update() and draw()
These two methods form the standard p5.js animation loop contract for
SWCharacter. Call them once per frame, in this order.
| Kind | Signature | Description |
|---|---|---|
| instance | update(dt: number) → void |
Advances all time-based animations by dt seconds.
Increments _etime (drives Breathe).
Increments _angle if Spin is on.
Rebuilds fillColor with a new hue if CycleHue is on.
Does not move the bug — position is set externally
by the sketch.
|
| instance | draw(grid: SWGrid) → void |
Renders the character at (bug.x, bug.y) converted to
screen pixels via grid.userToScreen().
Computes the current display size (applying Breathe if active),
converts it through grid.xScale, then issues a
push / translate / rotate? / textFont / textSize / text / pop
sequence. Must be called inside the p5.js draw()
loop after colorMode(HSB) has been set in
setup().
|
SWCharacter.update()
does not call bug.move() or otherwise advance the
bug's position. The sketch is responsible for setting bug.x
and bug.y each frame (via noise-driven walking, steering logic,
or any other movement system) before calling ch.draw(grid).
This separation keeps the character class focused on display, not locomotion.
Setters API
All configurable properties can be changed at runtime through setter methods.
Setters named setShouldXxx(b) take a boolean and coerce it with !!b.
Numeric setters call Number() on their arguments.
| Setter | Argument | Effect |
|---|---|---|
setSymbol(sym) |
string |
Changes the displayed character (first char of sym is used). |
setFontFamily(f) |
string |
Changes the CSS font-family string. |
setBaseSize(s) |
number |
Changes the rest size in user units. Breathe oscillates around this new value immediately. |
setStrokeWt(w) |
number |
Changes the stroke weight in screen pixels. |
setFillColor(c) |
SWColor|null |
Replaces the fill color. Pass null to call noFill(). |
setStrokeColor(c) |
SWColor|null |
Replaces the stroke color. Pass null to call noStroke(). |
setShouldBreathe(b) |
boolean |
Enables or disables Breathe animation. |
setBreatheAmount(a) |
number |
Changes breathe amplitude in user units. |
setBreatheSpeed(s) |
number |
Changes breathe frequency in cycles per second. Changing this while running shifts the phase because _etime is not reset. |
setShouldSpin(b) |
boolean |
Enables or disables Spin animation. |
setSpinSpeed(s) |
number |
Changes spin rate in radians per second. |
setShouldCycleHue(b) |
boolean |
Enables or disables CycleHue animation. |
setHueCycleSpeed(s) |
number |
Changes hue advancement rate in degrees per second. |
Key public fields (no setter needed)
| Field | Type | Notes |
|---|---|---|
_etime |
number |
Elapsed-time accumulator in seconds; drives the Breathe sinusoid.
Public by convention (no underscore enforcement in JavaScript).
Reset to 0 to re-phase breathing — used
by the Assemble feature in Stage 5a to synchronize all letters
to the same phase simultaneously.
|
breatheSpeed |
number |
Directly readable (as well as settable via setBreatheSpeed()).
Read by the Assemble logic to compute the group average before
locking all letters to the same speed.
|
bug |
SWBug |
The embedded position manager. Access ch.bug.x and
ch.bug.y to read or set the character's current
position in user coordinates.
|
fillColor |
SWColor|null |
Directly readable for inspecting the current fill (e.g., copying a letter's color to its bug trail). |
Usage Example
The pattern below reflects how SWCharacter is used in
Stage 5a — the most complex stage — where each letter of a word
wanders independently with Perlin-noise steering, then assembles toward a home
position when the user presses Assemble.
1. Build a word
2. Animate each frame
3. Assemble: steer toward home and lock
4. Synchronize breathing when all letters are home
Class Library Value Analysis
After building all five stages of the SWCharacter Saga, we can evaluate the SW class library from a position of hard-won experience. The question is concrete: would this have been easier to build from scratch?
The honest answer is: the classes were a significant net win — but not for free. Here is the detailed breakdown.
Where the Classes Paid Off
1. SWCharacter — Zero render code in the sketch
The single heaviest benefit. Every letter is 80+ lines of raw p5.js:
a coordinate transform, a Breathe sinusoid computation, a font-size scaling
step, a push / translate / rotate / textFont / textSize / text / pop
sequence, and HSB color management — all repeated for every character, every frame.
With SWCharacter, the entire render loop collapses to two lines:
2. SWBug — distanceFromOriginal as a precision tool
The Assemble feature's snap-home test is:
Without SWBug's origin property and
distanceFromOriginal getter, this would require the sketch to
manually track home positions and compute
Math.sqrt((x − hx)² + (y − hy)²) inline for every letter every
frame. The getter makes a complex proximity check a one-liner — and more
importantly, it reads as meaning rather than arithmetic.
3. SWGrid — Resize math disappears
At every canvas resize, grid.updateScreenBounds() recomputes
the pixel-per-user-unit ratio (xScale) and the origin offset.
Every subsequent ch.draw(grid) call then produces
correctly-scaled, correctly-positioned output — no sketch code needed.
From-scratch code would require every render path to re-derive those transforms on resize. With 12 letters wandering independently, the potential for a missed update is high.
4. SWColor — Copy safety with zero effort
The constructor's SWColor.copy(opts.fillColor) call silently
protects every character from aliasing bugs. Without it, passing the same
SWColor reference to 12 characters and then running CycleHue on
one of them would mutate all 12 simultaneously. The protection is built into
the constructor — you get it at no cost regardless of how the sketch is written.
Where Friction Appeared
1. Manual trail management
The saga moves letters by directly assigning bug.x and
bug.y each frame rather than calling bug.move().
This was a deliberate choice — the Perlin-noise steering and Assemble
steering both need fine control that move()'s random-walk
model does not provide. But it created a gap: SWBug's
trail-recording logic is coupled to move(). Setting
bug.x directly bypasses the trail, so the sketch had to
manually push points onto bug.trail[] and also manually
clear it (bug.trail = []) when a letter snapped home.
Better API: A bug.setPosition(x, y, recordTrail)
method would have absorbed this cleanly.
2. _etime requires direct field access
The unison-breathe feature in Stage 5a resets all letters to the same
phase by writing ch._etime = 0 directly. The leading
underscore signals "internal by convention" but JavaScript's class
system provides no enforcement. More importantly, the class has no
resetPhase() or syncTo(other) method, so the
sketch had to reach into a private-by-convention field to accomplish
a natural animation task.
Better API: ch.resetBreathePhase() would have
made the intent clear and insulated the sketch from the internal
implementation.
3. Grid coordinate overhead for simple geometric tests
The Assemble steer uses grid user coordinates throughout, which is the right decision overall — but it means that bounds-clamping and steering distance calculations all live in user-unit space. For multi-letter layout math (computing letter home positions, centering the word, setting spacing), the sketch must transform between spaces carefully. A simpler sketch with a fixed canvas size would not need this layer at all, and the coordinate abstraction would feel like overhead rather than help.
Verdict: SWGrid's value scales with sketch complexity. It shines when the canvas resizes; it creates friction when the sketch is simple enough to work purely in pixels.
Verdict
From scratch would have been harder. The SW library's value
was conceptual compression:
each letter is one SWCharacter with one SWBug,
and those two objects map cleanly to the mental model of
"a letter that wanders and has characteristics."
That model alignment made the saga stages easy to reason about and
almost trivial to extend.
The friction points were API gaps — places where the library
covered 80% of a task but left the last 20% to the sketch. None of
them were architectural flaws; they were missing convenience methods
(setPosition(), resetBreathePhase()) that
the class could easily gain without changing its design.
The clearest single evidence: building Stage 5a — wandering letters with
noise steering, Assemble homing, proximity snap, trail wipe, and
synchronized breathing — took a few hours. Without SWCharacter absorbing
the render math, and without SWBug's distanceFromOriginal
making the snap test a single line, the same feature set would likely
have taken twice as long and been twice as hard to debug.
SWCharacter
Absorbed the entire render pipeline. Two lines per letter per frame.
SWBug
distanceFromOriginal turned a math expression into intent. Trail system built-in.
API gaps
Missing setPosition() and resetBreathePhase() — easy future additions.