🛰️ Stage 3 Laundry List

Same orbit. Dramatically better JavaScript.

✨ The S.P.A.R.K. That Launched This Page

🤖  Prompt given to GitHub Copilot (Claude Sonnet 4.6) — 2026-05-12

“Please study the code in this current stage of development and suggest any improvements we should make before we implement SketchWave classes. Please write these up in a file: stage3OrbitAppLaundryList.html and link to that file in the stage 2 card. I have enclosed a past laundry list file for you to model after.”

🧺 What Is a Laundry List?

A laundry list is a planning tool — a checklist of specific improvements to make before you start typing. It prevents “spaghetti sessions” where you try to fix five things at once and accidentally break everything.

The non-negotiable rule for Stage 3: when we’re done, the orbit must look exactly the same as Stage 2. Earth in the same place. Moon still orbiting. Station still upright. Stars still drifting. The only thing changing is how the code is written and organized. This is refactoring.

If you haven’t already read Crosby’s Bomb Saga — Stage 2 Laundry List, do that first. That page walks through each improvement with full before/after examples and introduces the principle of named constants in detail. This page builds on those ideas.

💣 The TNT Recap

The Bomb Saga’s Stage 2 laundry list introduced TNT — three targeted improvements that go beyond the general ROCKETS checklist. Use it to remember the highest-priority refactoring moves we make before SketchWave integration.

💣 Improvement Key Idea
T Tackle the full ROCKETS checklist Header, named constants, decomposed helpers, explicit drawing state
N Name the magic numbers inside classes Named constants extend inside classes, not just at the top of the file
T Terminate dead code Remove zombie variables, zombie config, and redundant calls

🔗 Full before/after examples for every item above: Crosby’s Bomb Saga — Stage 2 Laundry List

🪐 What the Orbit App Brings New

The Stage 2 orbit sketch is already much better than Stage 1. It has a file header, section comments, decomposed helper functions (drawEarth, drawMoon, drawStation, initLandMasses), and even a nicely proportioned space station built with local derived variables instead of raw magic numbers. A lot of ROCKETS and TNT was already applied — great work!

But three patterns remain that need cleanup before SketchWaveJS classes are introduced:

  • Inline fraction literals — values like width * .25, width * .4, 0.005, and 0.01 are scattered across four functions. They have no names and no explanation. Why does the Moon orbit at width * 0.4? Why is Earth 3× the speed of the Moon’s orbit counter? A reader has to mentally decode these every time they read the code.
  • State mutation interspersed with rendering — in drawMyDesign, earthRotation is incremented between drawEarth and drawMoon. This creates a hidden ordering dependency: Earth and Moon are drawn in different rotation states each frame. That’s not a bug today, but it becomes a trap when you add more animated objects.
  • Initialization buried in the draw loop — land mass generation runs inside drawEarth every frame behind a guard check (if (landMasses.length === 0)). Star field initialization lives in the template file’s setup(), not in the user sketch where stars is declared. Initialization code should live where the data lives.

These three improvements form their own acronym: ORB. Of course they do.

🌌 The ORB Improvements

🛰️ Improvement Key Concept Visual Change?
O Organize orbital constants at the top Every inline fraction gets a name — speeds, radii, land mass config, star field None
R Render first, update state after All draws happen in Phase 1; all state mutations happen in Phase 2 None
B Bootstrap initialization in setup() One-time init runs once, in the right file, via the template hook None
O Organize Orbital Constants at the Top

Stage 2 already named the station’s internal proportions (coreW, coreH, panelW, etc.) — great instinct! But the outer layer of the sketch still uses raw fraction literals for the orbital mechanics: width * .25 for Earth’s size, width * .4 for the Moon’s orbit radius, 0.005 for Earth’s rotation speed. These numbers appear in four different functions with no cross-reference and no explanation.

Pulling them all to the top of the file as named constants gives you a mission control panel for the solar system: one place where every speed and every size is labeled and explained.

