🖌️ Stage 7: Source Code Showcase

CSS takes the wheel — the page gets a glow-up and the sky reacts to the explosion.

📂 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.
🖌️ The Big Idea: Separation of Concerns

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.
🟣 SWGradient — The Sky Reacts

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 base

The same exploded flag that hides the bomb and spawns particles now also transforms the entire sky. No new state variable is needed.
bombStg7.html — live — click the bomb to detonate — click again to reset!

🔁 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

🔴 Stages 1–6 — bare <body>
<body>
    <main id="mainElem">
        <!-- p5.js appends canvas here -->
    </main>
    <!-- scripts ... -->
</body>
p5 decides where the canvas goes. No header. No footer. No layout.
🟣 Stage 7 — layout shell
<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>
CSS flexbox centers the whole wrapper. Header and footer travel with the canvas.

Example 2 — Reset Hint: p5 Text vs HTML Element

🔴 Stage 6 — 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();
}
6 lines + a function call. Text is pixels in the canvas. Styled with p5 color constants.
🟣 Stage 7 — hintEl.style() in draw()
// drawResetHint() deleted entirely.
// One line in draw() replaces it:

hintEl.style('visibility',
    (exploded && particles.length === 0)
        ? 'visible'
        : 'hidden');
1 expression. Text is HTML, styled by CSS. CLR_HINT constant removed.

Example 3 — drawBackground(): Static Sky vs Reactive Sky

🔴 Stage 6 — always skyGradient
function drawBackground() {
    skyGradient.draw();
}
Same blue sky every frame, whether calm or exploded.
🟣 Stage 7 — two gradients, state-driven
function drawBackground() {
    if (exploded) {
        // dark red top → fireball orange
        explosionGradient.draw();
    } else {
        // deep blue zenith → pale horizon
        skyGradient.draw();
    }
}
Sky reacts to state. Same exploded flag, new visual consequence.

Example 4 — mousePressed(): Click Anywhere vs Click the Bomb

🔴 Stages 1–6 — any click detonates
function mousePressed() {
    if (bombVisible) {
        // detonates on ANY click
        bombVisible = false;
        exploded    = true;
        // ... spawn particles
    } else if (exploded &&
               particles.length === 0) {
        bombVisible = true;
        exploded    = false;
    }
}
Click anywhere on the canvas to detonate. Sky, clouds, corners — it all works.
🟣 Stage 7 — only detonates when ON the bomb
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;
    }
}
One && mouseInBomb() guard. Miss the bomb — nothing happens.

Example 5 — The <head>: No Favicon vs Favicon

🔴 Stages 1–6 — no favicon
<head>
    <meta charset="utf-8">
    <title>Crosby's Bomb…</title>
    <!-- no favicon -->
    <link rel="stylesheet" …>
</head>
Browser tab shows a generic page icon (or nothing).
🟣 Stage 7 — favicon declared
<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>
One <link> tag. The browser tab now shows the app's own icon.
🔖 Why the Favicon Matters

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.

💡 SWGradient Constructor and draw()

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

skyGradient (exploded = false)
// 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);
HSB 210° (cool blue) → HSB 197° (pale washed-out haze). Saturation drops from 58% to 18% — the horizon always feels brighter and more distant.
explosionGradient (exploded = true)
// 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);
HSB 10° at 15% brightness (nearly black) → HSB 28° (orange, matching particle hue range 0–30°). The sky base “glows” the same color family as the particles.
📌 SWColor HSB Values — All Four Colors
All values are on the p5.js scale: H 0–360, S 0–100, B 0–100, A 0–100.
ConstantHSB Visual description
SKY_TOP (zenith)2105876 Rich mid-blue, cooler than the horizon
SKY_HZN (horizon)1971897 Nearly white, very faint blue — atmospheric haze
EXP_TOP (zenith)107515 Scorched near-black deep red
EXP_BOT (base)2810096 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.

📍 Step 1 — Convert Pixel Coordinates to User Coordinates

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)
The y flip is important: canvas pixels increase downward, but user coordinates increase upward (math convention). Without it, a click at the top of the canvas would report a large positive y, but the bomb head sits at BOMB_CY_U = 100 (upper half of the canvas in user space).
💪 Step 2 — mouseInBomb(): Three-Shape Hit Test

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_U
The 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) <= halfH
The 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.
🧮 Step 3 — pointInTriangle() and cross2d()

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:

cross2d(edge_vec, point_vec) = edge_x×point_y − edge_y×point_x

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.
💻 Step 4 — mouseMoved(): Cursor Affordance

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 center in each system: 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

The Rule
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.
← the actual CSS renders this

2 — Flexbox Centering

The Rule
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

The Rules
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

The Rule
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

The Rules
#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

The Rules
#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

bombStg7.html HTML5
Loading bombStg7.html…
sketches/bombStg7Sketch.js JavaScript
Loading bombStg7Sketch.js…
styles/bombStg7Styles.css CSS
Loading bombStg7Styles.css…

💪 Try It Yourself

Use the Copy button above to grab any file, then try these experiments.

  1. Experiment 1 — change the explosion sky color: In bombStg7Sketch.js, find EXP_BOT_H = 28 (fireball orange) and change it to 130 (green). The sky base after the explosion will shift to a sickly toxic green. Try 280 for a purple “magic bomb” effect.
  2. Experiment 2 — make the hint text dramatic: In bombStg7Styles.css, find the #hintText rule and increase font-size to 1.2rem. Change color to #f9a825 (amber). The reset hint should now appear large and warm, matching the explosion palette.
  3. Experiment 3 — tint the box-shadow to match the explosion: In the canvas { box-shadow } rule, change the first shadow's color from rgba(30, 90, 180, 0.28) to rgba(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 how hintEl.style() works and apply the same pattern to the canvas element.)
  4. 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 calmGradient for that moment (State C: exploded === true && particles.length === 0) and update drawBackground() to show all three states.
  5. Challenge — animate the gradient transition: Instead of an instant switch between skyGradient and explosionGradient, interpolate between them over 0.5 seconds using a transitionT value (0 → 1) driven by millis(). You will need to create intermediate SWColor objects each frame by lerping the HSB values.
  6. Experiment 6 — shrink (or grow) the hit zone: In mouseInBomb() in bombStg7Sketch.js, find the circle test and change BOMB_R_U (90) to 30. Now only the center clock-face area triggers the explosion — click the edge of the bomb and nothing happens. Try BOMB_R_U * 1.5 to 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”.