Back to Gravity Lab S.P.A.R.K. Chat Log  •  07/23/2026
requestAnimationFrame • Velocity • Gravity • Elastic Collisions • S.P.A.R.K.

Gravity Lab — S.P.A.R.K. Chat Log

The sequel to Drag & Drop Circles. Circles that move on their own, obey gravity, and bounce off each other using real collision physics. A teacher’s design notebook explaining every decision.

For Students — How to Read This Log

This log is written differently from the Drag & Drop Circles log. There, a developer (klp) prompted an AI assistant and the log recorded that conversation. Here, the AI is in the teacher role: Claude proposed this app, designed the pedagogy, wrote the code, and explains every decision in these pages. klp reviews and responds as the collaborating teacher.

Watch for S.P.A.R.K. banners framing each phase, decision boxes explaining why each design choice was made, math blocks showing the equations behind the physics, and a final challenge section giving students specific things to try, modify, and build on.

🎯
S — Set Goal Students who completed Drag & Drop Circles can create objects, handle mouse events, and draw on a canvas. The natural question is: what if the objects moved on their own? The goal of Gravity Lab is to answer that question with the simplest physics simulation that is still genuinely interesting — circles with velocity, gravity, and collisions.
Why This App Is the Right Next Step

Every game, simulation, and animation shares one fundamental pattern: a loop that runs ~60 times per second, updating the world state and then drawing it. Drag & Drop Circles never introduced this loop — it redraws only in response to user events. Gravity Lab introduces requestAnimationFrame, which is that loop. Without this concept, students cannot build Asteroids, Snake, Flappy Bird, or any simulation. This is the single most important idea in graphics programming.

The choice of physics is deliberate. Velocity, gravity, and wall bounce introduce exactly three new mathematical ideas in the simplest possible order:

  1. Velocity: position changes by adding (vx, vy) each frame — Euler integration.
  2. Gravity: vy increases by a constant each frame — constant acceleration.
  3. Wall bounce: reverse the relevant velocity component — reflection.

Circle-circle collision is the “boss level”: it requires vector decomposition, the concept of a collision normal, and the impulse equation. It is optional (students can toggle collisions off) but deeply satisfying when they turn it on and watch circles exchange momentum.

P — Prompt
💬
P — Prompt klp challenges Claude to design and build the app, then document the rationale in a chatlog. The format mirrors the Drag & Drop Circles log, but with the AI in the teacher role.
klp

I have a big challenge for you now. Claude you are a high school computer science teacher and developer of the TNT site. You have just studied the drag and drop app and want to make a ‘sequel’ that builds on the knowledge students presumably gained from it. Let’s make a new app: claudesJSGraphicsChallenge.html (with associated scripts and styles) to take our novices to the next stage of their JS evolution. This is a teaching moment.

Let’s mirror our chatlog from earlier and also create: claudesJSGraphicsChallengeChatlog.html where YOU teach US about the new app and your rationale for its design. You can use the framework for the drag and drop app. Once you build yours, we can come up with the associated artwork and icons.

Copilot

Accepted. Here is the plan: a physics sandbox I’m calling Gravity Lab, built in the same OOP + canvas framework as Drag & Drop Circles but introducing five new concepts: the requestAnimationFrame animation loop, the velocity vector, gravitational acceleration, wall-bounce restitution, and circle-circle elastic collision. Every design decision is documented below.

Prompt Assessment

This prompt works well because it is role-specific (“you are a high school CS teacher”), goal-specific (“sequel that builds on drag-and-drop knowledge”), and gives structural freedom (“you can use the framework”). It does not over-specify the physics, which leaves room for pedagogical judgment about what complexity level is appropriate.

The phrase “once you build yours, we can come up with the associated artwork” is a good constraint: it keeps the first session focused on the code and defers visual design to a second pass where both parties can collaborate with full context. Trying to design the icon before the app is built is a common premature-optimization trap.

