Back to Galton Board Simulator Bell Curve Tutorial Movie Clips S.P.A.R.K. Chat Log  •  10/31/2026
S.P.A.R.K. with AI — Development Dialog

Building the Galton Board Simulator

From a student’s RGB sketch to an HSB-palette, portrait-canvas, statistics-teaching app.

About This Log

This log documents the build of the Galton Board Simulator — a p5.js interactive that lets students drop balls through a triangular peg array and watch the binomial distribution converge to a bell curve. The project grew from a student-origin sketch (Stranger Things theme, RGB colors) into a TNT-standard app with HSBA colors, a portrait canvas, and a full statistics control panel.

Designed as a dual-audience tool: CS students see p5.js physics, HSBA color design, and the module pattern; AP Stats students see the Central Limit Theorem emerge in real time.

session 1 — 10/30/2026 — initial build
🎯
S — Set Goal Build the first-pass Galton Board Simulator from a student-origin sketch. Five deliverables: galtonBoardSimApp.html, galtonBoardSimAppStyles.css, galtonBoardSimAppSketch.js, galtonBoardSimAppScripts.js, galtonBoardSimAppChatlog.html, and galtonBoardSimAppChatlogStyles.css. Controls left, portrait canvas right, HSBA color mode throughout.
P — Prompt
💬
P — Prompt A rich first prompt: three named roles, six tagged reference files, hero image as palette source, explicit HSBA requirement, layout preference (controls left / canvas right), parameters to expose, portrait orientation rationale, and an invitation for SPARK documentation.
KLP

You are a web developer, high school computer science teacher and AP Stats teacher and a co-creator here at TNT. You have looked at a former app done by a student (loading icons) and are very familiar with our SPARK process where we document our app developments and use them with our novices to teach them how to ‘vibe code.’ I’ve attached our SPARK method docs and CRAFT prompting techniques. We’d like to use David’s app as a basis for a new app, galtonBoardSimApp.html with associated files galtonBoardSimAppStyles.css, galtonBoardSimAppSketch.js, and galtonBoardSimAppScripts.js. We’ve got a parent folder and a suggested sketch, galtonBoardOrigSketch.js that can be used as a starting place for our p5js sketch associated with this app. This HTML/CSS/Bootstrap5/JavaScript p5js app should serve as a training ground for computer science students learning JavaScript and Processing p5js and an inspiration for AP Stat students learning about ‘the bell curve.’ I’ve got a hero image for the hero background you can use for a color palette. Unlike the original script, we want to use HSBA not rgb in our design. This will be our first prompt in the series, and as we did in other chatlogs (I attached two) I want to include my prompts and your grade/critique of them along with the work you do to get our final working app. The canvas should be on the right and the controls should be on the left. I’d like to be able to influence the pertinent parameters of the simulation including speed, gravity, number of pegs, peg spacing, etc. The canvas can be oriented as portrait to accommodate the pegs and the collection bins in the bottom of the canvas. Let’s get an initial version up and running and we can tweak it from there.

We’ll want galtonBoardSimAppChatlog.html along with its style sheet.

A — Analyze
🔍
A — Analyze Prompt evaluated under C.R.A.F.T. criteria. Six reference files studied. Hero image palette extracted. Key design decisions made before writing a single line of code.
C.R.A.F.T. Prompt Critique — Grade: A−

C — Context: Excellent. Six tagged files deliver the full workspace: the original student sketch (physics logic to adapt), David’s app (page structure template), two chatlogs (SPARK format reference), and two SPARK/CRAFT documentation pages. The hero image as palette source is an efficient way to establish visual identity without enumerating color codes — it delegates the palette extraction to the AI while anchoring the design intent in a concrete image.

R — Role: Three named, all relevant. Web developer (implementation), CS teacher (novice-accessible code structure), AP Stats teacher (bell curve pedagogy). Naming all three simultaneously signals that every design decision should serve all three lenses. The “co-creator at TNT” addition establishes shared ownership of the aesthetic conventions.

A — Action: Multi-layered and correctly sequenced. Build six files. Use David’s app as template. Start from the existing sketch. Expose specific simulation parameters. Document as SPARK Session 1. The phrase “let’s get an initial version up and running and we can tweak it from there” correctly sets expectations: this is a first-pass build, not a finished product. That framing prevents over-engineering in Session 1.

F — Form: Explicit and cross-referenced. All six file names given. Layout specified (controls left, canvas right — the opposite of David’s app). Portrait canvas explicitly motivated (needs height for pegs + bins). SPARK format by reference to the two attached chatlogs, which is more efficient than re-describing the format.

T — Tight Constraints: One critical, one implied. “HSBA not RGB” is the critical constraint — it determines how every color is specified in the sketch. “Portrait orientation” is the implied constraint, with a clear physical rationale (pegs cascade vertically into bins at the bottom). Minor gap: the list of parameters (“speed, gravity, number of pegs, peg spacing, etc.”) leaves the “etc.” open — a slightly tighter list would avoid guessing. Addressed in the R section.

Design Decision — Palette from the Hero Image

The hero image (robotGaltonBoard1.jpg) shows a STAT-BOT robot presenting a Galton Board to enthusiastic students in a statistics classroom. The dominant colors extracted and converted to HSB:

--gb-bg:     HSB(222, 45, 10)   →  near-black navy   (classroom shadows)
--gb-blue:   HSB(210, 75, 80)   →  steel blue        (STAT-BOT body color)
--gb-gold:   HSB(40,  85, 95)   →  warm amber/gold   (classroom lighting, ball colors)
--gb-light:  HSB(210, 15, 95)   →  near-white        (whiteboard, clean text)
--gb-card:   HSB(222, 40, 16)   →  dark blue-gray    (control card backgrounds)

Ball colors use HSBA with 8 hues spread evenly across the wheel (0, 45, 90, 135, 180, 225, 270, 315), all at S=85, B=95. This produces a rainbow that maps naturally to the bins across the board — the bell curve emerges as a color gradient from the edges toward the center.

Design Decision — Why HSBA Instead of RGB

The original student sketch used RGB arrays for every color: [233, 69, 96] for pegs, [255, 0, 128] for ball colors. These numbers are opaque — you cannot tell by reading them whether the color is warm or cool, vivid or muted.

With colorMode(HSB, 360, 100, 100, 100):

HSB(210, 75, 80)  →  "a moderately saturated steel blue at 80% brightness"
HSB(40,  85, 95)  →  "a vivid warm amber at near-maximum brightness"
HSB(0,   85, 95, 60) →  "a vivid red at 60% opacity"

This is readable, designable, and directly connected to the color theory students learn when they study UI design. Ball hues can be spread evenly across the wheel with hue = i * (360 / numBallColors) — a formula that is immediately understandable in HSB and completely opaque in RGB.

Design Decision — Portrait Canvas (440×720)

The Galton Board is physically tall: pegs cascade down row by row, and the collection bins need headroom to show growing histograms. A landscape canvas forces the peg spacing to be compressed horizontally, making the cascading physics hard to see. Portrait gives:

  • Enough vertical travel for physics to feel realistic (balls noticeably accelerate)
  • Room for up to 16 peg rows without crowding
  • Collection bins with bars tall enough to display meaningful height differences
  • A natural top-to-bottom reading direction that mirrors how a real Galton Board works

At 440×720, the canvas fits in the right column of a Bootstrap row on tablet and desktop, with controls in the left column. On mobile it stacks naturally.

Design Decision — Parameters Exposed

The prompt listed “speed, gravity, number of pegs, peg spacing, etc.” The “etc.” was resolved by asking: what would an AP Stats teacher and a CS student each want to control?

Parameter Range Why it matters
Speed 0.2–3.0× Slow down to see individual bounces; speed up to accumulate statistics faster
Gravity 0.1–1.0 Low gravity: slower fall, more visible bouncing; high: fast dramatic drop
Bounce Factor 0.2–0.9 Energy retained after peg collision; affects randomness of deflection
Peg Rows 4–16 More rows = more coin flips = tighter bell curve (CLT in action)
Peg Radius 4–14 px Visual clarity; larger pegs make collisions more visible
Ball Radius 3–10 px Ball-to-peg size ratio affects deflection realism
Drop Interval 50–500 ms Controls crowding; slow interval shows individual trajectories
R — Refine
✏️
R — Refine Six files delivered for the initial build. The original sketch was adapted from RGB to HSBA throughout. Statistics panel and bell-curve overlay added beyond the original.
Session 1 File Inventory

galtonBoardSimApp.html — Standard TNT page. Processing active in navbar. Hero with robotGaltonBoard1.jpg overlay. About section explaining Galton, the binomial→normal connection, and the coin-flip model. Two-column layout: controls left (col-lg-4), portrait canvas right (col-lg-8).

styles/galtonBoardSimAppStyles.css — Navy dark background, steel blue accents, warm gold highlights. --gb-* custom properties. Slider groups, control sections, button variants, statistics panel, responsive stack at mobile.

sketches/galtonBoardSimAppSketch.js — Adapted from galtonBoardOrigSketch.js. colorMode(HSB, 360, 100, 100, 100) at setup. 440×720 portrait canvas. All colors as HSBA arrays. Added: bell-curve overlay (golden line showing theoretical binomial PMF), improved bin visualization (stacked ball representation), peg glow effect in HSB. Public API: gbDropBall(), gbDropMany(n), gbToggleContinuous(), gbReset(), gbSet*(v) functions.

scripts/galtonBoardSimAppScripts.js — Slider and button handlers. DOMContentLoaded wiring. 500 ms interval polling to update statistics display from sketch state.

galtonBoardSimAppChatlog.html — This file. SPARK Session 1 with CRAFT critique, design decisions, file inventory.

styles/galtonBoardSimAppChatlogStyles.css — Navy/steel-blue chatlog palette. SPARK banner variants, chat bubbles, annotation boxes, code snaps.

K — Know & Share
🧠
K — Know & Share Three patterns worth keeping: the HSBA color model as a teaching tool, the Galton Board as a CLT demonstration, and the coin-flip analogy as the bridge between AP Stats and CS.
Pattern — The Galton Board Is a Physical Central Limit Theorem

Sir Francis Galton built the first Quincunx board in 1889 to demonstrate that the binomial distribution converges to the normal as n grows. Each ball faces n binary choices (left or right at each peg), each with probability 1/2. After n rows, the ball lands in bin k with probability:

P(bin k | n rows) = C(n, k) × (1/2)^n

This is the binomial PMF with p=0.5. For large n, it converges to N(μ=n/2, σ²=n/4). With 12 peg rows, the distribution is nearly indistinguishable from normal to the eye. With 16 rows and 500 balls, the bell curve emerges unmistakably.

