Stage 2a is a refactor of Stage 2's movement engine.
The controls are identical — Perlin noise strength, speed, bounce/wrap,
and trail — but the internal heading update is redesigned to eliminate
the jitter and wall-skimming that the Stage 2 implementation produced
at medium-to-high Perlin strength.
What's New vs. Stage 2
-
Angular velocity model — Perlin noise now
controls how fast the heading turns (angular velocity),
not what absolute direction to aim at. This single change
removes open-field jitter entirely and makes wall bounces stable
without any special guard logic.
-
Wall-escape guard removed —
Stage 2 needed a per-frame guard (ESCAPE_ZONE, MIN_OUTWARD) to
prevent Perlin from overriding a bounce correction. In Stage 2a
that guard is gone; the standard three-layer bounce is sufficient.
Why Stage 2 Had Jitter
Stage 2 computed the Perlin heading as:
perlinAngle = noise(seed + t) × TWO_PI × 4
bugAngle = lerp(bugAngle, perlinAngle, strength)
Because noise() returns values in [0, 1], multiplying by
4π maps the output to an 8π range (four full rotations).
Consecutive noise samples are smooth, but they may point at very different
absolute directions within that 8π space. The lerp() pulls
bugAngle sharply toward the new target every frame, producing
visible jitter even far from walls. Near a wall the bounce corrects the
heading outward, but the very next lerp() overrides it —
causing the wall-skimming behaviour.
Angular Velocity Fix
Stage 2a uses Perlin noise as a turn-rate delta instead:
bugNoiseT += 0.005;
randomNudge = random(-0.3, 0.3) × (1 - strength);
perlinNudge = (noise(seed + t) - 0.5) × strength × MAX_TURN × 20;
bugAngle += randomNudge + perlinNudge;
-
noise() − 0.5 — centers the
noise output at zero. Values above 0.5 curve right; values below
curve left. The heading evolves continuously — no target angle,
no sudden jump.
-
MAX_TURN = 0.08 — caps the
per-frame turn contribution. Noise values change slowly (0.005/frame
time step) so the actual turn accumulates gently over many frames,
producing wide organic arcs. Increase toward 0.15 for tighter spirals.
-
Random component fades with strength — at
strength 0 there is no Perlin contribution and the bug does a
pure random walk (±0.3 rad/frame nudge). At strength 1 the
random nudge is zero and the trajectory is entirely Perlin-driven.
Mixed values blend both proportionally.
Movement Modes
-
Random walk (slider = 0) — heading is nudged
±0.3 radians (about ±17°) each frame. Smooth directional drift that
accumulates over time into an unpredictable wander.
-
Perlin flow (slider = 100) — heading turns at a
rate driven by Perlin noise. The slow time increment (0.005/frame)
produces long, sweeping arcs that repeat loosely — a flowing, organic
quality clearly different from random walk.
-
Mixed (slider 1–99) — both components are active
in proportion to the slider value.
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),
a speed of 40 u/s crosses the full grid in about 10 seconds. Speed 0 = stationary.
Boundary Behavior
-
Bounce (default, checkbox unchecked) — elastic
wall reflection using signed-component decomposition. With the AV model
the bounce works cleanly: turning Perlin incrementally cannot override
the corrected heading fast enough to send the bug back into the wall
on the same frame.
-
Wrap (checkbox checked) — crossing an edge
teleports the character to the opposite side (Pac-Man / torus topology).
Three-Layer Bounce Fix (retained from Stage 2)
-
Signed-component reflection —
vx = abs(vx) / vx = −abs(vx)
guarantees the outward sign is always correct, immune to accumulated
floating-point drift.
-
Hard clamp —
constrain(newX, minX, maxX)
after reflection prevents large-step overshoot from landing outside
the grid.
-
Wall kick — within 2 user units of any wall the
outward velocity component is forced to at least
WALL_KICK = 0.3, so the character always visibly peels
away from the wall on the next frame.
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.
Each placement randomizes the initial heading and Perlin noise seeds,
and resets the time accumulator and trail, ensuring 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