BEFORE — magic fractions scattered across four functions
// ── In drawMyDesign ───────────────────────────────────────────────────────────
function drawMyDesign(ctx) {
    drawStars();
    drawEarth(width * .25);         // ← Earth is 25% of width... why?
    earthRotation += 0.005;
    drawMoon(width * .1);           // ← Moon is 10% of width... is that intentional?
    drawStation();
    angle += 0.01;
}

// ── In drawMoon ───────────────────────────────────────────────────────────────
function drawMoon(d) {
    push();
    translate(width / 2, height / 2);
    rotate(angle * 0.3);            // ← Moon moves at 30% of station speed — nowhere documented
    translate(width * .4, 0);       // ← Moon orbits at 40% of width — same as station? different?
    // ...
}

// ── In drawStation ────────────────────────────────────────────────────────────
function drawStation() {
    push();
    translate(width / 2, height / 2);
    rotate(-angle);
    translate(-width * .25, 0);     // ← station orbits at 25%... same as Earth's diameter — coincidence?
    rotate(+angle);
    let coreW = width * 0.05;
    // ...
}

// ── In initLandMasses ─────────────────────────────────────────────────────────
function initLandMasses(r) {
    let numLands = 15;              // ← why 15?
    let numPts = 8;                 // ← why 8?
    let baseRadius = r * 0.12;      // ← blob size = 12% of Earth radius — documented nowhere
    let maxRadiusFactor = 1.5;      // ← max spike = 1.5× base — magic
    // ...
    let dist = random(maxDist * .4, maxDist * .95);  // ← placement range — magic
}

// ── In setup (base template file!) ────────────────────────────────────────────
for (let i = 0; i < 500; i++) {
    stars.push({
        x: random(width), y: random(height),
        size: random(1, 3),
        speed: random(0.1, 0.5)
    });
}
AFTER — a mission-control constants block at the top of the user sketch
// ── Orbital scales (fraction of canvas width) ────────────────────────────────
const EARTH_SCALE          = 0.25;  // Earth diameter  = 25 % of canvas width
const MOON_SCALE           = 0.10;  // Moon  diameter  = 10 % of canvas width
const MOON_ORBIT_RADIUS    = 0.40;  // Moon orbit radius from center
const STATION_ORBIT_RADIUS = 0.25;  // Station orbit radius from center
const CORE_SCALE           = 0.05;  // Station core width = 5 % of canvas width

// ── Animation speeds (radians per frame) ─────────────────────────────────────
const EARTH_ROTATION_SPEED = 0.005; // Earth day/night cycle
const ORBIT_SPEED          = 0.01;  // Station orbit speed (radians / frame)
const MOON_ORBIT_FACTOR    = 0.30;  // Moon is 30 % as fast as the station

// ── Earth appearance ──────────────────────────────────────────────────────────
const SHADOW_ALPHA         = 50;    // 0–100 — how dark the night-side arc gets
const ATMOSPHERE_WEIGHT    = 3;     // px — stroke weight of the atmosphere rim

// ── Land mass generation ──────────────────────────────────────────────────────
const LAND_COUNT           = 15;    // number of continent blobs
const LAND_VERTICES        = 8;     // polygon points per blob
const LAND_BASE_RADIUS     = 0.12;  // blob base size = 12 % of Earth's radius
const LAND_MAX_SPIKE       = 1.50;  // max outward spike factor (makes blobs irregular)
const LAND_DIST_MIN        = 0.40;  // closest blob center (fraction of max safe dist)
const LAND_DIST_MAX        = 0.95;  // farthest blob center (fraction of max safe dist)
const LAND_JITTER_MIN      = 0.60;  // min vertex radius multiplier (inward dent)

// ── Star field ────────────────────────────────────────────────────────────────
const STAR_COUNT     = 500;
const STAR_SIZE_MIN  = 1;
const STAR_SIZE_MAX  = 3;
const STAR_SPEED_MIN = 0.1;        // slowest drift (closer stars, warmer feel)
const STAR_SPEED_MAX = 0.5;        // fastest drift (parallax depth effect)
AFTER — functions now read as clean prose
function drawMyDesign(ctx) {
    drawStars();
    drawEarth(width * EARTH_SCALE);
    drawMoon(width * MOON_SCALE);
    drawStation();
    earthRotation += EARTH_ROTATION_SPEED;
    angle         += ORBIT_SPEED;
}

