SWGhost Saga

Class Reference — SWGhost and Its Supporting Cast

What Is SWGhost?

SWGhost is a self-contained composite class that draws a complete, animated ghost character using the p5.js canvas library. It bundles a rounded head disk, a rectangular body, configurable toe-bump disks along the bottom edge, and two optional SWEyeball instances — all positioned relative to a single anchor point so the whole figure moves as one.

Rather than managing a dozen separate shape variables in your sketch (six disks, one rectangle, two point anchors…), you create one SWGhost, set a few properties, and call three methods per frame. The class grew over six motivated stages recorded in the SWGhost Saga.

How the Layers Fit Together

Inside every SWGhost instance, _build() assembles these parts from one anchor point:

┌──────────────────────────────────────────────────────────────┐ │ SWGhost │ │ │ │ Anchor: _headCenter SWPoint — single source of truth │ │ │ │ Part 1 _ghostToes SWDisk[] — toe bumps, drawn first │ │ Part 2 _ghostBody SWRectangle — body, drawn second │ │ Part 3 _ghostHead SWDisk — head cap, drawn third │ │ Part 4 _leftEye SWEyeball — on head, drawn last │ │ Part 5 _rightEye SWEyeball — on head, drawn last │ │ │ │ Each part is rebuilt by _build() any time the ghost moves │ └──────────────────────────────────────────────────────────────┘ Coordinate geometry: headCenter.y ← dome top of head headCenter.y (body top) ← equator, where body begins headCenter.y - bodyHeight ← bottom of body rectangle headCenter.y - bodyHeight ← centers of toe disks (half-buried) Draw order (back → front): toes → body → head → left eye → right eye

Quick-Start: A Ghost in a Sketch

Minimum code to get a draggable, eye-equipped ghost on screen:

// ── setup() ───────────────────────────────────────────────────────── let grid, ghost; function setup() { createCanvas(windowWidth, windowHeight); colorMode(HSB, 360, 100, 100, 100); initializeSWColors(); grid = new SWGrid(width, height, 20); ghost = new SWGhost( new SWPoint(0, 5), // headCenter — grid user-units 6, // radius (head cap) 9, // bodyHeight 4, // numToes swCyan, // fill color swBlack, // stroke color [swWhite, swBlack], // eyeColors: [scleraFill, scleraBorder] [swBlack, swBlue] // pupilColors: [pupilFill, irisStroke] ); } // ── draw() ────────────────────────────────────────────────────────── function draw() { background(220, 20, 15); ghost.drawOnGrid(grid); } // ── interaction ───────────────────────────────────────────────────── function mousePressed() { ghost.handleMousePressed(mouseX, mouseY, grid); } function mouseDragged() { ghost.handleMouseDragged(mouseX, mouseY, grid); } function mouseReleased() { ghost.handleMouseReleased(); }

The _build() Pattern

SWGhost uses a compose-on-move approach: all child parts (SWDisk, SWRectangle, SWEyeball) are created (or recreated) by the private _build() method. This fires in the constructor and again every time the ghost changes position or a structural property changes. This keeps coordinate math in one single, testable place.

Why recreate objects instead of just moving them?
SWRectangle pre-computes its four vertex coordinates at construction time, so moving one means rebuilding it from scratch. Rather than special-casing each child type, SWGhost rebuilds all of them uniformly on every position change — simple, predictable, and correct.

Dependency Chain

SWGhost ├── SWDisk (head cap — re-uses _headCenter SWPoint reference) ├── SWRectangle (body — fresh SWPoint bodyCenter each _build) ├── SWDisk[] (toes — fresh SWPoint toe centers each _build) └── SWEyeball × 2 (optional; fresh instances each _build) ├── SWDisk × 4 (sclera, pupil, top glint, bottom glint) └── SWPoint × 4 (centers of each disk) All classes consume: SWColor (fill and stroke colors) SWGrid (user-unit ↔ screen-pixel conversion)

Constructor Signature