For AP Statistics students: this is the CLT in physical form. The simulation makes the theorem tangible before the abstract proof. For CS students: it shows how randomness in a single simple rule (50/50 at each peg) produces organized structure in the aggregate — the same principle behind many natural phenomena.

Pattern — HSB as a Teaching Color Model

RGB is the computer’s color model — it maps directly to hardware (red, green, blue phosphors). HSB is the designer’s color model — it maps to how humans describe color (hue, saturation, brightness). When teaching color in a CS class:

  • “Change the hue from 0 to 120” = “shift from red to green” — immediately visible and intuitive
  • “Change the saturation from 100 to 20” = “wash out the color” — describable in English
  • “Hue = binIndex × (360 / numBins)” = rainbow spread across the histogram — one formula

The original student sketch had 6 hardcoded RGB arrays for ball colors. The HSBA version replaces them with one formula. This is the kind of simplification that makes code both more readable and more maintainable — a real-world CS lesson hiding inside a stats simulation.

DWR Alert — The Coin-Flip Bridge

The Galton Board is built from a single rule: at each peg, a ball goes left or right with equal probability. This is a coin flip. After n pegs, the ball has effectively flipped n coins and counted the heads. Bin k = exactly k heads in n flips.

This coin-flip framing bridges AP Statistics and CS in a concrete way:

  • AP Stats: P(X=k) = C(n,k)(0.5)^n → binomial distribution → normal approximation → z-scores → everything in the course
  • CS: random(-0.3, 0.3) added to the bounce angle creates the coin flip. One line of code. Remove it and every ball falls straight down the center.

The simulation makes this connection visual. Run it with randomInfluence = 0 and all balls stack in the center. Add randomness and the bell curve emerges. The parameter controls in the app let students discover this themselves.

session 2 — 10/30/2026 — rectangle layout & layout toggle
🎯
S — Set Goal Add a rectangular peg layout (full quincunx grid) alongside the existing triangle, with a toggle switch that locks once balls are in play. Improve bell-curve clarity and prevent edge leakage.
P — Prompt
💬
P — Prompt Observes a specific visual problem (edge leakage, weak bell curve), proposes the solution (rectangular quincunx), specifies the UX constraint (toggle locked during simulation), and requests chatlog documentation.
KLP

Currently, the peg layout is that of a triangle. With this orientation, many balls are leaking to the sides and the bell distribution is less apparent. Let’s keep this configuration, but allow for a rectangular one as well, where the rows alternate from n and n+1. (Fill up the edges of the triangle, essentially, all the way up from the bottom). Let’s provide a switch that lets us toggle orientation when the game is in a reset/start status. Integrate this prompt, its critique and your response in our chatlog.

A — Analyze
🔍
A — Analyze Prompt evaluated. Root cause of edge leakage identified. Rectangle layout geometry derived. UX locking strategy decided.
C.R.A.F.T. Prompt Critique — Grade: A

C — Context: Strong. Four tagged files including the live sketch. The observed problem (“balls leaking to the sides”) is stated concretely, not vaguely. This is the kind of field-observation prompt that drives real iterative improvement.

R — Role: Implicit. The AP Stats teacher role is doing work here: “the bell distribution is less apparent” is a pedagogical concern, not a visual one. That distinction shapes the solution.

A — Action: Precise. Keep the triangle; add a rectangle; alternate n and n+1 pegs per row; toggle locked during simulation. Each action is specific and independently testable.

F — Form: Chatlog integration specified.

T — Tight Constraints: “When the game is in a reset/start status.” This is a precise UX constraint that prevents mid-run layout changes from producing undefined states. One sentence; complete specification.

The Two Layouts — Geometry

Triangle (existing): Row k has k pegs, centred. The first row has 1 peg at the centre; the last row has pegRows pegs. Only the bottom rows span near the edges. Balls entering upper rows have a wide gap on each side — they can deflect beyond the outermost peg and escape the board, landing in invisible bins or the walls.

Rectangle (new) — the classic quincunx:

Even rows (0, 2, 4…): binCount = pegRows+1 pegs, first at boardLeft, last at boardRight
Odd  rows (1, 3, 5…): pegRows pegs, each centred between two even-row pegs (offset ½ spacing)

Peg spacing:  boardW / pegRows   (so even-row pegs fill left edge to right edge exactly)

Example, pegRows = 4:
Row 0: o   o   o   o   o     (5 pegs)
Row 1:   o   o   o   o       (4 pegs, offset)
Row 2: o   o   o   o   o     (5 pegs)
Row 3:   o   o   o   o       (4 pegs, offset)

The edge pegs on every even row act as walls: a ball reaching the far left or far right hits a peg immediately and deflects inward. Leakage is structurally prevented.

UX Decision — Toggle Locking Strategy

The toggle must be disabled once the first ball has been dropped, and re-enabled when Reset is pressed. The reasons:

  • Switching layouts mid-run would call initBoard(), which clears all balls and counts — surprising and data-destroying.
  • The peg array, bin widths, and ball-collection boundaries all depend on the layout. Allowing mid-run switching would require rebuilding everything live while balls are in flight.

Implementation: a 500 ms setInterval in scripts.js calls updateLayoutToggle(), which reads gbGetStats().total and sets input.disabled and opacity accordingly. The hint text also changes: “Switch before dropping balls” → “Reset to change layout.”

The gbSetLayout(mode) sketch function has a guard: it only applies the change if gbTotalDropped === 0. This provides a second layer of protection even if the DOM toggle is somehow manipulated.

R — Refine
✏️
R — Refine Four files modified. No files added. Changes are surgical.
Changes Made

Sketch — added var gbLayoutMode = 'triangle'; refactored generatePegs() into a dispatcher that calls either generateTrianglePegs() or generateRectanglePegs(); added public API gbSetLayout(mode) (with gbTotalDropped === 0 guard) and gbGetLayout().

HTML — added Bootstrap form-switch (id="layoutToggle") with “Triangle / Rectangle” labels at the top of the Board Configuration section.

Scripts — added onLayoutChange(), updateLayoutToggle(); updated onReset() to call updateLayoutToggle(); added 500 ms polling interval.

CSS — added .layout-toggle-wrap, .toggle-label, .gb-layout-switch (steel-blue when checked), .toggle-hint.

K — Know & Share
🧠
K — Know & Share The quincunx is a better teaching tool than the triangle for large ball counts. The triangle is better for watching individual trajectories.
When to Use Each Layout

Triangle: Use for classroom demonstrations of individual ball trajectories. Start with one ball, click Step, watch it deflect left and right at each peg. The sparse upper rows make each decision point visible. Good for introducing the coin-flip analogy. Poor for accumulating a clear bell curve because edge leakage distorts the distribution.

Rectangle: Use for demonstrating the Central Limit Theorem. Drop 200 or 500 balls with Auto-Drop. The edge pegs contain all balls within the board, the bins fill symmetrically, and the bell-curve overlay matches the histogram closely. Good for AP Stats discussions of mean, standard deviation, and the normal approximation to the binomial.

Suggested classroom activity: run rectangle mode with 12 rows until the distribution is clearly bell-shaped, then ask students to predict what happens when you increase to 16 rows (taller, narrower bell — smaller standard deviation relative to the number of bins). This is the CLT in action.

session 3 — 10/31/2026 — ball start position: center of the funnel
🎯
S — Set Goal Remove the random lateral offset from ball creation; start every ball at the exact horizontal center of the funnel, halfway between the top of the canvas and the funnel tip.
KLP

The balls start at a position to the left or right of the middle peg in the top row. There is a funnel shape above that middle peg. Start each ball at the center of that funnel half way between the top of the canvas and the end of the funnel.

🔍
A — Analyze Root cause: random(-8,8) on x and random(-0.4,0.4) on vx. Both removed. Starting y computed from funnel geometry.
The Fix — Three Lines Changed
/* Before */
x:   GB_CANVAS_W / 2 + random(-8, 8),   // ← lateral jitter
y:   gbBoardTop + 44,                    // ← just below funnel tip
vx:  random(-0.4, 0.4),                  // ← initial horizontal push

/* After */
x:   GB_CANVAS_W / 2,                   // exact center
y:   (gbBoardTop + 40) / 2,             // halfway between canvas top (y=0) and funnel tip
vx:  0,                                  // falls straight down

The funnel tip is at y = gbBoardTop + 40 = 100. Canvas top is y = 0. Halfway = y = 50. The ball falls through the funnel area under gravity, exits the funnel tip, and meets the first peg row perfectly centered. All randomness now comes exclusively from peg collisions — which is where it should come from in a Galton Board.

session 4 — 10/31/2026 — elastic collision physics fix
🎯
S — Set Goal Replace the broken speed-times-angle collision formula with proper elastic vector reflection. Remove the guard that was silently killing every upward bounce.
KLP

I notice that even when the ‘Bounce Factor’ is at its max, when a ball enters the canvas and hits the first peg, it hits and doesn’t bounce: it stops then falls to either the left or right. When a ball hits any peg it should bounce elastically with each collision. It isn’t a perfectly elastic collision, but there should be noticeable energy and momentum changes happening.

🔍
A — Analyze Two bugs found in resolvePegCollision(): wrong reflection formula and a guard that overwrote every legitimate upward rebound.
Root Cause — Two Bugs Working Together

Bug 1 — Wrong reflection formula. The old code computed the reflected velocity as cos(angle)×speed×bounceFactor, which only captures the normal component and discards the tangential component. For a head-on collision (ball falling straight onto a peg) this happens to work, but for glancing collisions it loses energy that should be preserved.

Bug 2 — The silent killer. The old code ended with:

if (ball.vy < 0.5) { ball.vy = 0.5; }   // "minimum downward velocity"

When a ball hits the top of a peg, the correct reflected vy is negative (upward): vy_reflected = −vy_incoming × bounceFactor. Since any negative number is less than 0.5, this guard fires on every correct upward bounce and replaces it with 0.5 — a tiny downward creep. The ball appeared to “stop then drop” because all elastic energy was destroyed by this one line.

Together: Bug 1 computed a reasonable-looking result for simple cases, and Bug 2 erased it. Even at maximum Bounce Factor (0.9), every peg collision reduced to a 0.5-pixel-per-frame drop.

The Fix — Proper Vector Reflection
// Unit normal from peg centre to ball centre
var nx = dx / d,  ny = dy / d;

// Dot product: velocity component along the normal
var vDotN = ball.vx * nx + ball.vy * ny;

// Reflect only if moving INTO the peg
if (vDotN < 0) {
    ball.vx = (ball.vx - 2 * vDotN * nx) * gbBounceFactor;
    ball.vy = (ball.vy - 2 * vDotN * ny) * gbBounceFactor;
    ball.vx += random(-0.45, 0.45);   // Galton lateral nudge
}

