📂 The Stage 7 Files
Stage 7 upgrades the page, not the canvas.
The bomb, clock, fuse, clouds, sky gradient, and explosion are
completely unchanged from Stage 6 — the drawing code is
identical, pixel for pixel. The upgrade lives in three places:
bombStg7Styles.css, the new HTML layout shell
in bombStg7.html, and a new hit-detection feature
in bombStg7Sketch.js that makes the bomb only
explode when you click on it.
The canvas now floats on a dark atmospheric background,
framed by a monospace title header, a drop shadow with a
blue-tinted glow, and rounded corners. The reset hint
— which used to be white text() drawn
inside the canvas — becomes a styled HTML element in
the footer. The sky gradient reacts to the explosion,
driven by SWGradient. And for the first time,
missing the bomb is possible — clicking the sky or clouds
does nothing.
| File | What it does | Stage 7 changes |
|---|---|---|
bombStg7.html |
HTML shell — loads p5.js + SW classes + sketch | Full layout shell: #pageWrapper,
#appHeader,
#canvasContainer,
#appFooter,
#hintText span;
favicon <link rel="icon"> added |
bombStg7Sketch.js |
The p5.js sketch — all drawing & logic | cnv.parent(),
hintEl = select(),
hintEl.style() replaces
drawResetHint(),
explosionGradient added,
mouseInBomb() hit detection,
mouseMoved() cursor feedback
|
bombStg7Styles.css |
CSS presentation layer | Everything — this file finally does real work for the first time in the saga. |
Stages 1–6 had an identical two-rule CSS file:
margin: 0; padding: 0; and
canvas { display: block; }.
The CSS file existed, but it did almost nothing.Stage 7 enforces a clean division:
• HTML owns structure — what elements exist and how they nest.
• CSS owns presentation — color, layout, shadows, typography.
• JavaScript owns behavior — what gets drawn and when.
The bridge between them is exactly one line of sketch code:
hintEl.style('visibility', 'visible' | 'hidden')The sketch no longer draws the reset hint as p5.js pixels — it simply tells CSS to show or hide an HTML element.
Stage 6 had one
SWGradient instance:
skyGradient. Stage 7 adds a second:
explosionGradient.drawBackground() checks the exploded
boolean every frame and switches between them:•
exploded === false →
skyGradient.draw()
— deep blue zenith fading to pale horizon haze•
exploded === true →
explosionGradient.draw()
— scorched near-black sky top, fireball orange baseThe same
exploded flag that hides the bomb and
spawns particles now also transforms the entire sky.
No new state variable is needed.
🔁 Stage 6 vs Stage 7: What Changed
Three things changed in the sketch; two more upgrades live in the HTML file. The comparisons below show each change side-by-side. The 🔴 Stage 6 code is always on the left; the 🟣 Stage 7 upgrade is on the right.
Example 1 — HTML Structure: Bare Body vs Layout Shell
<body>
<body> <main id="mainElem"> <!-- p5.js appends canvas here --> </main> <!-- scripts ... --> </body>
<div id="pageWrapper"> <header id="appHeader"> <span class="stage-label">Stage 7</span> <span class="app-title">Crosby's Bomb</span> </header> <main id="canvasContainer"> <!-- canvas parented here by sketch --> </main> <footer id="appFooter"> <span id="hintText">Click to reset!</span> </footer> </div>
Example 2 — Reset Hint: p5 Text vs HTML Element
drawResetHint()
function drawResetHint() { noStroke(); fill(CLR_HINT.col); textSize(HINT_TEXT_SIZE); textAlign(CENTER, CENTER); text(HINT_TEXT, CANVAS_W / 2, CANVAS_H / 2); } // called from draw(): if (exploded && particles.length === 0) { drawResetHint(); }
hintEl.style() in draw()
// drawResetHint() deleted entirely. // One line in draw() replaces it: hintEl.style('visibility', (exploded && particles.length === 0) ? 'visible' : 'hidden');
CLR_HINT constant removed.
Example 3 — drawBackground(): Static Sky vs Reactive Sky
function drawBackground() { skyGradient.draw(); }
function drawBackground() { if (exploded) { // dark red top → fireball orange explosionGradient.draw(); } else { // deep blue zenith → pale horizon skyGradient.draw(); } }
exploded flag, new visual consequence.
Example 4 — mousePressed(): Click Anywhere vs Click the Bomb
function mousePressed() { if (bombVisible) { // detonates on ANY click bombVisible = false; exploded = true; // ... spawn particles } else if (exploded && particles.length === 0) { bombVisible = true; exploded = false; } }
function mousePressed() { if (bombVisible && mouseInBomb()) { // only fires when click hits the bomb bombVisible = false; exploded = true; // ... spawn particles } else if (exploded && particles.length === 0) { bombVisible = true; exploded = false; } }
&& mouseInBomb() guard. Miss the bomb — nothing happens.
Example 5 — The <head>: No Favicon vs Favicon
<head> <meta charset="utf-8"> <title>Crosby's Bomb…</title> <!-- no favicon --> <link rel="stylesheet" …> </head>
<head> <meta charset="utf-8"> <title>Crosby's Bomb — Stage 7</title> <!-- Favicon --> <link rel="icon" type="image/png" href="images/favicon-32x32.png" sizes="32x32"> <link rel="stylesheet" …> </head>
<link> tag. The browser tab now shows the app's own icon.
A favicon (short for favorites icon) is the tiny image the browser shows in the tab, the bookmarks bar, and the history list. One
<link> tag in the <head> is all it takes:<link rel="icon" type="image/png" href="images/favicon-32x32.png" sizes="32x32">Why it matters for a student app:
• Identity. When you have five tabs open, a recognizable icon tells you instantly which tab is your app. Without one the browser shows its own generic page icon — your app looks unfinished.
• Polish. A favicon is one of the first things visitors notice (even subconsciously). It signals that someone thought about the whole experience, not just the code inside the canvas.
• Bookmarking. If someone bookmarks your app, the bookmark entry shows the favicon. A custom icon makes it findable at a glance;
a missing one blends in with every other bookmark.
The four attributes explained:
•
rel="icon" — tells the browser this link
is a favicon (not a stylesheet or a preload).•
type="image/png" — the MIME type. PNG is the
most universally supported format; ICO also works but PNG is simpler.•
href="images/favicon-32x32.png" — path to the
image file, relative to this HTML file.•
sizes="32x32" — tells the browser the
image is 32×32 pixels. Browsers prefer 32px for tabs; 16px is the
minimum. You can list multiple <link rel="icon"> tags
with different sizes for different contexts.
🟣 SWGradient — How the Sky Is Drawn
Stage 6 introduced SWGradient to replace the flat
background(CLR_SKY.col) fill with a vertical color
transition. Stage 7 uses it twice — one instance for each
sky state.
new SWGradient(topSWColor, bottomSWColor, angleDeg = 0)topSWColor |
An SWColor object. Drawn at the top of the canvas
when angleDeg = 0. |
bottomSWColor |
An SWColor object. Drawn at the bottom of the canvas. |
angleDeg |
Gradient direction. 0 = vertical (top→bottom).
90 = horizontal (left→right). Positive = CCW. |
skyGradient.draw() — called with no arguments — fills
the entire canvas (0, 0, width, height) with horizontal color strips, blending
from topSWColor at y = 0 to
bottomSWColor at y = height.
The number of strips equals the canvas diagonal, so coverage is complete
at any angle.
The Two Gradient Instances in Stage 7
// Top: deep blue zenith (cooler sky) const clrSkyTop = new SWColor(210, 58, 76, 100); // Bottom: pale horizon haze const clrSkyHzn = new SWColor(197, 18, 97, 100); skyGradient = new SWGradient(clrSkyTop, clrSkyHzn);
// Top: scorched near-black sky const clrExpTop = new SWColor(10, 75, 15, 100); // Bottom: fireball orange-yellow const clrExpBot = new SWColor(28, 100, 96, 100); explosionGradient = new SWGradient(clrExpTop, clrExpBot);
All values are on the p5.js scale: H 0–360, S 0–100, B 0–100, A 0–100.
| Constant | H | S | B | Visual description |
|---|---|---|---|---|
| SKY_TOP (zenith) | 210 | 58 | 76 | Rich mid-blue, cooler than the horizon |
| SKY_HZN (horizon) | 197 | 18 | 97 | Nearly white, very faint blue — atmospheric haze |
| EXP_TOP (zenith) | 10 | 75 | 15 | Scorched near-black deep red |
| EXP_BOT (base) | 28 | 100 | 96 | Full-saturation bright orange — fireball base |
🎯 Hit Detection — Clicking ON the Bomb
The most satisfying glow-up is behavioral, not visual: the bomb now only detonates when you click directly on it. Clicking the sky, the clouds, or the background does nothing. Three functions work together to make this happen.
p5.js gives us
mouseX and mouseY in canvas pixels
(0–400). The bomb is defined in SWGrid user coordinates
(x: −200–+200, y: −200–+200, origin at canvas center, y points UP).The conversion formula, derived from the grid constants:
user_x = mouseX − 200 |
pixel 0 → user −200 pixel 400 → user +200 |
user_y = 200 − mouseY |
pixel 0 → user +200 pixel 400 → user −200 (y flips) |
BOMB_CY_U = 100
(upper half of the canvas in user space).
The bomb is not a single shape — it is a circle, a rectangle, and a triangle layered together.
mouseInBomb() tests all three and returns
true if the converted mouse position is inside any of them:| Test 1: circle |
dist(ux, uy, BOMB_CX_U, BOMB_CY_U) <= BOMB_R_UThe round bomb head at (0, 100) with radius 90.
dist() is the Pythagorean distance formula built into p5.
|
| Test 2: rectangle |
abs(ux - cx) <= halfW && abs(uy - cy) <= halfHThe body panel at center (0, 10), 180×180 user units.
AABB (axis-aligned bounding box) test — two comparisons.
|
| Test 3: triangle |
pointInTriangle(ux, uy, apex, right, left)The lower body wedge (apex at top, base at y = −130).
Uses the cross-product sign test — see below.
|
The circle and rectangle tests are simple comparisons. The triangle test requires a little more geometry.
The cross-product sign test works like this:
Walk the three edges of the triangle in order (A→B, B→C, C→A). For each edge, compute whether the point P is to the left or right of the edge using the 2D cross product:
If all three cross products are ≥ 0 (all “left”) or all ≤ 0 (all “right”), the point is inside the triangle. If the signs are mixed — some positive, some negative — the point is outside.
This works for any triangle, convex or not, in any orientation. No square-root, no division — just multiplications and additions.
Knowing whether the mouse is over the bomb lets us do more than just filter clicks — we can also give the user a visual hint before they click.
mouseMoved() runs every frame the mouse moves.
It calls the same mouseInBomb() check and switches
between two p5.js cursors:•
cursor(HAND) — the classic pointer hand.
Appears when hovering over the bomb (when the bomb is visible).
Communicates “this specific thing is clickable.”•
cursor(CROSS) — the precision crosshair.
Appears everywhere else, matching the CSS cursor: crosshair
rule in bombStg7Styles.css.p5.js implements
cursor() by setting the canvas element's
inline style.cursor property, which takes precedence
over the CSS class rule. The CSS rule acts as the default; p5 overrides
it live as the mouse moves.
The Coordinate System at a Glance
Canvas pixels (mouseX, mouseY):
(0,0) ┌────────────────────────┐
│ x increases → │
│ │
│ y increases ↓ │
└────────────────────────┘ (400,400)
SWGrid user coords (ux, uy):
(-200,+200) ┌─────────┐ (+200,+200)
│ (0,0) │ ↑ y up
│ center │
└─────────┘
(-200,-200) (+200,-200)
BOMB_CX_U = 0, BOMB_CY_U = 100 in user coords
→ pixel (200, 100) — horizontally centered,
upper quarter of the canvas.
Try console.log(mouseX, mouseY) in
mousePressed() and click the bomb to verify!
🎨 bombStg7Styles.css — The Design Choices
Every CSS rule in bombStg7Styles.css was added for a specific
reason. The sections below explain the professional and aesthetic
thinking behind each decision.
1 — Dark Atmospheric Body Background
body { background: radial-gradient(ellipse at 50% 40%, #1a2d45 0%, #0a1220 60%, #040810 100%); }The Reason
A plain white page background treats the canvas as a document element. A dark atmospheric background transforms it into a window — the canvas becomes a contained, self-illuminating world rather than an object sitting on paper.
The radial gradient (ellipse centered slightly above center, at
50% 40%) is deliberate: it puts the lightest dark at
the center of visual attention, where the canvas sits, and fades to
near-black at the corners. This creates depth without any explicit
3D technique — the page feels like it recedes away from you.
The deep navy (#1a2d45) at the center is blue-tinted, echoing the
sky gradient inside the canvas and making the two feel like a
continuous environment rather than separate elements.
2 — Flexbox Centering
body { min-height: 100vh; display: flex; justify-content: center; align-items: center; }The Reason
Before Stage 7, the canvas appeared at the top-left of the viewport — wherever the browser's document flow placed it. This is functional but aesthetically weak. A centered composition is one of the most fundamental rules of visual design: it signals intent, creates balance, and gives the content room to breathe on all sides.
min-height: 100vh ensures the body fills the full viewport
height even when the canvas is short, so the centering works correctly
on large screens. Flexbox is preferred over manual margin tricks because
it handles both axes simultaneously with two properties, works at any
viewport size without JavaScript, and adapts automatically when the
content height changes.
3 — Canvas Drop Shadow & Rounded Corners
canvas { border-radius: 8px; box-shadow: 0 0 48px rgba(30, 90, 180, 0.28), 0 12px 40px rgba(0,0,0,0.80); }The Reasoning
border-radius: 8px — A perfectly square canvas on a dark background looks like a hard cut. 8px of rounding (modest — not pill-shaped) signals that the canvas is a polished product panel rather than a raw HTML element. It is the same visual vocabulary that modern apps use for cards, dialogs, and image thumbnails. Crucially, it is not so aggressive that it clips content at the corners.
box-shadow — two layers:
• The outer layer (
0 0 48px rgba(30, 90, 180, 0.28)) is a
wide, diffuse blue-tinted glow. The RGB values (30, 90, 180) are a desaturated
version of the sky gradient's dominant hue. This "bleeds" the internal color
story outward onto the dark page, making the canvas feel like it is emitting
light. This is called environmental lighting in design —
the surroundings take on the color temperature of the subject.• The inner layer (
0 12px 40px rgba(0,0,0,0.80)) is a
conventional deep shadow below the canvas. It grounds the element, preventing
it from feeling like it is floating untethered.
4 — Crosshair Cursor
canvas { cursor: crosshair; }The Reason
Affordances matter: users should be able to tell that something is clickable before they click it.
cursor: pointer (the hand)
communicates “this is a link or button.” That is technically
accurate, but it does not match the genre of the interaction — this
is a target, not a menu item.cursor: crosshair implies precision targeting, which
matches the detonation action perfectly. It is the cursor used in drawing
tools, image editors, and shooter games for exactly this reason.
It distinguishes the canvas interaction from all other page interactions
at a glance.
5 — Monospace Title Header
#appHeader { font-family: 'Courier New', Courier, monospace; text-transform: uppercase; letter-spacing: 0.12em; }.stage-label { font-size: 0.6rem; color: #4a6880; }
.app-title { font-size: 0.85rem; color: #90b8d8; }The Reasoning
Monospace + uppercase + wide letter-spacing is a deliberate aesthetic choice that communicates retro-technical — the visual language of control panels, terminals, and old electronics. This is appropriate for a bomb-detonation app.
The two-tier hierarchy (small muted Stage 7 label + larger blue Crosby's Bomb title) borrows from UI design patterns where context (e.g., “Settings › Account”) appears smaller than the primary content title. The stage label is visually subordinate but still readable; it does not compete with the app title for attention. Color muted blue-grey (#4a6880 and #90b8d8) ties the header to the canvas sky palette.
6 — Fixed-Height Footer & visibility: hidden
#appFooter { height: 1.5rem; display: flex; align-items: center; }#hintText { visibility: hidden; }The Reason
height: 1.5rem on the footer — Without this, the canvas would visually jump downward when the hint text appears and upward when it disappears. Layout shifts are one of the most jarring UX problems in web design. Reserving the space permanently (even when the text is hidden) eliminates the shift entirely. This is the same technique used by form validation messages and tooltip containers in production UI libraries.
visibility: hidden vs display: none —
display: none
removes the element from the layout flow completely, which would
cause the layout shift described above.
visibility: hidden makes the element invisible but
preserves its space in the layout. This is exactly the distinction
that matters here. The JavaScript toggles between them with
hintEl.style('visibility', ...).
Summary: Every Rule and Its Purpose
| Selector / Property | Design Category | Professional Purpose |
|---|---|---|
| *, *::before, *::after { box-sizing: border-box } | Reset | Predictable box model — padding and border included in declared width/height |
| html, body { margin: 0; padding: 0 } | Reset | Remove browser default whitespace for a clean starting point |
| body { background: radial-gradient(…) } | Atmosphere | Dark depth gradient — frames the canvas as a self-lit world, not a document object |
| body { min-height: 100vh; display: flex; justify/align: center } | Layout | Full-viewport flex centering — canvas floats at the visual center of the screen |
| #pageWrapper { display: flex; flex-direction: column; align-items: center; gap: 10px } | Layout | Groups header + canvas + footer as a single vertical unit; gap replaces margin hacks |
| #appHeader { font-family: monospace; text-transform: uppercase; letter-spacing: 0.12em } | Typography | Retro-tech aesthetic — monospace + uppercase signals a control panel / terminal genre |
| .stage-label / .app-title { different sizes + colors } | Typography | Visual hierarchy — context label is subordinate to the primary title |
| #canvasContainer { line-height: 0 } | Fix | Removes the invisible baseline gap browsers add below inline elements |
| canvas { display: block } | Fix | Prevents the same baseline gap on the canvas element itself |
| canvas { border-radius: 8px } | Polish | Softens the square; signals a finished product panel over a raw element |
| canvas { box-shadow: blue-glow + dark-shadow } | Depth + atmosphere | Environmental lighting (blue echoes the sky); deep shadow grounds the canvas |
| canvas { cursor: crosshair } | Affordance | Communicates "precision target" rather than generic link/button click |
| #appFooter { height: 1.5rem } | Layout stability | Reserves space for hint text — prevents layout shift when text appears/disappears |
| #hintText { visibility: hidden } | UX | Hidden but in flow — element takes up space while invisible, no layout jump |
📜 Source Code
Loading bombStg7.html…
Loading bombStg7Sketch.js…
Loading bombStg7Styles.css…
💪 Try It Yourself
Use the Copy button above to grab any file, then try these experiments.
-
Experiment 1 — change the explosion sky color:
In
bombStg7Sketch.js, findEXP_BOT_H = 28(fireball orange) and change it to130(green). The sky base after the explosion will shift to a sickly toxic green. Try280for a purple “magic bomb” effect. -
Experiment 2 — make the hint text dramatic:
In
bombStg7Styles.css, find the#hintTextrule and increasefont-sizeto1.2rem. Changecolorto#f9a825(amber). The reset hint should now appear large and warm, matching the explosion palette. -
Experiment 3 — tint the box-shadow to match the explosion:
In the
canvas { box-shadow }rule, change the first shadow's color fromrgba(30, 90, 180, 0.28)torgba(200, 80, 10, 0.35). The canvas glow will now be warm orange. Can you make the box-shadow switch with the sky gradient using JavaScript? (Hint: look at howhintEl.style()works and apply the same pattern to the canvas element.) -
Experiment 4 — add a third gradient state:
What if the sky turns a cool pale green
after the explosion fades but before reset?
Create a
calmGradientfor that moment (State C:exploded === true && particles.length === 0) and updatedrawBackground()to show all three states. -
Challenge — animate the gradient transition:
Instead of an instant switch between
skyGradientandexplosionGradient, interpolate between them over 0.5 seconds using atransitionTvalue (0 → 1) driven bymillis(). You will need to create intermediateSWColorobjects each frame by lerping the HSB values. -
Experiment 6 — shrink (or grow) the hit zone:
In
mouseInBomb()inbombStg7Sketch.js, find the circle test and changeBOMB_R_U(90) to30. Now only the center clock-face area triggers the explosion — click the edge of the bomb and nothing happens. TryBOMB_R_U * 1.5to make the bomb “magnetic” so it detonates even when you click slightly past its edge. What radius feels most satisfying?
Bonus: skip the circle test entirely and remove it — the rectangle + triangle still cover the lower body. How much does the dead zone at the top matter?
⚠️ The Copy button requires https:// or localhost.
On a bare file:// URL, right-click → “View Page Source”.