What Is SWEyeball?
SWEyeball is a self-contained class that draws a complete, animated eyeball
using the p5.js canvas library. It encapsulates four separate disk
objects and all of the geometry needed to make the pupil move, track a target, and
stay constrained inside the white of the eye — so the sketch that uses the
class only needs a few lines of setup code.
Rather than building the eye directly in a sketch (managing eight separate disk and
point variables, writing custom drag math, and recomputing glint positions every
frame), you create one SWEyeball and call a handful of friendly methods.
The class is the result of ten stages of motivated development recorded in the
SWEyeball Saga.
How the Layers Fit Together
Each eyeball is composed of four layers drawn back-to-front every frame:
Quick-Start: Two Eyes in a Sketch
The minimum code to get two draggable, mouse-following eyes onto a p5.js canvas:
The Polar Coordinate System
The pupil position is stored internally as a polar offset from the eye
center — a distance (radius) and a direction (angleDeg).
All methods that move the pupil use this system:
Constructor Signature
setup() — after
calling colorMode() and initializeSWColors(). The glint
geometry uses p5.js cos() and radians(), which are only
available after p5 is running.
Public Properties You Can Read
| Property | Type | Description |
|---|---|---|
eyeCenter | SWPoint | Center of the entire eyeball. Changing this moves the whole eye. |
radius | number | Sclera radius in user-units. |
pupilFactor | number | Fraction of radius used for the pupil disk (e.g. 0.4 → 40%). |
pupilOffsetRadius | number | Current distance of pupil center from eyeCenter. Updated live by drag and setPupilOffset(). |
pupilOffsetAngleDeg | number | Current gaze direction in degrees [0, 360). Updated live. |
scleraColor | SWColor | Fill color of the white sclera. |
borderColor | SWColor | Stroke color of the sclera border. |
pupilColor | SWColor | Fill color of the dark pupil center. |
irisColor | SWColor | Stroke (ring) color around the pupil — the colored iris. Change live with setIrisColor(). |
isPupilDraggable | boolean | Whether the pupil can be dragged. Default: true. Set before rebuild(). |
Glint Customization Properties
Glints are the small white highlights that make the eye look shiny and alive. Override
these before calling rebuild() to change their appearance:
| Property | Default | Description |
|---|---|---|
glintFactor | 0.4 | Top glint radius = pupilRadius × glintFactor. |
glintColor | swWhite | Color applied to both glint disks. |
glintAngleDeg | 135° | Direction of the primary (top-left) glint from the pupil center. 135° = upper-left. |
glintBottomAngleDeg | 315° | Direction of the secondary (bottom-right) glint. 315° = lower-right. |
glintDistanceFactor | 1.0 | Top glint distance = pupilRadius × factor. |
glintBottomDistFactor | 0.7 | Bottom glint distance = pupilRadius × factor. |
Private / Internal References
These are set by _buildComponents() and are available for reading (but
should not be overwritten directly):
| Property | Type | Description |
|---|---|---|
eyeBase | SWDisk | The white sclera disk. |
pupil | SWDisk | The iris/pupil disk. Its .center (SWPoint) is what moves. |
topGlint | SWDisk | Primary top-left highlight disk. |
bottomGlint | SWDisk | Secondary bottom-right highlight disk. |
_maxPupilOffset | number | Maximum pupil travel distance = radius − pupilRadius. Read-only. |
_pupilRadius | number | Cached pupil disk radius in user-units. |
Drawing
drawOnGrid(grid) — Call every frame from draw().
Renders all four internal disks in the correct z-order (sclera → pupil → glints).
The grid parameter is the SWGrid instance that handles the
user-unit → pixel coordinate conversion.
draw() — Bypasses the grid and draws in raw screen pixels. Rarely needed; prefer drawOnGrid().
Programmatic Pupil Positioning
setPupilOffset(offsetRadius, angleDeg)
Moves the pupil to a polar position relative to the eye center.
offsetRadius is automatically clamped to [0, _maxPupilOffset]
so you can safely pass large values and the pupil stops at the boundary.
angleDeg is normalized to [0, 360).
lookAt(targetUserX, targetUserY)
Points the pupil directly toward a target position (grid user-units),
always using maximum offset so the gaze direction is fully visible.
This is designed for high-frequency use inside draw() — it performs no
console logging and is as fast as possible.
lookAt() with the same target, each computes the angle
from its own center — so each eye points at a slightly different angle.
This produces a natural "turning inward" (vergence) effect that was never explicitly
programmed. It just falls out of the geometry.
lookAtAngle(angleDeg)
Companion to lookAt() for Track-ON mode, when a single shared angle has
already been computed externally and both eyes should point in exactly the same
direction.
Mouse Interaction
handleMousePressed(mx, my, grid, tolerance=10) → boolean
Call from p5.js mousePressed(). Returns true if the user
clicked on this eye's pupil (within tolerance screen pixels). That
true signals that the click was "consumed" — other click handlers
(like grid-toggle) should be skipped.
handleMouseDragged(mx, my, grid) → boolean
Call from p5.js mouseDragged() every frame while the button is held.
Internally it converts screen → user coordinates, clamps the pupil inside the
sclera boundary, moves the pupil and both glints, and syncs
pupilOffsetRadius / pupilOffsetAngleDeg.
Returns true while this eye is the one being dragged.
The constraint math inside handleMouseDragged():
handleMouseReleased()
Call from p5.js mouseReleased(). Clears the drag state.
Getters (Read-Only Properties)
isDragging → boolean
True while the user is actively dragging this eye's pupil. Use to prevent other code (like mouse follow) from overriding an in-progress drag.
pupilOffset → { radius, angleDeg }
Returns the current polar position as a convenience object. Stays in sync with both setPupilOffset() and interactive drag.
Live Color Change
setIrisColor(swColor)
Changes the iris ring color immediately, without requiring a rebuild().
Also updates the stored irisColor so any future rebuild picks up the new value.
setBaseAlpha(alpha)
Sets the sclera fill transparency (0 = invisible, 100 = fully opaque). Handy during development to see what's underneath.
Rebuilding After Glint Changes
rebuild()
Re-runs _buildComponents() after you change glint properties like
glintFactor, glintAngleDeg, or glintColor. The
existing pupil offset is preserved through the rebuild.
Utility
toString() → string
SWEyeball is built on three smaller, reusable classes. Each one handles
exactly one responsibility — a design principle called
separation of concerns.
SWPoint — A Position in Space
SWPoint stores an (x, y) coordinate in grid user-units along with
optional styling (stroke weight, color, label). It is the fundamental
anchor for everything else: every disk, line, and triangle in the
SketchWave system is positioned by giving it an SWPoint as its center
or endpoint.
Key features:
setPosition(x, y)— move the point to new coordinates.containsPoint(mx, my, grid, tolerance)— hit-test: is the mouse withintolerancepixels of this point on screen? Used for drag detection.setDraggable(bool)— marks the point as interactive.SWPoint.copy(other)— static deep-copy constructor. Creates an independent duplicate, including the pen trail and label offset.- Pen trail — optional: call
point.penOn = trueto record a history of positions, then draw the trail for motion effects. drawOnGrid(grid)— draws the point as a visible dot on the grid. Useful during debugging.
A plain pair of numbers can't be labeled, colored, hit-tested, or subscribed to by other objects.
SWPoint is a named, styled, interactive object —
which is why SWDisk takes one as its center rather than raw
coordinates.
SWColor — A Color in HSB Space
SWColor stores a color as four numbers: hue (0–360),
saturation (0–100), brightness (0–100), and
alpha transparency (0–100). It works with p5.js's
colorMode(HSB, 360, 100, 100, 100).
Key features:
new SWColor(h, s, b, a, name)— constructor. Hue is automatically wrapped to [0, 360).SWColor.copy(other)— static deep copy. All SW classes store copies, so an external change to a color doesn't silently ripple through.SWColor.fromHex(hexStr, name)— static factory that converts a CSS hex string (like"#3a7fd5") to HSB. Used to bridge HTML color pickers to the SketchWave system.darken(factor),brighten(factor),saturate(factor),desaturate(factor),withAlpha(a)— return a new adjusted color without modifying the original.initializeSWColors()— call once insetup()to initialize all global presets:swRed,swBlue,swGreen,swWhite,swBlack, and 30+ others.
HSB maps directly to how artists think: hue = "which color?", saturation = "how vivid?", brightness = "how dark?". Darkening a color means subtracting from brightness — one number, predictable result. In RGB you'd have to adjust all three channels and guess.
SWDisk — A Styled Circle
SWDisk draws a filled, stroked disk (circle) at an
SWPoint center position, in either screen pixel coordinates or
grid user-units. It is the fundamental drawn primitive inside
SWEyeball.
Key features:
new SWDisk(center, radius, thickness, fillColor, strokeColor)— all colors are copied on construction (no shared-reference surprises).drawOnGrid(grid)— call every frame. Converts center from user-units to pixels, then draws the disk.shouldShowCenter— boolean; show the centerSWPointas a visible dot (useful for debugging). Default:true. Set tofalsewhen used insideSWEyeball.setFillAlpha(alpha),setStrokeAlpha(alpha)— change transparency after construction without a full rebuild.breathe(sinusoid, t)— modulate radius with anSWSinusoidwave over time for animated pulse effects.area,circumference— computed at construction.
SWDisk. When you call
leftEye.drawOnGrid(grid), it simply calls
drawOnGrid(grid) on each of its four disks in order.
Files are loaded directly from the project folder. Use the Copy button to copy any file's full text to the clipboard.
Loading swEyeball.js…
Loading swPoint.js…
Loading swColor.js…
Loading swDisk.js…