The Big Picture
When I was asked to make art, the immediate question was: what is the difference between having a class library and not having one? The answer is more interesting than it might seem. It isn't just about writing less code — it's about thinking at a higher level of abstraction.
In raw p5.js, I would have been thinking in pixels and canvas API calls. With SketchWave, I was thinking in objects, user-space coordinates, and named colors. The scene I had in my head — a wizard in a moonlit clearing — could be expressed almost directly in code. Every noun in the scene description became a variable name. Every visual property became a constructor argument.
The SketchWave version is not shorter by accident. It's shorter because the library absorbed all the coordinate mapping, color management, and canvas state into the class layer — and then exposed a clean, readable interface. The variable is named wizNose. Its position is (1.5, 3.82) in scene units. Even someone who has never seen p5.js can read that code and understand what it does.
Shape Chronicle — All 31 Classes
Here is how I used each SketchWave class, what role it played in the scene, and what I noticed about working with it.
Infrastructure Classes
| Class | Role in the Wizard Scene | What I Noticed |
|---|---|---|
| SWColor | Every single color in the scene — sky, robe, face, orb rings, stars, ghost — is an SWColor. Named colors like "robeColor" and "moonSilver" made the palette self-documenting. |
HSB mode is far more intuitive for art. Tweaking saturation/brightness to darken or soften a color feels natural. Naming them was invaluable for debugging. |
| SWPoint | Every shape position — 30+ instances — is an SWPoint. The entire scene is laid out in user-unit coordinates (−10 to +10) rather than pixel coordinates. |
The separation of user space from pixel space was the single biggest enabler. I composed the whole scene without ever writing map() manually. |
| SWGrid | The grid is created once by the template engine and passed to every drawOnGrid(grid) call. It serves as the universal coordinate translator for all 31 shapes. |
The template's grid.updateScreenBounds() pattern means the whole scene automatically scales if the canvas resizes — no recalculation on my end. |
| SWSinusoid | Three instances drive all animation: sinTwinkle (star breathing), sinSparkle (orb pulse radius), and sinBob (ghost float). |
Declaring what the wave does (A, B, C, D) and calling getValue(t) is elegant. The phase-offset trick — temporarily setting sinTwinkle.D = i * 0.38 per star — gave each star a unique twinkle cycle. |
Sky & Background Scene
| Class | Role in the Wizard Scene | What I Noticed |
|---|---|---|
| SWGradient | Covers the entire canvas with a dark-blue → dark-indigo sky gradient. Drawn first, it overwrites the flat background color and gives the night sky its depth. | Drawing it every frame (not just once) means it always covers any grid lines cleanly. The gradient is so subtle that the scene never looks garish — just atmospheric. |
| SWTwoTonedDisk | The full moon — silver bright side, gray-blue shadow side — in the upper-right corner. The dividing diameter rotates each frame, making the moon slowly turn. | Using startAngle=90 put the bright half on top (matching how the moon actually looks). The slow rotate(0.3) per frame is barely perceptible — which makes it feel right. |
| SWWheel | An 8-spoke magical mandala deep in the background left side. The named-parameter constructor let me tune hub size, spoke count, rim color independently. | The named-parameter pattern (new SWWheel({...})) is noticeably friendlier than positional arguments for an object this complex. rotate(15, etime) gives a time-based spin that resets cleanly on Reset. |
| SWStar ×16 | 16 twinkling stars scattered across the night sky. Built from an array of position/size data; each star gets a slightly different hue (gold→white-blue range). | breathe(sinTwinkle, null, etime) scales only the outer tips — the inner valleys stay fixed. Combined with phase offsets, the star field looks genuinely alive rather than mechanically synchronized. |
| SWArch ×2 hills | The rolling green hills on the horizon. The key insight: a < 0 makes a dome arch (summit at top); the filled region between the parabolic arc and the straight base chord creates a perfect hill silhouette. |
The a parameter is one of the most elegant design decisions in the library. One number switches the shape from a bowl to a dome. Both hills are placed at different depths by using slightly different greens. |
| SWRectangle | The dark-green ground strip across the bottom of the scene. Width = 22 (wider than the 20-unit grid) so the edges are never visible regardless of canvas size. | A simple but essential shape. Making it slightly wider than the canvas is a composition trick — the scene feels grounded rather than floating. |
| SWTwoTonedSquare ×2 | Two stone floor tiles near the wizard's feet. Using n=2 (diagonal split) and n=3 (opposite quadrants) on adjacent tiles creates a visible pattern. |
Two tiles, same size, same colors, different n — that small variation is enough to read as "patterned stone." A simple way to imply texture without any bitmap images. |
| SWRoundedRectangle | The ancient spell-tome lying on the ground. The small corner radius (0.25) makes the rectangular book look worn and leatherbound rather than perfectly sharp. |
The corner radius is a small parameter with a big impact on perceived age of the object. A sharp corner reads as modern; a gentle rounding reads as antique. Very useful for set-dressing. |
| SWPolygon | A Z-shaped golden lightning bolt in the mid-sky, built from 8 explicit SWPoint vertices. Positioned to avoid overlapping the moon. |
The ability to hand-define an arbitrary polygon with named vertex colors is essential for irregular natural shapes. This was the most "manual" shape in the scene — and intentionally so. |
| SWGhost | A semi-transparent floating ghost in the upper-left. At 65% alpha fill, stars can be seen through it. It bobs on a SWSinusoid via setPosition() each frame. |
SWGhost packs an enormous amount of geometry (head disk, body curve, toe bumps, two eyes) into a single high-level constructor call. Without it, the ghost would have been the most complex shape to draw. |
Wizard Body & Costume
| Class | Role in the Wizard Scene | What I Noticed |
|---|---|---|
| SWTriangle robe | The large isosceles-like triangular robe body. Three vertices: collar (top), left hem, right hem. The wide base gives the classic "wizard silhouette" proportions. | The triangle is deceptively powerful as a costume shape. Placing the apex near the neck and the base well below the grid center instantly reads as a flowing robe — no additional shaping needed. |
| SWSector ×2 shoes | Two pointed shoes peeking from the robe hem. The vertex is at the pointed toe; the fan opens backward under the robe. theta=90° gives a slender pointed-shoe profile. |
Using a sector (pizza-slice fan) for a shoe is an unexpected but effective trick — the pointed vertex naturally becomes the toe. Getting the startAngle right for left/right mirroring required careful thought. |
| SWCross | A gold plus-sign emblem on the robe chest area. When running, it gently spins via rotate(18, etime) — just barely perceptible at 18°/sec. |
SWCross extends SWWheel with 4 spokes and no rim, which means it inherits the rotate() animation method for free. Setting initialRotation=0 aligns the arms with the x/y axes. |
| SWSquare | A gold belt buckle at the wizard's waist. Small, square, and centered — exactly what a belt buckle should look like. | This could have been a SWRectangle with equal width and height, but SWSquare communicates intent more clearly. The slightly darker stroke gives it a beveled-edge appearance. |
| SWStar ×2 robe | Two embroidered decorative stars on the robe — one 5-pointed gold star, one 4-pointed magenta diamond — placed asymmetrically for visual interest. | Using different point counts (5 vs. 4) and different colors for the two robe stars prevents them from reading as a matched pair, which would look too designed. Asymmetry here feels more organic. |
| SWRectangle hat brim | The wide flat brim at the base of the wizard's hat. It spans wider than the hat cone above it, creating the classic mushroom-brim profile. | Using the same class (SWRectangle) for both the ground strip and the hat brim, but with completely different dimensions, shows how versatile a simple rectangle is in a scene like this. |
| SWTriangle hat | The tall pointed hat cone. Same class as the robe, but narrower and taller — a different silhouette from the same constructor. The apex is at y=9.5, near the top of the grid. |
Two triangles, same class, completely different visual roles — robe (wide, low) vs. hat (narrow, tall). The triangle is the single most versatile shape in the wizard's wardrobe. |
| SWCircleSegment | A circular segment (arc + chord region) forming a gold crescent-moon badge on the hat. theta=60° at the right startAngle produces the crescent wedge shape. |
The circle segment is one of the more subtle shapes in the library — it's not an arc and not a sector, but the region between them. Using it for a hat badge felt like exactly the right tool for the job. |
| SWStar hat tip | A 6-pointed gold star near the hat tip that slowly spins via rotateAboutCenter(20, etime). The rotation is smooth and time-based. |
Having three different SWStar instances at three different scales and point counts in one scene demonstrates how much range a single class can cover through parameterization. |
Wizard Face & Features
| Class | Role in the Wizard Scene | What I Noticed |
|---|---|---|
| SWEllipse | The oval face — radiusX=0.9, radiusY=1.1. Making the vertical radius slightly larger than the horizontal gives a naturally face-shaped oval. |
This is one of the few shapes where the two-radius parameterization is directly useful. A circle for a face looks too perfectly round; a slight vertical stretch reads as human. |
| SWArch beard | The wizard's white fluffy beard — a > 0 (bowl shape). The vertex is the lowest point of the beard; the filled region hangs naturally below the chin line. |
This is the same class as the hills, but with a > 0 instead of a < 0. The bowl/dome duality of SWArch is one of the cleverest design choices in the library — one parameter completely inverts the semantic role of the shape. |
| SWEyeball ×2 | Both wizard eyes — sclera, pupil, and a magical cyan iris ring. The eyes are the most complex face elements but the simplest to code: one constructor, all geometry handled internally. | SWEyeball is where the library's investment pays off most clearly. A complete eye assembly — sclera disk, iris ring, pupil disk, glint — in one constructor call. Without the class, this would have been ~40 lines of geometry per eye. |
| SWArc ×2 brows | Thick arcing strokes above each eye. theta=130° is a long bushy sweep; thickness=5px makes them read as full brows rather than fine lines. |
The 'round' cap style at the arc ends is a small but important detail — square-capped brow ends look harsh, rounded ends look natural. SWArc exposing the cap style in its constructor was exactly what was needed. |
| SWDisk | The wizard's small circular nose. Small radius, slightly darker skin tone, thin border — the simplest shape in the face layer. | Sometimes the right shape is the simplest one. A dot for a nose is a classic cartoon/illustration convention, and SWDisk delivers it in one line. No geometry needed beyond center + radius. |
Staff & Magic Orb
| Class | Role in the Wizard Scene | What I Noticed |
|---|---|---|
| SWLine | The long wooden staff pole, running from the ground up to just below the orb. thickness=5 and capStyle='ROUND' make it look like a sturdy carved staff. |
Drawing the staff before the orb (painter's algorithm) means the orb appears to rest atop the staff pole rather than being skewered by it. Sequence of draw calls is everything in layered art. |
| SWRegularPolygon | A hexagonal gold jewel set into the staff at mid-height. circumRadius=0.40, 6 sides, gold fill. A single constructor call for a perfectly regular hexagon. |
The hexagon was chosen over a circle or square for the gem because it has a distinctly gemstone-like quality — six facets. SWRegularPolygon makes choosing any polygon a simple number change. |
| SWRing ×3 | Three concentric halo rings orbiting the orb at radii 1.3, 1.75, and 2.2 — cyan, magenta, and gold respectively. Static but visually rich. | Three rings with three different colors, all at the same center but different radii, required only three constructor calls. The visual complexity is high but the code complexity is low — exactly the right trade-off. |
| SWArc magic arc | A thick sweeping arc that continuously rotates around the orb. theta=150°, thickness=6px, blue — and each frame calls magicArc.rotate(1.5) (delta accumulation). |
rotate(delta) accumulating angle (not time-based) means the arc always moves the same amount per frame regardless of what etime is doing. Good for a looping spin where consistency matters more than sync. |
| SWTriDisk | The magic orb itself — three overlapping disks (bright blue, electric cyan, deep purple) create a convincing 3D sphere illusion. axisAngle=45° simulates light from upper-left. |
SWTriDisk is extraordinary. Three disks, a single class, and the result reads as a glowing sphere. Changing axisAngle shifts the apparent light source. The rotate(1.2) animation makes the sphere appear to spin in 3D. |
| SWPie | A 6-color starburst at the orb center. Each frame it both rotates (orbSparkle.rotate(2.0)) and pulses its radius via breatheRadius(sinSparkle, etime). |
Simultaneously rotating and pulsing a pie chart produces a spinning sparkle that feels genuinely magical. The six-slice pie with six distinct colors catches the eye immediately against the dark background. |
| SWArrow | A frozen energy bolt shooting from the orb toward the upper-right. Static — not animated — which makes it feel like a just-cast spell caught mid-flight. | Choosing to leave the arrow static while everything else moves was a deliberate composition choice. The eye naturally interprets the stationary arrow as faster — it has already "left" the orb. |
What the Library Made Possible
Every position in the scene is expressed in user units (−10 to +10 on both axes). SWGrid handles the translation to canvas pixels transparently. This meant I could think compositionally — "the orb is at (−0.8, 7.5), above center and left of the hat" — without ever computing a pixel coordinate.
When the canvas is resized, the entire scene scales correctly with zero extra code. That's not magic — it's what an abstraction layer is supposed to do.
Building a cohesive night-scene palette in HSB is genuinely easier than in RGB or hex. Knowing that the sky colors should have high saturation, very low brightness (dark), and the orb should have high saturation, high brightness (vivid) is natural in HSB — those adjustments require changing exactly one number.
Named colors — "moonSilver", "robeColor", "orbCyan" — made the code read as a description of the scene rather than a list of operations.
Every animation in the scene — star twinkling, moon rotation, orb spin, ghost bobbing, arc sweeping — was implemented by calling built-in methods with one line of code per animation. In raw p5.js, each of these would require tracking state variables, computing angle offsets, and managing timing manually.
SWSinusoid was especially powerful: declare a wave once, pass time in, get a value out. Three sinusoids drove the entire animation system.
The shape chronicle above was easy to write because the code is already well-named. Variable names like wizBeard, hatCrescent, staffGem, and orbRing2 created a direct correspondence between the scene in my imagination and the code on the screen.
This matters especially in a teaching environment: a student reading the code can follow the painter's algorithm and understand why each shape is drawn in the order it is, without needing to decode pixel math.
Honest Comparison: SketchWave vs. Raw p5.js
Not everything about the class library is pure upside. Here is an honest side-by-side comparison.
| Dimension | SketchWave Classes | Raw p5.js |
|---|---|---|
| Coordinate system | ✔ User units, auto-mapped by SWGrid | ~ Canvas pixels only; map() required everywhere |
| Color model | ✔ Named HSB colors; self-documenting palette | ~ HSB available but no naming or reuse convention |
| Complex shapes | ✔ SWGhost, SWEyeball, SWTriDisk, SWWheel provided | ✘ Must build all geometry from primitives manually |
| Animation | ✔ breathe(), rotate(), SWSinusoid.getValue(t) | ✘ All state management and math by hand |
| Canvas resize | ✔ Automatic via grid.updateScreenBounds() | ✘ All position/size math must be recomputed |
| Code readability | ✔ Reads as a scene description | ~ Reads as a sequence of drawing operations |
| Learning curve | ~ Must learn each class's constructor contract | ✔ Only p5.js primitives to learn |
| Flexibility | ~ Shapes outside the library require raw p5 code | ✔ Any geometry expressible with canvas primitives |
| Gotchas | ~ breatheRadius sets directly (not multiplicative); SWEyeball requires p5 ready; a > 0 vs < 0 for SWArch | ~ Push/pop state management; pixel-density issues |
| Code length for this scene | ✔ ~573 lines (heavily commented) | ✘ Estimated 1,400–1,700 lines for equivalent scene |
Things I Learned Along the Way
The same class draws hills (a < 0, dome) and the wizard's beard (a > 0, bowl). This is not just convenient — it reflects a deep design decision that the arch shape and the bowl shape are the same mathematical curve viewed differently. Reusing one class for two semantically opposite shapes is an elegant teaching moment about parameterization.
SWRing.breatheRadius(sin, t) and SWSector.breatheRadius(sin, t) set this.radius = sinusoid.getValue(t) directly. This means the sinusoid's C parameter must equal the shape's natural resting radius. By contrast, SWStar.breathe() multiplies the sinusoid output against the current radius. Knowing this distinction prevented a frustrating bug where the orb sparkle would pulse wildly instead of gently.
SWEyeball calls cos() and radians() internally during construction to position the glint geometry. These are p5.js functions that are only available after setup() has run. Constructing any SWEyeball at the module top level (before the first draw frame) would silently produce corrupted glint coordinates. The solution: use a one-time initWizard() gate called from the first draw frame.
Sixteen stars all breathing on the same SWSinusoid would look mechanical — a single pulse rippling through the sky. The fix was simple: before each star's breathe() call, temporarily set sinTwinkle.D = i * 0.38 (a different phase offset per star). This desynchronizes the twinkling completely, and the night sky instantly feels alive. The D parameter (phase shift) is perhaps the most underrated parameter in the sinusoid model.
Drawing order matters absolutely. The staff pole must be drawn before the orb (so the orb covers the pole tip, appearing to sit atop it). The robe must be drawn before the belt buckle. The sky gradient must be drawn before the stars. Getting a single shape out of order produces a visual inconsistency that immediately breaks the scene's spatial logic. The numbered drawing order in the code comments was not decoration — it was essential documentation.
Closing Thoughts
The question of whether I am "a wizard" for producing this scene is generous — but I think the real wizard is the class library itself. It transformed a task that would have required thousands of lines of pixel math into something I could reason about compositionally: What shapes make a wizard? What colors make a night sky? What animation makes a star field feel alive?
That shift — from thinking about how to draw to thinking about what to draw — is what a good abstraction layer is supposed to produce. SketchWave delivers it.
If I had built this scene in raw p5.js, the code would exist, but the story it tells would be harder to read. A student looking at wizNose.drawOnGrid(grid) understands the scene. A student looking at fill(25, 55, 75); circle(map(1.5, -10, 10, 0, width), ...); understands only the mechanics. Teaching code should tell its own story — and SketchWave makes that possible.