SWCharacter Class Reference

Documenting SWCharacter — a single animated typographic symbol driven by SWBug, SWGrid, and SWColor

🎨 Try the SWCharacter Designer →

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 position
  • bug.origin — home position
  • bug.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 transform
  • grid.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):

// Inside SWCharacter.draw(grid) — called every frame: // 1. Pixel size = user-unit size × pixels-per-user-unit const screenSize = sizeUser * grid.xScale; // 2. Bug position (user units) → screen pixels const sp = grid.userToScreen(this.bug.x, this.bug.y); // 3. Render at sp.x, sp.y with screenSize font translate(sp.x, sp.y); textSize(screenSize); text(this.symbol, 0, 0);

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.

Rule: Always set 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

new SWCharacter(symbol, bug, opts = {})
// Minimal — position only, no animation const bug = new SWBug(100, 200, 'r', 0); const ch = new SWCharacter('A', bug); // Full options const ch = new SWCharacter('A', bug, { fontFamily: 'Georgia, serif', baseSize: 18, // user units fillColor: swOrange, strokeColor: swBrown, strokeWt: 2, shouldBreathe: true, breatheAmount: 5, // user units amplitude breatheSpeed: 1, // cycles per second shouldSpin: false, spinSpeed: 1.0, // radians per second shouldCycleHue: false, hueCycleSpeed: 30, // hue degrees per second });

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:

// Breathe formula — inline SWSinusoid A·sin(B·t) + C const A = this.breatheAmount; // amplitude (user units) const B = this.breatheSpeed * Math.PI * 2; // angular frequency → cycles/sec const C = this.baseSize; // vertical shift / rest size sizeUser = A * Math.sin(B * this._etime) + C; sizeUser = Math.max(1, sizeUser); // never zero or negative
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.

// update(dt) — accumulate angle if (this.shouldSpin) { this._angle += this.spinSpeed * dt; // rad/s × s = rad } // draw(grid) — apply within push/pop around (sp.x, sp.y) translate(sp.x, sp.y); if (this.shouldSpin) rotate(this._angle); text(this.symbol, 0, 0);

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.

if (this.shouldCycleHue && this.fillColor) { const newH = (this.fillColor.h + this.hueCycleSpeed * dt) % 360; // Rebuild SWColor so the internal p5.js color object is refreshed this.fillColor = new SWColor(newH, this.fillColor.s, this.fillColor.b, this.fillColor.a, this.fillColor.name); }

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.

// Inside p5.js draw(): const dt = 1 / frameRate(); // seconds elapsed since last frame ch.update(dt); // 1. advance all animations ch.draw(grid); // 2. render at current position
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().
Position is external. 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)
FieldTypeNotes
_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
// For each letter, create a SWBug at the home position and a SWCharacter on it function buildWord(wordStr) { chars = []; const totalW = wordStr.length * LETTER_SPACING; let cx = grid.UL.x + (grid.LR.x - grid.UL.x) * 0.5 - totalW * 0.5; const cy = (grid.UL.y + grid.LR.y) * 0.5; for (const ch of wordStr) { const bug = new SWBug(cx, cy, 'g', 0); // stationary at home bug.origin = { x: cx, y: cy }; // store home for Assemble const swChar = new SWCharacter(ch, bug, { baseSize: 20, fillColor: randomLetterColor(), shouldBreathe: true, breatheAmount: 3, breatheSpeed: (0.4 + Math.random() * 0.8), // randomized per letter shouldSpin: true, spinSpeed: (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 1.5), }); chars.push({ char: swChar, homeX: cx, homeY: cy, isHome: false, ... }); cx += (ch === ' ' ? SPACE_WIDTH : LETTER_SPACING); } }
2. Animate each frame
// In p5.js draw(): const dt = 1 / frameRate(); for (const s of chars) { if (s.isHome) continue; // locked letters skip movement // Move: update bug.x / bug.y via noise steering or assemble steer moveLetter(s, dt); // Advance animations (breathe, spin, hue) s.char.update(dt); // Render character at its current position via SWGrid s.char.draw(grid); }
3. Assemble: steer toward home and lock
// Assemble branch inside moveLetter() — steer angle toward homeX/homeY if (assembling) { const dx = s.homeX - bug.x, dy = s.homeY - bug.y; let diff = Math.atan2(dy, dx) - s.angle; // Normalize diff to [-π, π] to find the shortest rotation while (diff > Math.PI) diff -= TWO_PI; while (diff < -Math.PI) diff += TWO_PI; s.angle += diff * ASSEMBLE_STEER_RATE; // proportional steer s.speed = max(ASSEMBLE_MIN_SPEED, bug.distanceFromOriginal * 1.2); } // Snap when close enough — lock the letter in place if (bug.distanceFromOriginal < ASSEMBLE_THRESHOLD) { bug.x = s.homeX; bug.y = s.homeY; s.char.setShouldSpin(false); s.char.setSpinSpeed(0); s.isHome = true; bug.trail = []; // wipe trail }
4. Synchronize breathing when all letters are home
if (assembling && chars.every(s => s.isHome)) { // Average all current breathe speeds const avg = chars.reduce((acc, s) => acc + s.char.breatheSpeed, 0) / chars.length; for (const s of chars) { s.char._etime = 0; // reset phase → permanent unison s.char.setBreatheSpeed(avg); // equalize speed } assembling = false; }

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:

// Without SWCharacter — 80+ lines of p5.js per letter per frame // push(); translate(grid.userToScreen(x,y).x, ...); rotate(angle); ... // With SWCharacter — same result, two lines ch.update(dt); ch.draw(grid);
2. SWBug — distanceFromOriginal as a precision tool

The Assemble feature's snap-home test is:

if (bug.distanceFromOriginal < ASSEMBLE_THRESHOLD) { ... // snap }

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.