function drawMoon(d) {
    push();
    translate(width / 2, height / 2);
    rotate(angle * MOON_ORBIT_FACTOR);    // Moon is 30 % as fast as the station
    translate(width * MOON_ORBIT_RADIUS, 0);
    // ...
}

function initLandMasses(r) {
    let numLands       = LAND_COUNT;
    let numPts         = LAND_VERTICES;
    let baseRadius     = r * LAND_BASE_RADIUS;
    let maxRadiusFactor = LAND_MAX_SPIKE;
    let maxReach       = baseRadius * maxRadiusFactor;
    // ...
    let dist = random(maxDist * LAND_DIST_MIN, maxDist * LAND_DIST_MAX);
    // ...
    let rad = baseRadius * random(LAND_JITTER_MIN, maxRadiusFactor);
    // ...
}
🧑‍🏫 Teacher’s note: Look at what the constants block reveals: MOON_ORBIT_RADIUS = 0.40 and STATION_ORBIT_RADIUS = 0.25 are different values — the Moon orbits farther out than the station. In the original code, both are inline fractions in different functions and there’s no easy way to compare them at a glance. Now they’re on adjacent lines and the orbital geometry is immediately obvious.

The same goes for speeds: ORBIT_SPEED = 0.01 and MOON_ORBIT_FACTOR = 0.30 together tell the whole story — the Moon’s angular speed is 0.01 × 0.3 = 0.003 radians per frame. You can read that from the constants table in two seconds. From the original code, you’d have to trace two functions and multiply mentally.
R Render First, Update State After

In Stage 2, drawMyDesign mixes rendering and state mutation. Specifically, earthRotation += 0.005 is called between drawEarth and drawMoon. That means Earth and Moon are drawn using different values of earthRotation in the same frame — Earth uses the old value, then the mutation fires, then Moon uses the new value.

Right now that doesn’t cause a visible problem because earthRotation only affects Earth’s land masses. But it establishes a dangerous habit: ordering matters and there’s no signal in the code that it does. Add one more object that reads earthRotation, place the call in the wrong order, and you’ll spend a long time debugging a one-frame glitch that only appears at high frame rates.

The fix is a two-phase structure: Phase 1 renders everything (reading state only), then Phase 2 advances all state. Every professional animation loop — from game engines to physics simulators — uses exactly this pattern.

BEFORE — state mutation fires between draw calls
function drawMyDesign(ctx) {
    drawStars();
    drawEarth(width * .25);
    earthRotation += 0.005;   // ← mutation fires HERE, between Earth and Moon draws
    drawMoon(width * .1);     //   Moon sees the NEW earthRotation value this frame
    drawStation();
    angle += 0.01;            // ← at least this one is at the end
}
AFTER — two explicit phases: render then update
// ── drawMyDesign ──────────────────────────────────────────────────────────────
// Two-phase animation pattern:
//   Phase 1 — RENDER  (read state, draw everything back-to-front)
//   Phase 2 — UPDATE  (advance state for the next frame)
//
// Keeping these phases separate means every function in Phase 1 sees a
// consistent snapshot of the world. No function's output depends on whether
// another draw call happened to fire a state mutation before it ran.
function drawMyDesign(ctx) {

    // ── Phase 1: Render (back → front) ────────────────────────────────────
    drawStars();
    drawEarth(width * EARTH_SCALE);
    drawMoon(width * MOON_SCALE);
    drawStation();

    // ── Phase 2: Advance state for the next frame ──────────────────────────
    earthRotation += EARTH_ROTATION_SPEED;
    angle         += ORBIT_SPEED;

}
🧑‍🏫 Teacher’s note: This is one of those changes that looks almost trivial — you’re just moving two lines down by a few lines. But the intent becomes completely explicit. Anyone reading drawMyDesign immediately understands: “everything above the Phase 2 comment is rendering; everything below is state mutation.” No mental simulation required.

