Nº 647 of 677
Pebblefall peg drop
A physics peg board: drag the dropper, let go, and a pebble bounces through 75 pegs that flash on contact into seven scoring slots, five per round.
Preview
Use it
Free to use · credit appreciated1 Once, in your project · Node 22.20+
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Pebblefall peg drop" (piece minigame-peg-drop) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 647 · "Pebblefall peg drop" · www.designlounge.live -->
# Pebblefall peg drop
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A garden-toy peg board from a fictional casual game, Pebblefall. A moss-green hopper rides a dotted rail at the top of a pale sage board; you drag it sideways and let go, and a terracotta pebble falls through ten staggered rows of moss pegs under real gravity, bouncing, flashing each peg marigold where it touches, and settling into one of seven slots scored 80 · 30 · 10 · 50 · 10 · 30 · 80. Five pebbles make a round, then a result card staggers in the five scores and counts up the total. It is a toy, not a casino: no money, no coins, no lights, just clay colours and Gabarito numerals. The detail worth copying: the simulation runs at a fixed 1/120s step inside rAF, so the bounce feel is the same at 60Hz and 120Hz, and a 35ms hit-stop plus a 5px board nudge make the 50 and 80 slots land with weight.
## Reference behaviour
1. First frame is mid-round: Round 3, score 130, "2 left" with two filled pebble dots and three rings, the log shows 80 and 50, a pebble is already falling through row 5 with two pegs fading from marigold, and the hopper is loaded at 42% of the rail. A pill "Drag, then let go" with a hand icon sways 60px beside the hopper three times (2.4s each).
2. Pointer down anywhere on the board (not the slot plates): pointer capture, the hopper scales to 1.06 and jumps to the finger's x, clamped to `16 + 9 + 4` px from each wall. Pointer move drags it. Release drops a pebble from the hopper and hides the hint for good. A tap with no drag also drops.
3. A pebble spawns at the hopper's x, y 46, with vx ±8px/s random and vy 0. Only one pebble may be in the top 120px at a time; the hopper shows empty (its pebble scales to 0) until the last one clears that band.
4. Physics per 1/120s step: vy += 1500·dt; vx × (1 − 0.15·dt); speed capped at 900px/s; position += v·dt. Pegs (r 5) and the pebble (r 9) collide as circles: push out along the normal, reflect the normal velocity with restitution 0.52, add ±20px/s random tangential jitter. Side walls reflect with 0.52. Slot dividers are 2px-radius capsules from the slot top to the floor. Pebbles also collide with each other (equal mass, 0.52).
5. Peg hit: the peg's flash goes to 1 and decays at 2.4/s; a marigold halo of radius 5 + 8·flash at 45%·flash alpha is drawn under it, and the peg fill turns `#E39A1E` while flash > 0.05. Hits faster than 260px/s throw four 3px marigold sparks that fall under 900px/s² gravity and fade over 380ms.
6. Squash: on each hit the pebble squashes along the contact normal by up to 28% (and stretches 20% across), proportional to impact speed / 700, recovering at 6/s.
7. Floor: if |vy| > 120 the pebble bounces at 0.3; otherwise it lands. The slot index is `floor((x − 16) / slotWidth)`. The slot plate turns marigold and lifts 3px for 520ms, a "+80" pill rises 64px and fades over 900ms, the score and log update, and the live region says "+80. Score 210. 1 left."
8. Landing in a 50 or 80 slot: the simulation freezes for 35ms (hit-stop) and the board nudges down 5px then up 2px over 300ms.
9. A pebble moving under 40px/s for 0.5s without landing gets a 140px/s push toward the centre and a 80px/s hop, so it can never wedge.
10. Keyboard: ← / → move the hopper 12px (Shift: 36px). Space or Enter drops. P or Escape toggles pause. The hopper is a focusable `role="slider"` reporting 0–100.
11. Pause: the loop stops (no rAF scheduled), a sheet rises over a 45% moss scrim: "Paused / Pebbles hold still", Resume and Restart round.
12. Round end (700ms after the fifth pebble lands): the result sheet reads "Round 3 · 5 pebbles", then the five scores rise in one at a time (380ms spring, 300ms then 140ms apart; the highest marigold), then the total counts up from 0 over 800ms (cubic out), then "Next round" fades in and takes focus. If the total beats 410: "New best this week. Old best 410."
13. Next round: Round 4, score 0, five pebbles, log reads "Five pebbles, no drops yet".
## Structure
```
390 × 844, ground #D7E3D0
┌──────────────────────────────────────┐
│ 54px clear │
│ Pebblefall ROUND 3 (II) │ hud 52h, padding 0 16 0 20
│ 2 left ●●○○○ 130 │ score 30px 800
│ ┌──────────────────────────────────┐ │ stage: margin 6 10 0, h 604, r 28
│ │ · · · ·[▣]· · · · · · · · · · · │ │ rail y 40, hopper 52×46 at top 14
│ │ ( ✋ Drag, then let go ) │ │ hint pill top 66
│ │ • • • • • • • │ │ 10 peg rows, y 118 + 38n
│ │ • • • • • • • • │ │ peg spacing (W−32)/8
│ │ … │ │
… 165 more lines. Copy or download for the full brief. Loading the source…Brief
205 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A garden-toy peg board from a fictional casual game, Pebblefall. A moss-green hopper rides a dotted rail at the top of a pale sage board; you drag it sideways and let go, and a terracotta pebble falls through ten staggered rows of moss pegs under real gravity, bouncing, flashing each peg marigold where it touches, and settling into one of seven slots scored 80 · 30 · 10 · 50 · 10 · 30 · 80. Five pebbles make a round, then a result card staggers in the five scores and counts up the total. It is a toy, not a casino: no money, no coins, no lights, just clay colours and Gabarito numerals. The detail worth copying: the simulation runs at a fixed 1/120s step inside rAF, so the bounce feel is the same at 60Hz and 120Hz, and a 35ms hit-stop plus a 5px board nudge make the 50 and 80 slots land with weight.
Reference behaviour
- First frame is mid-round: Round 3, score 130, “2 left” with two filled pebble dots and three rings, the log shows 80 and 50, a pebble is already falling through row 5 with two pegs fading from marigold, and the hopper is loaded at 42% of the rail. A pill “Drag, then let go” with a hand icon sways 60px beside the hopper three times (2.4s each).
- Pointer down anywhere on the board (not the slot plates): pointer capture, the hopper scales to 1.06 and jumps to the finger’s x, clamped to
16 + 9 + 4px from each wall. Pointer move drags it. Release drops a pebble from the hopper and hides the hint for good. A tap with no drag also drops. - A pebble spawns at the hopper’s x, y 46, with vx ±8px/s random and vy 0. Only one pebble may be in the top 120px at a time; the hopper shows empty (its pebble scales to 0) until the last one clears that band.
- Physics per 1/120s step: vy += 1500·dt; vx × (1 − 0.15·dt); speed capped at 900px/s; position += v·dt. Pegs (r 5) and the pebble (r 9) collide as circles: push out along the normal, reflect the normal velocity with restitution 0.52, add ±20px/s random tangential jitter. Side walls reflect with 0.52. Slot dividers are 2px-radius capsules from the slot top to the floor. Pebbles also collide with each other (equal mass, 0.52).
- Peg hit: the peg’s flash goes to 1 and decays at 2.4/s; a marigold halo of radius 5 + 8·flash at 45%·flash alpha is drawn under it, and the peg fill turns
#E39A1Ewhile flash > 0.05. Hits faster than 260px/s throw four 3px marigold sparks that fall under 900px/s² gravity and fade over 380ms. - Squash: on each hit the pebble squashes along the contact normal by up to 28% (and stretches 20% across), proportional to impact speed / 700, recovering at 6/s.
- Floor: if |vy| > 120 the pebble bounces at 0.3; otherwise it lands. The slot index is
floor((x − 16) / slotWidth). The slot plate turns marigold and lifts 3px for 520ms, a “+80” pill rises 64px and fades over 900ms, the score and log update, and the live region says “+80. Score 210. 1 left.” - Landing in a 50 or 80 slot: the simulation freezes for 35ms (hit-stop) and the board nudges down 5px then up 2px over 300ms.
- A pebble moving under 40px/s for 0.5s without landing gets a 140px/s push toward the centre and a 80px/s hop, so it can never wedge.
- Keyboard: ← / → move the hopper 12px (Shift: 36px). Space or Enter drops. P or Escape toggles pause. The hopper is a focusable
role="slider"reporting 0–100. - Pause: the loop stops (no rAF scheduled), a sheet rises over a 45% moss scrim: “Paused / Pebbles hold still”, Resume and Restart round.
- Round end (700ms after the fifth pebble lands): the result sheet reads “Round 3 · 5 pebbles”, then the five scores rise in one at a time (380ms spring, 300ms then 140ms apart; the highest marigold), then the total counts up from 0 over 800ms (cubic out), then “Next round” fades in and takes focus. If the total beats 410: “New best this week. Old best 410.”
- Next round: Round 4, score 0, five pebbles, log reads “Five pebbles, no drops yet”.
Structure
390 × 844, ground #D7E3D0
┌──────────────────────────────────────┐
│ 54px clear │
│ Pebblefall ROUND 3 (II) │ hud 52h, padding 0 16 0 20
│ 2 left ●●○○○ 130 │ score 30px 800
│ ┌──────────────────────────────────┐ │ stage: margin 6 10 0, h 604, r 28
│ │ · · · ·[▣]· · · · · · · · · · · │ │ rail y 40, hopper 52×46 at top 14
│ │ ( ✋ Drag, then let go ) │ │ hint pill top 66
│ │ • • • • • • • │ │ 10 peg rows, y 118 + 38n
│ │ • • • • • • • • │ │ peg spacing (W−32)/8
│ │ … │ │
│ │ ▕ ▕ ▕ ▕ ▕ ▕ ▕ ▕ │ │ slot bay y H−118 → H−56, 6 dividers
│ │ [80][30][10][50][10][30][80] │ │ plates 36h, gap 4, bottom 14
│ └──────────────────────────────────┘ │
│ THIS ROUND [ Drop SPACE ] │ bar 60h, margin 14 16 0
│ [80][50] │ Drop ≥132 × 60, r 20
│ 34px clear │
└──────────────────────────────────────┘
.hud: brandb,div.left(count + fiveidots),.score(smallround +span),button.pause..stage:canvas(pegs, dividers, pebbles, sparks),.rail,div.dropper[role=slider],.ghost,.slotsof seven.slotplates..bar:ol#logof drop values,button.dropwith akbd.- Two
section.sheet[role=dialog]: pause and result (h2total,ol.drops,button.cta).
Tokens
:root {
--ground: #D7E3D0; /* page */
--board: #EDF2E6; /* stage, result chips */
--board-edge: #C3D1BB; /* rail dots, lips */
--bay: #DCE5D4; /* slot bay fill (canvas) */
--plate: #FBF8F0; /* slot plates, pills, sheet */
--big-plate: #FFF1CF; /* 80-point plates */
--ink: #1F2D26; /* text, Drop button */
--ink-2: #3F5247; /* captions */
--moss: #3E5A4A; /* pegs, dividers, hopper */
--moss-lip: #2B4235;
--pebble: #C9552F; --pebble-hi: #F08A5D; --pebble-lo: #9E3D1E;
--marigold: #F2B33D; /* hit plates, score pops, top result */
--peg-hit: #E39A1E;
--font: "Gabarito", system-ui, sans-serif;
--fs-total: 64px; --fs-score: 30px; --fs-brand: 22px; --fs-btn: 18px; --fs-slot: 15px; --fs-cap: 11px;
--r-board: 28px; --r-plate: 10px; --r-btn: 20px; --r-sheet: 30px;
--board-shadow: inset 0 2px 0 #fff, 0 1px 0 var(--board-edge), 0 18px 40px -18px rgba(31,45,38,.35);
--btn-lip: 0 5px 0 #0F1813;
--ease: cubic-bezier(.2,.7,.2,1);
--sheet-ease: cubic-bezier(.32,.72,0,1);
--expo: cubic-bezier(.16,1,.3,1);
--spring: cubic-bezier(.34,1.56,.64,1);
/* physics */
--g: 1500; /* px/s² */
--step: 1/120; /* s */
--restitution: .52;
--pebble-r: 9px; --peg-r: 5px;
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking |
|---|---|---|---|---|---|
| Result total | Gabarito | 64px | 900 | 1 | −0.04em, tabular |
| Score | Gabarito | 30px | 800 | 1 | −0.02em, tabular |
| Brand | Gabarito | 22px | 900 | 1 | −0.02em |
| Score pop | Gabarito | 22px | 900 | 1 | 0 |
| Result chips | Gabarito | 20px | 800 | 1 | 0 |
| Drop, Next round | Gabarito | 18px | 800 | 1 | 0 |
| Slot values | Gabarito | 15px | 800 | 1 | tabular |
| Pebbles left, body | Gabarito | 13–15px | 500–600 | 1.35 | 0 |
| Captions (ROUND 3, THIS ROUND) | Gabarito | 11px | 700 | 1 | 0.12em upper |
Motion
| What | Trigger | From → to | Duration / easing | Reduced motion |
|---|---|---|---|---|
| Pebble flight | drop | physics, fixed 1/120s step | until landed | unchanged (it is the game) |
| Peg flash + halo | hit | flash 1 → 0 | 2.4/s linear decay | unchanged |
| Sparks | hit > 260px/s | 4 squares fly and fall | ~380ms | none |
| Squash | hit | up to 28% / 20% | 6/s recovery | unchanged |
| Hit-stop | 50 or 80 slot | sim frozen | 35ms | none |
| Board nudge | 50 or 80 slot | 0 → 5px → −2px → 0 | 300ms --ease | none |
| Slot plate | land | lift 3px, marigold | 160ms bg, 200ms --spring, held 520ms | colour only |
| Score pop | land | y 0 → −64px, scale .6 → 1.1 → 1, fade | 900ms --expo | static at −30px, fades |
| Hopper | drag | scale 1 → 1.06 | 160ms --spring | instant |
| Hint pill | load | sway 60px | 2.4s × 3, 500ms delay | static |
| Sheets | pause / end | translate(-50%,105%) → 0 | 440ms --sheet-ease | instant |
| Result chips | end | y 10 → 0, fade in | 380ms spring, 300 + 140ms·i | all visible at once |
| Total | after chips | 0 → total | 800ms cubic out | final at once |
States
- Hopper loaded / empty: pebble inside at scale 1 / 0 (empty while a pebble is in the top 120px or none are left).
- Hopper held: scale 1.06.
- Drop disabled:
#56675Dfill, no lip, when no pebbles are left or the game is paused. - Peg hit: marigold fill and halo, fading.
- Slot hit: marigold plate lifted 3px; 80 plates rest on
--big-plate. - Paused: loop stopped, sheet up, scrim 45%.
- Result: top score chip(s) marigold; Next round hidden until the count-up ends.
- Focus-visible: 3px
--inkoutline, 3px offset.
Accessibility
- The hopper is
role="slider",aria-label="Dropper position",aria-valuenow0–100, focusable, witharia-keyshortcuts. - Keys: ← → aim, Shift for big steps, Space/Enter drop, P/Escape pause. Space on a focused button presses that button instead.
- The canvas is
aria-hidden; a polite live region announces every landing (“+50. Score 180. 2 left.”), pause, and the round total. - Pebble dots carry
aria-label="2 pebbles left"; the log is a labelled list. - Dialogs are
role="dialog",aria-modal, labelled by theirh2; focus goes to Resume or to Next round when it appears. - Contrast:
--inkon--plateand on marigold above 8:1;--ink-2on the ground above 6:1. - Hit targets: the whole board is the drag surface; Drop is 60px tall; pause 44px.
Responsive rules
- Layout is computed from the stage’s width at load and on resize: peg spacing
(W − 32) / 8, slot width(W − 32) / 7, so 360 wide keeps ten rows and seven slots; the hopper keeps its relative position across a resize. - Taller screens: keep the stage at 604px and let the gap below the bar grow.
- At tablet width the column stays 390px, centred on the ground colour.
Acceptance checklist
Always
- Physics runs at a fixed 1/120s step with dt capped at 50ms, inside rAF, and stops when paused or ended.
- Pegs flash on contact and decay; hard hits throw sparks.
- The pebble squashes along the contact normal and recovers.
- Walls, dividers and other pebbles all collide; no pebble can wedge (stall rescue after 0.5s).
- Drag or arrows aim, release or Space drops, P/Escape pauses.
- Only one pebble in the top band at a time; the hopper shows empty until then.
- Big slots add a 35ms hit-stop and a board nudge, both off under reduced motion.
- Result card staggers the drops, then counts up, then shows the action.
- Closed sheets are
visibility:hidden; scroll width is 390. - No money, coins or casino styling anywhere.
This demo
- Slots 80 · 30 · 10 · 50 · 10 · 30 · 80; five pebbles a round; best this week 410.
- First frame: Round 3, 130 points, 2 left, log 80 and 50, one pebble mid-board.
- Pebble terracotta
#C9552F, pegs moss#3E5A4A, hits marigold#F2B33D.
Implementation notes
Fixed step inside rAF. Accumulate real time, step in 1/120s slices, draw once per frame. The hit-stop just skips stepping:
function loop(t) {
if (state !== 'play') { rafId = 0; return; }
const dt = last ? Math.min(0.05, (t - last) / 1000) : 0; last = t;
if (freeze > 0) freeze -= dt;
else { acc += dt; while (acc >= STEP) { step(STEP); acc -= STEP; } }
draw(); rafId = requestAnimationFrame(loop);
}
One collision routine for pegs and dividers. A divider is a capsule: collide with the closest point on its segment as if it were a 2px peg.
function hit(b, px, py, rr) {
const dx = b.x - px, dy = b.y - py, m = rr + BR, d2 = dx * dx + dy * dy;
if (d2 >= m * m) return false;
const d = Math.sqrt(d2) || 0.01, nx = dx / d, ny = dy / d;
b.x = px + nx * m; b.y = py + ny * m;
const vn = b.vx * nx + b.vy * ny;
if (vn < 0) { b.vx -= (1 + E) * vn * nx; b.vy -= (1 + E) * vn * ny; b.vx += (Math.random() - 0.5) * 40; }
return -vn;
}
for (const x of dividers) if (b.y > slotTop - BR) hit(b, x, clamp(b.y, slotTop, floorY), 2);
Edge pegs touch the wall. A peg whose gap to the wall is smaller than the pebble (18px) but non-zero traps it. Move any peg closer than 2·BR + PR to a wall flush against it (x = wall ± 5) so the pebble rolls off inward. Keep the stall rescue anyway.
Common mistakes: integrating with the raw frame delta (bounces change with refresh rate), resolving overlap without reflecting velocity (pebbles sink into pegs), and running the loop while paused.
Details
- Palette
- Type
- Gabarito
- Motion
- Rich motion
- Build
- Multi-step
- Tags