new SWGhost( headCenter, // SWPoint — anchor in grid user-units; head dome center radius, // number — head-cap radius (body width = radius × 2) bodyHeight, // number — height of the rectangular body in user-units numToes, // number — toe-bump disks along bottom (0–8); 0 = flat color, // SWColor — fill color for head, body, and toes strokeColor, // SWColor — border color; pass null / undefined for none eyeColors, // SWColor[2] — [scleraFill, scleraBorder]; null = no eyes pupilColors // SWColor[2] — [pupilFill, irisStroke]; null = no eyes )
Important: Construct inside p5.js setup()after calling colorMode() and initializeSWColors(). The eye geometry uses p5.js random() which is only available once p5 is running. Omitting eyeColors or pupilColors (or passing null) renders the ghost without eyes — useful for early stages.

Public Properties You Can Read and Write

PropertyTypeDescription
radiusnumberHead-cap radius in user-units. Also controls body width (radius × 2) and eye sizing.
bodyHeightnumberCurrent height of the body rectangle. Modified each frame by stretching().
numToesnumberNumber of toe-bump disks (0–8). Change via setNumToes() to trigger a rebuild.
colorSWColorFill color applied to all ghost parts. Change via setGhostColor() to trigger a rebuild.
strokeColorSWColorBorder color. null / undefined = no stroke.
shouldShowStrokebooleanMaster stroke toggle. false suppresses borders on all parts even if strokeColor is set. Default: true.
isDraggablebooleanWhether the ghost responds to mouse drag. Default: true.
eyeColorsSWColor[2][scleraFill, scleraBorder]. null = no eyes.
pupilColorsSWColor[2][pupilFill, irisStroke]. null = no eyes.

Eye Geometry (Randomized at Construction)

Eye sizing is computed once at construction from two random factors and then locked for the lifetime of the instance. This gives each ghost slightly unique eyes.

// Internal eye geometry (read-only in practice): _eyeSpanFactor = random(0.70, 0.95) // (leftEdge_L → rightEdge_R) / headDiameter _eyeGapFactor = random(0.00, 0.10) // gap between eyes / headDiameter // Derived values (computed in _build each frame): span = _eyeSpanFactor × headDiameter // total eye zone width gap = _eyeGapFactor × headDiameter // center gap eyeR = (span - gap) / 4 // radius of each eye ctc = (span + gap) / 2 // center-to-center distance

Stretch Rest Height

PropertyTypeDescription
_baseBodyHeightnumber privateRest body height captured at construction. stretching() oscillates bodyHeight around this value. Read via ghost._baseBodyHeight if needed.

Private / Internal References

Set by _build(); available for reading but should not be overwritten directly:

PropertyTypeDescription
_headCenterSWPointThe single anchor point. All other positions derive from it.
_ghostHeadSWDiskThe semicircular head cap disk. Re-uses the _headCenter reference directly.
_ghostBodySWRectangleThe rectangular torso. Fresh instance each _build().
_ghostToesSWDisk[]Array of toe-bump disks. Empty when numToes === 0.
_leftEyeSWEyeballLeft eye instance. null when eyes are disabled.
_rightEyeSWEyeballRight eye instance. null when eyes are disabled.
_irisColorSWColorPersisted iris color so setIrisColor() survives rebuilds. Initially null.
_irisThicknessnumberPersisted iris ring weight. Default: 6 px.
_isDraggingbooleanTrue while a ghost-body drag is in progress.

Drawing

drawOnGrid(grid)Call every frame from draw().

Renders all ghost parts in the correct z-order (toes → body → head → eyes). Applies the current shouldShowStroke toggle before drawing so you can hide borders at any time without a rebuild.

// In p5.js draw(): ghost.drawOnGrid(grid);

Position

setPosition(x, y)

Moves the head anchor to (x, y) in grid user-units and calls _build() to reposition every child part. This is the correct way to move a ghost programmatically — for example during sinusoid-driven hover animation.

// Simple hover animation using an SWSinusoid: const etime = millis() / 1000; ghost.setPosition( anchorX + hSin.getValue(etime), // horizontal drift anchorY + vSin.getValue(etime) // vertical bob );

headCenterSWPoint (read-only getter)

Returns the internal head-anchor SWPoint. Use headCenter.x and headCenter.y to read the current position.

Animation

stretching(sinusoid, t)

Drives bodyHeight with an SWSinusoid each frame, making the ghost grow and shrink vertically. The head stays fixed; the body rectangle and toe bumps shift downward to accommodate the changing height. Analogous to SWRectangle.breathe().

Call each frame from draw(), before drawOnGrid(). Can run simultaneously with hover (setPosition()) — hover fires first to set the correct position, then stretching() modifies the height at that new position.

// Formula: // bodyHeight = max(0.1, _baseBodyHeight + sinusoid.getValue(t)) // Then calls _build() to recompute all child positions. // Typical setup: const sSin = new SWSinusoid(0.5, 0, 0, 0); // A=0.5, B=0, C=0, D=0 sSin.setPeriod(2.5); // 2.5-second cycle // In draw(): const etime = millis() / 1000; ghost.stretching(sSin, etime); ghost.drawOnGrid(grid);
Three animations in one draw() call:
Hover, stretch, and Perlin-noise eye wander can all run simultaneously because they modify different aspects of the ghost. The draw order that keeps them correct:
  1. ghost.setPosition(x, y) — moves anchor (hover)
  2. ghost.stretching(sSin, t) — adjusts body height (stretch)
  3. Eye look/wander calls — update pupil offsets (eye)
  4. ghost.drawOnGrid(grid) — renders everything

Live Appearance Changes

setGhostColor(color)

Changes the fill color of the entire ghost (head, body, toes) and triggers an immediate _build() so the new color takes effect this frame.

// Wired to a color picker 'input' event: ghost.setGhostColor(SWColor.fromHex(colorInput.value, 'ghostColor'));

setNumToes(n)

Sets the number of toe-bump disks (0–8, clamped). Triggers an immediate _build().

ghost.setNumToes(3); // three toes ghost.setNumToes(0); // flat bottom

setIrisColor(color)

Updates the iris ring color on both eyes and stores it internally so the color persists through all subsequent _build() calls. Without this persistence, the color would reset whenever the ghost moved (because setPosition triggers a rebuild that creates brand-new SWEyeball instances).

ghost.setIrisColor(SWColor.fromHex(irisInput.value, 'iris'));

setIrisThickness(val)

Sets the iris ring stroke weight in screen pixels. Persisted exactly like setIrisColor().

ghost.setIrisThickness(3); // thin iris ring ghost.setIrisThickness(8); // thick iris ring

Mouse Interaction

handleMousePressed(mx, my, grid, tolerance=0)boolean

Call from p5.js mousePressed(). Eye-pupil clicks are checked first and take priority over ghost-body drags. Returns true if this ghost consumed the event so other handlers can be skipped.

handleMouseDragged(mx, my, grid)boolean

Call from p5.js mouseDragged(). If an eye pupil is being dragged, the eyes handle the event and the ghost body stays still. Otherwise, the entire ghost moves, preserving the original click offset so the ghost doesn't jump.

// Standard interaction wiring: function mousePressed() { if (ghost.handleMousePressed(mouseX, mouseY, grid)) return; // ... other handlers (e.g. toggle grid) ... } function mouseDragged() { ghost.handleMouseDragged(mouseX, mouseY, grid); } function mouseReleased() { ghost.handleMouseReleased(); }

handleMouseReleased()

Clears the drag state on both the ghost body and both eyes.

Getters (Read-Only Properties)

isDraggingboolean

True while the user is actively dragging the ghost body. Does not reflect eye-pupil drags.

Accessing the Eyes Directly

The left and right SWEyeball instances are exposed as _leftEye and _rightEye. You can call any SWEyeball method on them directly — useful for eye animation such as lookingAround(), lookAt(), and lookAtAngle().

// Perlin-noise idle wander (Stage 5+): if (!ghost._leftEye.isDragging && !ghost._rightEye.isDragging) { if (followMode) { const { x: ux, y: uy } = grid.screenToUser(mouseX, mouseY); ghost._leftEye.lookAt(ux, uy); ghost._rightEye.lookAt(ux, uy); } else if (trackMode) { // Both eyes share one angle computed from the midpoint const mx2 = (ghost._leftEye.eyeCenter.x + ghost._rightEye.eyeCenter.x) / 2; const my2 = (ghost._leftEye.eyeCenter.y + ghost._rightEye.eyeCenter.y) / 2; const { x: ux, y: uy } = grid.screenToUser(mouseX, mouseY); const ang = (degrees(Math.atan2(uy - my2, ux - mx2)) + 360) % 360; ghost._leftEye.lookAtAngle(ang); ghost._rightEye.lookAtAngle(ang); } else { ghost._leftEye.lookingAround(lookStrength); ghost._rightEye.lookingAround(lookStrength); } }

Utility

toString()string

console.log(ghost.toString()); // → "SWGhost @ (0.00, 5.00), r=6, bodyH=9, toes=4, eyes: r=2.10"

The Ghost Saga — Six Stages of Development

The SWGhost Saga documents how the SWGhost class was built from scratch through six progressive stages, adding one major capability per stage. Each stage has a live interactive demo you can open directly.

Stage 1 A Framework

The ghost is assembled by hand from primitive shapes directly in the sketch: a head disk, a body rectangle, and toe disks. No class yet — just proof that the geometry works. Establishes the anchor-point system and draw order (toes → body → head) that all later stages inherit.

Open Ghost 1 →

Stage 2 SWGhost Class

All Stage 1 geometry is wrapped inside the new SWGhost class. The constructor, _build(), drawOnGrid(), setPosition(), and full mouse-drag interaction are established. The sketch reduces to a handful of lines.

Open Ghost 2 →

Stage 3 The Eyes Have It

Two SWEyeball instances are added to SWGhost, wired through the optional eyeColors / pupilColors constructor parameters. Eye geometry is randomized once at construction, giving each ghost slightly unique eyes. Pupil drag and mouse-follow are both supported.

Open Ghost 3 →
Stage 4 Haunted Hover

Sinusoid-driven hover animation is added. Two independent SWSinusoid objects (hSin for horizontal drift, vSin for vertical bob) drive setPosition() every frame. A Control Panel exposes amplitude and period sliders. SPACE bar toggles the animation.

Open Ghost 4 →
Stage 5 Living Eyes

Perlin-noise eye animation is added via SWEyeball.lookingAround(). When idle the pupils wander organically; Follow mode tracks the cursor; Track mode locks both eyes to a shared gaze angle. Perlin noise state is preserved through every _build() rebuild so the wander continues seamlessly during hover. A 13-parameter Control Panel covers Eye, Hover, and Ghost & Canvas settings.

Open Ghost 5 →
Stage 6 Stretching

The new SWGhost.stretching(sinusoid, t) method drives bodyHeight with a third SWSinusoid each frame, analogous to SWRectangle.breathe(). The head stays fixed while the body and toes shift downward, creating a rhythmic grow-and-shrink effect. All three animations (hover H, hover V, stretch S) run simultaneously via sinusoids with incommensurable periods, producing non-repeating composite motion.

Open Ghost 6 →
Identifying the “big ideas” in each stage: Every demo page includes a Ghost Info modal (click the ⓘ icon in the hero banner) that lists the key concepts introduced in that stage, along with a summary table of every control in the Control Panel.

SWGhost is composed entirely of smaller, reusable SketchWave classes. Each one has a single, clear responsibility.

SWDisk — A Styled Circle

SWDisk draws a filled, stroked circle at an SWPoint center. It is the primitive used for SWGhost's head cap and all toe bumps, and internally by SWEyeball for each of its four layers.

Key features:

  • new SWDisk(center, radius, thickness, fillColor, strokeColor)
  • drawOnGrid(grid) — renders the disk; call every frame.
  • shouldShowCenter — show the center dot (debugging). Set false inside SWGhost.
  • setFillAlpha(a), setStrokeAlpha(a) — change transparency after construction.
  • breathe(sinusoid, t) — pulse the radius with a sinusoid wave.
const head = new SWDisk( new SWPoint(0, 10), // center at (0, 10) in user-units 6, // radius 2, // border thickness swCyan, // fill swBlack // stroke ); head.shouldShowCenter = false; head.drawOnGrid(grid);

SWRectangle — A Styled Rectangle

SWRectangle draws a filled, stroked rectangle centered on an SWPoint. It pre-computes its four vertex coordinates at construction, which is why SWGhost must call _build() (not just move an x/y value) whenever the ghost changes position.

Key features:

  • new SWRectangle(center, width, height, color, options)
  • drawOnGrid(grid) — renders the rectangle each frame.
  • breathe(sinusoid, t) — modulate height with a sinusoid (the inspiration for SWGhost.stretching()).
  • Options include strokeColor, strokeWeight, showVertices, showCenter.
const body = new SWRectangle( new SWPoint(0, 5), // center 12, // width (= ghost diameter) 9, // height (= bodyHeight) swCyan, { strokeColor: swBlack, strokeWeight: 2, showVertices: false, showCenter: false } ); body.drawOnGrid(grid);
Why does SWGhost rebuild on every move?
SWRectangle stores vertex positions computed at construction. Moving the body requires making a new SWRectangle — there is no setPosition() on it. SWGhost._build() exists precisely to re-run this construction each time the anchor changes.

SWEyeball — An Animated Eyeball

SWEyeball encapsulates four disks (sclera, pupil, two glints) and all the geometry to move, constrain, and animate the pupil. Two instances live inside every SWGhost when eyes are enabled.

Key features:

  • drawOnGrid(grid), handleMousePressed/Dragged/Released()
  • lookAt(userX, userY) — point pupil toward a world position.
  • lookAtAngle(angleDeg) — point pupil toward a shared pre-computed angle.
  • lookingAround(strength) — Perlin-noise idle wander.
  • setIrisColor(color) — live iris color change (no rebuild needed).
  • noiseSpeed — how fast the Perlin noise clock advances per frame.
  • isDragging — true while pupil is being dragged.
// Access the eyes through the ghost: ghost._leftEye.lookingAround(0.8); ghost._rightEye.lookingAround(0.8); // Or set noise speed for character differentiation: ghost._leftEye.noiseSpeed = 0.003; // slow / dreamy ghost._rightEye.noiseSpeed = 0.008; // slightly quicker
Perlin noise state persistence: SWGhost._build() saves and restores each eye's _noiseT, _noiseOffsetX, and _noiseOffsetY when creating new SWEyeball instances. Without this, the eyes would snap to a random position every frame during sinusoid-driven animation (because every frame triggers a rebuild).

SWSinusoid — A Sinusoid Wave

SWSinusoid models one full sinusoidal wave: y = A ⋅ sin(B ⋅ (x − D)) + C. Call getValue(t) each frame with elapsed time in seconds to get the current wave output. Used by SWGhost for hover (two sinusoids) and stretching (one sinusoid).

Key features:

  • new SWSinusoid(A, B, C, D) — amplitude, frequency, DC offset, phase shift.
  • setPeriod(seconds) — convenience method; derives B from period.
  • getValue(t) — returns the wave output at time t seconds.
  • A = 0 effectively disables the sinusoid (output = C).
// Hover animation — two independent sinusoids: const hSin = new SWSinusoid(2.0, 0, 0, 0); // H amplitude = 2 user-units hSin.setPeriod(5.0); // 5-second horizontal cycle const vSin = new SWSinusoid(1.5, 0, 0, 0); // V amplitude = 1.5 user-units vSin.setPeriod(3.0); // 3-second vertical cycle // In draw(): const etime = millis() / 1000; ghost.setPosition( anchorX + hSin.getValue(etime), anchorY + vSin.getValue(etime) ); // Stretch animation — a third sinusoid: const sSin = new SWSinusoid(0.5, 0, 0, 0); sSin.setPeriod(2.5); ghost.stretching(sSin, etime);
Incommensurable periods — a design principle:
If the three periods (5s, 3s, 2.5s) share no simple integer ratio, the composite motion never exactly repeats. The ghost always looks slightly different — a subtle but effective way to make animation feel alive rather than mechanical.

SWPoint — A Position in Space

SWPoint stores an (x, y) coordinate in grid user-units together with optional styling (stroke weight, color, label). It is the fundamental anchor for every shape in the SketchWave system. SWGhost's single _headCenter SWPoint is the source of truth from which all other positions derive.

  • setPosition(x, y) — update coordinates.
  • containsPoint(mx, my, grid, tolerance) — hit test in screen pixels.
  • SWPoint.copy(other) — deep copy (the copy moves independently).

SWColor — A Color in HSB Space

SWColor stores a color as H (0–360), S (0–100), B (0–100), A (0–100) compatible with p5.js colorMode(HSB, 360, 100, 100, 100).

  • SWColor.fromHex(hexStr, name) — convert a CSS hex string (e.g. from an HTML color picker) to HSB.
  • withAlpha(a) — return a semi-transparent copy without modifying the original.
  • darken(f), brighten(f), saturate(f), desaturate(f) — return adjusted copies.
  • initializeSWColors() — call once in setup() to create all global presets: swRed, swBlue, swCyan, swGhost, and 30+ more.
// From a color picker: ghost.setGhostColor(SWColor.fromHex(colorInput.value, 'ghostColor')); // Semi-transparent background for trail effect: background(...swBackground.withAlpha(bgOpacity).toArray());

Files are loaded from the project's shapeClasses/ folder. Use the Copy button to copy any file's full text to the clipboard. This page must be served over HTTP (not opened as a file:// URL) for the source fetch to work.

Loading swGhost.js…
Loading swEyeball.js…
Loading swDisk.js…
Loading swRectangle.js…
Loading swSinusoid.js…
Loading swPoint.js…