This pattern also makes drawMyDesign a true “table of contents” for the sketch. Phase 1 lists what appears on screen (stars, Earth, Moon, station) in back-to-front order, like layers in Photoshop. Phase 2 lists what animates (Earth rotation, orbital angle). Reading those six lines tells you everything the sketch does.

A professional-level bonus: this pattern is a stepping stone toward the update/render separation used in game engines like Unity (Update() vs. OnRenderObject()), p5.js’s own draw() contract, and React’s render() function. Learning it now means it’ll feel natural later.
B Bootstrap Initialization in setup(), Not in the Draw Loop

Stage 2 has two initialization problems that are worth fixing before Stage 3.

Problem 1 — lazy init with a draw-loop guard

initLandMasses(r) is called inside drawEarth() behind a guard:

BEFORE — guard check executes every frame for the life of the app
function drawEarth(d) {
    push();
    translate(width / 2, height / 2);
    // ...
    push();
    rotate(earthRotation);

    let r = d / 2;
    if (landMasses.length === 0) initLandMasses(r);  // ← runs the check 3,600 times/minute

    fill(swForestGreen.col);
    for (let land of landMasses) {
        drawLand(land.x, land.y, land.verts);
    }
    pop();
    // ...
}

This guard executes at every frame for the entire life of the sketch — 60 checks per second, every second, to answer a question whose answer never changes after the first frame. It also has a subtle bug: if the canvas is ever resized, landMasses won’t regenerate, because the guard only fires when the array is empty.

More importantly, a draw function should have no side effects on global state. A reader of drawEarth doesn’t expect it to quietly populate a global array the first time it runs.

Problem 2 — user data initialized in the template file

The stars array is declared in jacksonOrbit1Sketch.js (the user file), but it is populated inside setup() in jacksonOrbitBase1Sketch.js (the template file). This means if someone only reads the user sketch, they see an empty array that is mysteriously filled by the time drawStars() runs. The template has to “know” about the user’s data, which reverses the intended direction of dependency.

BEFORE — star initialization lives in the template file, not the user sketch
// ── In jacksonOrbitBase1Sketch.js (the TEMPLATE file) ───────────────────────
// Inside setup(), after establishCanvas():
for (let i = 0; i < 500; i++) {
    stars.push({               // ← 'stars' is declared in the user's file —
        x: random(width),      //   the template "knows" the user's data exists
        y: random(height),
        size: random(1, 3),
        speed: random(0.1, 0.5)
    });
}
noLoop();
AFTER — declare initMyDesign() in the user sketch; call it from the template hook
// ── In jacksonOrbit1Sketch.js (the USER sketch) ──────────────────────────────
// Called once from the template's setup() after the canvas is established.
// This is the same pattern the template uses for initThemeColor().
// All one-time user-data setup goes here.
function initMyDesign() {

    // ── Star field ─────────────────────────────────────────────────────────
    for (let i = 0; i < STAR_COUNT; i++) {
        stars.push({
            x:     random(width),
            y:     random(height),
            size:  random(STAR_SIZE_MIN, STAR_SIZE_MAX),
            speed: random(STAR_SPEED_MIN, STAR_SPEED_MAX)
        });
    }

    // ── Land masses ────────────────────────────────────────────────────────
    // Earth diameter at the current canvas width; r is the radius used by
    // initLandMasses to place blobs within the Earth circle.
    const r = (width * EARTH_SCALE) / 2;
    initLandMasses(r);

}//end initMyDesign
AFTER — template setup() calls the hook; one change in the template
// ── In jacksonOrbitBase1Sketch.js (the TEMPLATE file) ───────────────────────
// Inside setup(), right after the existing initThemeColor hook:

if (typeof initThemeColor   === 'function') initThemeColor();
if (typeof initMyDesign     === 'function') initMyDesign();  // ← add this line