The formula v_r = v − 2(v·n)n is the standard optical/physics reflection formula. It preserves the tangential component exactly and reverses only the normal component. Multiplying by gbBounceFactor introduces controlled energy loss. The if (vDotN < 0) guard prevents double-bouncing when the ball is already moving away from the peg.

The old minimum-velocity guard was removed entirely. Balls that legitimately bounce upward will decelerate under gravity, pause, and fall back down — exactly as they should. No artificial floor is needed.

session 5 — 10/31/2026 — edge effects: discussion, decision & config reset
🎯
S — Set Goal Discuss edge-effect options for both layouts; implement Option 1 (inset rectangle edge pegs); add explanatory note to the About section; add a Reset Configuration button.
KLP

[Session 5 opened with a discussion across two turns. Summary: observed that in Triangle mode a gap exists between the outermost pegs and the board walls large enough for balls to escape; in Rectangle mode, depending on row settings, balls can get pinched between edge pegs and the wall after elastic collisions. Asked for options and whether either issue is a “feature rather than a bug.” Decided: fix Rectangle with Option 1 (inset edge pegs); keep Triangle gap and document it. Also requested a Reset Configuration button after the sliders and before statistics.]

🔍
A — Analyze Five options considered for each edge effect. Triangle gap kept as feature; rectangle sticking fixed geometrically. Config reset added as UX improvement.
The Two Edge Problems — Different Causes, Different Treatments

Triangle gap: The outermost pegs of the bottom row sit ~20 px inside the board walls. After Session 4’s elastic bounce fix, a ball that just misses the last peg reflects off the wall and falls cleanly — but it may fall outside the peg field entirely. This is identical to the behavior of physical Galton boards: real boards have the same edge effect. It is best presented as a pedagogical feature: this is exactly why the rectangular quincunx layout was invented.

Rectangle sticking: Even-row edge pegs were positioned at gbBoardLeft exactly (the wall). A ball reflecting elastically off an edge peg can get a velocity component toward the wall, creating a gap < 1 px that the ball cannot exit. This is a genuine bug: stuck balls corrupt the statistics and stall the simulation.

Options Considered

Five options were evaluated:

Option Description Verdict
1. Inset edge pegs Move outermost rectangle pegs inward by ballRadius+pegRadius+2px ✓ Chosen
2. Soft wall impulse Add artificial inward force near walls at peg rows Physically dishonest
3. Wider board border Draw decorative border outside peg field Visual mismatch
4. Bin edge balls Send wall-reaching balls directly to outer bins Inflates edge bins
5. Feature, not bug Document as edge effect; keep both behaviors ✓ Applied to Triangle
Option 1 Implementation — Inset Calculation
var inset    = gbBallRadius + gbPegRadius + 2;   // minimum clearance from wall
var innerW   = boardW - 2 * inset;               // usable width after inset
var rSpacing = innerW / gbPegRows;               // new spacing within inset grid

// Edge peg positions:
startX = gbBoardLeft + inset                     // leftmost: no longer at wall
         ... to ...
gbBoardLeft + inset + gbPegRows * rSpacing       // rightmost: gbBoardRight - inset

With default values (ballRadius=5, pegRadius=7): inset = 14 px. The outermost peg sits 14 px from the board wall — enough clearance for the ball (radius 5) plus the peg (radius 7) plus a 2 px safety margin. Pegs become fractionally closer together (rSpacing slightly smaller), but the visual change is imperceptible.

Reset Configuration Button

Placed after the last Board Configuration slider and before the Statistics display. The “Reset” button in Ball Controls resets the simulation (clears balls and counts). The new Reset Configuration button resets all seven sliders to their default values and calls the corresponding gbSet* sketch API functions.

Defaults are stored in a DEFAULTS object in scripts.js for maintainability — changing a default in one place updates both the DOM and the sketch.

The button is visually distinct from the simulation Reset (different icon: fa-sliders vs fa-rotate-left) to prevent confusion.

R — Refine
✏️
R — Refine Three files changed; one explanatory note added to About.
Teaching Moment — Why the Triangle Gap Is a Feature

When Sir Francis Galton first demonstrated the bean machine in 1889, he used a triangular peg arrangement. Contemporary illustrations of his device show exactly the same edge behavior we see in our simulator: the outermost bins collect fewer balls because balls sometimes slip past the last peg on each row.

This is historically accurate and pedagogically rich. The About section now explicitly explains that this edge effect is what motivated the rectangular quincunx layout — and that our simulator lets students compare both directly using the layout toggle. The triangle gap is not a defect; it is a demonstration of why the rectangular design is superior for producing clean bell curves.

session 6 — 10/31/2026 — gap geometry fix: ball diameter clearance
🎯
S — Set Goal Correct the inset formula so the gap between the board wall and each edge peg is always at least one ball diameter. Trigger peg recalculation whenever ball radius or peg radius changes.
KLP

There are no longer any pegs on the actual board edge. I think that’s a good thing and needs to stay that way. Now, there are balls that get stuck between the wall and the edge balls in longer rows. We just need to tweak the configuration so that gap is large enough to accommodate a ball’s diameter. I’m thinking that check should happen whenever we change peg rows, radii, or ball radius: we can just adjust the size of the board, right? Write this up in our chatlog with your critique and observations and solutions.

C.R.A.F.T. Prompt Critique — Grade: A

C — Context: Precise field observation. Three tagged files. The observation (“no pegs on the board edge — good; balls still stuck”) shows that the Session 5 fix achieved its visual goal but didn’t fully solve the physics. The user is doing the right thing: observing, testing, refining.

A — Action: Three distinct directives. (1) Fix the gap ≥ ball diameter. (2) Recalculate whenever peg rows, peg radius, or ball radius changes. (3) Chatlog with critique. All three are specific and actionable.

T — Tight Constraints: “We can just adjust the size of the board, right?” This phrase correctly identifies the solution direction: the board geometry should be dynamic, not hard-coded. The answer is yes — specifically by making the inset a function of ball and peg radius so the effective peg field narrows when larger objects need more clearance.

The Geometry — Why Balls Still Got Stuck

The previous inset formula was gbBallRadius + gbPegRadius + 2. Working through the geometry:

Board wall at:           x = gbBoardLeft
Edge peg centre at:      x = gbBoardLeft + inset
Edge peg surface at:     x = gbBoardLeft + inset − pegRadius

Gap (wall to peg face):  inset − pegRadius
                       = (ballRadius + pegRadius + 2) − pegRadius
                       = ballRadius + 2

Ball diameter:           2 × ballRadius

With defaults (ballRadius=5): gap = 7,  diameter = 10  →  ball CANNOT pass (7 < 10)
With ballRadius=3:            gap = 5,  diameter = 6   →  ball CANNOT pass (5 < 6)
With ballRadius=2:            gap = 4,  diameter = 4   →  barely passes (no margin)

The Session 5 inset prevented balls from being squeezed at the wall level, but the gap between the board wall and the peg face was still smaller than a ball diameter. A ball bouncing off the edge peg toward the wall found a space it could enter but not exit.

The Fix — Two Changes

1. Corrected inset formula:

/* Before: gap = ballRadius + 2  (smaller than ball diameter) */
var inset = gbBallRadius + gbPegRadius + 2;

/* After:  gap = 2×ballRadius + 4  (ball diameter + 4px clearance) */
var inset = 2 * gbBallRadius + gbPegRadius + 4;

With defaults: gap = 14, diameter = 10 → 4px clearance. Ball passes freely. With ballRadius=10: gap = 24, diameter = 20 → 4px clearance. Still passes.

2. initBoard() called when peg radius or ball radius changes:

/* Before: only gbSetRows called initBoard */
function gbSetPegRadius(r)  { gbPegRadius  = parseInt(r, 10); }
function gbSetBallRadius(r) { gbBallRadius = parseInt(r, 10); }

/* After: all three structural setters regenerate the board */
function gbSetPegRadius(r)  { gbPegRadius  = parseInt(r, 10); initBoard(); }
function gbSetBallRadius(r) { gbBallRadius = parseInt(r, 10); initBoard(); }

This is the “adjust the board size” the user described: since inset is computed inside generateRectanglePegs() and initBoard() calls that function, changing any structural parameter now recalculates the clearance gap automatically.

Key Insight — Inset Needs to Scale With Two Radii

A ball is stuck when it can enter a gap but not exit under normal physics. The minimum gap that allows a ball to pass freely is ball diameter + margin = 2 × ballRadius + margin. The inset from wall to peg center must be gap_needed + pegRadius. So:

Required inset = (2 × ballRadius + margin) + pegRadius

The original formula had only 1 × ballRadius, not 2. This is a common geometric error: the clearance needed for an object to pass through a gap is its full diameter, not its radius. The inset formula must account for both the peg radius (to reach the peg surface) and the full ball diameter (to clear the ball through the gap).

session 7 — 10/31/2026 — ball-to-ball collisions: design discussion
🎯
S — Set Goal Answer whether balls interact with each other physically; explain the pedagogical implications; decide whether to add ball-ball collisions and in what form.
KLP

It’s difficult to discern: do the balls themselves interact with one another from a physics perspective? Elastic collisions/momentum exchange?

Current State — Confirmed No Ball-Ball Interaction

The updateAndDrawBalls() loop processes each ball independently:

for each ball:
    updateBallPhysics(ball)    // gravity + walls
    checkPegCollisions(ball)   // ball vs. all pegs
    checkBinCollection(ball)   // reached bins?
    drawBall(ball)

There is no checkBallCollisions() step. Balls pass through each other entirely. A ball’s trajectory is only influenced by gravity, pegs, and walls — never by other balls. This is not an oversight; it is a deliberate design choice with a precise statistical justification.

Why Independence Is the Correct Model

The Galton Board’s mathematical property — that it produces a binomial distribution converging to Normal — depends critically on one assumption: each ball’s path is an independent sequence of Bernoulli trials. Ball-to-ball collisions violate this assumption.

If Ball A deflects Ball B at peg row 7, Ball B’s subsequent trajectory is no longer independent of Ball A’s. The probability of Ball B landing in bin k becomes conditioned on Ball A’s path. The resulting distribution is no longer binomial; it is a correlated joint distribution whose shape depends on timing, drop rate, and the specific collision history.

In AP Statistics language: adding ball-ball collisions breaks the independence condition for the Central Limit Theorem to apply. Students would see a distribution that looks bell-shaped at low drop rates (few balls on board at once) but develops systematic distortions at high drop rates (many simultaneous balls). This would undermine the pedagogical goal of the simulation.

Could Ball-Ball Collisions Be Added? Yes — As an Optional Mode

