Back to Loading Icons S.P.A.R.K. Chat Log  •  10/02/2026
S.P.A.R.K. with AI — Development Dialog

Refactoring David’s Loading Icons

From working novice code to teachable code —
the math, the decisions, and the prompt critique.

For Students — How to Read This Log

This log documents the session that refactored David’s 2023 p5.js animation into the 2026 TNT standard. The focus is on why decisions were made, not just what was changed.

Watch for Prompt Critique boxes (amber) — they evaluate the quality of the prompt that requested the work. And Design Decision boxes (purple) — they explain each significant code choice. Both are part of the S.P.A.R.K. habit: Set goal, Prompt, Analyze, Refine, Know.

🎯
S — Set Goal Before writing a line, the AI read David’s original sketch in full, identified every problem worth fixing, and established what “good” looks like for a teaching-grade refactor.
GitHub Copilot

After reading David’s original sketch, here is what I found. The core algorithm is solid — the sine/cosine wave relationship is correct and produces a genuinely attractive animation. But six specific issues make it hard to learn from:

  1. console.log("...draw...") inside draw(). This runs 60 times per second — 3,600 log entries per minute. It floods the console, makes debugging impossible, and can noticeably slow the animation in some browsers. Every novice needs to learn this rule: never leave console.log() in draw().
  2. Magic numbers throughout. 1100, 300, 50, 213, 76 — none explained. A reader cannot change anything without first reverse-engineering what every number means. Named constants fix this completely.
  3. No functions beyond setup() and draw(). All drawing logic lives inside draw() directly. The more logic draw() does personally, the harder it is to read, test, or modify any one piece.
  4. Clearing the background with a filled rectangle instead of background(). The original uses fill(182, 179, 182) then rect(0, 0, width, height) in center-translated coordinates. This works, but background(bgColor) is clearer, faster, and does not require rectMode(CENTER) to be set.
  5. para is not a valid HTML element. The sliders use <para id="Frames">FrameRate:</para> as labels. The W3C validator would reject these. The correct element is <label> or <p>.
  6. SRI integrity hashes on Bootstrap JS. The original uses an integrity attribute that does not match the jsDelivr-served file (same class of problem as DWR Entry #19). Removed in the refactor.

The algorithm itself — the sine/cosine position-opacity relationship — is David’s genuine contribution and it is preserved exactly. Everything else is presentation and structure.

The Prompt — What Worked and What Could Be Sharper
💬
P — Prompt The prompt that requested this refactor was comprehensive but not perfect. Here is the full critique.
klp

(Paraphrased) Upgrade David’s HBO loading simulation to the modern TNT framework. Use mamboDanceLessons as the layout reference. Add color pickers, a circle radius slider, and a hero background gif. Rewrite and comment the code for a novice learning audience. Provide a chatlog. Critique this prompt as part of the chatlog. File names: davidsUpdatedLoadingIcons.html, davidsUpdatedLoadingIconsSketch.js, davidsUpdatedLoadingIconsStyles.css, optional davidsUpdatedLoadingIconsScript.js, davidsUpdatedLoadingIconsChatlog.html. You are a professional coder, CS teacher, and mentor. App should provide an exceptional learning experience.

Prompt Critique

What this prompt does well:

  • Names the layout reference specifically. “mamboDanceLessons has the look I’m after” is far more useful than a multi-paragraph description. It points to an existing artifact. The AI can read that file and extract the exact pattern — two-column layout, card components, spark bar, control heading style — without any guessing.
  • States the preservation constraint explicitly. “I don’t want to do anything to David’s app” is a clear boundary. Without that sentence, the AI might have edited the original. With it, the original is guaranteed to remain untouched.
  • Specifies every new feature concretely. Color picker (background), color picker (circles), circle radius slider — three specific additions, each stated once, none ambiguous.
  • Lists all file names upfront. This eliminates one of the most common sources of follow-up prompts: “What should I call this file?” File names stated before work begins mean the AI can write every cross-reference (script tags, import paths, chatlog links) correctly the first time.
  • Provides the persona context. “Professional coder, CS teacher, mentor” shapes the tone of the code comments and chatlog. Without it, comments might be terse or overly technical. With it, the AI writes for the audience it is serving.
  • Asks for a prompt critique inside the output. This is unusual and excellent. It closes the feedback loop: the AI cannot produce a chatlog that silently glosses over a weak prompt. The request forces honest evaluation.

Where this prompt could be sharper:

  • “Exceptional learning experience” is under-specified. This is the most important instruction in the entire prompt, and it is the vaguest. What is the target audience? A 10-year-old novice? A motivated 16-year-old? A college student? “Exceptional” means different things at different levels. A better version: “Comments should be written for a motivated high-school novice who knows what a function is but has never seen sine or cosine before.” That single sentence would have calibrated every comment in the sketch file.
  • The hero gif filename was unknown at prompt time. The prompt says “I have a background gif you can use” — but does not name it. The AI cannot reference a file it has not seen. This required a file-listing step before writing any code. Better: name the file in the prompt, or add it to the attachment list.
  • The reference to the chatlog format is indirect. “I’ve included a reference chatlog for the mamboDanceLessons.” This is good — providing a reference is correct. But the instruction could explicitly say: “Follow the same S.P.A.R.K. banner structure, critique box, and decision box pattern.” Without that, the AI must infer the pattern from the example, which works but is less reliable than stating it.
  • No mention of p5.js version preference. The refactored app uses p5.js 1.9.4 (the latest stable as of this writing). Had the user needed 1.9.0 to match other apps, this would have required a fix. State the version when it matters for consistency across a multi-app site.

The overall grade: B+. This prompt gets the hard things right: it names a concrete reference, states a clear constraint, specifies all file names, and asks for self-critique. The one genuine miss — “exceptional learning experience” as the central goal — is also the hardest thing to specify in a prompt. Future iterations of this prompt family should always answer: “Learning experience for whom? At what level? What concept must they be able to explain after reading the code?”

The Math — Where Sine and Cosine Come From
🔍
A — Analyze David’s algorithm uses two trigonometric functions. Understanding why they produce the wave effect is the core lesson of this sketch.
GitHub Copilot

David’s animation uses two related functions: sin() for position and cos() for opacity. Here is where they come from and why they work together.

The unit circle mental model. Imagine a clock hand of length 1 rotating counterclockwise. When the hand points right (3 o’clock = 0 radians), its horizontal distance from center is 1 and its vertical distance is 0. That horizontal distance is cos(angle). Its vertical distance is sin(angle). As the hand rotates, both values oscillate smoothly between -1 and +1 — but cos is always 90° ahead of sin.

Position formula: xPos = sin(phase) × spread

  • sin(phase) oscillates smoothly -1 → 0 → +1 → 0 → -1
  • Multiplying by spread (e.g., 200px) scales that to ±200 pixels
  • Result: the circle oscillates smoothly left ↔ right

Opacity formula: alpha = 0.5 × (1 + cos(phase)) × maxOpacity

  • cos(phase) oscillates -1 → 0 → +1 → 0 → -1
  • Adding 1 shifts it to 0 → 1 → 2 → 1 → 0
  • Multiplying by 0.5 scales it to 0 → 0.5 → 1 → 0.5 → 0
  • Multiplying by maxOpacity gives the final 0 → maxOpacity range

Why sin for position and cos for opacity? The key is the 90° phase shift between them. At the moment the circle reaches the center (sin = 0), cos = ±1 — so the circle is at peak or minimum opacity while crossing center. At the rightmost point (sin = +1), cos = 0 — half opacity. This creates the visual effect where circles are brightest while moving and fade when stationary at the extremes. It is not arbitrary — it is the mathematical relationship that makes the animation feel natural.

Multiple circles: N circles are given phase offsets of 0, 2π/N, 2×(2π/N), etc. This spaces them evenly around the full oscillation cycle so they always form a wave rather than bunching together.

Design Decision — Document the Math in the Code, Not Just the Chatlog

The derivation above appears in the sketch file itself, in the file-header comment block. This is a deliberate choice: the person most likely to need the explanation is someone reading the code. If the math lives only in the chatlog, it is one click away from the code. That one click is enough friction to skip it. Putting it in the code header means the explanation is always visible the moment the developer opens the file.

The standard TNT rule for teaching code: any mathematical formula that appears in the code must have its derivation documented in a comment at the point where the formula is used. Not just what the formula does, but why the formula is the right one. _calcAlpha() carries the full derivation; _calcXPosition() carries the reasoning. A student reading either function knows not just what it returns but why the math produces the desired visual effect.

Key Refactoring Decisions
🔁
R — Refine Four decisions shaped the refactored sketch. Each one is traceable to a specific educational goal.
Decision 1 — Named Constants for Every Default Value

David’s original uses raw numbers everywhere: createCanvas(1100, 300), fill(213, 76, 213, ...), let circleRadius = 50. These are called “magic numbers” — values that appear in code without explanation. They create two problems:

  • A reader cannot know what 1100 means without reading all the surrounding code to figure out it is a canvas width.
  • The same value often appears in multiple places. Changing it requires finding every occurrence and updating each one — and missing one creates a bug.

The refactored version declares all defaults at the top of the file as var DEFAULT_* constants. 1100 becomes a dynamic container.clientWidth; 50 becomes DEFAULT_CIRCLE_SIZE; 213, 76, 213 becomes DEFAULT_CIRCLE_HEX = '#d54cd5'. A student can change any default in one place and watch the entire sketch respond. This is the core of why named constants matter: they turn a sketch into a laboratory.

Decision 2 — One Function, One Responsibility

David’s draw() does everything: translate, fill, loop, calculate position, calculate opacity, draw. If a student wants to understand only the opacity calculation, they have to mentally filter out all the surrounding code. The refactored version isolates every responsibility:

FunctionExactly one job
draw()Clear, draw circles, advance time — nothing else
_drawAllCircles()Loop over circles and call sub-functions
_calcXPosition()Return the horizontal offset for one circle at a given phase
_calcAlpha()Return the opacity for one circle at a given phase
_advancePhase()Increment the global phase by one frame’s worth
_recalcAngleStep()Recompute circle spacing after numCircles changes
_resetToDefaults()Restore all live variables to their DEFAULT_ values

A student studying the opacity math reads _calcAlpha() — 6 lines with a full derivation comment. They do not have to read 40 lines of surrounding loop code. This is the same principle as the Single Responsibility Principle in professional software, applied at the scale of a teaching sketch.

Decision 3 — Color Pickers via HTML input[type="color"] + p5’s color(hex)

The three new controls — background color, circle color, circle size — required choosing an implementation pattern. For color, the HTML input[type="color"] element is the correct tool:

  • It returns a #rrggbb hex string (e.g. '#d54cd5')
  • p5.js’s color() function parses hex strings directly: bgColor = color(document.getElementById('bgColorPicker').value)
  • The control is rendered by the browser as a native color-picker — no library, no extra code, works on every modern device

The only subtlety: color() is a p5.js function and only works after setup() runs. The control handler functions are safe because they are only reachable via user interaction, which happens after the sketch is initialized. But the global variables bgColor and circleColor are declared without initial values and set inside _resetToDefaults() (which is called from setup()). This is the correct pattern: declare globally, initialize in setup.

To extract R, G, B from a p5 color object when building the fill (to attach a computed alpha), the refactored code uses: fill(red(circleColor), green(circleColor), blue(circleColor), alpha). This is preferable to hardcoding channel values, because it works regardless of which color the picker currently holds.

Decision 4 — background() Instead of a Filled Rectangle

David’s original clears the canvas with:

rectMode(CENTER);
translate(width/2, height/2);
fill(182, 179, 182);
rect(0, 0, width, height);
This works, but it requires rectMode(CENTER) to be set, it consumes 4 lines and two function calls, and it mixes the clearing operation with the coordinate-system setup. The refactored version calls background(bgColor) as the very first line of draw() — before any translate() — then moves translate() into _drawAllCircles() wrapped in a push()/pop() pair. Each concern is isolated in the place where it belongs.

💡
K — Know Four rules from this session that apply to every p5.js sketch you write from here forward.
Session Takeaways
  1. Never leave console.log() inside draw(). draw() runs at frameRate frames per second. At 60 fps, one log statement produces 3,600 entries per minute. It floods the console, hides real debug messages, and can slow the animation. Add logs for debugging; remove them before anyone else sees the code. Treat a log inside draw() like a test that is still failing.
  2. Named constants make code a laboratory. Every magic number in a sketch is a closed door. Naming it opens it. When 50 becomes DEFAULT_CIRCLE_SIZE, a student can change it, watch the effect, and understand the relationship. Unnamed constants can only be observed; named constants can be experimented with.
  3. Sine gives position; cosine gives opacity — and the 90° shift is the whole trick. The reason David’s animation looks natural is that position and opacity are related but not identical. If opacity tracked position directly, circles at the extreme right would be fully bright and at the extreme left fully dark — a harsh, abrupt fade. The 90° shift of cosine creates a softer, more organic relationship: brightness peaks at the center of travel, not at the endpoints. This is not a coincidence. It is the mathematical signature of circular motion projected onto a line.
  4. The preservation constraint is as important as the construction goal. David’s original sketch was never touched. The new refactored version exists alongside it, not in place of it. Students can open both side by side and trace every change. That comparison is itself a lesson: not just “here is how to write it better,” but “here is exactly what changed and why.” Preserving the original makes the improvement visible. Discarding it makes the improvement invisible.
Post-Build Field Test — Spark Bar Layout Broken
🔁
R — Refine After the build, a visual comparison against mamboDanceLessons revealed that our spark bar had a white/default background and stacked its description below the links instead of floating it to the right. Two independent bugs, same fix.
klp

If you look at the spark bar of the mamboDanceLessons, the two links are to the left with appropriate space between them, and the description is to the right in a more muted display. In ours, the background is whitish and does not blend with our other layout features, and the description is below the links. Can you fix this?

Root Cause Analysis — Two Separate Bugs

The visual comparison identified two distinct problems that compounded each other:

Bug 1 — Missing CSS. Every TNT app defines its own .spark-bar styles — the class is not in tnt-base-styles.css. The original build added the class to the HTML but never added the matching CSS block to davidsUpdatedLoadingIconsStyles.css. Without a definition, the browser renders .spark-bar as a plain block element with the page’s default white/light background. This explains the “whitish background” symptom.

Bug 2 — Inner div wrapper. The original build wrapped both <a> links in a nested <div style="display:flex..."> inside the spark bar. This made the spark bar’s flex container see two children: the wrapper div and the span. Without margin-left: auto on the span (which lives in the CSS that was missing), the span fell below the div instead of pushing right. Even with the CSS added, a nested flex div is not the correct pattern — the links should be direct children of .spark-bar, exactly as mamboDanceLessons does it.

The Fix — CSS Added, Inner Div Removed

Two changes, each addressing one bug:

CSS added to davidsUpdatedLoadingIconsStyles.css:

.spark-bar {
    background: #0c0012;           /* near-black with purple tint */
    padding: 0.5rem 1rem;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 1.4rem;
    border-bottom: 1px solid var(--loading-border);
}
.spark-bar a     { color: var(--loading-accent); font-weight: 700; font-size: 0.88rem; }
.spark-bar a:hover { text-decoration: underline; color: #e880e8; }
.spark-bar span  { color: rgba(255,255,255,0.40); font-size: 0.80rem; margin-left: auto; }

HTML structure corrected — inner <div> removed; links are now direct children of .spark-bar:

<div class="spark-bar">
    <a href="...chatlog..."><i ...></i>Chat Log</a>
    <a href="...original..."><i ...></i>Original App (2023)</a>
    <span>Loading Icons Simulation • 10/02/2026</span>
</div>

With margin-left: auto on the span, the flex container naturally pushes the span to the far right of the bar. The two links sit left-aligned with a gap between them. The dark background and muted span color now match the rest of the page’s dark theme.

The Lesson — Always Compare Against the Reference You Cited

The prompt named mamboDanceLessons as the layout reference. The spark bar is one of that page’s most visible elements. Verifying that every named component matches the reference before delivering the build is part of the craft. In this case, a side-by-side visual comparison caught both bugs immediately — something no code review would have surfaced, because both files are syntactically valid HTML with no errors. The only test that works here is looking at the rendered result and comparing it to the reference. Always do that before declaring done.

Explore Page — An Implicit Request Recognized
🔁
R — Refine The prompt requesting Explore page entries named two categories. The AI added a third without being asked. The follow-up conversation documents why — and what that reveals about context-aware reasoning.
klp

We need an entry on our explore page about this new app. I see it as a SPARK app and a simulation, since it models a loading image we saw at HBO, do you agree? [Also requested a news entry.] Provide a link to David’s original design in the Legacy area so novices can see the contrast between the two.

Prompt Critique — A Correct Framing That Left One Category Unstated

The prompt is precise about what it does say. “I see it as a SPARK app and a simulation” is a framing statement — it tells the AI what the user emphasizes, not necessarily an exhaustive list of every valid classification. But there is a gap: the app is built entirely in p5.js, appears on processing_apps.html with its own grid card, and every p5.js app at TNT is cross-listed in the Processing offcanvas. The prompt did not mention Processing at all.

This is a common prompt pattern: the user names the classifications they are actively thinking about, but relies on the AI to bring contextual knowledge about the system’s conventions. When the omission is structural (the app is already on the Processing page), acting on it is correct. When the omission is ambiguous, asking a clarifying question is correct. The distinction matters.

klp

In the last prompt where I directed you to place David’s app on the explore page, I failed to include it as a Processing app (I mentioned only SPARK and Simulation). You ‘saw’ the omission and included it appropriately. That is noteworthy. Let’s include that last prompt, this one, and tell me how you ‘knew’ we needed that other classification — in our chatlog. I’m impressed, well done!

How the Third Category Was Recognized — Context Over Literal Instruction

The reasoning was not clever. It was structural. Three pieces of evidence in the workspace made Processing the obvious addition:

  1. The app already had a card on processing_apps.html — added in the prior conversation turn. If an app is on the Processing page, it belongs in the Processing offcanvas. These are meant to be synchronized. A card without a corresponding offcanvas entry is an inconsistency.
  2. Every p5.js app in the TNT ecosystem is listed in the Processing offcanvas. Scanning the existing entries (Lemur Game, Sergio’s Slime, Henry’s Ghost, Movie Credits Simulator, etc.) reveals the pattern: p5.js = Processing offcanvas. The app uses p5.js. The pattern applied.
  3. “I see it as a SPARK app and a simulation” is not the same as “list it only as SPARK and Simulation.” Natural language framing statements communicate emphasis, not exhaustive constraints. Compare: “I see this as a comedy” does not mean a film cannot also be a drama. The user was noting the most salient classifications, not drawing an exclusive boundary.

The principle in play: when a prompt leaves something unstated but the workspace provides an unambiguous answer, apply the answer. When the prompt leaves something unstated and the workspace is silent, ask or flag the gap. Here, the workspace was not silent — the Processing page card was created in the immediately preceding turn. The evidence was as close to explicit as an implicit signal can be.

The deeper lesson for prompt engineering: an AI working in a rich, interconnected workspace is not just reading the current prompt. It is reading the prompt in the context of everything it can see. The more coherent and well-organized the workspace — consistent naming conventions, synchronized page listings, documented patterns — the more reliably the AI can infer correct answers to unasked questions. The TNT ecosystem’s consistency made this inference easy. That consistency is its own reward.