Building Jayden’s Jammin’ Emoji Showcase
From a student’s original p5.js sketch to a live TNT showcase —
CSS-only hero, static canvas adapter, and the arc geometry behind the headphone band.
This log documents the construction of jaydensJamminEmojiOriginal.html:
a TNT showcase page that displays Jayden’s original p5.js sketch alongside its live rendering.
The template is Ethan’s Crazed Emoji series — same greenbar display technique, same
instance-mode adapter pattern, same S.P.A.R.K. chatlog structure.
Two key differences from Ethan’s page distinguish the design decisions here. First, no hero image was provided — the hero is CSS-only, using layered radial gradients to create a music-stage atmosphere. Second, Jayden’s sketch is static, not animated — a distinction that changes how the instance-mode adapter is structured. Both differences are documented below. A Stage 2 Planning section maps out the six-phase refactoring roadmap that follows Ethan’s progression.
Jayden created an original emoji (sketch code attached). We’d like to
feature it like we did for Ethan’s emoji. Let’s create
jaydensJamminEmojiOriginal.html with associated jaydensOrigSketch.js
(attached) and jaydensJamminEmojiOriginalStyles.css and if needed,
jaydensJamminEmojiOriginalScript.js. We’ll also want a chatlog for
this process, jaydensJamminEmojiChatlog.html. Design a creative thematic
background for the hero area for now. Ultimately this app will be similar to Ethan’s
so use his work as a template for what will follow.
“Feature it like we did for Ethan’s emoji” is the design brief in six words. It specifies: dark page, live canvas next to greenbar code display, SPARK chatlog, spark-bar navigation, instance-mode adapter pattern. Every structural decision from Ethan’s series is inherited. The prompt does not need to repeat those decisions — “like Ethan’s” imports them.
“Design a creative thematic background for the hero area for now”
is a deliberate placeholder. Ethan’s hero used cuckoo.jpg — a real photograph
provided with the project. Jayden’s folder contains no hero image yet. The phrase “for now”
signals that an image will come later; the CSS-only hero is a fully functional holding solution, not a
workaround. This distinction matters: a placeholder that teaches CSS gradient technique has more value
than a placeholder that just waits for a file.
“If needed, jaydensJamminEmojiOriginalScript.js”
is conditional file creation stated explicitly. The original sketch page has no interactive controls
— no color picker, no slider — so the script file is not needed. Asking whether a file
is needed rather than creating it automatically reflects professional discipline: don’t create files
that have no purpose.
“Use his work as a template for what will follow” extends the Ethan model to the future phases. The chatlog’s Stage 2 Planning section maps that progression: modularization, named constants, HSB color mode, push/pop discipline, canvas scalability, and teaching breadcrumbs — adapted for the specifics of Jayden’s sketch.
Ethan’s hero used cuckoo.jpg as a background image placed inside a
dark gradient overlay. That photograph gave the hero a grounding texture and a specific emotional
register (“cuckoo” = crazed). Jayden’s project folder contains no photograph yet.
Rather than ship an empty gray rectangle, the hero is built entirely from CSS gradients that
evoke the sketch’s subject matter.
The technique: three radial-gradient() layers stacked on a dark
linear-gradient() background.
- Two blue radial glows at 15% and 85% horizontal — left and right speakers, or two stage wash lights. They bracket the hero text the same way headphone ear pieces bracket the face.
- One gold radial glow rising from below center — stage footlights or spotlighting from beneath, evoking the golden face of the emoji.
- A subtle
repeating-linear-gradientoverlay creates vertical lines at 10px intervals with ~4% opacity — the texture of an equalizer grid or stage lighting bars. Barely visible; removes the flatness of a pure gradient.
When a hero image becomes available, it replaces or layers under these gradients with no structural change to the page. The CSS-only hero is designed to be upgraded, not abandoned.
Ethan’s sketch calls rotate(frameCount * 2) for the spinning eyes
and uses sin(frameCount * 8) for the tongue oscillation. Every call to
draw() produces a visually different frame — the animation is the sketch.
Calling noLoop() on Ethan’s sketch would freeze it mid-frame and render the whole
point of the work invisible.
Jayden’s sketch contains no reference to frameCount,
sin(), cos(), or any time-varying expression. Every call in
draw() produces identical output on every frame. The browser would execute
draw() approximately 60 times per second, redrawing the same pixels each time,
producing no visible change and consuming CPU for no reason.
The instance-mode adapter calls p.noLoop() in p.setup().
This tells p5.js to call draw() exactly once and then stop. The rendering is identical
to a looping sketch; the CPU usage drops to near zero after the first frame. This is not a workaround
for the static sketch — it is the correct pattern for any sketch that does not animate. The
teaching note in the adapter comment makes this explicit: “static sketch — render once
and hold.”
Ethan’s sketch opens draw() with
translate(width / 2, height / 2), moving the origin to the center of the canvas.
Every drawing command uses coordinates relative to center: the face at (0, 0),
the eyes at (-60, -20) and (60, -10). This makes the sketch
implicitly proportional — when the instance-mode adapter creates a smaller
canvas, the centered origin automatically keeps every feature visible.
Jayden’s sketch does not center the origin. The face is at
ellipse(200, 210, 240, 240); the ear pieces are at rect(70, 190, …)
and rect(295, 190, …). All coordinates are absolute pixel positions
on a 500×400 canvas. Creating a smaller canvas does not move these coordinates — the sketch
would simply render at the wrong position and potentially overflow.
The adapter uses p.scale(scaleFactor) to resolve this. The setup
computes the scale factor from the container width:
var w = Math.min(container.offsetWidth || 460, 500); var h = Math.round(w * 0.80); // preserves 500×400 aspect ratio scaleFactor = w / 500; // 1.0 at full width, <1.0 when constrained
The draw function applies the scale before any drawing command. The background fills
the full canvas first (unaffected by scale), then every coordinate is multiplied by
scaleFactor:
p.draw = function() { p.background(230, 245, 255); // fills full canvas — before scale p.scale(scaleFactor); // all subsequent coords multiplied by this factor p.noStroke(); p.fill(255, 215, 0); p.ellipse(200, 210, 240, 240); // still writes 200, p5 handles the multiplication ... };
At full width (500px container), scaleFactor = 1.0 and the sketch
renders identically to the original. At 400px, scaleFactor = 0.80 and every coordinate
is proportionally reduced. The face stays centered, the ear pieces stay aligned with the headphone
band, and the sketch remains visually correct at any container width.
The headphone band is drawn with:
arc(200, 195, 210, 210, PI, TWO_PI).
In p5.js (default RADIANS mode), angles are measured clockwise from the positive x-axis — 0 points
right, PI/2 points down (screen coordinates with y increasing downward), PI points left,
3*PI/2 points up, TWO_PI returns to right.
The arc from PI to TWO_PI starts at the leftmost point,
travels clockwise through the top of the circle, and ends at the rightmost point — the upper
semicircle. The arc center at (200, 195) is just above the face center at
(200, 210), so the upper semicircle arcs over the top of the golden face.
The arc endpoints at approximately (95, 195) and (305, 195) land precisely
where the rectangular ear pieces begin.
The student’s comment — “PI tells the arc to start at 180, and
TWO_PI tells it to stop at 360” — demonstrates understanding of the radian-to-degree
correspondence. The use of PI and TWO_PI (p5.js named constants) rather than
hardcoded values like 3.14159 and 6.28318 is a professional instinct worth
naming: named constants are self-documenting; magic floating-point numbers are not.
The smile uses the complementary arc: arc(200, 225, 90, 70, 0, PI) —
from 0 (right) clockwise to PI (left) = the lower semicircle. Together the two arcs demonstrate the
full circle partitioned into named halves. A student who understands both arcs in this sketch has
mastered p5.js arc geometry.
The prompt includes “if needed, jaydensJamminEmojiOriginalScript.js”.
The original sketch has no interactive controls, no external state bridge, and no DOM manipulation
beyond what Bootstrap and the inline sketch adapter provide. Creating an empty or
near-empty JavaScript file would add a network request and a maintenance burden for no
benefit.
The decision not to create a file is itself a professional judgment. Future phases (particularly a Phase 3 color-picker equivalent) will introduce an external script. At that point, the file earns its existence. Deferring file creation until the file has a reason to exist is the discipline this conditional phrasing in the prompt was pointing toward.
Ethan’s palette was driven by the sketch: dark navy background because the yellow face needed a high-contrast stage; red accent because “crazed” calls for an edge. The same reasoning applies to Jayden’s theme. The sketch defines the palette:
- Background:
#07080fnear-black — a recording studio or performance venue with the lights down, putting the screen in focus. - Primary accent (gold,
#ffd700): the face color. Used for hero title, link text, attribute highlights — the first color the eye finds. - Secondary accent (electric blue,
#0078ff): the headphone color. Used for the spark-bar border, intro card top border, stage-badge background — the color that says “music equipment.”
Gold and blue are complementary in the warm-cool contrast sense: the warm gold face against the cool blue headphones is the visual tension Jayden built into the sketch. The page palette honors that choice. The spark-bar’s blue border echoes the headphone band; the hero’s gold glow echoes the face. The page is the sketch, extended.
| File | Status | Notes |
|---|---|---|
jaydensJamminEmojiOriginal.html |
Created | Main showcase page: CSS-only hero, intro card, greenbar + live canvas, footer |
styles/jaydensJamminEmojiOriginalStyles.css |
Created | Dark navy + gold + blue palette, CSS equalizer-bar hero texture, greenbar nth-child, canvas panel |
jaydensJamminEmojiChatlog.html |
Created | This page |
sketches/jaydenOrigSketch.js |
Pre-existing — untouched | Jayden’s original p5.js sketch. Source of the JAYDEN_CODE template literal; never executed by the showcase page. |
jaydensJamminEmojiOriginalScript.js |
Not created | No interactive controls in the original page. Will be created when Phase 3 (HSB color picker) requires it. |
- Static and animated sketches require different adapter patterns.
Ethan’s adapter omits
noLoop()because every frame is visually different. Jayden’s adapter callsnoLoop()because every frame is identical. The adapter serves the sketch, not a fixed template. Reading the sketch before writing the adapter is not optional — it is the only way to know which pattern applies. - Absolute coordinate sketches need a scale() wrapper; centered sketches do not.
When a sketch uses
translate(width/2, height/2)as its first move, every feature is expressed relative to canvas center — a resize is transparent. When a sketch positions features at absolute pixel values, a resize changes the canvas without changing the coordinates.p.scale(w / originalWidth)before any drawing command bridges that gap with a single line. - A CSS-only hero is a first-class design artifact, not a placeholder. Layered radial gradients that echo the sketch’s color palette, combined with a subtle repeating-linear-gradient texture, produce a hero that teaches CSS gradient technique while waiting for a photograph. If the photograph never arrives, the CSS hero is complete on its own terms.
- Don’t create files that have no work to do. The conditional “if needed” in the prompt is the right instinct. A JavaScript file that contains only comments and an empty structure adds maintenance surface without adding value. Every file in the project should earn its existence. That discipline becomes easier to maintain when the question is asked at creation time rather than retrospectively during cleanup.
Before the roadmap, the honest teacher notes what the student built correctly. Jayden’s sketch demonstrates four professional instincts:
- Inline comments on every feature section (
//background,//face,//headphone band, etc.). The sketch is self-navigating before any refactoring begins. A first-time reader can find any feature in under five seconds. PIandTWO_PIused as named constants for the arc calls rather than hardcoded floating-point values. The student understands that the arc from PI to TWO_PI is the top half of the circle, and they documented why.- The corner-radius argument on
rect()for the ear pieces (rect(70, 190, 35, 80, 10)). Rounded rectangles are not the default; using them shows attention to the visual quality of the sketch, not just its correctness. - Consistent stroke management — explicit
noStroke()before fill-only shapes and explicitstroke()before outlined shapes. The student understood that p5.js carries stroke state between shapes and managed it deliberately.
Why first: Jayden’s draw() is currently one linear
sequence of ~25 drawing commands. After Phase 1 it becomes five readable calls. Every subsequent phase
is cleaner to implement when each feature has its own named home.
| Function | What it contains | Teaching focus |
|---|---|---|
drawFace() | noStroke, fill gold, ellipse | Simplest extraction — the face is one shape with no dependencies |
drawHeadphones() | arc band + two rounded rects | One feature made of three shapes — the function name explains the group |
drawEyes() | Two dark ellipses | Demonstrates a function that draws two of the same shape |
drawCheeks() | Two pink ellipses | Same pattern as eyes — students recognize the repeated structure immediately |
drawSmile() | noFill, stroke, arc from 0 to PI | The complementary arc to the headphone band — pairs well as a teaching example |
After Phase 1, draw() reads: background, drawFace,
drawHeadphones, drawEyes, drawCheeks, drawSmile.
Seven lines. A student can predict the emoji’s structure without reading a single drawing command.
Key constants to extract:
var FACE_X = 200; var FACE_Y = 210; var FACE_DIAM = 240;— face center and size. Phase 5 will deriveFACE_DIAMfrom canvas width.var BAND_HUE = 0; var BAND_G = 120; var BAND_B = 255; var BAND_WEIGHT = 18;— the headphone blue, named. Phase 3 will convert these to HSB.var EAR_L_X = 70; var EAR_R_X = 295; var EAR_W = 35; var EAR_H = 80; var EAR_RADIUS = 10;— ear piece geometry. WhenEAR_R_X - EAR_L_X= 225, the relationship between face width and ear placement becomes explicit.var BG_R = 230; var BG_G = 245; var BG_B = 255;— the sky-blue background, named. Phase 3 converts to HSB.
Teaching payoff: Change FACE_DIAM from 240 to 180. The face shrinks
but the headphone band stays wide. The mismatch reveals which constants are independent and which
need to be derived from each other — the setup lesson for Phase 5.
Why third: Phase 2 gives every color a name; Phase 3 makes those names
interpretable without a color picker. BAND_HUE = 213 — 213° = cool sky blue on
the color wheel — is a more honest description of the headphone color than
BAND_R=0, BAND_G=120, BAND_B=255.
Key conversions: face gold fill(255,215,0) →
fill(50, 100, 100) (50° = warm yellow-gold, full saturation, full brightness);
headphone blue stroke(0,120,255) → stroke(213, 100, 100)
(213° = vivid sky blue); background background(230,245,255) →
background(205, 10, 100) (205° = pale blue-white at low saturation).
Interactive component: Phase 3 is where jaydensJamminEmojiOriginalScript.js
is created. A color picker for the face color and a slider for the headphone hue make HSB
experiential rather than theoretical. A student who drags the face hue from 50 (gold) to 120 (green)
and watches the sketch update has learned HSB in 30 seconds.
Key difference from Ethan: Jayden’s original sketch contains
no push() / pop() calls, because it contains no
translate() or rotate() calls. Phase 4 does not fix existing push/pop
violations — it introduces push/pop as a disciplinary pattern in preparation
for Phase 5.
Phase 4 wraps each feature function in a push/pop pair, even where no transform is currently active. The pattern becomes a habit:
function drawHeadphones() { push(); // save state — ready for Phase 5 translate // headphone band noFill(); stroke(BAND_HUE, BAND_SAT, BAND_BRI); strokeWeight(BAND_WEIGHT); arc(BAND_X, BAND_Y, BAND_W, BAND_H, PI, TWO_PI); // ear pieces noStroke(); fill(BAND_HUE, BAND_SAT, BAND_BRI); rect(EAR_L_X, EAR_Y, EAR_W, EAR_H, EAR_RADIUS); rect(EAR_R_X, EAR_Y, EAR_W, EAR_H, EAR_RADIUS); pop(); // restore — transform stack exactly as before this call }//end drawHeadphones
Teaching payoff: Ask: what happens if you add translate(20, -10)
inside drawFace() without push/pop? Every feature drawn after it shifts. With push/pop,
the translate is contained. Students who see the uncontained version once remember push/pop
permanently.
Why fifth: Jayden’s sketch uses absolute coordinates (face at 200, 210 on a 500×400 canvas). Phase 5 re-centers the coordinate origin and derives every position as a fraction of canvas size.
Center the origin first:
translate(width / 2, height / 2) in draw(). Now the face center shifts
from (200, 210) to approximately (0, 10)
(slightly below center, which is Jayden’s creative choice for headroom). All other positions
become relative to that center. This is the same move Ethan made explicitly; Phase 5 makes it
for Jayden retrospectively.
Key derived constants:
var FACE_DIAM = width * 0.48 (240px at 500px wide = 48%);
ear piece width = FACE_DIAM * 0.146; ear piece separation = FACE_DIAM * 1.15.
The math is the lesson — deriving these ratios from the original hardcoded values teaches
proportional thinking.
Teaching payoff: Change canvas to createCanvas(800, 640). The emoji
scales to fill the larger canvas without changing a single drawing command. That is scalability,
stated in a line of code a student can run in ten seconds.
Why last: Breadcrumbs in messy code are noise. Breadcrumbs in clean, named, structured, HSB-colorized, push/pop-disciplined, scalable code are road signs in a well-planned city.
SHOULD_SHOW_BREADCRUMBSflag at the top of the file. Matches the Ethan convention; a student reading both series sees the same pattern.- Annotate the arc geometry in
drawHeadphones(): explain why PI to TWO_PI draws the upper half, why the band center is 15 pixels above the face center, and how the arc endpoints connect to the ear piece positions. - Annotate the smile arc in
drawSmile(): contrast with the headphone arc (0 to PI = lower half vs PI to TWO_PI = upper half). The two arcs together are a complete lesson in p5.js arc geometry.
Teaching payoff: Open DevTools. Set SHOULD_SHOW_BREADCRUMBS = true.
The Console prints each function name once (not 60 times, because noLoop()). A student
who sees five messages, once, instantly immediately understands what “static sketch”
means as a runtime behavior, not just as a definition.
| Phase | Focus | Core Lesson | Test |
|---|---|---|---|
| 1 | Functions | One feature = one function; draw() = recipe | Comment any call; only that feature disappears |
| 2 | Named Constants | Magic numbers lie; names explain intent | Change FACE_DIAM; face changes, band stays wide |
| 3 | HSB Color Mode | HSB maps to human color thinking | Drag face hue; color shifts while structure holds |
| 4 | Push/Pop Discipline | Every transform opened must be closed | Add an unguarded translate; watch Phase 5 break; restore pop() |
| 5 | Canvas Scalability | Absolute coords break; ratios hold | Change canvas size; emoji scales automatically |
| 6 | Breadcrumbs | Static sketches log once; animated log 60×/s | Set breadcrumbs flag; see 5 Console messages, not 300 |
I’m pleased with this version and like the suggested refactoring sequence.
Since this design is substantially easier than Ethan’s, let’s be ambitious and do
the entire refactoring as jaydensRefactoredJamminEmoji.html along with its associated
sketch file and any extra JavaScript you might need. We’ll need a similar style sheet so
we can leave the original version untouched. I’m giving you Ethan’s Phase3 in case
it helps since it worked well. Let’s update our chatlog and use this as a teaching
experience of how a simple design can become more professional and flexible by following your
developmental steps incrementally.
“Since this design is substantially easier than Ethan’s” is the analytical insight that changes the entire approach. Ethan’s six phases required six separate files because each phase introduced animation, complex push/pop chains, or interactive controls that built on the previous one. Jayden’s sketch has no animation, no transformations to debug, and a flat structure that allows all six phases to compose cleanly in a single file. The user correctly identified that the simpler sketch enables a more ambitious refactoring strategy.
“Similar style sheet so we can leave the original version untouched”
repeats the series’ core discipline: version control by file. A new CSS file with a new canvas
target (#jaydenRefactoredCanvas) is the correct move. No shared CSS means no accidental
coupling. The original page’s visual identity is preserved independently of any changes to
the refactored page.
“Use this as a teaching experience of how a simple design can become more professional and flexible by following your developmental steps incrementally” is the most important sentence. The refactored page is not just a better version of Jayden’s sketch — it is a demonstration that any design can be improved one concern at a time. Structure before scale before style. That sequence is repeatable. Any student who understands why the phases are ordered the way they are can apply the same sequence to their own code.
Ethan’s series produced six separate sketch files because each phase was a
distinct showcase with its own navigation context. Jayden’s refactor is a single destination:
jaydenRefactoredSketch.js contains all six phases simultaneously. A student opens one
file and sees the complete picture. The phase labels are comment headers, not separate files.
The key insight: Phase 1 (modularization) and Phase 5 (scalability) are complementary,
not sequential. Once recalcConstants() exists and draw() is a recipe,
adding colorMode(HSB) (Phase 3) and push()/pop() (Phase 4) are
two-line changes per function. The phases compose cleanly precisely because Phase 1 created clean
boundaries first.
The SHOULD_SHOW_BREADCRUMBS flag (Phase 6) is worth special attention
for this sketch. Because noLoop() runs draw() exactly once, setting the
flag to true produces exactly 5 console messages — one per feature function.
This is the Phase 6 teaching payoff stated in the chatlog: a static sketch logs once;
an animated one would log 300 times per minute. Those 5 messages are the concrete definition of
“static sketch” as runtime behavior.
The entire scalability mechanism lives in one function. Every size constant is a
ratio of FACE_DIAM, which is a ratio of width. The ratios were derived
by dividing original hardcoded values by the original FACE_DIAM = 240:
| Original value | Ratio | Constant | At 240px face |
|---|---|---|---|
| Band diameter: 210px | 210/240 = 0.875 | BAND_DIAM = FACE_DIAM * 0.875 | 210px ✓ |
| Ear width: 35px | 35/240 = 0.146 | EAR_W = FACE_DIAM * 0.146 | 35px ✓ |
| Eye x-offset: 45px | 45/240 = 0.188 | EYE_X = FACE_DIAM * 0.188 | 45px ✓ |
| Smile width: 90px | 90/240 = 0.375 | SMILE_W = FACE_DIAM * 0.375 | 90px ✓ |
The table is the lesson: deriving ratios from hardcoded values teaches proportional
thinking. A student who computes 35/240 = 0.146 has not just written a constant —
they have understood the relationship between ear width and face diameter. That understanding
survives any canvas size change. The hardcoded 35 does not.
The refactored page includes three controls: face color picker (hex → HSB),
headphone hue slider (0–360°), and stroke brightness slider (0–100%). All three
communicate with the running sketch through window.jaydenSettings — the same
bridge pattern as Ethan’s Phase 3.
The headphone hue slider is the new addition Jayden’s page earns over Ethan’s.
Ethan’s sketch has many colors (face, eyebrows, mouth, tongue, spiral) tied together in complex
ways. Jayden’s headphones use a single color (BAND_HUE, BAND_SAT, BAND_BRI) applied
to both the arc and both ear pieces. A hue slider that controls all three with one value is a clean,
high-payoff control: drag from 212° (blue) to 0° (red) and the entire headphone assembly
changes. The connection between the constant and the visual is immediate and total.
The stroke brightness slider demonstrates the derived constant principle: the slider
sets STROKE_BRI_PCT; the sketch computes faceBri × strokeBriPct / 100
every redraw. Change the face color and the stroke adapts automatically. Set stroke brightness to
100% and the outline disappears into the fill. These are not features — they are demonstrations
of the Phase 2 lesson that named constants can encode relationships, not just values.
| File | Status | Notes |
|---|---|---|
jaydenRefactoredSketch.js |
Created | Standalone global-mode sketch with all 6 phases. Displayed in greenbar; never executed by the page. |
jaydensRefactoredEmojiFaceColorScript.js |
Created | Interactive controls: hex→HSB, window.jaydenSettings bridge, face picker / headphone hue / stroke brightness. |
styles/jaydensRefactoredJamminEmojiStyles.css |
Created | Palette identical to original; canvas target #jaydenRefactoredCanvas; adds .phase-table, .compare-block, .color-controls. |
jaydensRefactoredJamminEmoji.html |
Created | Refactored showcase: phase summary table, before/after comparison, greenbar, live canvas with three interactive controls. |
jaydensJamminEmojiOriginal.html |
Updated | Full Refactor link added to spark-bar. Original code untouched. |
jaydensJamminEmojiChatlog.html |
Updated | Full Refactor link added to spark-bar. This section added. |
- Simpler source code enables more ambitious teaching goals. Ethan’s sketch needed six pages because each phase introduced complexity that required its own showcase. Jayden’s sketch is simple enough that all six phases compose cleanly in one file. The ambition of the refactor is proportional to the clarity of the original. Start clean; extend ambitiously.
- Deriving ratios is the work; using them is the reward.
Computing
35 / 240 = 0.146for the ear width takes thirty seconds. The resulting constant works correctly at every canvas size forever. The thirty seconds is not overhead — it is the act of understanding the relationship. Students who compute their own ratios understand scalability. Students who copy constants do not. - Interactive controls make abstract constants observable.
BAND_HUE = 212is a number. A slider that moves from 212 to 0 and changes the headphone color from blue to red is an experience. The number becomes interpretable when it is connected to something the student can see and control. The interactive layer does not add a feature — it adds legibility to the constants that were already there.
Jayden failed to center the emoji in the canvas originally. Since we are no longer using ‘magic numbers’ I’m thinking that centering the emoji should be very easy. Am I right? If so, let’s center it in the refactored design. Let’s comment on this in the chatlog too.
The original off-center position: Jayden’s sketch places the
face at ellipse(200, 210, 240, 240) on a 500×400 canvas. The canvas center is
(250, 200). The face is 50px to the left of center and 10px below center. This is not uncommon in
student work — students often pick coordinates by eye and accept a slightly off-center result
without identifying it as a structural problem.
What centering required with the original code: Shifting the face to center (250, 210) would require updating every single absolute coordinate in the sketch — the band arc center, both ear piece rects, both eyes, both cheeks, the smile arc, and the face itself. Eight separate coordinates to find, recalculate, and verify. One mistake and a feature drifts.
What centering required with the refactored code: One word.
// Before: face translated to its original off-center position translate(width * 0.40, height * 0.525); // 200px, 210px on a 500×400 canvas // After: face centered at canvas midpoint translate(width / 2, height / 2); // always the exact center, any canvas size
Every feature function — drawFace(), drawHeadphones(),
drawEyes(), drawCheeks(), drawSmile() — expresses its
geometry as an offset from (0, 0), which is the face center. None of those functions change.
They have no knowledge of where the face is on the canvas; they only know where each feature is
relative to the face. Moving the face moves everything attached to it, automatically.
This is Phase 5’s value, stated as a fact: before the refactor, centering required changing eight coordinates in eight places. After the refactor, it required changing two numbers in one place. The difference is not cosmetic. It is the architectural consequence of separating “where is the face on the canvas” from “where are the features on the face.” Those are two different concerns; Phase 5 puts them in two different places. When the concerns are separated, each one is trivial to change.
For this next step, let’s create additional files so we can compare/contrast
with the former work. To demonstrate how important it is to have a ‘scalable’ image,
I’m thinking we could create a version where we have a slider that controls the radius of
the emoji’s face. Every other element should be scaled accordingly, so the image can shrink
or grow based on one parameter. Let’s call this jaydensScalableEmoji.html along
with the accompanying files it will need: sketch files, style files and any other JS files. I don’t
want any former apps to ‘break’ with this upgrade, of course, and we’ll want to
update the chatlog.
“Compare/contrast with the former work” is the pedagogical
reason this page exists. The refactored sketch already implemented Phase 5; a student who reads the code
can understand that FACE_DIAM = width * 0.48 makes things proportional. But reading about
proportionality and dragging a slider and watching twenty numbers change are not the same
experience. The scalable demo is not a new feature — it is the same architecture made observable.
“Based on one parameter” is the exact description of what
recalcConstants() achieves. The slider is the root parameter made tangible.
Every slider move triggers recalc(faceDiam) in the adapter’s draw(),
which propagates the new diameter to all 20 derived constants before the canvas re-renders.
The student does not have to understand the cascade intellectually; they can see it in the
constants readout as they drag.
In the refactored sketch, FACE_DIAM was the first thing computed by
recalcConstants(): FACE_DIAM = width * 0.48. This was already scalability
— but the root parameter was canvas width, not a user-controlled value.
The scalable sketch removes that one line. FACE_DIAM is now a plain
var declared at the top of the file, set directly:
var FACE_DIAM = 240; // try 80 (tiny), 240 (original), 380 (large) function recalcConstants() { // FACE_DIAM is already set — recalc reads it and derives everything else FACE_STROKE_W = FACE_DIAM * 0.033; BAND_DIAM = FACE_DIAM * 0.875; ... }
In the live page, the slider calls window.jaydenScalableP5.redraw().
The adapter’s p.draw() reads window.jaydenScalableSettings.faceDiam
and passes it to recalc() at the top of every draw call. The feature functions
never see FACE_DIAM directly — they only read the derived constants.
That is why no feature function needed to change.
The most distinctive element of this page is the constants readout: a live grid showing seven key derived constants updating as the slider moves.
| Constant | Ratio | At 240px | At 80px | At 380px |
|---|---|---|---|---|
FACE_DIAM | root | 240px | 80px | 380px |
BAND_DIAM | × 0.875 | 210px | 70px | 333px |
BAND_STROKE_W | × 0.075 | 18px | 6px | 29px |
EAR_W | × 0.146 | 35px | 12px | 55px |
EYE_X | × 0.188 | 45px | 15px | 71px |
SMILE_W | × 0.375 | 90px | 30px | 143px |
A student who watches these numbers change while dragging the slider does not need to be told that “scalability means proportional sizing.” They have already seen it. The constants readout turns an abstract principle into a data stream.
In the refactored page, the controls script calls a separate function to update
constants when settings change. The scalable adapter takes a cleaner approach: recalc(faceDiam)
is called at the top of every p.draw() call:
p.draw = function() { recalc(s().faceDiam || 240); // always fresh — no sync required p.background(BG_HUE, BG_SAT, BG_BRI); p.translate(p.width / 2, p.height / 2); ... };
This guarantees that the canvas always reflects the current settings, regardless of when or how they changed. There is no risk of a stale draw. The cost — 20 multiplications per redraw — is negligible for a static sketch that redraws only on user interaction. This pattern is simpler to reason about: draw() is always consistent with settings.
| File | Status | Notes |
|---|---|---|
jaydensScalableEmojiSketch.js |
Created | Global-mode sketch. FACE_DIAM is a plain variable; recalcConstants() uses it as-is. |
jaydensScalableEmojiScript.js |
Created | Controls: size slider, color picker, headphone hue, stroke brightness. updateConstantsReadout() updates the live grid. |
styles/jaydensScalableEmojiStyles.css |
Created | Same palette. Adds .size-control (gold border — the star), .constants-readout (3-column grid). |
jaydensScalableEmoji.html |
Created | Showcase page: greenbar, live canvas, face size slider, constants readout, color controls. Exposed as window.jaydenScalableP5. |
jaydensJamminEmojiOriginal.html |
Updated | Scalability Demo link added to spark-bar. |
jaydensRefactoredJamminEmoji.html |
Updated | Scalability Demo link added to spark-bar. |
jaydensJamminEmojiChatlog.html |
Updated | Scalability Demo link added to spark-bar. This section added. |
- Seeing is different from knowing.
A student who reads
BAND_DIAM = FACE_DIAM * 0.875knows that the band scales with the face. A student who drags the slider from 80 to 380 and watches the band go from 70px to 333px in the constants readout while the canvas updates simultaneously has experienced proportional scaling as a physical event. The readout is not a feature; it is a mirror that shows the cascade that was previously invisible inside the code. - The slider is the most honest demo of Phase 5.
In the refactored sketch, scalability is triggered by canvas resize — an event the
student has to engineer deliberately. The slider makes the same cascade happen on every pixel
of drag travel. The cost of each invocation of
recalcConstants()becomes observable: 20 multiplications, instant re-render, zero feature-function changes. That is what “separation of concerns” delivers at runtime.
Looking carefully at the sketch code for both updated files: we don’t really need push/pop there right? They are there in case later we decide to translate and/or rotate? If I’m right, let’s be sure our novices are trained on this fact in case you haven’t mentioned it. If I’m wrong, please redirect me and make sure I understand their purpose. Most novices need detailed training on these ideas, so let’s give them some good training.
In Jayden’s refactored and scalable sketches, the feature functions contain
no calls to translate(), rotate(), or scale().
Every function draws using the current origin (canvas center after the top-level
translate(width/2, height/2) in draw()) without moving it.
In this specific sketch, the push/pop pairs do nothing observable.
They are there because Phase 4 introduced them as prophylactic discipline: build the habit of wrapping feature functions in push/pop before you need transforms, so that when you add them the container is already in place. The push/pop in Jayden’s sketch are a promise, not a necessity — a promise that says: “if someone adds a transform to this function tomorrow, nothing outside it will break.”
But “they do nothing” is only true in this sketch right now. Understanding exactly what they would do if transforms were present — and what happens in sketches like Ethan’s where they are required — is one of the most important p5.js lessons a novice can learn.
Most novices assume push/pop only deal with movement. That is half the story.
In p5.js, a single push() saves a snapshot of two separate things:
- The transformation matrix — the accumulated effect of
every
translate(),rotate(), andscale()call made so far. This is the coordinate system: where (0, 0) is, which direction is “up,” and what one unit of distance means. - The drawing style state — every current setting for
fill(),stroke(),strokeWeight(),colorMode(),rectMode(),textSize(), and more. All of these persist between function calls in p5.js unless explicitly changed.
pop() restores both to exactly what they were at the matching
push(). They work as a stack (Last In, First Out): nested push/pop
pairs work correctly because each pop() undoes only its own matching push().
The most important thing to understand: p5.js transforms accumulate.
Every translate() call moves the origin by the given amount from wherever it
currently is. Every rotate() call turns the coordinate system by the given amount
from its current angle. They are additive, not absolute.
In Ethan’s sketch, drawEyes() calls push() before
translating to each eye, then pop() after drawing. Here is what happens with vs. without:
| Step | WITH push/pop | WITHOUT push/pop |
|---|---|---|
After translate(width/2, height/2) in draw() |
Origin: canvas center | Origin: canvas center |
push() before left eye |
State saved: origin = canvas center | (no save) |
translate(-60, -20) for left eye |
Origin: 60px left, 20px up from center | Origin: 60px left, 20px up from center |
rotate(spiralSpeed) |
Coord system rotated at left eye position | Coord system rotated at left eye position |
| Draw spiral eye | Draws correctly | Draws correctly |
pop() after left eye |
Origin restored: canvas center again | (no restore) |
translate(60, -10) for right eye |
Origin: 60px RIGHT, 10px up from center — ✓ | Origin: 60px right of where we already were (-60+60=0), and still rotated — ✗ |
drawMouth() after eyes |
Origin restored to center, mouth at correct position — ✓ | Origin is the accumulated result of all prior transforms; mouth appears in wrong position — ✗ |
The “without push/pop” column is not just wrong — it gets
progressively more wrong with each frame because frameCount grows.
By frame 60, the rotation is 120° larger than frame 0. The mouth appears to orbit the eyes.
The tongue flies off-screen. This is the most vivid p5.js bug a student can observe,
and it is entirely caused by forgetting one pop().
Now compare with Jayden’s refactored draw():
push(); drawFace(); pop(); push(); drawHeadphones(); pop(); push(); drawEyes(); pop(); push(); drawCheeks(); pop(); push(); drawSmile(); pop();
Today, none of these functions call translate() or
rotate(). The push/pop pairs save and restore the coordinate system —
which never changed. They are doing zero visible work.
But consider what happens if a student later adds a translate to
drawFace() to nudge the face slightly:
function drawFace() { translate(10, -5); // nudge face 10px right, 5px up ... }
With push/pop already in place in draw():
The face is nudged. When drawFace() returns and pop() runs,
the origin snaps back to canvas center. drawHeadphones() draws from
canvas center as if nothing happened. Correct output.
Without push/pop: The face is nudged. The origin is now
10px right and 5px up of canvas center. drawHeadphones() draws the headphone
band from that shifted origin. The headphones no longer align with the face.
Every subsequent feature is shifted by the same amount. Finding this bug requires
understanding the accumulation problem above — which novices have not yet seen.
The push/pop in Jayden’s sketch are a contract: each feature function is guaranteed not to affect the others, even if transforms are added later. A student who writes feature functions inside push/pop from the start never learns the hard way why they matter. That is the definition of good discipline.
Setup for every exercise: Go to
editor.p5js.org.
Clear the default code. Paste the full contents of jaydenRefactoredSketch.js
into the editor. Click ► Run. The static emoji appears.
Start each exercise from this clean state.
Architecture reminder: The push() and pop()
in this sketch are in draw(), around each function call —
push(); drawEyes(); pop(); — not inside the feature functions themselves.
When the exercises say “add a line inside drawEyes()” they mean
inside the curly braces of that function’s body. When they say “modify the line
in draw()” they mean the push(); drawXxx(); pop(); line
that calls it.
-
Exercise 1 — Push/Pop Saves the Rest of draw() from a Spinning Eye
-
In
setup(), delete the linenoLoop();. This lets draw() loop soframeCountgrows each frame. Click Run — the emoji should still appear (one frame drawn). Good. -
Find the
drawEyes()function. Add these two lines as the very first lines inside its curly braces, beforenoStroke();:translate(-45, -20); // shift the origin to the left-eye position rotate(frameCount * 2); // spin the coordinate system a little more each frame
Do not change anything indraw()yet. Thepush(); drawEyes(); pop();line in draw() stays intact. -
Click Run. The eyes spin and shift.
Look at the cheeks and smile — they are still perfectly centered.
The
push()beforedrawEyes()saved the canvas-center origin. Thepop()after restored it. The spinning transform insidedrawEyes()never escaped. -
Now break it. In
draw(), find the line:
push(); drawEyes(); pop();
Change it to just:
drawEyes();
(Removepush();from the front ANDpop();from the end. The transforms inside drawEyes() itself stay unchanged.) -
Click Run. The cheeks and smile now spin and drift. Without
pop()to restore the state, thetranslateandrotatefrom insidedrawEyes()accumulate into the coordinate system thatdrawCheeks()anddrawSmile()inherit. The further into the animation, the more wrong it gets. -
Restore: change
drawEyes();back topush(); drawEyes(); pop();. Click Run. Cheeks and smile snap back to center. One pop() restored order.
-
In
-
Exercise 2 — The Prophylactic Pattern: Push/Pop Contains a Future Translate
-
Start from the clean paste (with
noLoop()still present — the sketch is static). Click Run — centered emoji. Good. -
Find the
drawFace()function. Add this as the very first line inside its curly braces, beforestroke(...);:translate(30, 0); // nudge the face 30px to the rightDo not touchdraw()yet. -
Click Run. The face is 30px right of center.
The headphones, eyes, cheeks, and smile are all still at center.
The
push(); drawFace(); pop();indraw()saved the origin beforedrawFace()ran and restored it after. The translate was contained inside that one function call. -
Now break it. In
draw(), find the line:
push(); drawFace(); pop();
Change it to:
drawFace();
(Removepush();andpop();only from arounddrawFace(). Leave the others intact.) -
Click Run. Every feature is now 30px to the right —
headphones, eyes, cheeks, smile, all of it. The
translate(30, 0)insidedrawFace()ran with nopop()to undo it, sodrawHeadphones(),drawEyes(), and everything after drew from the shifted origin. Restorepush(); drawFace(); pop();and Run — only the face shifts again. That is what push/pop buys you: transforms inside a function stay inside that function.
-
Start from the clean paste (with
-
Exercise 3 — Global vs. Local: Where a Transform Lives Determines What It Moves
-
Start from the clean paste with
noLoop()present. Click Run. Good. -
In
draw(), add a new line aftertranslate(width / 2, height / 2);but before the firstpush();. The relevant section of draw() should now look like:translate(width / 2, height / 2); // existing line translate(0, 40); // ADD THIS LINE HERE push(); drawFace(); pop(); push(); drawHeadphones(); pop(); ...
-
Click Run. The entire emoji moves 40px down.
This second
translateruns before any push(), so nothing ever restores it. All five feature functions inherit the shifted origin. -
Remove the
translate(0, 40);fromdraw(). Instead, addtranslate(0, 40);as the first line insidedrawFace(), beforestroke(...);. Leavedraw()otherwise unchanged (all push/pop intact). -
Click Run. Only the face moves 40px down.
The headphones, eyes, cheeks, and smile stay at center.
The same
translate(0, 40)has a completely different effect depending on whether it runs outside push/pop (global: moves everything) or inside a function that is wrapped in push/pop (local: moves only that feature). -
The rule made visible: a transform in
draw()before the push/pop pairs is global to the frame — it shifts the canvas for everything. A transform inside a function that is wrapped in push/pop is local to that function — it shifts only what that function draws. That is the transform stack expressed as geometry.
-
Start from the clean paste with
If a function calls translate(), rotate(), or
scale(), it must open with push() and close with
pop(). A function that moves the coordinate system without restoring it is a bug
waiting to express itself — possibly 300 frames from now, in a place that looks
unrelated. Wrap every function that uses transforms. Do it even when you think you don’t
need to. The habit costs three characters on each side. The bug it prevents costs an afternoon.