The physics is straightforward. For two balls of equal mass and radius hitting elastically:

// Normal vector from ball1 to ball2
nx = (b2.x-b1.x)/d,  ny = (b2.y-b1.y)/d

// Relative velocity along normal
dvDotN = (b2.vx-b1.vx)*nx + (b2.vy-b1.vy)*ny

// Equal-mass elastic collision: exchange normal-component impulse
if (dvDotN < 0) {
    b1.vx += dvDotN*nx;    b1.vy += dvDotN*ny;
    b2.vx -= dvDotN*nx;    b2.vy -= dvDotN*ny;
}

The cost is O(n²) per frame — 100 active balls = 4,950 pair-checks per frame at 60 fps. This is manageable but noticeable at high drop rates.

A toggle labelled “Balls interact” could be added alongside the layout toggle, with a warning in the statistics panel when it is on: “Ball interactions enabled — distribution may deviate from theoretical binomial.”

Recommendation: Leave balls independent by default (correct statistical model). A future session could add the toggle for the physics-class use case, where demonstrating momentum exchange between billiard balls is the goal rather than demonstrating the CLT.

Classroom Use Case for Each Mode

Balls independent (current default): AP Statistics. Drop 500 balls. Demonstrate binomial → Normal convergence. Discuss independence as a condition for the CLT. Compare the gold theoretical curve to the live histogram.

Balls collide (future option): AP Physics / CS. Drop balls slowly. Watch elastic momentum exchange. Discuss conservation of kinetic energy and momentum. Observe that the distribution distorts when balls are densely packed and interfering with each other — a direct demonstration of why the independence assumption matters in statistics.

session 8 — 10/31/2026 — ball-ball physics toggle implemented
🎯
S — Set Goal Add an optional ball-to-ball elastic collision toggle with prominent warnings in both the DOM and on the canvas; update the About section; document in chatlog.
KLP

I think having a feature to turn on the physics for the balls would be very interesting, provided we have a prominent notification of how it impacts our statistics simulation. We’ll want to write this up in the chatlog and address it in our ‘About this simulation’ area too.

C.R.A.F.T. Prompt Critique — Grade: A

A — Action: Three clear deliverables. Add the toggle. Add a prominent warning. Update the About section. Each is actionable and specific.

T — Tight Constraints: “Prominent notification.” This word choice is doing real work. Not just a label — prominent means it must be impossible to miss when ball physics is on. The implementation uses two simultaneous warnings: a DOM banner in the control card AND an amber overlay on the canvas itself. Neither one alone would be “prominent” enough.

What Was Built — Five Changes

Sketch: checkBallCollisions() function — O(n²) pair detection. For each colliding pair: push balls apart to resolve overlap; compute relative velocity along normal; apply equal-mass elastic impulse exchange (scaled by gbBounceFactor). Called from updateAndDrawBalls() only when gbBallPhysics is true.

Sketch: canvas overlay — drawBallPhysicsWarning() draws an amber band at the top of the board when ball physics is on: “⚠ Ball Physics ON — distribution deviates from binomial.”

Sketch: public API — gbSetBallPhysics(flag) and gbGetBallPhysics() added. Toggle can be turned on/off at any time (unlike the layout toggle, ball physics can be toggled mid-run — it doesn’t change the board structure).

HTML: toggle + DOM warning — Bootstrap form-switch in a new “Ball–Ball Physics” section, just above Statistics. The #ballPhysicsWarning div is hidden by default and shown immediately when the toggle fires, with amber gold styling and clear language about statistical independence violation.

About section updated — explains the toggle, its effect on the independence assumption, and when to use each mode (physics OFF for AP Stats; physics ON for AP Physics/CS exploration).

Why Two Warnings

A student using the app at high drop rate might glance at the histogram and statistics panel without reading the control card. A student watching the canvas might not notice a small text label in the controls. Using both channels simultaneously ensures that nobody misses the mode change:

  • DOM banner — visible in the statistics context, where its meaning is most directly relevant. It sits between the toggle and the numbers it affects.
  • Canvas overlay — visible on the board itself, adjacent to the action. An amber band across the top of the board is a visceral visual signal that the mode has changed.
session 9 — 10/31/2026 — bell curve priority toggle
🎯
S — Set Goal Give the user control over Rectangle edge-peg placement: “Anti-Stuck” (current — inset pegs, no sticking, some edge leakage) vs. “Bell Curve Priority” (near-wall pegs, maximum containment, occasional sticking). Update About section with the tradeoff explanation.
KLP

We know that in the triangle configuration, there are ‘edge effects’ such that the ends of the histogram are often prominent, whereas a bell curve would have them showing as smaller than other bars in the graph. I’m noticing that in the rectangular configuration as we have it now, the same thing is happening: there are prominent leaks to the edge, so to speak, that didn’t happen with past rectangular configurations where longer-row pegs end balls blocked the passing of balls between them and the edge, although that caused some ‘sticking’ of the balls. Let’s let the user decide whether to have the ‘stuck ball prevention’ or not: I’d rather them have an option to actually see a bell curve creation if there are stuck balls in the board. Of course, we will need to write up an explanation of this setting and the pros and cons in the about area as well as in the chatlog.

C.R.A.F.T. Prompt Critique — Grade: A

C — Context: Excellent field observation. The user correctly identifies that our Session 6 inset fix solved sticking but introduced the same edge leakage problem the rectangle layout was designed to prevent. This is a real engineering tradeoff, not a bug: we solved one problem and re-introduced the other. The observation is precise and testable.

T — Tight Constraints: “I’d rather them have an option.” The user explicitly prefers empowering students to make the tradeoff themselves over choosing one side for them. This is the right pedagogical instinct: understanding the tradeoff IS the lesson.

F — Form: About section + chatlog specified. Both documentation targets named.

The Core Tradeoff
Anti-Stuck (default) Bell Curve Priority
Edge peg position Inset: 2×ballRadius + pegRadius + 4 Near-wall: pegRadius + 2
Wall-to-peg gap ≥ ball diameter — ball passes freely ≈ 2 px — ball cannot pass
Edge leakage Yes — outer bins slightly inflated Minimal — pegs block edge path
Sticking risk None Occasional — hit Reset to clear
Best for Long uninterrupted auto-drop runs AP Stats demonstrations (clean bell curve)
Implementation — gbBellCurvePriority
// In generateRectanglePegs():
var inset = gbBellCurvePriority
    ? gbPegRadius + 2                    // near-wall: ~2px from wall face
    : 2 * gbBallRadius + gbPegRadius + 4; // clearance: full ball-diameter gap

The toggle is locked when balls are in play (same rule as the layout toggle) since changing the inset mid-run would reposition pegs under active balls. It is greyed-out entirely when the Triangle layout is selected (edge-peg inset is irrelevant in Triangle mode).

The hint text updates to describe the active mode: “Bell Curve: near-wall pegs — cleaner distribution, occasional sticking.” or “Anti-Stuck: inset pegs — no sticking, slight edge leakage.”

Teaching Moment — Why This Tradeoff Exists

The rectangular Galton board was invented precisely to solve the triangle’s edge leakage problem. Physical rectangular Galton boards work because the pegs at the edge of each row sit very close to the board walls — close enough that a ball cannot fit between them. In a real board, the pegs are fixed metal pins and balls are small steel beads; the geometry is precise and sticking doesn’t occur because beads are much smaller than the gap.

In our simulation, balls are relatively large compared to the peg spacing (to make them visible). With elastic collision physics, a ball that hits an edge peg at an oblique angle can acquire a velocity component toward the wall and get pinched. Bell Curve Priority mode recreates the physical board’s intent (containment) while accepting the simulation’s limitation (occasional sticking). Students benefit from knowing both exist and why.

session 10 — 10/31/2026 — reset cancels pending drop timers
KLP

I notice that when I drop 200 balls for example, and reset, it does reset but the balls keep dropping. A Reset should stop everything that’s been happening prior.

Root Cause & Fix

gbDropMany(n) used setTimeout to schedule each ball drop at a future time. Calling gbReset() cleared the balls array and counts, but the scheduled callbacks were still queued in the browser’s timer system. When they fired they called gbDropBall(), which pushed new balls in regardless of the reset.

/* Before: timer ID was discarded */
setTimeout(function() { gbDropBall(); }, d);

/* After: timer ID stored; cancelled on reset */
var gbPendingDrops = [];
var tid = setTimeout(function() { gbDropBall(); }, d);
gbPendingDrops.push(tid);

/* In gbReset(): */
for (var t = 0; t < gbPendingDrops.length; t++) { clearTimeout(gbPendingDrops[t]); }
gbPendingDrops = [];

Two lines added to gbDropMany() (capture the ID, store it). Three lines added to gbReset() (loop, cancel, clear). Reset now immediately stops all pending multi-drops, clears active balls, and zeros the counters — exactly what “stop everything” should mean.

session 11 — 10/31/2026 — rectangle + bell curve as default; layout philosophy
KLP

Let’s set the default for Rectangle configurations to ‘Bell Curve’ and make any necessary changes to our explanations. I’d like the default to give us the bell curve look if possible. Also, given that statement, do you think it would be best to start with a Rectangular configuration with the option to go Triangular? Discuss the pros and cons of that approach.

Design Discussion — Triangle-First vs Rectangle-First

The user’s question raised a genuine design choice with valid arguments on both sides. Here is the reasoning we worked through:

Argument for Rectangle-First (with Triangle as option)

  • The rectangle quincunx produces a cleaner bell curve out of the box, especially with Bell Curve Priority enabled — the best first impression for CLT demonstrations.
  • The Bell Curve Priority toggle only activates in Rectangle mode; making Rectangle the default means both power settings are immediately accessible and correctly paired.
  • AP Statistics students benefit most from seeing a textbook-quality normal distribution first, then exploring why the triangle looks different as a follow-up question.
  • The first-run experience is the most important UX moment — students who drop 50 balls and see a beautiful bell curve are more engaged than students who see a ragged-edge triangle and wonder “is this broken?”

Argument for Triangle-First (classical default)

  • Galton himself used a triangle. The triangle IS the canonical Galton Board. Defaulting to rectangle risks confusing students who look up the Galton Board elsewhere and see a different shape.
  • The triangle is mathematically simpler to explain: row r has r + 1 pegs, each ball makes exactly n binary left/right decisions, and the bin number equals the count of right-bounces. Clean.
  • The triangle edge-leakage effect, while visually imperfect, is itself a teaching moment: physical Galton boards have the same issue, and discussing why is authentic statistics education.
  • Renaming “Rectangle” as the default subtly implies the triangle is unusual — a slight misrepresentation of the history.

Decision

