DS - Gravity Toy Design Specification
Version: 0.2.1 Date: 2026-04-13 Author: Rens Roosloot / Claude collaboration
1. Purpose
This document defines the design specification for the current implementation of the Gravity Toy described in URS.md and FS.md.
2. Implementation Strategy
- single standalone page at
site/visuals-gravity.html - all JavaScript inline in the HTML file (consistent with other visuals)
- shared
site/i18n.jsfor language handling viainitLanguage(strings) - no external runtime dependencies
- static-site compatible
3. Technology Direction
- HTML5 Canvas 2D API for rendering
- plain JavaScript (no frameworks, no libraries)
requestAnimationFramefor the simulation loop- separate
mousedown/mousemove/mouseupandtouchstart/touchmove/touchendhandlers for drag-to-launch (mouse: canvas+document; touch: canvas start, document move/end/cancel) localStoragefor language preference (managed byi18n.js)
4. File Structure
site/
visuals-gravity.html ← standalone page
visuals-gravity/
docs/
URS.md
FS.md
DS.md
TEST_PLAN.md
RISK_ASSESSMENT.md
IQ.md
OQ.md
The page reuses existing shared assets:
site/styles.csssite/i18n.js
5. Constants
| Constant | Value | Purpose |
|---|---|---|
G |
0.2 | Gravitational constant, tuned for canvas scale |
SOFTENING |
50 | Added to distSq to prevent force spike at near-zero distance |
TRAIL_LENGTH |
45 | Max positions stored per orb trail |
ESCAPE_MARGIN |
200 | Px beyond canvas edge before orb is culled |
PULSE_LIFE |
35 | Frames an absorption pulse ring lives |
FLASH_LIFE |
14 | Frames of white flash on surviving orb after merge |
LAUNCH_LIFE |
20 | Frames of spawn burst ring |
MAX_ORBS |
60 | Hard cap on orb count |
DRAG_VELOCITY |
0.05 | Scale factor: px of drag → px/frame launch velocity |
MAX_HOLD_MASS |
600 | Maximum orb mass achievable via hold time |
6. Data Model
6.1 Orb Object
{
x: Number, // canvas x position (logical px)
y: Number, // canvas y position (logical px)
vx: Number, // velocity x (px/frame)
vy: Number, // velocity y (px/frame)
mass: Number, // mass (arbitrary units)
radius: Number, // visual radius derived from mass
trail: Array, // [{x, y}] recent positions, max TRAIL_LENGTH entries
flashAge: Number, // frames remaining for post-merge white flash (0 = none)
fixed: Boolean // if true, does not update position or velocity (Solar star only)
}
6.2 Pulse Object
{ x: Number, y: Number, age: Number, r0: Number }
// r0 = surviving orb radius at time of merge
6.3 Launch Object
{ x: Number, y: Number, age: Number, r: Number }
// r = spawned orb radius
6.4 Drag State
dragState = { x, y, cx, cy, t }
// x, y: canvas press position (spawn point)
// cx, cy: current cursor position (updated on move)
// t: Date.now() at press start (for hold-mass calculation)
6.5 Simulation State
let orbs = []; // active orb objects
let pulses = []; // active absorption pulse rings
let launches = []; // active spawn burst rings
let paused = false;
let activePreset = 'solar'; // 'solar' | 'chaos' | 'binary' | 'playground'
let frameCount = 0; // increments only when unpaused; drives hint fade
let dragState = null;
7. Color System
function orbHSL(orb) {
if (orb.mass > 800) return { h: 45, s: 90, l: 72 }; // warm gold
if (orb.mass > 200) return { h: 30, s: 85, l: 65 }; // medium gold
// velocity-mapped: slow = blue-violet, fast = warm yellow
const speed = sqrt(vx² + vy²);
const t = min(speed / 3, 1);
return { h: 220 - t*180, s: 70 + t*20, l: 65 + t*15 };
}
8. Radius Formula
function massToRadius(mass) {
return Math.pow(mass, 0.45) * 1.5;
}
Representative values:
| Mass | Radius (px) |
|---|---|
| 20 (small planet) | 5.5 |
| 70 (large planet) | 9.9 |
| 120 (Sandbox anchor) | 12.2 |
| 300 (user heavy launch) | 17.6 |
| 600 (max hold) | 22.9 |
| 2000 (Solar star) | 35.4 |
| 3800 (merged binary) | 47.3 |
9. Physics Design
9.1 Force Accumulation (O(n²) unique pairs)
for i in 0..n:
for j in i+1..n:
dx = orbs[j].x - orbs[i].x
dy = orbs[j].y - orbs[i].y
distSq = dx² + dy² + SOFTENING
dist = sqrt(distSq)
force = G * orbs[i].mass * orbs[j].mass / distSq
fx = force * dx / dist
fy = force * dy / dist
ax[i] += fx / orbs[i].mass
ay[i] += fy / orbs[i].mass
ax[j] -= fx / orbs[j].mass
ay[j] -= fy / orbs[j].mass
9.2 Position Update
for i in 0..n:
if orbs[i].fixed: continue
orbs[i].vx += ax[i]
orbs[i].vy += ay[i]
orbs[i].x += orbs[i].vx
orbs[i].y += orbs[i].vy
append {x, y} to trail, trim to TRAIL_LENGTH
9.3 Absorption
absorbed = Set()
for i in 0..n:
for j in i+1..n:
if absorbed contains i or j: skip
dist = sqrt((xi-xj)² + (yi-yj)²)
if dist < ri + rj:
survivor = larger mass orb
newMass = mi + mj
survivor.vx = (mi*vxi + mj*vxj) / newMass // momentum conservation
survivor.vy = (mi*vyi + mj*vyj) / newMass
survivor.mass = newMass
survivor.radius = massToRadius(newMass)
survivor.flashAge = FLASH_LIFE
push pulse at survivor position
mark smaller orb as absorbed
filter orbs to remove absorbed
9.4 Orbital Velocity Helper
For Solar System preset, initial speed of a planet at distance r from star of mass M:
v = sqrt(G * M / r)
Applied perpendicular to the radial direction. A small radial perturbation adds eccentricity.
9.5 Two-Body Orbital Velocity (Binary preset)
vA = mB * sqrt(G / (mTotal * d))
vB = mA * sqrt(G / (mTotal * d))
Where d = sep * 2 (total separation). Applied in opposite vertical directions.
10. Preset Definitions
10.1 Solar System
star: mass=2000, fixed=true, at canvas center
planets: 8 orbs
radii: [60, 90, 118, 150, 182, 215, 248, 278] px from star
masses: [12, 28, 18, 70, 25, 55, 20, 8]
angle: evenly spaced + small random offset (±0.15 rad)
speed: sqrt(G * starMass / r) * (0.96 ± 0.04)
radial perturbation: ±8% of tangential speed
10.2 Chaos
3 clusters, 10 orbs each = 30 orbs total
cluster A: center (w*0.22, h*0.30), velocity (+2.2, +1.6)
cluster B: center (w*0.78, h*0.70), velocity (-2.0, -1.8)
cluster C: center (w*0.50, h*0.18), velocity (+0.3, +2.8)
each orb: spread ±24 px from cluster center, vel ± 0.9 px/frame random
mass range: 8–63 (uniform random)
10.3 Binary
starA: mass=2000, at (cx-120, cy), vy = -mB * sqrt(G / (mTotal * 240))
starB: mass=1800, at (cx+120, cy), vy = +mA * sqrt(G / (mTotal * 240))
debris: 15 orbs, radii 80–270 px from center
partial orbital velocity: 0–65% of sqrt(G * mTotal / r)
random velocity: ±0.9 px/frame
mass: 8–30
10.4 Sandbox
1 orb: mass=120, at canvas center, vx=vy=0
11. Drag-to-Launch Design
canvas mousedown / canvas touchstart:
dragState = { x, y, cx, cy, t: Date.now() }
document mousemove / document touchmove:
dragState.cx = cursor.x
dragState.cy = cursor.y
document mouseup / document touchend:
dx = cx - x, dy = cy - y
holdMs = Date.now() - t
mass = min(30 + holdMs * 0.08, MAX_HOLD_MASS)
vx = dx * DRAG_VELOCITY
vy = dy * DRAG_VELOCITY
spawn orb at (x, y) with (vx, vy, mass)
push launch burst at (x, y)
dragState = null
document touchcancel:
dragState = null ← clears ghost preview if browser interrupts the gesture
mousemove/touchmove/touchend/touchcancel are on document (not canvas) so drags that finish
outside the canvas boundary — or are cancelled by the browser (e.g. incoming call) — are handled
cleanly without leaving a hanging ghost preview.
12. Render Pass Order
| Pass | Content | Reduced-motion |
|---|---|---|
| 1 | Background field (mass > 800 only) | shown |
| 2 | Force lines between close pairs | shown |
| 3 | Gradient trails (3 opacity segments) | suppressed |
| 4 | Outer glow per orb | shown |
| 5 | Orb cores + merge flash | flash suppressed |
| 6 | Absorption pulses (double ring) | suppressed |
| 7a | Launch burst rings | suppressed |
| 7b | Drag preview (ghost + arrow) | shown |
| 8 | Hint text (fades after ~4.5 s) | suppressed (guarded by !reducedMotion — not by frameCount, which never advances when auto-paused) |
13. Performance Design Notes
- O(n²) force computation acceptable for n < 60 (MAX_ORBS cap)
- O(n²) force-line check uses early
dist > thresholdskip - trail buffer capped at 45 points per orb
devicePixelRatioscaling applied for sharp HiDPI rendering- radial gradients created fresh each frame (no caching); acceptable at this scale
14. Security and Deployment Notes
- no secrets in client code
- no third-party scripts
- all production files within
site/ - no user input written to DOM (canvas only)
15. Implementation Sequence (as built)
- Page shell, header, breadcrumb, canvas, controls, pager, footer
- Canvas sizing and resize handler
- Physics loop: force accumulation, position update, fixed-orb skip
- Preset definitions (Solar, Chaos, Binary, Sandbox)
- Absorption detection and momentum conservation
- Radius formula and color system
- Orb rendering: glow hierarchy (5 passes)
- Gradient trails
- Absorption pulse (double ring) and merge flash
- Drag-to-launch (mouse + touch), launch burst ring
- Drag preview (ghost circle, velocity arrow)
- i18n via
initLanguage(strings)andlanguagechangeevent - Hint text with fade
- Reduced-motion handling and boundary cull
- Solar star fixed-position fix
16. Traceability
URSinteraction requirements → Sections 11, 12URSvisual requirements → Sections 7, 12URStechnical constraints → Sections 2, 3, 14FSphysics model → Sections 9, 10FSpreset definitions → Section 10FSrendering behavior → Section 12FScontrols → Sections 11, 13