A — Analyze
🔍
A — Analyze What must change from Drag & Drop Circles, and what can stay the same? The OOP approach, the TNT shell, and the canvas API all carry over. The rendering model, the class design, and the entire interaction paradigm are new.
Drag & Drop vs. Gravity Lab — Comparison
ConceptDrag & Drop CirclesGravity Lab
Rendering model Redraw on event (mouse move) requestAnimationFrame loop — ~60fps continuous
Circle position Set directly by mouse event Updated by velocity each frame
OOP class Circle(x, y, r) BouncingCircle(x, y, r, vx, vy)
Circle colour Random at spawn, fixed Computed from speed each frame (heat map)
User adds circles Click-to-place Drop (zero velocity) or Fling (drag to aim)
Circle interaction Drag to reposition Drag to reposition AND releases with velocity
Physics None — purely user-driven Gravity, wall bounce, circle-circle elastic collision
Math required Distance formula (contains()) Pythagorean theorem, vector decomposition, impulse equation
JS features used var, ES5 style, DOM events const/let, getter properties, requestAnimationFrame
R — Refine
🔁
R — Refine Six design decisions shaped the final implementation. Each is explained below with the alternatives that were considered and rejected.
Decision 1 — requestAnimationFrame, Not setInterval

Both requestAnimationFrame (rAF) and setInterval can drive an animation loop, but they behave very differently:

setInterval(fn, 16)requestAnimationFrame(fn)
Calls per second~62 (fixed, regardless of display)~60 (synced to display refresh)
Pauses when tab hidden?No — wastes CPUYes — browser throttles it
Timing accuracyCan drift over timePrecisely synced to vsync
CancelclearInterval(id)cancelAnimationFrame(id)
setInterval (old way)
setInterval(function () {
    update();
    draw();
}, 16);  // ~62fps, not synced
requestAnimationFrame (modern)
function animate() {
    update();
    draw();
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

rAF is the industry standard for all browser animation. Students should learn it first and reach for setInterval only for non-visual timers (e.g., polling a server every 30 seconds).

Decision 2 — Euler Integration for Position Updates

The physics update uses Euler integration — the simplest possible method for advancing a simulation one time-step:

velocity_y += gravity // acceleration updates velocity
position_x += velocity_x // velocity updates position
position_y += velocity_y

This is a first-order approximation: it assumes velocity is constant over the entire time step. For a 60fps simulation with small velocities, the error is imperceptible. More accurate methods (Verlet, Runge-Kutta) exist but add significant complexity for negligible visual improvement at this scale.

Key insight: order matters. Update velocity before position (symplectic Euler) for better energy conservation. If you update position first, the simulation leaks energy over time, and circles slowly gain speed from nothing.

Decision 3 — Speed-to-Colour Mapping (Visual Physics Feedback)

In the drag-and-drop app, each circle had a random fixed colour. Gravity Lab maps the circle’s current speed to its hue each frame:

t = clamp(speed / MAX_SPEED, 0, 1) // normalise to 0–1
hue = 220 − t × 220 // blue (220) → red (0)
sat = 80 + t × 15 // vivid when fast
lit = 42 + t × 16 // bright when fast
fill = `hsl(${hue}, ${sat}%, ${lit}%)`

The result is a live heat map of kinetic energy. Students can immediately see: a circle speeding up after a collision flashes orange-red; a circle losing energy to the floor turns blue. This is not decoration — it is information, and it teaches students to connect a number (speed) to a visual (colour) in a way that is intuitive and memorable.

This technique appears in real visualisation tools (plasma displays, thermal cameras, satellite imagery). Teaching it here introduces data-driven colour mapping as a design pattern.

Decision 4 — Fling Mode (User-Controlled Launch Velocity)

Drop Mode (click to spawn with zero velocity) shows gravity clearly but limits student agency. Fling Mode lets students aim and launch:

  1. Press mouse button on empty canvas — record the anchor point.
  2. While held, draw a rubber-band dashed line from anchor to cursor.
  3. On release: vx = (anchor.x − release.x) × SCALE and similar for vy.
  4. Spawn circle at anchor with those velocities.

The velocity is deliberately opposite to the drag direction (anchor → cursor gives pull-back direction, like a slingshot). This matches intuition: drag right to launch left. The yellow line makes the direction and magnitude visible before launch. The v = X px/frame label shows the numerical velocity. This turns an abstract number into a physical sensation students can develop intuition for.

Decision 5 — The Trail Effect (Semi-Transparent Clear)

Normal clear wipes the canvas completely before drawing. The trail effect replaces that wipe with a semi-transparent fill:

Normal clear
ctx.clearRect(0, 0, W, H);
Trail effect
ctx.fillStyle = 'rgba(8, 13, 28, 0.16)';
ctx.fillRect(0, 0, W, H);

Each frame, old circle drawings are dimmed by 16% but not erased. After ~6 frames they are nearly invisible. The result is a comet-tail motion trail. The alpha value (0.16) is a tuneable parameter — smaller = longer trails, larger = shorter. Students can change this one number and see the visual effect immediately. It is a perfect micro-experiment for a classroom exercise.

Decision 6 — The Elastic Collision Algorithm (The Boss Level)

Circle-circle collision is the most mathematically demanding part of the app. The impulse method is used because it is derivable from first principles, correct, and short enough to fit on one screen.

Step 1 — Find the collision normal. When two circles overlap, the direction of the impact force is the line connecting their centres:

n = (B.pos − A.pos) / |B.pos − A.pos| // unit vector A → B

Step 2 — Separate overlapping circles. Because circles move in discrete steps, they can overlap between frames. Before fixing velocities, push them apart along the normal in proportion to their masses (heavier circle moves less):

A.pos −= n × overlap × (B.mass / totalMass)
B.pos += n × overlap × (A.mass / totalMass)

Step 3 — Relative velocity along the normal. Only the component of relative velocity along the collision normal causes a collision response. The tangential component passes through unchanged (no friction):

relVn = (A.vel − B.vel) · n // dot product

Step 4 — Impulse magnitude. An impulse J changes momentum without changing time. For an elastic collision with restitution e:

J = (1 + e) × relVn / (1/mA + 1/mB)
= (1 + e) × relVn × mA × mB / (mA + mB)

Step 5 — Apply impulse. Velocity changes are proportional to the impulse divided by each circle’s mass:

A.vel −= (J / mA) × n
B.vel += (J / mB) × n

This conserves linear momentum exactly and kinetic energy scaled by e². For e = 1 (perfectly elastic), total kinetic energy is preserved. Watch for the special case of equal masses: the circles exchange velocities, just like Newton’s cradle. This is testable. Drop two equal circles onto a stationary one; the stationary one moves at the same speed as the incoming one. Newton’s cradle in code.

Files Created — Complete Summary
New Files
FileStatusWhat it contains
claudesJSGraphicsChallenge.html New Main app: TNT navbar, hero, SPARK bar, canvas, control panel, footer.
scripts/BouncingCircle.js New OOP class: velocity, mass, restitution, Euler physics update, speed-to-colour, draw, fling, contains/overlaps helpers. Heavily commented as a teaching document.
scripts/physics_lab.js New Animation loop, elastic collision resolver, spawning, event handling, all toggle logic. Documented with inline derivations.
styles/physicsLabStyles.css New Hero, SPARK bar, canvas frame, stats bar, control buttons, colour-key legend.
claudesJSGraphicsChallengeChatlog.html New This document.

Note: Hero and app icon artwork are pending. The hero uses a dark gradient fallback. The images/ subfolder will receive assets once artwork is finalised.

K — Knowledge
💡
K — Knowledge Gained After building and studying Gravity Lab, students should own five ideas permanently. These are not TNT-specific tricks — they are the vocabulary of every interactive program ever written.
Five Permanent Takeaways
  1. The animation loop is fundamental. Every game, simulation, and real-time app runs a loop. requestAnimationFrame is how browsers provide it. The loop has exactly three jobs: clear, update, draw. If something in your program isn’t working in a game, ask yourself: is it in the right phase of the loop?
  2. Physics is just math applied to position. Velocity adds to position. Acceleration adds to velocity. That is Newtonian mechanics. The gravity constant in this app (0.38 pixels/frame²) is just a number you can change to make things feel like the Moon or Jupiter. Physics engines are not magic — they are these two lines running very fast.
  3. Elastic collision conserves momentum. When circles collide and one stops, the other takes its speed. This is not magic in the code — it is conservation of momentum falling out of the equations automatically. Test it: make two circles the same size and fire one at a stationary one. They swap velocities. That is Newton’s cradle.
  4. Data-driven colour is a visualisation pattern. Mapping a number (speed) to a visual property (hue) is the basis of heat maps, weather radar, medical imaging, and financial dashboards. Learn to compute hsl(hue, sat%, lit%) from a normalised value. You will use this technique throughout your career.
  5. Toggles reveal causality. The best way to understand what a piece of code does is to turn it off and see what breaks. Gravity off → circles float. Collisions off → circles pass through each other. Vectors on → velocity becomes visible. Each toggle is a micro-experiment. Build toggle switches into every simulation you write.
Student Challenges — Try These

The code is deliberately written to be modifiable. Each of these can be done by changing a small number of lines or constants:

  1. Moon gravity. Change GRAVITY in physics_lab.js from 0.38 to 0.063 (about 1/6 of Earth). What happens? Try 3.7 (Mars is actually higher gravity than Moon). What does Jupiter feel like?
  2. Perfectly elastic. Set this.restitution = 1.0 in BouncingCircle.js and turn off damping (DAMPING = 1.0 in physics_lab.js). Does total energy stay constant? How can you tell?
  3. Super-bouncy. Set this.restitution = 1.2. The circles gain energy on each bounce. Watch what happens over 30 seconds.
  4. Colour by size. Change getColor() in BouncingCircle.js to map radius (not speed) to hue. Tiny circles are red, big circles are blue. What information does this show vs. speed?
  5. Mass-equals-radius. Change this.mass = radius * radius to this.mass = radius. Do the collisions feel different? Why?
  6. Gravity well. Instead of constant downward gravity, make circles accelerate toward the canvas centre. In update(), replace the gravity line with: this.vx += (centreX − this.x) × 0.0003; and similar for vy. You just built a solar system simulator.
Where This Leads

Gravity Lab is a minimal physics engine. The same code, scaled up, becomes:

  • Asteroids — add a player-controlled polygon, rotation, and projectiles.
  • Billiards / pool — add friction on the floor, fixed pockets as goal zones.
  • Molecular dynamics — thousands of tiny particles with Lennard-Jones forces.
  • Browser physics engines (Matter.js, Box2D) — they solve these same equations, just with more shapes, joints, and constraints.

Stage 2 Hero Image & Ecosystem Integration

klp

Well, you outdid yourself, Claude! I added an image, gravityOutput.png, you can use for the hero background. Please wire this up in Explore as a S.P.A.R.K. and Graphics app, and in the JS Apps file, and also add a news item announcing its introduction! Update our chatlog as well!

Copilot

Thank you — four places updated in one pass. Here is what changed and why each decision was made the way it was.

Stage 2 Changes — Complete Summary
FileChange
styles/physicsLabStyles.css Hero background upgraded from pure dark gradient to gravityOutput.png with a gradient overlay. The image sits in the root of the app folder; CSS references it as url('../gravityOutput.png') (one level up from styles/). The overlay opacity was reduced from 0.88/0.72 to 0.82/0.68 to let the image breathe.
explore.html — Graphics card Gravity Lab added as second featured item (alongside Drag & Drop Circles). The card now shows two apps in the featured list, filling the Graphics category visually.
explore.html — Graphics offcanvas Two new entries added after Drag & Drop Circles Chat Log: the Gravity Lab app link and its chatlog link.
explore.html — S.P.A.R.K. offcanvas Two new entries added after Drag & Drop Circles Chat Log: Gravity Lab app and chatlog. The app is cross-listed in both Graphics and S.P.A.R.K. because it is both a canvas graphics demo and a teacher-authored S.P.A.R.K. design document.
js_apps.html New <figure> added after Drag & Drop Circles, using gravityOutput.png as the icon. The same image serves double duty as both the hero background and the grid icon — appropriate since it is a screenshot of the live simulation.
news.html Entry #037 added at the top of the 2026 accordion. Covers: the rAF architecture shift, the physics model (velocity, gravity, restitution, elastic collision), both interaction modes (Drop and Fling), the colour heat map, and the six student challenges.
claudesJSGraphicsChallengeChatlog.html This stage (Stage 2).
Hero Image Strategy — Why gravityOutput.png Works as Both Hero and Icon

A screenshot of the running simulation is an unusually effective hero image for an educational app. Unlike a decorative stock photo, it shows the student exactly what they will see when they open the app: coloured circles of varying sizes in mid-bounce, a dark canvas, some trailing ghost paths if they have ever turned on Trail mode. There is no gap between promise and delivery.

It also works as a grid icon in JS Apps for the same reason. The icon at 80×80px will be a compressed version of the simulation, which is still legible as “circles on a dark background” — distinguishable from every other icon on the page that uses a vector line-art style.

The CSS gradient overlay (dark-blue to dark-navy, 82% / 68% opacity) keeps the hero text legible on any device without the image needing to be pre-darkened. Reducing the overlay from the original pure-gradient values (88% / 72%) lets the background image contribute visibly instead of being mostly hidden. The 14% / 8% reduction is small but makes a meaningful difference on bright screens.

Cross-Listing in Both S.P.A.R.K. and Graphics

Gravity Lab appears in both the S.P.A.R.K. offcanvas and the Graphics offcanvas in Explore. This is correct because the app genuinely belongs in both categories:

  • Graphics: The app generates visual content entirely on an HTML5 canvas using JavaScript. Every pixel is computed. No images are drawn, no SVG is used, no video is played back. This exactly matches the TNT Graphics category definition.
  • S.P.A.R.K.: The app was designed using the S.P.A.R.K. methodology with a published chat log documenting every decision. The chat log doubles as a student teaching document with a comparison table, six decision boxes, math derivations, and a challenge section.

Stage 3 Validation Fixes

klp

I noticed this page did not validate. Two errors: (1) Duplicate attribute href on line 105, and (2) Duplicate ID controls on line 200. Please fix and discuss in the chatlog.

Copilot

Both fixed. Here is what each error was and how it was introduced.

Error 1 — Duplicate href Attribute (Line 105)

The “Previous: Drag & Drop Circles” link in the SPARK bar had two href attributes on the same <a> tag:

<!-- INVALID — two href attributes -->
<a href="dragDropCirclesSPARK2026-07-23/dragDropIndex.html"
   href="../dragDropCirclesSPARK2026-07-23/dragDropIndex.html" ...>

This was a copy-paste error during initial authoring: the first href used a relative path without the ../ prefix (wrong — this page is inside the app subfolder, so it needs one level up), then a corrected version was added as a second attribute instead of replacing the first. Browsers silently use the first attribute and ignore the duplicate, so the page appeared to work, but it is invalid HTML.

Fix: Removed the first (incorrect) href and kept the second (correct) path: ../dragDropCirclesSPARK2026-07-23/dragDropIndex.html.

Error 2 — Duplicate ID controls (Line 200)

Two separate <div> elements in the app section both carried id="controls":

<!-- First controls div (button row) -->
<div id="controls" class="d-flex flex-wrap gap-2 ...">
    <button id="btnAddRandom" ...>...</button>
    ...
</div>

<!-- Second controls div (status / hint line) -->
<div id="controls" class="d-flex flex-wrap gap-3 ...">
    <p id="modeHint" ...>...</p>
    <p id="circleCount" ...>...</p>
</div>

HTML requires every id to be unique within a document. The second id="controls" was unnecessary: the JavaScript and CSS in this app do not reference #controls by ID at all — the interactive elements inside it (#modeHint, #circleCount) each carry their own IDs and are addressed directly.

Fix: Removed id="controls" from the second <div>, leaving it as a plain structural wrapper. The first button-row <div id="controls"> is unchanged.

Why Validation Matters (and What Browsers Do With Invalid HTML)

Both errors fell into a category that makes them especially insidious for beginners: the page appears to work anyway. Browsers apply error-recovery rules when they encounter invalid markup, and both of these have benign fallbacks (use the first href, treat the second ID as if it were not there). The absence of a visible bug does not mean the markup is valid.

The practical risks are real:

  • Duplicate IDs break document.getElementById() in some browser implementations (it returns the first match, ignoring later ones with the same ID). If physics_lab.js ever needed to address the second #controls div, it would silently get the button row instead.
  • Duplicate attributes are unpredictable across browsers. The HTML spec says the first occurrence wins, but this is not universal — some parsers keep the last, some flag an error. Any code that reads element.getAttribute('href') might get either value depending on the runtime.

The validator caught both issues before the page was uploaded. Running the W3C validator on every page before launch is a TNT habit worth keeping. The validate icon in the TNT footer links to the validator pre-populated with the page URL — one click, immediate results.

Stage 4 Hero Image Positioning

klp

In both the gravity lab and its chatlog, we need to use the gravityOutput image, but the image’s bottom edge should be at the bottom edge of the hero area. Please identify where you make this adjustment in code and explain in the chatlog how you did it.

Copilot

Two lines of CSS — one in the external stylesheet, one in the chatlog’s inline styles. Both change a single keyword in the background shorthand.

The CSS Change — One Keyword Does It

A CSS background shorthand can encode both the size and position of a background image in one declaration, separated by a /:

url('image.png') <position> / <size> <repeat>
Before (centred)
url('../gravityOutput.png') center / cover no-repeat
After (bottom-aligned)
url('../gravityOutput.png') bottom / cover no-repeat

The position keyword bottom is shorthand for center bottom — it horizontally centres the image while vertically aligning its bottom edge with the element’s bottom edge. With cover sizing the browser scales the image until it fills the hero in both dimensions, then applies the position offset. The result: the image is always cropped from the top, never from the bottom.

For a physics simulation screenshot, this is the right choice. Circles accumulate near the floor when gravity is on, which is where the most visually interesting content sits — the multi-coloured pile-up of bouncing circles. center would show the middle of the image (mostly empty dark canvas), while bottom shows the action.

Where the Change Lives in Each File
FileLocationChange
styles/physicsLabStyles.css #hero rule, background property center / coverbottom / cover
Path: url('../gravityOutput.png') (one level up from styles/)
claudesJSGraphicsChallengeChatlog.html Inline <style> block, #hero rule Image added (was gradient-only) + positioned bottom / cover
Path: url('gravityOutput.png') (same folder as the chatlog HTML)

Note the path difference: the app’s CSS file lives in styles/, so it needs ../ to reach the image in the app’s root. The chatlog HTML is at the app’s root, so it references the image directly without a prefix. Same physical file, two different relative paths depending on which file is loading it.

Background Position Keywords — Quick Reference

The full set of CSS background-position keywords, showing where each anchors the image:

left top center top right top
left center center (default) right center
left bottom bottom ← used here right bottom

Single-word values (top, bottom, left, right) imply center for the other axis. You can also use percentage values (50% 100% is identical to bottom) or pixel offsets (0px 100%) for precise sub-pixel control.