We went with Rectangle + Bell Curve Priority as the default, because the primary goal of the simulator is to show students that the bell curve emerges naturally from independent random decisions — and the cleanest possible visual payoff should be the first thing they see. The triangle is one toggle away with a clear label and an honest explanation in the About section of why it behaves differently.

Changes Made
  • sketch.js globals: gbLayoutMode = 'rectangle' and gbBellCurvePriority = true — sketch initializes in the best-for-CLT state.
  • app.html layout toggle: checked attribute added so the UI matches the sketch default on load.
  • app.html bell curve toggle: checked attribute added and disabled/opacity:0.42 removed — the toggle is live and checked at startup (rectangle + bell curve mode).
  • Hero subtitle: Updated from “triangular array of pegs” to “rectangular quincunx… or switch to the classic triangular layout” to honestly describe both modes.
  • About section: Restructured to lead with the rectangle/Bell Curve default, explain its rationale, then describe Triangle as the historical alternative. Anti-Stuck and Ball–Ball Physics notes retained and reordered.
  • scripts.js onResetConfiguration(): Now also resets the layout and bell curve toggles back to Rectangle + Bell Curve Priority (previously only reset sliders).
  • scripts.js DOMContentLoaded: Added an immediate updateLayoutToggle() call so hint text and opacity are correct from the very first frame, not after the first 500 ms poll.
session 12 — 10/31/2026 — tuned default settings after live playtesting
KLP

I played with some initial settings. In Rectangle mode, let’s make the default/on-load settings to be 14 rows, peg radius 7, ball radius 5, drop interval 300, speed 1, gravity .8 and bounce factor .5

Settings Update — Playtested Defaults

After hands-on experimentation with the Rectangle + Bell Curve configuration, the user identified a better-feeling default parameter set. The changes and their rationale:

Parameter Old New Effect
Peg Rows1014More pegs = more binary decisions per ball = CLT convergence is faster and the bell shape is cleaner. With the binomial distribution, mean is n/2 and σ ≈ √(n/4), so 14 rows gives σ ≈ 1.87 bins — a noticeably tighter, taller bell curve.
Peg Radius77Unchanged.
Ball Radius55Unchanged.
Gravity0.30.8Higher gravity pulls balls down faster, reducing the time they spend bouncing sideways between rows. The result is a crisper, less chaotic fall through the peg field.
Bounce Factor0.60.5Slightly less elastic bounce means balls shed lateral energy faster after each peg collision, settling into a more decisive left/right decision rather than bouncing ambiguously.
Drop Interval150 ms300 msDoubles the spacing between balls. With 14 rows and higher gravity, the board is fuller at any given moment; 300 ms prevents crowding and keeps each ball’s path visually trackable.
Speed1.01.0Unchanged.

All four touched files were updated in tandem: sketch.js globals, app.html slider value attributes and display spans, and the DEFAULTS object in scripts.js — so Reset Configuration restores this playtested set, not the old values.

session 13 — 10/31/2026 — snapshot button — live canvas capture
KLP

Let’s include a button to take a screen shot of the board. Arrange it such that it does not interfere with the action of the simulation, even when we save it. I’ve noticed that sometimes that action resets the app. Is this possible to do? It would be nice to show a screen shot of the balls in action rather than just once all balls have fallen.

Options — Screenshot Without Disrupting the Simulation

The “resets the app” behavior has a known cause: p5.js’s built-in saveCanvas() function manipulates the DOM when it creates its temporary download link, which in some browsers fires a resize event. If a windowResized() handler exists, p5.js reinitializes the canvas — effectively a reset. Four options were considered:

Option Interrupts? Notes
p5.saveCanvas()Yesp5.js built-in. DOM manipulation can trigger a canvas resize/reset. Root cause of the bug the user observed.
Native toDataURL() + anchorNoReads the GPU framebuffer directly from the canvas element. Zero p5.js involvement. Simulation keeps running; captures whatever is on screen at that instant, including balls mid-flight. Chosen.
toBlob() + createObjectURL()NoSame safety guarantee as option 2, but uses a Blob — better for very large canvases. Our 440×720 canvas is small enough that a data URL is fine; this would be overkill.
OS / browser screenshotNoOutside the app’s control; no filename convention, no integration.

Why the live-frame capture is especially useful: Because toDataURL() reads the canvas at the exact moment the button is clicked, the downloaded PNG shows balls in mid-flight. Students can screenshot a dramatic cascade of 50 balls halfway down the board — a much more dynamic illustration of the random walk than a completed histogram.

Filename convention: galton-board-YYYYMMDD-HHMMSS.png so multiple screenshots sort chronologically in a downloads folder. ES5-safe zero-padding uses the n < 10 ? '0' + n : '' + n pattern rather than ES6 String.padStart().

Changes Made
  • sketch.js — added gbSaveCanvas(): queries #sketchContainer canvas, calls .toDataURL('image/png'), creates and immediately removes a hidden <a> element to trigger the browser download. p5.js is not involved at any step.
  • scripts.js — added thin onSaveCanvas() wrapper (same pattern as all other public-API callers).
  • app.html — added a gold Snapshot button (.btn-gb-snap) directly below the Reset button in the Ball Controls section. A title attribute surfaces the “simulation keeps running” note as a tooltip.
  • styles.css — added .btn-gb-snap rule: gold border and text, transparent background, matching hover lift — visually distinct from the blue action buttons and muted reset button, signalling it is a “capture” not a “control” action.
session 14 — 10/31/2026 — snapshot reload fix (toBlob) + median & mode stats
KLP

I took a snapshot and then saved it. Once I did that, the app had re-loaded. That’s the behavior I was mentioning earlier. The image/save process reloaded the app. What are the best options here then? Also, in the stats area, let’s include a mode and median calculation too, for completeness.

Root Cause — Why toDataURL() Still Caused a Reload

Even though the previous session avoided p5.js’s saveCanvas(), the replacement used canvas.toDataURL() which returns a data: URL. Several browsers (notably Safari, and Chrome under certain security flags) treat data: URLs with a download attribute as a navigation event — the browser briefly navigates to data:image/png;base64,... and then back, which triggers a page reload. The simulation state is lost. Three better approaches exist:

Option Interrupts? Notes
toBlob() + URL.createObjectURL()NoCreates a blob:// URL. Browsers universally treat blob URLs as file downloads, never as navigation. Revoke the URL after the click to free memory. Chosen.
window.open(dataURL)NoOpens the image in a new tab. Current page is completely unaffected. Downside: popup blockers may intercept it; user must right-click → Save As manually.
Bootstrap modal with <img>NoShows the snapshot in an overlay on the page; user right-clicks to save. Best UX (see-before-save), but requires a full modal implementation. Worth considering if the snap feature becomes a primary workflow.

Why toBlob() is async but still captures the right frame: toBlob() reads the canvas pixel data at the moment it is called. The callback fires a few milliseconds later with a finished Blob object. By that time the draw loop may have painted one or two more frames, but the captured image is always from the frame at click time — not the frame when the callback fires.

Memory cleanup: URL.revokeObjectURL() is called 200 ms after the click, after the browser has had time to register the download request. Skipping cleanup would leak the blob from memory until the page unloads; this way it is freed almost immediately.

Stats Panel — Median and Mode Added

The stats panel now shows all three measures of center alongside spread and shape:

  • Median Bin — calcMedianBin(): walks gbBallCounts[] accumulating a running sum until it reaches or exceeds half the total. The first bin that crosses that threshold is the median. For a symmetric bell curve the median should equal the mean (both near n/2).
  • Mode Bin — calcModeBin(): returns the index of the bin with the highest count. For a well-formed normal distribution the mode, median, and mean should all converge on the same central bin as ball count grows — a direct, live demonstration of the relationship between the three measures of center.

Display order: Total → Mean → Median → Mode → Std Dev → Shape. This follows the AP Statistics convention of presenting measures of center together before spread. Both new elements are cleared to “—” on Reset alongside the existing stats.

session 15 — 10/31/2026 — snapshot rethink — modal overlay approach
KLP

Here is the current behavior: I start the app, and let it run for a bit. I take a screenshot and immediately a panel opens to let me save it. The app is still running in the background. Once the image is saved, the windows panel dispatches and the browser window returns. A notification from Chrome shows the downloads history popup and meanwhile, the page reloads. The action was terminated by the save. The chatlog indicates we took care of this problem but the screenshot→save sequence still reloads the page.

True Root Cause — Chrome Focus Return + Downloads Notification

The previous two fixes (Sessions 13 and 14) addressed the wrong layer of the problem. Both toDataURL() and toBlob() + createObjectURL() were improvements, but neither eliminated the triggering sequence. The actual chain is:

  1. Snapshot button clicked → OS “Save File” dialog opens → browser window loses focus (blur event)
  2. User saves the file → OS dialog closes → browser regains focus (focus event)
  3. Chrome shows the downloads notification UI → browser viewport height changes slightly → resize event fires
  4. p5.js in global mode has internal resize listeners. In some configurations, the resize or focus events trigger a sketch re-initialization that looks like a page reload

The problem is not the URL scheme (data: vs blob:). It is the OS save dialog → Chrome downloads notification sequence. Any mechanism that triggers a native OS dialog and a downloads-bar notification will have this problem.

Options considered:

Option Interrupts? Notes
Bootstrap modal (in-page overlay)NoImage displayed in a modal on the current page. No OS dialog, no Chrome notification, no download mechanism triggered. User right-clicks the image to save. Simulation draws behind the overlay. Chosen.
window.open(blobUrl)NoCurrent page unaffected; image opens in a new tab. Popup blockers can intercept it, and the user must right-click → Save As in the new tab. Viable fallback.
Any download (data: or blob:)YesAll download mechanisms trigger the OS dialog → focus return → Chrome notification sequence. The reload is a Chrome-side effect on the hosting page, not a bug in our code per se.
Changes Made
  • sketch.js gbSaveCanvas(): Completely rewritten. Now uses canvas.toDataURL() (synchronous, safe as an <img src>) and sets the result as the source on #snapshotImg inside the modal, then calls bootstrap.Modal.getOrCreateInstance().show(). No download, no OS dialog, no Chrome notification.
  • app.html modal: Bootstrap 5 modal added before </body>. Contains #snapshotImg, a right-click instruction, and a Close button. The fade class gives a smooth overlay animation. The simulation draw loop continues behind the semi-transparent backdrop.
  • styles.css .gb-snapshot-modal: Navy card background, gold title, steel-blue borders — matches the app palette.
  • Snapshot button tooltip updated to describe the new behavior: “right-click the image to save.”

