Building Grayson’s Showcase
From an original p5.js sketch to a live showcase —
map(), scan-line rasterization, and a six-phase refactoring roadmap.
This log documents the construction of graysonsP5JSOriginal.html
and provides a full tutorial on the techniques Grayson used — some of them surprisingly advanced for a novice. Two things make this log different from Ethan’s: the sketch is static (noLoop()), and Grayson was already using HSB color mode before anyone asked. That second fact is the most interesting thing in the file.
The log is organized as: build decisions (the showcase itself), code analysis (what the sketch is doing and why), a map() tutorial, and a refactoring roadmap.
We just featured a design by novice Ethan and some upgrades to it. I have a file, graysonsOriginalSketch.js that features a design made by novice Grayson. I’d like to feature it like we did Ethan’s design and receive recommendations from you on how to deploy it professionally with laudable design features. Since it features some unusual characteristics, especially the use of the ‘map’ function, I’d like to make a tutorial out of it as well. Can you create graysonsP5JSOriginal.html using graysonsOriginalSketch.js and we’ll need graysonsOriginalSketchStyles.css. I’ll want a chatlog like we had for Ethan’s designs. I’m excited to see what you discover and what you can teach me from this unique code! I’ll supply a hero image later, for now let’s have a thematic hero background that fits the design.
The most effective phrase in this prompt is the last one: “I’m excited to see what you discover and what you can teach me from this unique code.” This does something most prompts do not: it explicitly hands the analysis role to the AI and asks for genuine discovery rather than mechanical execution. The result is an expanded analysis of what Grayson’s code actually does — including things Grayson may not have consciously intended.
The instruction to derive a thematic hero from the sketch is a strong design brief in disguise: it asks the AI to read the sketch for its color content before writing any CSS. This is the correct order of operations — understand the source material before creating the container for it.
What the prompt does not specify: which aspects of the code qualify as “unusual characteristics” beyond map(). This was left as an open-ended discovery invitation. That invitation produced five teaching observations that were not in the original prompt. Curiosity as a prompt ingredient consistently produces richer output than specification alone.
Ethan’s sketch calls loop() implicitly (by not calling noLoop()), so it animates at 60fps. The live canvas had to run constantly to show the work. Grayson calls noLoop() in setup(), so draw() runs exactly once. The entire image is computed in a single pass and then the sketch stops permanently.
This changes the showcase in one meaningful way: the canvas label reads “Live Render — Single Frame” instead of “Live Animation.” The instance-mode adapter still runs the sketch via p5.js, but the experience is a computed static image, not a loop. The caption documents this distinction: “Computed once in draw() — noLoop() stops the loop immediately.” That sentence is the entire lesson about noLoop().
The instance-mode adapter follows the same architectural separation established for Ethan: the displayed code (GRAYSON_CODE template literal) is Grayson’s original global-mode source. The running code is an instance-mode equivalent targeting #graysonsCanvas. The original file sketches/graysonsOriginalSketch.js is untouched.
One addition the Ethan adapter did not need: a floating-point guard around the square root. The loop bounds are computed as myHeight/2 ± myHeight * 0.375. The circle radius is Math.floor(myWidth * 0.75) / 2. With myWidth = myHeight, these values are exactly equal — meaning at the very first and last iterations, (y - k)² = r², and the expression under the square root is exactly zero. Floating-point arithmetic can produce tiny negative values near zero (e.g., −1.4e−12), which would make Math.sqrt() return NaN. The guard if (sqTerm < 0) { continue; } skips those at most two iterations and prevents NaN from propagating.
Also adapted: r**2 (ES2016 exponentiation) was replaced with r*r for maximal compatibility; p.int() replaced with Math.floor() for clarity; height (p5 global) replaced with myHeight (the explicit local variable).
The user indicated a hero image would come later. The interim solution is a purely CSS background derived from the sketch’s actual color palette:
background: radial-gradient(ellipse 70% 45% at 50% 32%, rgba(0,190,215,0.18) 0%, transparent 100%), radial-gradient(ellipse 70% 45% at 50% 68%, rgba(255,140,0,0.18) 0%, transparent 100%), radial-gradient(ellipse 40% 30% at 50% 50%, rgba(255,255,255,0.04) 0%, transparent 100%), var(--gp-dark);
The upper elliptical gradient suggests the cool cyan of the sketch’s top half; the lower suggests the warm amber of the bottom half; the central white hint suggests the desaturated equatorial blend zone. The three-layer CSS gradient communicates the sketch’s entire visual structure without the actual image. When the hero image is supplied, the url() call is added above the radial gradients and the CSS is complete. The comment in the CSS file marks exactly where.
The sketch’s own colors determined the page’s design variables:
| CSS Variable | Value | Source in sketch |
|---|---|---|
--gp-dark | #060612 | background(0, 30, 0) in HSB = brightness 0 = black; page uses near-black to match |
--gp-cyan | #00c8d8 | Upper-half hue range ~190–210° (blue→cyan) averaged and brightened for UI use |
--gp-amber | #ffaa00 | Lower-half hue range 38–55° (amber→yellow); 46° at full sat/bri ≈ #ffaa00 |
--gp-red | #ff2040 | fill(0, 100, 100) in HSB = H° 0, full sat/bri = vivid red (the smile) |
Cyan is the primary accent (nav border, headings, stage badge) because it represents the upper half — where the sketch begins. Amber is the tutorial accent color, signaling “educational callout.” Red is available for emphasis. The dark near-black background matches the sketch’s canvas, so the rendered orb appears to glow from its showcase context rather than sitting awkwardly on a contrasting background.
The intro card includes a separate amber-accented callout block that previews the map() lesson directly on the showcase page — not hidden inside the chatlog. This follows the Ask Copilot principle: if something is educational, put it where students will see it first. A student who arrives at the showcase page and reads nothing else should still encounter the core insight: “map() asks ‘what fraction of the way is y between 0 and height?’ and applies that fraction to another range. One function call. Infinite intermediate values.” That sentence is the lesson. The chatlog has the full proof; the showcase page has the punchline.
| File | Status | Notes |
|---|---|---|
graysonsP5JSOriginal.html |
Created | Main showcase page: hero with CSS gradient, intro card with map() callout, greenbar + live canvas, what’s-next card with refactoring preview, footer. |
styles/graysonsOriginalSketchStyles.css |
Created | Cyan/amber/near-black palette derived from sketch colors. CSS gradient hero with comment marking where hero image URL goes. .tutorial-callout amber-accent component added. |
graysonsP5JSOriginalChatlog.html |
Created | This page. Includes build decisions, full code analysis, map() tutorial, and refactoring roadmap. |
sketches/graysonsOriginalSketch.js |
Pre-existing — untouched | Grayson’s original p5.js sketch. Source of GRAYSON_CODE template literal; never executed by the page. |
The first line of setup() after canvas creation:
colorMode(HSB, 360, 100, 100, 100);
Grayson chose HSB independently, without being prompted, before any class discussion of color models. In Ethan’s six-phase refactoring roadmap, HSB is Phase 3 — something to work toward. Grayson started there. This is not a minor observation. It means Grayson intuitively understood that color should be described in human terms (hue, saturation, brightness), not in hardware terms (red, green, blue channel intensities). That instinct is the correct one, and it is genuinely unusual in a novice programmer.
The fourth argument to colorMode() — 100 for alpha max — is also deliberate and correct. Most introductory code omits the alpha parameter entirely.
p5.js’s built-in fill() function applies a single uniform color to any shape. It cannot apply a gradient that changes across the shape’s extent. Grayson solved this without being shown the solution: instead of drawing a filled circle, draw the circle as a stack of horizontal line segments, each one colored differently.
This technique is called scan-line rasterization. It is how computer graphics hardware has rendered filled shapes since the 1970s — scan from top to bottom, find the left and right edges at each row, fill between them. Grayson arrived at this technique from first principles, as a logical solution to the problem of “I need colors to change as I go down the circle.”
The loop:
for (let y = (myHeight/2 - myHeight*0.375); y < (myHeight/2 + myHeight*0.375); y += 1) { x1 = (-sqrt(r**2 - (y-k)**2) + h); // left edge of circle at row y x2 = ( sqrt(r**2 - (y-k)**2) + h); // right edge of circle at row y stroke(hue, s, b); line(x1, y, x2, y); // horizontal scan line }
Each iteration draws one horizontal line segment from the left boundary to the right boundary of the circle, colored according to the current y position. The loop runs 375 times (the circle’s diameter is 375px = myWidth * 0.75). The result is 375 individually colored horizontal lines that together form a smooth gradient-filled circle. A student who understands this loop understands how rasterization works at a level that most CS graduates have never thought about explicitly.
To draw the left and right boundaries of the circle at each y position, Grayson uses the standard circle equation:
(x − h)² + (y − k)² = r² ⇒ x = h ± √(r² − (y−k)²)
This is high school algebra applied directly in code. h and k are the center coordinates (myWidth/2, myHeight/2), and r is the radius. The minus sign gives the left edge; the plus sign gives the right edge. At the top and bottom of the circle, (y−k)² = r² and the square root is zero — the circle narrows to a point. At the center row, (y−k) = 0 and the width is maximum: x ranges from h−r to h+r.
Most beginning p5.js programmers draw circles with circle(x, y, d) and never think about the mathematical definition of a circle. Grayson applied the equation directly because the scan-line approach required it: to draw horizontal lines inside the circle, you need to know exactly where the circle boundary is at every row. This is the mathematical foundation of every filled circle ever rendered by every graphics system.
Near the circle’s equator, the saturation drops from 100% to 0% and back. The mechanism is an accumulator variable s that is declared but not initialized, then modified conditionally:
let s; // undefined — not 0, not 100 if (y >= myHeight/2 - 50 && y <= myHeight/2) s -= 2; // drops 2 per pixel else if (y <= myHeight/2 + 50 && y >= myHeight/2) s += 2; // rises 2 per pixel else s = 100; // full saturation elsewhere
Before the transition zone (y < 200), the else branch runs on every iteration: s = 100. When the loop reaches y = 200 (myHeight/2 − 50), it enters the first branch: s -= 2. Since s was 100 on the previous iteration, s becomes 98. Each subsequent step: 96, 94, …, 2, 0. At y = 250 (the equator), s = 0. In HSB, S = 0% means no saturation — pure white regardless of hue. The equatorial band bleaches to white. Then the second branch runs: s += 2 each step, climbing back to 100 over the next 50 pixels.
The result is a smooth V-shaped saturation dip centered at the equator — a soft, luminous blend zone where the blue-cyan and amber-yellow halves meet. Whether Grayson calculated this precisely or arrived at it through experimentation is unknown. The effect is sophisticated either way: it avoids the harsh hard edge that would occur if saturation stayed at 100 all the way to the center. The equatorial blending is the most visually refined element of the entire sketch.
The very first line of the sketch:
let myWidth, MyHeight; // MyHeight — capital M, capital H
Then in setup():
myWidth = 500; // correctly assigns the declared variable myHeight = 500; // myHeight was never declared — creates an implicit global
The declared variable is MyHeight (capital M, capital H). The variable used throughout the code is myHeight (all lowercase). In non-strict mode JavaScript, assigning to an undeclared variable creates it as a global property of the window object. The code works because myHeight is accessible everywhere after setup() runs. But MyHeight — the declared variable — is never used and never assigned. It is undefined for the entire lifetime of the program.
This is one of the most common JavaScript gotchas: a declaration with inconsistent capitalization creates a “ghost variable” that is never read, while the intended variable lives as an undeclared global. In strict mode ("use strict"), this would throw a ReferenceError at runtime. The fix is one character: change MyHeight to myHeight in the let declaration, making the naming convention consistent throughout. This is the kind of bug that automated linters catch immediately and that human reviewers often miss entirely.
The p5.js map() function asks a single question: where does this value fall proportionally in one range, and what is the equivalent position in another range?
map(value, inputMin, inputMax, outputMin, outputMax)
The formula:
output = outputMin + (value - inputMin) / (inputMax - inputMin) * (outputMax - outputMin)
In plain English: compute the fraction of the way value is between inputMin and inputMax, then apply that same fraction to [outputMin, outputMax].
Three worked examples from Grayson’s sketch:
For the upper half of the orb (y < myHeight/2 = 250), Grayson maps the y position to a hue between 240 (blue) and 180 (cyan).
| y value | Fraction of 500 | Hue result | Color |
|---|---|---|---|
| 0 | 0% | 240° | Blue |
| 62.5 (loop start) | 12.5% | 232.5° | Blue-violet |
| 125 | 25% | 225° | Violet-blue |
| 187.5 | 37.5% | 217.5° | Cyan-blue |
| 249.9 (just before center) | 50% | ≈ 210° | Cyan-blue |
Key insight: The input range is [0, 500] — the full canvas height — not [62.5, 250] — the actual circle extent. This means the full extremes of [240, 180] are never reached inside the circle. At the top of the circle (y = 62.5), the hue is 232.5°, not 240°. Grayson mapped from the canvas rather than the circle — a deliberate or accidental choice that softens the extremes and produces the deep-blue feeling at the top of the orb.
For the lower half (y ≥ 250), the same input range [0, 500] maps to [38, 55] — the amber-to-yellow band.
| y value | Fraction of 500 | Hue result | Color |
|---|---|---|---|
| 250 (center) | 50% | 46.5° | Amber-orange |
| 312.5 | 62.5% | 48.6° | Amber |
| 375 | 75% | 50.75° | Amber-yellow |
| 437.5 (loop end) | 87.5% | ≈ 52.9° | Yellow |
| 500 | 100% | 55° | Yellow-green (never reached) |
Key insight: The hue range is narrow (only 17° wide) compared to the upper half (60°). This produces a much subtler gradient in the lower half — the amber barely shifts to yellow. The visual effect is a warmer, more coherent lower half versus a more dramatic cool-to-warm transition from top to center. Whether intentional or not, the asymmetry between the two halves is aesthetically pleasing.
Grayson used map() for hue but used a manual accumulator (s -= 2, s += 2) for saturation. Both produce linear relationships. Here is the comparison:
// Grayson's approach for saturation — manual accumulation: if (y >= myHeight/2 - 50 && y <= myHeight/2) s -= 2; else if (y <= myHeight/2 + 50 && y >= myHeight/2) s += 2; else s = 100; // Equivalent using map() — produces identical output: let distFromCenter = abs(y - myHeight/2); s = (distFromCenter < 50) ? map(distFromCenter, 0, 50, 0, 100) : 100;
The map() version expresses the intent more directly: saturation goes from 0 (at the exact equator) to 100 (50 pixels away from the equator). The accumulator version arrives at the same result through incremental steps. Understanding that both are equivalent — and that map() is the cleaner expression — is a Phase 2/3 refactoring insight: the accumulator is a magic number pattern (-2, +2, 50) that map() names and makes configurable.
Why first: draw() currently handles three visually distinct things: the scan-line gradient orb, the ring-shaped eyes, and the red ellipse smile. Each should be its own function.
- Extract
drawOrb()— the scan-line loop, all 375 iterations. The mathematical heart of the sketch. - Extract
drawEyes()— the fourcircle()calls (two outer white, two inner black). - Extract
drawSmile()— the singleellipse()call with red fill. - Rewrite
draw()as a three-line recipe:drawOrb(); drawEyes(); drawSmile();
Teaching payoff: With Phase 1 in place, a student can comment out drawEyes() and see the orb without eyes. Comment out drawSmile() and see the face without its mouth. The orb, the eyes, and the smile become independently visible, independently understandable.
Why second: The sketch has many magic numbers. Each should be named in a settings panel at the top of the file.
var RADIUS_RATIO = 0.75;— the3/4inint(myWidth*(3/4))/2. Changing this one value scales the entire orb.var UPPER_HUE_START = 240; var UPPER_HUE_END = 180;— the input tomap()for the cool half. Change them to shift the color range.var LOWER_HUE_START = 38; var LOWER_HUE_END = 55;— the warm half. Interesting experiment: set both to the same hue and see the orb become monochrome.var SAT_BLEND_ZONE = 50;— the transition width. Increase it to widen the bleaching effect; decrease it for a harder equatorial edge.var EYE_OFFSET_X = 75; var EYE_OFFSET_Y = 100;— eye positions relative to center.var EYE_OUTER_D = 100; var EYE_INNER_D = 75;— ring-eye diameters.EYE_INNER_D / EYE_OUTER_Dcontrols how thick the ring appears.
Teaching payoff: Change UPPER_HUE_START from 240 to 300. The orb becomes purple-to-cyan instead of blue-to-cyan. Change LOWER_HUE_START to 0. The lower half becomes red. One constant, total color personality shift.
Why third: Grayson already uses HSB (Discovery 1) — Phase 3 of Ethan’s roadmap is already done. What Grayson has not done is apply map() to the saturation, which would make the equatorial blend as expressive as the hue gradient.
// Phase 3 refactor: replace the accumulator with map() let distFromCenter = abs(y - k); let s = (distFromCenter < SAT_BLEND_ZONE) ? map(distFromCenter, 0, SAT_BLEND_ZONE, 0, 100) : 100;
Teaching payoff: This version of the saturation makes the accumulator’s implicit behavior explicit. SAT_BLEND_ZONE = 50 in the named constants now controls exactly how wide the blend zone is, and the map() call shows exactly what relationship is being modeled. A student who reads this line reads a mathematical sentence: “Saturation is 0 at the equator and 100 at SAT_BLEND_ZONE pixels away.” No mental simulation required.
Why fourth: After the code is modular and readable, the variable inconsistency (Discovery 5) deserves a clean fix and a teachable annotation.
- Fix the declaration: Change
let myWidth, MyHeight;tolet myWidth, myHeight;. One character fix, eliminates the ghost variable. - Add
"use strict";at the top of the sketch. In strict mode, the original code would have thrown aReferenceErroron the first assignment tomyHeight(undeclared variable). Strict mode is the correct production setting. In an educational context it is especially valuable: it makes mistakes visible rather than silently allowing them. - Review all variable declarations for
x1,x2,hue,s,b. After Phase 1 extractsdrawOrb(), these local variables live cleanly inside that function’s scope.
Teaching payoff: Run the sketch in strict mode after fixing the declaration. It compiles and runs correctly. Then revert just the fix — change myHeight back to MyHeight in the declaration — and watch it fail immediately with a clear error. Students see in real time why strict mode is a diagnostic tool, not a constraint.
Why fifth: The eye positions (±75, −100) and the smile ellipse (175 × 40) are absolute pixel values. At 500×500 they are perfectly positioned. At 300×300 they would still be absolutely positioned — but the orb (scaled by RADIUS_RATIO) would be smaller while the eyes maintain their absolute offset. The face proportions would drift.
// Phase 5: replace absolute offsets with canvas-relative expressions var EYE_OFFSET_X = myWidth * 0.15; // 75 / 500 = 0.15 var EYE_OFFSET_Y = myHeight * 0.20; // 100 / 500 = 0.20 var SMILE_Y_OFFSET = myHeight * 0.20; // smile center also 20% below canvas center var SMILE_W = myWidth * 0.35; // 175 / 500 = 0.35
Teaching payoff: Change createCanvas(myWidth, myHeight) to createCanvas(300, 300). The orb, eyes, and smile all scale proportionally. The face looks the same at any canvas size. That is scalability: one set of ratios derived from the original hardcoded values, then everything follows.
Why last: Interactive controls are most useful when the code is clean enough to understand what each control changes. A slider on a tangle is noise; a slider on a named constant is an experiment.
- Hue-range sliders: Two pairs of sliders for
UPPER_HUE_START/ENDandLOWER_HUE_START/END. Drag one endpoint toward the other and watch the gradient compress. Drag them apart and watch it expand. Cross them (e.g.,UPPER_HUE_START = 0,UPPER_HUE_END = 240) and watch the gradient reverse direction. - Blend zone slider: Controls
SAT_BLEND_ZONE. At 0: sharp equatorial edge, two distinct color halves, no blending. At 200: almost the entire orb desaturates near white. The slider makes the saturation dip’s role visible in real time. - Re-render button: Since the sketch uses
noLoop(), changing a constant has no visual effect without redrawing. A “Render” button callsp.redraw()to trigger a fresh single-frame computation. This reinforces thenoLoop()lesson: the sketch draws when you tell it to, not continuously.
Teaching payoff: The orb responds to student exploration. A student who discovers that the two hue ranges can cross — producing a gradient that runs backward — has discovered something about map() that no lecture would deliver: map() works with inverted ranges. map(y, 0, 500, 240, 0) produces decreasing hue as y increases, mapping y=0 to hue=240 and y=500 to hue=0. The function does not care which end is larger. That discovery, made by playing with sliders, is worth more than reading about it.
- Student code contains more than students know they put in it. Grayson used scan-line rasterization, the circle equation, HSB color mode, and a linear saturation ramp — not because these concepts were taught, but because the problem required them and Grayson solved the problem. The teacher’s job is to name what the student built and build a curriculum around it. Every pattern in Grayson’s code has a professional name and a professional context. The code is the lesson; the teacher supplies the vocabulary.
map()is the most versatile function in creative coding. Any time a value in one range needs to drive an output in another range — y position to hue, mouse x to speed, time elapsed to opacity, data value to bar height —map()is the correct tool. It is the function underneath every animation easing, every data visualization, every audio-reactive visual, and every parametric design tool. Learning it from Grayson’s gradient is a concrete anchor that will activate every future use. The first time a student usesmap()they will remember the orb.- HSB arriving in student code uninvited is a signal. When a student chooses HSB before being asked, they have already understood something about color that most adult programmers never articulate: RGB describes hardware, HSB describes human perception. The refactoring roadmap for Grayson’s sketch starts at Phase 1 and skips Phase 3 because it is already done. The right response to a student who arrives at Phase 3 independently is: acknowledge it explicitly, name it precisely, and move on to what they have not yet named.
- Invisible bugs that produce the right output are the most educational kind.
The
MyHeight/myHeightinconsistency causes no visual error, produces correct output, and would pass any casual inspection. It is exactly the kind of bug that ships to production, lives for years, and breaks only when the code runs in strict mode or is refactored by someone who trusts the declared type. Showing students the bug, the fix, and the behavior in strict mode gives them the diagnostic instinct that catches this pattern automatically. The bug that is harmless today but dangerous tomorrow is the most important bug to study.
Let’s add a news entry discussing the current development of Grayson’s BiColor Orb, with a ‘teaser’ about upcoming upgrades. It will be id=“news-2026-071” above the most recent entry about Ethan’s emoji. Please follow that entry’s styling and layout, particularly with regard to the links at the bottom of the entry. Let’s also add Grayson’s entry into the explore page, as a SPARK entry as well as a Processing entry, as we did for Ethan’s work. Let’s update the chatlog with these developments and our next move will be to incorporate your suggestions of upgrades.
News entry #071 was inserted above entry #070 (Ethan’s Phase 1–3). Following the same accordion structure and link style, the entry covers four points:
- What Grayson built — the bicolor orb rendered with scan-line rasterization, the circle equation, and
map(), withnoLoop()producing a single-frame computation. - The five code discoveries: HSB already in use, scan-line rasterization, explicit circle equation, saturation V-dip accumulator, and the
MyHeight/myHeightghost variable. - The
map()tutorial teaser — worked hue values at key y positions, and the side-by-side accumulator vs.map()comparison. - A teaser for the six-phase refactoring series: modularization → named constants → saturation
map()→ strict mode fix → scalability → interactive controls with Render button.
Two links close the entry following entry #070’s layout exactly: the showcase page (fa-circle-half-stroke icon) and the S.P.A.R.K. Chat Log (fa-comments).
Grayson’s showcase was added to two offcanvas panels in explore.html, following the same pattern used for Ethan’s Crazed Emoji:
S.P.A.R.K. offcanvas — Two entries added after the Ethan’s Crazed Emoji Chat Log entry:
- Grayson’s Bicolor Orb (showcase link,
lang-badge lang-js p5.js,fa-circle-half-strokeicon) - Grayson’s Bicolor Orb — Chat Log (chatlog link,
lang-badge lang-js S.P.A.R.K.)
Processing offcanvas — One entry added after the Ethan’s Crazed Emoji entry (chatlog not duplicated here):
- Grayson’s Bicolor Orb (showcase link,
lang-badge lang-js p5.js)
| File | What Changed |
|---|---|
news.html |
Entry #071 added above entry #070. Title, four content paragraphs, and two closing links follow entry #070’s layout exactly. |
explore.html |
S.P.A.R.K. offcanvas: two entries added (showcase + chatlog). Processing offcanvas: one entry added (showcase only). |
graysonsP5JSOriginalChatlog.html |
Post-Build section added (this page). Next-steps section added previewing the Phase 1 and Phase 4/6 highlights from the refactoring roadmap. |
Phase 1 extracts three named functions from draw():
function draw() { drawOrb(); // scan-line gradient: the mathematical heart drawEyes(); // concentric circle rings × 2 drawSmile(); // red ellipse }
Each function has one job. Comment out drawEyes(): the orb and smile render, the eyes vanish. Comment out drawSmile(): a faceless orb. Comment out drawOrb(): a black canvas with two floating ring-eyes and a red ellipse. The modularization lesson is the same as Ethan’s — but the three-function recipe is even cleaner than Ethan’s five, and the visual feedback when each function disappears is dramatically different. Both refactoring series are in the TNT ecosystem; showing them side by side is itself a curriculum: one student’s sketch has five features in draw(), the other’s has three. Same principle. Different result. Both improved in the same way.
The drawOrb() function is also the natural container for Phases 2 and 3: its magic numbers (hue ranges, blend zone, radius ratio) become named constants, and its saturation accumulator becomes an explicit map() call. Each subsequent phase builds on the modular foundation Phase 1 creates.
Phase 4 (fix variable naming and scope) is unusually teachable because the bug is invisible in normal operation and immediately fatal in strict mode. The three-step classroom exercise:
- Open the sketch. It runs. The face renders. Nothing looks wrong.
- Add
"use strict";at the top of the file. Run again:ReferenceError: myHeight is not defined. The sketch crashes before drawing a single pixel. - Find
let myWidth, MyHeight;. ChangeMyHeighttomyHeight. Run again. Works. The fix was one character.
This sequence teaches three things simultaneously: camelCase consistency matters; non-strict JavaScript silently creates globals for undeclared variables; and strict mode is a diagnostic tool, not a constraint. The fact that the sketch appeared to work perfectly before the fix is not reassuring — it means the bug was hiding. Bugs that hide are the most dangerous ones. Strict mode is a flashlight.
Phase 6 adds interactive controls for the hue ranges, blend zone, and radius ratio. The architectural challenge: the sketch uses noLoop(), so changing a named constant has no visual effect until draw() runs again. The solution is a Render button that calls p.redraw():
// Controls update window.graysonsSettings on each input event // Render button triggers a fresh single-frame computation document.getElementById('renderBtn').addEventListener('click', function() { p.redraw(); // runs draw() exactly once, then stops again });
This is the lesson noLoop() was always setting up: the sketch draws when you tell it to. Ethan’s emoji runs continuously and updates every frame. Grayson’s orb computes once and waits. Both are valid p5.js architectures. The Render button makes the architectural difference tangible: press it and watch the orb recompute from the new hue ranges. The most educational experiment: drag the upper hue range from [240, 180] to [0, 60] and press Render. The cool upper half turns warm. Drag it back. The color change is not magic — it is exactly the map() call, with different inputs, producing different outputs. That is what the slider demonstrates. That is the lesson.