applyThemeColor();
establishCanvas();
// ... rest of setup ...
AFTER — drawEarth() is a pure draw function — no guard, no side effects
function drawEarth(d) {
    push();
    translate(width / 2, height / 2);

    // Base Ocean
    fill(swDeepBlue.col);
    noStroke();
    ellipse(0, 0, d, d);

    // Rotating Surface
    push();
    rotate(earthRotation);
    fill(swForestGreen.col);
    for (let land of landMasses) {   // pre-populated by initMyDesign() in setup()
        drawLand(land.x, land.y, land.verts);
    }
    pop();

    // Day/Night shadow
    fill(0, 0, 0, SHADOW_ALPHA);
    arc(0, 0, d + 2, d + 2, -HALF_PI, HALF_PI, CHORD);

    // Atmosphere rim
    noFill();
    stroke(swBlue.col);
    strokeWeight(ATMOSPHERE_WEIGHT);
    ellipse(0, 0, d, d);
    pop();
}
🧑‍🏫 Teacher’s note: Notice that the template already uses this exact hook pattern for initThemeColor(). Adding initMyDesign() is not inventing something new — it’s extending a pattern that already exists. That’s what good templates do: they define extension points so user code can plug in without touching the framework.

The “guard check every frame” anti-pattern is extremely common in student code, and in early professional code too. It works. But it introduces two subtle traps: (1) it silently couples a draw function to a global side effect, and (2) it makes one-time initialization logic hard to find because it’s buried inside a draw helper. Moving initialization into a named hook (initMyDesign) makes it findable, testable, and easy to call again on resize if needed.

✅ Stage 3 Checklist

🛰️ Task File to Edit Visual Change?
🪐 O — Organize orbital constants at the top
O Add orbital scale constants (EARTH_SCALE, MOON_SCALE, MOON_ORBIT_RADIUS, STATION_ORBIT_RADIUS, CORE_SCALE) jacksonOrbit1Sketch.js None
O Add animation speed constants (EARTH_ROTATION_SPEED, ORBIT_SPEED, MOON_ORBIT_FACTOR) jacksonOrbit1Sketch.js None
O Add Earth appearance constants (SHADOW_ALPHA, ATMOSPHERE_WEIGHT) jacksonOrbit1Sketch.js None
O Add land mass generation constants (LAND_COUNT, LAND_VERTICES, LAND_BASE_RADIUS, LAND_MAX_SPIKE, LAND_DIST_MIN/MAX, LAND_JITTER_MIN) jacksonOrbit1Sketch.js None
O Add star field constants (STAR_COUNT, STAR_SIZE_MIN/MAX, STAR_SPEED_MIN/MAX) jacksonOrbit1Sketch.js None
O Replace all inline literals with their constant names throughout the sketch jacksonOrbit1Sketch.js None
🔄 R — Render first, update state after
R Move earthRotation += ... to after all draw calls in drawMyDesign jacksonOrbit1Sketch.js None
R Add Phase 1 / Phase 2 comments inside drawMyDesign jacksonOrbit1Sketch.js None
🚀 B — Bootstrap initialization in setup()
B Add initMyDesign() function that populates stars and calls initLandMasses() jacksonOrbit1Sketch.js None
B Remove star initialization loop from template setup(); add if (typeof initMyDesign === 'function') initMyDesign(); jacksonOrbitBase1Sketch.js None
B Remove if (landMasses.length === 0) initLandMasses(r); guard from drawEarth() jacksonOrbit1Sketch.js None

🔭 What Comes After Stage 3?

Once all three ORB improvements are in place, the code is ready for SketchWaveJS class integration. That’s Stage 4 and beyond.

Think about what that will look like after this refactor:

  • Replacing the ellipse(0, 0, d, d) Earth body with an SWDisk call becomes a one-line change inside drawEarth() — a short, focused function that does one thing.
  • Replacing star drawing with an SWPoint instance is a one-function edit inside drawStars().
  • The constants block makes the SketchWave migration self-documenting: EARTH_SCALE = 0.25 maps directly onto whatever SWDisk’s radius parameter expects.

That’s the payoff of refactoring: every future change gets easier. The ORB work in Stage 3 isn’t overhead — it’s the runway that Stage 4 launches from.