Why setting img.src = dataURL is safe: Assigning a data: URL as the src attribute of an <img> element is handled entirely by the browser’s image decoder. It does NOT trigger a navigation event, does NOT change window.location, does NOT open an OS dialog, and does NOT fire any focus/resize events. It is categorically different from navigating the page to a data: URL.

session 16 — 10/31/2026 — snapshot confirmed workflow: open in new tab
KLP

I liked that idea. When I tried it a ‘Save image as’ still triggered a reload. But when I said to open the image in another tab, the process continued and then I could use that tabbed image to save it. That seems like a good compromise — we just have to clarify the sequence to our novices.

Live-Test Result — Final Confirmed Workflow

The modal approach (Session 15) was correct in principle, but live testing revealed one remaining edge case: right-clicking the modal image and choosing Save image as… still opened a native OS save dialog from within the modal. That dialog triggers the same Chrome focus-return sequence — and the simulation reloaded. So “Save image as” anywhere on this page causes the reload.

However, right-clicking → Open image in new tab did not reload the simulation. The new tab opened with the image; the original tab kept running. Saving from the new tab (OS dialog fires there, not here) left the original simulation completely intact.

This is the definitive safe workflow:

  1. Click Snapshot → modal opens with the captured frame
  2. Click Open in New Tab (the gold button in the modal footer)
  3. In the new tab: right-click → Save image as… or Ctrl+S
  4. Return to the original tab → simulation has been running the entire time

Why this works: Opening a new tab is a browser-native navigation in a different browsing context. The current page’s JavaScript context is untouched. The new tab owns its own focus cycle and its own OS dialog interactions. Any save-dialog reloads happen in the new tab, which contains only the PNG image.

Changes Made
  • sketch.js gbSaveCanvas(): Added snapshotTabLink.href = dataURL so the “Open in New Tab” anchor is pre-loaded with the current frame at click time.
  • Modal footer (app.html): Replaced the right-click hint with a prominent gold Open in New Tab button (<a target="_blank"> — a real anchor, not programmatic .click()). Added a red-tinted warning: “Do not right-click → Save image as here — it reloads the simulation.” Close button relabeled “Close & Resume” to reinforce that the simulation is still running.
  • Snapshot button tooltip updated to describe the new-tab workflow.
session 17 — 10/31/2026 — snapshot new tab fix — blob: url via atob()
KLP

I like the instructions and the ‘Open in new tab’ button, but when I use that (as opposed to a classic right mouse click and select save image in new tab) a new tab opens with no picture and hovering over the tab reveals ‘Loading’ — no picture loads.

Root Cause — Chrome Blocks data: URL Navigation in New Tabs

The previous session pre-wired the Open in New Tab anchor with href="data:image/png;base64,...". That worked when the user right-clicked and chose Open image in new tab (the browser handles that as a trusted image-context action), but not when the button was clicked directly to follow the link. The reason is a Chrome security policy introduced in Chrome 60 (2017):

Chrome blocks renderer-initiated navigations to data: URLs in new tabs and windows. The tab opens but Chrome refuses to load the data URL as a page, leaving it permanently in a “Loading” state.

This policy exists to prevent phishing via data: URL spoofing. It applies to <a href="data:..." target="_blank"> link-follows and to window.open("data:..."). It does not apply to blob:// URLs, which Chrome treats as legitimate file references.

Fix — Convert to blob: URL at Click Time

The <a> anchor was replaced with a <button onclick="onSnapshotOpenTab()">. The handler in scripts.js converts the data URL to a blob URL synchronously at click time using atob(), then opens it with window.open(blobUrl, '_blank'). Because window.open() is called directly inside a click event handler, it counts as a user gesture and popup blockers do not intercept it.

// data URL  →  Uint8Array  →  Blob  →  blob:// URL  →  window.open()
var bstr   = atob(b64);               // synchronous base64 decode
var u8arr  = new Uint8Array(n);
while (n--) { u8arr[n] = bstr.charCodeAt(n); }
var blob    = new Blob([u8arr], { type: 'image/png' });
var blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, '_blank', 'noopener,noreferrer');
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 15000);

The blob URL is revoked after 15 seconds — enough for the new tab to decode and display the full PNG. revokeObjectURL() frees the browser’s in-memory reference to the blob; once the new tab has painted the image it no longer needs the URL. The 15-second window is generous even on a slow machine.

session 18 — 10/31/2026 — snapshot ux polish — no more alert(), clearer instructions
KLP

I click the Open in New Tab button and navigate there. The app is still running. When I go back it reports: “Popup blocked. Please allow popups for this site and try again.” I don’t like the sound of that — most people don’t want popups. If I close and resume the app is still running, but if I go ahead in the new tab and save, the app resets. I can live with this as long as we clarify to the user: take snapshots at strategic times but wait to save until you are finished with your run. The main thing is the ominous popup warning.

Removing the alert() — Inline Notice Instead

The alert() dialog was the wrong choice for this situation. alert() is modal, blocks all JavaScript, and uses the word “popup” in the same breath as a security warning — immediately alarming to anyone who has been taught to distrust popups. Replacing it with a calm inline notice inside the snapshot modal achieves the same communication without the alarm:

  • The notice only appears inside the existing modal, not as a separate disruptive dialog
  • It uses neutral gold color (informational, not red/danger) and an info icon rather than a warning triangle
  • It directs the user to the browser’s own popup-icon in the address bar, which is the correct fix
  • It resets to hidden the next time Close & Resume is clicked, so it doesn’t linger
  • If the popup was not blocked (the normal case), the notice stays hidden — most users will never see it

Note on terminology: In browsers, “popup” is the technical term for any new tab or window opened programmatically by a page, not just the annoying advertising kind. Chrome’s popup blocker blocks all window.open() calls that are not direct user gestures. Our call IS a direct user gesture (button click), so in practice it should rarely be blocked — the alert() was solving a mostly-hypothetical edge case in the worst possible way.

Confirmed Workflow & Updated Instructions

The modal instructions were rewritten to match the live-tested, working workflow and to set the right expectations upfront:

“Click Snapshot as many times as you like during a run — each one opens here so you can review it. Click Open in New Tab to park it safely, then finish your run before saving. Saving from the new tab will end the current run, so save all your images after you are done.”

This framing treats the save-causes-reset behavior as a workflow rather than a bug: snapshot freely, collect your images in tabs, complete your run, then save everything at the end. For AP Stats students capturing a bell curve in progress, this is actually good practice — it encourages them to think about when the distribution is most statistically meaningful before capturing it.

session 19 — 10/31/2026 — gold curve explained + theoretical panel
KLP

I have a clarifying question about the gold bell curve shape overlaying the empirical histogram: is it the theoretical curve associated with the current standard deviation of the data? Is it like a ‘best fit curve’ based on the heights of the bars? How is it constructed? Would it be worth showing it underneath the stats panel?

What the Gold Curve Actually Is

Reading drawBellCurveOverlay() directly resolves the question with precision. The gold curve is neither derived from the empirical standard deviation nor a best-fit curve. It is the exact theoretical binomial PMF:

// For each bin k (0..n):
P(k) = C(n, k) × (0.5)^n

// Scale so the theoretical peak matches the tallest empirical bar (visual only):
var peakPMF = binomialPMF(n, floor(n / 2));
var scale   = maxCount / (peakPMF * totalInBins);

The curve uses only gbPegRows (n) as its mathematical input. It does not read the empirical mean, median, mode, or standard deviation. The scale factor is a purely visual adjustment to make the peak of the theoretical prediction align with the tallest bar in the empirical histogram — so the two shapes can be compared at the same visual scale. The scale factor is NOT a statistical fit.

Property Gold Curve Best-Fit (e.g., regression)
Derived fromProbability theory (n, p = ½)The data itself
Uses empirical values?No — zero data dependencyYes — minimizes residuals
Changes if you change rows?Yes (n changes)Yes (fitted to new data)
Changes if more balls fall?No (only scale shifts)Yes (residuals recomputed)

The pedagogical payoff: When the empirical histogram converges toward the gold curve, the student is watching the Central Limit Theorem happen in real time — not a curve chasing the data, but data converging toward a pre-ordained mathematical prediction.

Theoretical Panel Added Below Stats

A Gold Curve — Theoretical panel was added below the empirical stats. It shows:

  • The formula — P(k) = C(n,k) × (½)n with the live value of n
  • Predicted Mean — n/2 (exact, derived from binomial distribution properties)
  • Predicted σ — √(n × 0.25) (exact, the binomial standard deviation for p = ½)

Both values update live when the Peg Rows slider is moved (they are computed in updateStatsDisplay() every frame, independent of ball count). With 14 rows: Predicted Mean = 7.00, Predicted σ = 1.87. As balls accumulate, students can watch the empirical Mean Bin and Std Dev row approach those predicted values — a direct, live convergence demonstration.

The note paragraph uses a left gold border and subtle gold-tinted background to visually tie it to the gold curve on the canvas, making the connection between the panel and the canvas element unmistakable.

session 20 — 10/31/2026 — html validation fixes — aria, headings, and the ghost gif
KLP

Please fix the validation errors about aria-labelledby in line 446, the heading h5 skip from h1 (without changing the appearance) and the bad value for attribute src on img in line 458. — Then: there is a role validation error on the aria element in 446 and element img missing an attribute on line 458.

Fix 1 — aria-labelledby + aria-hidden="true" Conflict

The snapshot modal had aria-hidden="true" in the static HTML alongside aria-labelledby="snapshotModalLabel". These two attributes create a direct contradiction:

  • aria-labelledby says: “the accessible name for this element comes from element X”
  • aria-hidden="true" says: “this entire element (and all its descendants) is invisible to the accessibility tree”

That means the element being labeled and its label are both hidden — the aria-labelledby reference is useless. Bootstrap 5 manages aria-hidden dynamically via JavaScript (it adds it when the modal closes, removes it when the modal opens). It must not be set in the static HTML when aria-labelledby is also present. Fix: remove aria-hidden="true" from the modal <div>.

Fix 2 — Heading Hierarchy Skip (h1 → h5)

The modal used <h5 class="modal-title"> for its title. The page’s hero section uses an <h1>, and there are no h2, h3, or h4 elements between them. HTML validators require heading levels to descend one step at a time; skipping from h1 directly to h5 is an outline error that can confuse screen readers.

Fix: Change <h5 class="modal-title"> to <p class="modal-title h5 mb-0">. Bootstrap provides utility classes (.h1 through .h6) that apply the exact font-size, weight, and line-height of any heading level to any element. The result is visually identical — the modal title still looks like an h5 — but it is now a paragraph (<p>) in the document outline, which does not participate in the heading hierarchy at all.

Fix 3 — aria-labelledby Without a role

