Stage 2 builds on Stage 1a's color well controls and adds
continuous movement to the placed character. Click anywhere on the
canvas to place a character — it starts moving immediately and keeps
going until you click again or press Clear.
What's New in Stage 2
-
Movement card — A dedicated panel section controls
how the character moves: mode, speed, and boundary behavior.
-
Show row, col label — In Stage 2 this label tracks
the current grid cell as the character moves, not just the cell
where it was placed.
Movement Modes
-
Random walk — Each frame the heading angle is
nudged by a small random value (±0.3 radians, roughly ±17°). The result
is a smooth wander that changes direction gradually — more organic
than a purely random heading each frame, but clearly unpredictable over time.
-
Perlin noise — The heading is derived from p5.js's
noise() function, which produces smoothly correlated values.
A slow time increment (0.005 per frame) results in wide, sweeping arcs
that look visually distinct from random walk — flowing curves rather
than a jagged wander.
Speed
The speed slider sets user units per second (u/s). At the default
grid scale (20 units per cell, 30 columns × 20 rows = 600 × 400 user units),
a speed of 40 u/s crosses the full grid in about 10 seconds. Setting speed
to 0 freezes the character in place.
Boundary Behavior
-
Bounce (default, checkbox unchecked) — When the
character reaches a grid edge its heading is reflected elastically,
like a billiard ball off a wall. The implementation uses
signed-component reflection: instead of the classic
angle formula, the velocity is decomposed into
(vx, vy) = (cos θ, sin θ) and the
relevant component's sign is forced outward —
vx = abs(vx) at the left wall,
vx = −abs(vx) at the right wall, and so on.
This is mathematically identical to the classic formula when
the heading is clean, but immune to accumulated floating-point
drift or angle wrap-around that can cause the classic formula
to reflect inward.
-
Wrap (checkbox checked) — Crossing an edge
teleports the character to the opposite side with the same heading
(Pac-Man / torus topology). The transition is instantaneous.
Why Naïve Bounce Gets Stuck at Walls
A simple angle = π − angle reflection has two failure modes
that combine to cause the character to jitter along the border:
-
Near-parallel incoming angle. If the bug arrives at
a wall almost parallel to it (e.g., 5° off), the reflected heading is
also nearly parallel. The very next random nudge
(±0.3 rad / ±17°) can push the heading back into the wall,
causing an immediate second reflection — locking the character in a
rapid back-and-forth rattle.
-
Large-step overshoot. At high speeds, one frame's
movement can overshoot by more than the reflected distance, landing
the character outside the boundary even after the reflection is applied.
The next frame sees the position outside the wall and reflects again —
compounding the problem.
Three-Layer Bounce Fix
-
Signed-component reflection —
abs() guarantees the outward sign is always correct,
even if the angle has drifted or wrapped.
-
Hard clamp —
constrain(newX, minX, maxX)
after reflection prevents overshoot from placing the position outside
the grid regardless of step size.
-
Wall kick — Within 2 user units of any wall the
outward velocity component is forced to at least
WALL_KICK = 0.3. Because the random nudge is also
at most ±0.3, this ensures the bug always has a net outward component
on the very next frame, so it visibly peels away from the wall
instead of skating along it.
Why Perlin Causes Wall-Skimming
The bounce fix and wall kick fire only when the character actually
crosses a boundary. With strong Perlin noise a subtler problem
emerges: the bounce correctly redirects the heading outward on
frame N, but on frame N+1 the Perlin lerp() pulls
bugAngle back toward whatever direction the noise
function currently prefers — which may point straight at the same
wall again. At full Perlin strength this repeats every frame,
producing smooth, non-erratic wall-skimming where the character
glides along the border without escaping.
Wall-Escape Guard
The guard runs every frame, immediately after the heading
update and before movement is computed. Any time the bug is within
ESCAPE_ZONE = 40 user units (2 grid cells) of a wall
and its heading points toward that wall, the offending
velocity component is corrected in two ways:
-
Sign flip (
abs()) — forces the
component to point outward, as before.
-
Minimum outward component
(
max(abs(v), MIN_OUTWARD = 0.55)) — the critical
addition. A plain abs() flip still leaves the
component nearly zero if the bug was heading nearly parallel to the
wall (e.g., gvx = 0.05 → 0.05). Perlin's
lerp() immediately overrides that tiny value, and the
bug barely moves away. Clamping to 0.55 ensures the corrected heading
departs at least ~33° off the wall surface — steep enough that even
full-strength Perlin must work against it for several frames before
the bug could return.
The escape zone was widened from 12 to 40 user units so the guard
engages two grid cells away from the wall, giving the redirect
enough distance to visibly curve the trajectory before the boundary
is reached. Because the guard only fires when heading toward
the wall, open-field movement is not affected.
Under the Hood — moveBug(dt)
Movement is handled entirely by the sketch function
moveBug(dt), called once per draw() frame
before currentChar.update(dt). The
SWBug is used purely as a position holder
(bug.x / bug.y in user coordinates);
its own internal move() method is not used in Stage 2.
Each placement randomizes the initial heading angle and Perlin noise
seeds, and resets the time accumulator. This ensures every click
produces a fresh, independent trajectory.
All Stage 1a Features Retained
- Color wells + per-color alpha sliders for background, fill, and stroke
- Breathe (size pulse), Spin, and Cycle Hue animations combine with movement
- Show bug point crosshair (Environment panel)
- G toggles grid visibility
- Save Image button exports a timestamped PNG