After removing aria-hidden, the validator raised a second complaint: the <div> hosting aria-labelledby had no role attribute. aria-labelledby is only meaningful on elements whose role is understood by the accessibility tree. A plain <div> has an implicit role of “generic,” which is not a valid host for a labeling relationship.

Fix: Add role="dialog" to the modal <div>. Bootstrap adds this via JavaScript at runtime, but the HTML validator only sees the static markup. Declaring it explicitly in the HTML makes the static document valid AND is redundant-but-harmless once Bootstrap’s JS runs.

Fix 4 — The “Ghost GIF” Placeholder for img src

The snapshot <img> element had no src attribute, because its source is set entirely by JavaScript when the Snapshot button is clicked. The HTML specification, however, requires every <img> to have a src attribute with a non-empty, valid URL. Removing src="" (an empty value, also invalid) still leaves the attribute absent — still invalid.

The fix — the 1×1 transparent GIF:

src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="

This is a data URI — an entire file embedded directly in the HTML as a base64-encoded string. Decoded, it is a 35-byte GIF89a image measuring exactly 1×1 pixel, coloured fully transparent. It is the smallest legal image file that a browser will accept. The browser renders it as nothing visible (one invisible pixel) and the attribute is fully valid.

When JavaScript runs imgEl.src = dataURL on a Snapshot click, the browser replaces this placeholder with the real canvas frame. The tiny GIF is discarded — it was only ever there to satisfy the validator.

This is a well-known technique among experienced web developers. It solves a class of problem that comes up whenever an image’s source is not known at page-load time — lazy-loaded images, dynamically generated previews, drag-and-drop upload targets, and so on. The 1×1 transparent GIF placeholder has been in use since the late 1990s and remains the go-to solution today because it is the smallest possible valid value for a src attribute.

session 21 — 10/31/2026 — live-server snapshot failure — blob url revoked too early
KLP

I just loaded the Galton Board live and attempted to save an image as before. This time, I’m getting a notification that the file wasn’t saved and that I need to check my Internet connection. My connection is fine. Why would this be happening now when it wasn’t when I was doing field testing in our sandbox here in VS Code?

Root Cause — Blob URL Revoked Before the User Could Save

The error “file wasn’t saved, check your internet connection” is Chrome’s generic response when it tries to fetch a URL and gets nothing back. It is not a real network error — it means the URL no longer exists. In our case, the URL was a blob:// URL that had already been revoked.

Here is the exact chain:

  1. User clicks Snapshot → modal opens
  2. User clicks Open in New Tab → URL.createObjectURL(blob) creates a blob:// URL; window.open(blobUrl) sends it to a new tab; a setTimeout(revoke, 15000) is started in the background
  3. User returns to the main tab and watches the simulation continue — more than 15 seconds pass
  4. setTimeout fires → URL.revokeObjectURL(blobUrl) destroys the blob URL in the parent tab’s memory registry
  5. User switches to the new tab and presses Ctrl+S → Chrome tries to “download” the current page, which means fetching its URL — the blob URL — but it has been revoked. Chrome reports: “File wasn’t saved. Check your Internet connection.”

Why it worked in VS Code but not live: During sandbox testing, everything happened within 15 seconds — the user took a snapshot and saved almost immediately. On the live server, the user watched the simulation continue for longer before switching tabs to save. The 15-second timeout was an arbitrary guess; the correct answer is that the blob URL must stay alive for as long as the user needs it, not for a fixed interval.

A note on blob URL scope: A blob:// URL is scoped to the document that created it. When the parent tab calls URL.revokeObjectURL(), the URL is removed from the parent document’s object URL registry. Even though the new tab has the URL as its page URL, Chrome must look it up in the originating document’s registry when the user tries to save — and if it has been revoked, the lookup fails. This is why noopener,noreferrer does not help: the revocation happens in the parent, and the blob URL belongs to the parent.

The fix: Remove the setTimeout entirely. Instead, store the current blob URL in a module-level variable (_snapshotBlobUrl). Each time the user clicks Open in New Tab, revoke the previous blob URL (if any) before creating the new one. The memory footprint is trivial (one canvas PNG ≈ 50–150 KB) and the browser automatically releases all blob URLs when the page unloads anyway.

Change Made
  • Added var _snapshotBlobUrl = null; as a module-level variable in scripts.js
  • In onSnapshotOpenTab(): before createObjectURL(), call revokeObjectURL(_snapshotBlobUrl) if it exists — this cleans up the previous snapshot’s blob, not the current one
  • Removed setTimeout(revokeObjectURL, 15000) entirely — the current snapshot’s blob URL now persists for the lifetime of the page session
  • The user can now take as long as they need before saving from the new tab — the blob URL will not expire
session 22 — 10/31/2026 — bell curve tutorial — from coin flips to clt for algebra 2
KLP

You are an AP Statistics teacher, web designer and co-author of apps at TNT. You realize that many students who would enjoy this app may only be in Algebra2. They know nothing about bell curves, let alone the ‘Central Limit Theorem.’ Can you write a webpage, bellCurveTutorial.html with associated bellCurveTutorialStyles.css that takes the hand of a bright Algebra2 student and helps them understand this fun idea? I can run the app to generate pictures that you can use for illustrations, just tell me what you need. Make the page fit into this ecosystem and cross link it with the app and the chatlog. When we’re done, a student should understand the binomial expansion idea, Pascal’s triangle, and the relationship of those ideas to probability. You are the ‘Richard Feynman’ of statistic clarity and the tutorial will be fun and engaging.

C.R.A.F.T. Prompt Critique
Element Assessment Grade
ContextAudience specified precisely (Algebra 2 students), prerequisite knowledge stated (no bell curves, no CLT), ecosystem context given (TNT app ecosystem)A
RoleThree distinct roles specified: AP Stats teacher, web designer, TNT co-author. The Feynman analogy adds a powerful style constraint that is immediately actionableA
ActionClear deliverables: HTML + CSS files with specified filenames. Cross-linking specified. Three explicit learning outcomes named (binomial expansion, Pascal’s, probability connection)A
FormTwo-file deliverable with names specified. Integration with existing ecosystem mentioned. Image sourcing delegation (user will capture from app) is an elegant workflow choiceA
TightSlight room to tighten: section count and page length are unspecified. A strong prompt might add “10 sections max” or “keep it under X words.” The Feynman constraint partially substitutes for this, but length discipline could be more explicitB+

Overall: A− — One of the strongest prompts in this chatlog. The Feynman style directive is particularly effective: it specifies not just what to produce but how to think while producing it — start with what the student knows, build intuition before formulas, use concrete numbers first. The image-sourcing arrangement (“tell me what you need”) is also sophisticated prompt engineering: it divides the task correctly between AI (structure and content) and human (live app screenshots).

Tutorial Structure & Design Decisions

The tutorial (bellCurveTutorial.html) follows a strict Feynman arc: each section begins with something the student already knows and ends with something they did not know they were about to learn.

  1. One Peg = One Coin Flip — anchors to a familiar analogy
  2. Two Rows — Counting Every Path — introduces path counting with a visual tree; no formulas yet
  3. Pascal’s Triangle — emerges naturally as “the pattern you were looking for”; built in pure HTML/CSS (no image needed)
  4. Your FOIL Skills Pay Off — the binomial expansion (L+R)n uses algebra students already know; the coefficients are visibly Pascal’s Triangle
  5. The Formula — P(k) = C(n,k) × (½)n introduced after the student has already seen it in disguise as path counts
  6. More Rows = Better Bell — a CSS bar-chart comparison of n=4, n=8, n=14 makes the convergence visual without requiring a screenshot
  7. Why the Middle Always Wins — the combinatorial intuition in plain language
  8. Data Chases the Formula — convergence; empirical vs theoretical
  9. The Big Idea — CLT named and explained without proof; heights, scores, measurement error as real-world examples
  10. Try the Simulator — four structured experiments with specific settings

Image requests for KLP: Three specific app screenshots are needed. Each placeholder in the tutorial shows exact app settings and a suggested filename. Images A and B show 4-row and 14-row distributions. Image C shows convergence at 400 balls. Pascal’s Triangle, the path tree diagram, the binomial expansion table, and the row-comparison bars are all rendered in HTML/CSS — no screenshots needed for those.

session 23 — 10/31/2026 — 4-row triangle gives flat distribution — peg density analysis
KLP

Perhaps our triangle arrangement needs adjustment because 30 balls with 4 rows is basically an even distribution. Even 230 balls had heights of 45, 42, 47, 54 and 42. Not a compelling illustration of our point. Could the distance between rows be an issue or perhaps larger pegs and larger balls? What ‘should’ we have gotten with 30 balls with 4 rows?

What the Theory Predicts vs What We Got

Pascal’s Row 4: 1, 4, 6, 4, 1 — total = 24 = 16 paths. Theoretical predictions:

Bin P(k) Expected (30 balls) Expected (230 balls) Observed (230 balls)
0 (edge)6.25%~2~1445
125%~7~5742
2 (middle)37.5%~11~8647
325%~7~5754
4 (edge)6.25%~2~1442

Theory predicts the middle bin should have 6× more balls than the edge bins. The observed distribution is essentially uniform (46 per bin ≈ 230/5). Something is preventing the binomial probability model from operating correctly.

Root Cause — Peg Density Too Low for Reliable Ball-Peg Interaction

The binomial model requires that every ball makes exactly n independent left/right decisions — one at each peg row. In the simulation this only happens if the ball cannot physically pass between two adjacent pegs without triggering a collision. The numbers reveal why 4 rows fails:

// Triangle mode, 4 rows:
gbPegSpacing = (440 - 60) / (4 + 1)  = 76 px   ← gap between pegs
collision radius = ballRadius + pegRadius = 5 + 7 = 12 px
"live" capture zone per gap = 2 × 12 = 24 px
coverage = 24 / 76 ≈ 32%   ← only 1 in 3 ball-widths can hit a peg

// 14 rows (default):
gbPegSpacing = 380 / 15 = 25 px
coverage = 24 / 25 ≈ 96%   ← nearly every ball hits every peg

With only 32% coverage per gap, most balls fall between pegs without triggering a collision. A ball that misses a peg row makes no left/right decision for that row — it continues in whatever direction it was already traveling. The result is that the empirical distribution reflects initial drift and wall bounces rather than binomial probability, and converges toward uniform.

The Triangle layout compounds this because Row 0 has only ONE peg (at the exact center). If a ball exits the funnel with any horizontal drift, it can miss that center peg entirely and cascade past subsequent rows with limited interaction.

This is also why physical Galton boards use many short pegs packed closely together — not a few widely-spaced ones. The board in the Natural History Museum’s Galton Board replica has hundreds of pegs. The density is the mechanism, not the geometry.

Fix — Tutorial Updated to Use 8 Rows

The tutorial’s Image A request was updated from “Triangle mode, 4 rows, 30 balls” to Rectangle mode, 8 rows, Bell Curve Priority ON, 100 balls. Reasons:

  • 8 rows gives peg spacing ~47 px → ~51% coverage → most balls reliably interact with every row
  • Rectangle mode ensures full-width coverage; no ball can slip past a row without encountering a peg
  • Bell Curve Priority ON prevents edge leakage and keeps the empirical distribution close to theoretical
  • Row 8 of Pascal’s Triangle: 1, 8, 28, 56, 70, 56, 28, 8, 1 — center (70) is 70× more common than the edge (1), giving a dramatic and visually unambiguous bell shape

The tutorial text was also updated with an honest explanation of the peg-density effect — framed as a teaching point: real physical devices must be engineered to match their mathematical models, and the 4-row case is a perfect example of where that engineering breaks down. Empirically confirmed settings for Image A: Peg Radius 10, Ball Radius 8. At 8 rows the inter-peg gap is ~47 px; with combined collision radius = 18 px, the coverage zone = 36/47 = 77% — sufficient for a convincing bell shape at 100 balls. The default radii (peg 7, ball 5) give only 51% coverage at this row count and produce a less convincing result.

session 24 — 10/31/2026 — normal distribution pdf added to tutorial
KLP

I know there’s a really fancy equation for the bell curve using the Math constant e. I think it is worth mentioning that as we finalize the tutorial. We have the probability equation, why not the actual bell curve equation with a description of their interrelationship?

The Normal Distribution PDF and Its Relationship to the Binomial PMF

A subsection titled “The Formula’s Famous Cousin” was added to Section 5 of the tutorial, extending it from the binomial PMF to the continuous normal distribution. The two formulas:

Binomial PMF:   P(k) = C(n,k) × (½)n   [discrete]
Normal PDF:     f(x) = (1/(σ√(2π))) × e−½((x−μ)/σ)²   [continuous]

The de Moivre–Laplace Theorem (1733, extended 1812) proves they converge as n → ∞. The connection is the mathematical spine of the Central Limit Theorem.

Pedagogical choices for Algebra 2 level:

  • e and π both introduced as “fundamental constants like π from geometry” — Algebra 2 students know π, so framing e as equally fundamental gives it the right weight without requiring a formal definition via limits
  • The exponent decoded step by step: the fraction (x−μ)/σ is introduced as “how many standard deviations from center,” then squared (always positive), then negated (larger distance = smaller height) — the entire bell shape explained without calculus
  • A four-row decay table shows eE at z = 0, 1, 2, 3 (100%, 61%, 14%, 1%) making the drop-off pattern concrete and memorable
  • The convergence bridge formula shown in the insight box connects the two formulas visually so students can see the binomial and normal as versions of the same thing
  • Historical credit: de Moivre (1733) and Laplace (1812) named — statistics has real people with a real history

CSS additions: .tut-subheading, .tut-subheading-sm (lighter heading variants for subsections), and .gb-hr (section divider line matching the Galton Board app’s border color).

session 25 — 11/01/2026 — e^E replaced with e^(−½z²) — introducing the z-score
KLP

In our equation featuring e, you use a symbol, E, for the exponent. I think it can be confusing to see eE. What other option could we use to denote the exponent for more clarity?

Notation Options Considered
Option Form Assessment
Keep EeEVisually confusing — the eye sees two instances of the same letter and may read it as a tautology or typo.
Full inlinee−½((x−μ)/σ)²Correct but the superscript becomes very crowded; hard to read at typeset sizes.
exp() notationexp(−½z²)Standard in mathematics and code; avoids the letter entirely. Good but unfamiliar to Algebra 2 students.
z-scoree−½z²  where z = (x−μ)/σBest. The z-score is the single most important standardization concept in statistics. Naming it z here introduces the concept precisely where it naturally lives, and the formula e−½z² is unambiguous.

The z-score choice also adds pedagogical value: “You will meet the z-score again throughout statistics; it is one of the most useful ideas in the whole subject.” An Algebra 2 student who remembers that the bell curve involves e−½z² has already internalized why bell curves are always centered at the mean and symmetric.

Changes Made
  • Formula box 1: eE → e−½z²
  • Formula box 2 (where clause): E = −½ × (x−μ)²/σ² → z = (x−μ)/σ with label “the z-score: standard deviations from center”
  • Prose: rewritten to introduce z-score by name, note that it recurs throughout statistics
  • Decay table headers: E = −½z² and eE → Exponent −½z² and e−½z²
session 26 — 11/01/2026 — systematic left bias fixed — closest-peg collision only
KLP

I ran the simulation for 200 balls several times with a ball radius of 6 and a peg radius of 7 for 14 rows. Twice I’ve gotten an alarming bias: the bell curve is shifted to the left of the board. It’s a beautiful bell but it’s not centered as we’d expect. Can you ensure the board is ‘playing fair?’ — Follow-up: When I back the ball radius down to 5, the bell is more centered.

Diagnosis — Sequential Double-Collision Creates Systematic Left Drift

The observed distribution (empirical peak at bins 4–5 vs theoretical center at bin 7) represents a shift of ~2.5 bins, roughly 19 standard deviations from the expected mean. This is impossible by chance. The board IS symmetric in its peg layout (center peg of every even row lands at exactly x = 220). The bias comes from the collision resolution code when the ball is large enough to overlap two adjacent pegs simultaneously.

The critical threshold — ball diameter vs peg gap:

// With ball radius 6, peg radius 7, Bell Curve Priority, 14 rows:
rSpacing       = 362 / 14 ≈ 25.86 px   (gap between peg centres)
peg surface gap = rSpacing - 2 × pegRadius = 25.86 - 14 = 11.86 px
ball diameter   = 2 × ballRadius = 12 px

12 > 11.86  →  ball CANNOT fit between adjacent pegs without touching both.

// With ball radius 5 (default):
ball diameter = 10 px < 11.86 px  →  fits cleanly  →  no double-collisions.

Why double-collisions cause LEFT drift: checkPegCollisions() iterates through pegs in LEFT-TO-RIGHT array order. When both the left peg (L) and right peg (R) are simultaneously within collision distance:

  1. Left peg processed first → ball pushed right to L.x + 14 = 221
  2. Right peg processed second → ball pushed to R.x - 14 = 233 - 14 = 219
  3. Net: ball moved from 220 → 219 — 1 px left every double-collision

The right peg gets the last word on position because it is processed second. Because the ball was moved right by the left peg first, it is now closer to the right peg, so the right peg’s position correction is stronger. The bias is small per collision but accumulates across 14 rows and compounds into a visible ~2.5-bin leftward shift.

Fix — Resolve Only the Single Closest Peg Collision Per Frame

checkPegCollisions() was rewritten to find the closest peg within collision range and resolve only that one collision per ball per frame. This eliminates the double-collision position-correction sequence entirely:

// Before: resolved ALL pegs in range, left-to-right → systematic left drift
for each peg:
    if (d < minD) { resolvePegCollision(ball, peg); }   // all in range

// After: resolve only the SINGLE closest peg → unbiased
var closestPeg = null, closestDist = Infinity;
for each peg:
    if (d < minD && d < closestDist) { closestPeg = peg; closestDist = d; }
if (closestPeg) { resolvePegCollision(ball, closestPeg); }

This is also more physically realistic — in real collisions, a ball touches one peg at a time. The randomness that produces the 50/50 left/right decision comes from the ±0.45 nudge already present in resolvePegCollision(), which still fires exactly once per collision per frame.

Biased distribution: 14 rows, ball radius 6, peg radius 7 — histogram peaks at bins 4–5 instead of center bin 7. Gold curve and empirical bars widely separated.
Before fix — peak at bins 4–5, shifted ~2.5 bins left
Fixed distribution: same settings after closest-peg collision fix — histogram now peaks at bin 7, well centered under the gold theoretical curve.
After fix — peak at bin 7, centered under the gold curve
Lesson for Novice Developers — Hidden Bias and the Need to Test Multiple States

This bug has a crucial teaching dimension. The bias was invisible under most settings:

  • 13 rows, same radii — no bias. With 13 rows, peg spacing = 362/13 ≈ 27.8 px and peg surface gap = 27.8 − 14 = 13.8 px. Ball diameter = 12 px < 13.8 px → ball fits cleanly → no double-collisions → no bias.
  • 14 rows, ball radius 5 — no bias. Ball diameter = 10 px < peg gap 11.86 px → same reason.
  • 14 rows, ball radius 6 — biased. Exactly ONE combination triggered the bug. Every other test looked fine.

This is the most dangerous category of software bug: one that is invisible in most configurations but catastrophic in specific ones. A developer who tested only with the default settings (ball 5, peg 7) would have shipped a simulator that appeared to work perfectly — and would have produced subtly wrong statistics for any user who adjusted radii to larger values.

“We need to be sure to explore multiple settings to ensure the app is valid in multiple states!” — KLP

The standard in professional software is edge-case testing: deliberately test combinations at or near known boundaries (here, the threshold where ball diameter exceeds peg gap). The diagnostic rule that emerged from this session: whenever 2 × ballRadius > rSpacing − 2 × pegRadius, double-collisions become possible and the board can develop systematic bias. Any simulation claiming statistical validity should verify that this inequality does NOT hold for its published settings.

session 27 — 11/01/2026 — galton board movie clips page — stranger things + real board demo
KLP

This simulation was first inspired by a few scenes from Stranger Things where Eleven was attempting to influence the results of disks falling through a series of pegs. Research led me to discover its name and some history: the Galton Board. I have two movie clips I’d like to showcase for the idea and link to our app. Please add a movie clip page and cross-link it. Some brief history about the Galton Board’s use in Stranger Things, Galton himself, and why the Galton Board is a prominent idea in probability and statistics.

Files Created & Updated
  • movie_clips/galtonBoardInspirationMC.html — new movie clip page following the babySharkMC.html template. Navy/gold palette, hero background from robotGaltonBoard2.jpg. Three expandable sections: (1) Sir Francis Galton and his bean machine, (2) Stranger Things — Eleven and the peg board experiment, (3) TNT connection and simulator features. Two embedded clips.
  • movie_clips.html — new grid card with the red bell-curve probability icon, linked to the new page.
  • galtonBoardSimApp.html SPARK bar — Movie Clips link added.
  • galtonBoardSimAppChatlog.html back-bar — Movie Clips link added.
  • bellCurveTutorial.html SPARK bar — Movie Clips link added.
  • images/noun_probability_7969006_FF001C_@128.png — red bell-curve icon copied to root images folder for use in the movie grid.