Gravity Toy DS

Volledige gerenderde weergave van DS.md.

Laatst gesynchroniseerd: 13 april 2026

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.js for language handling via initLanguage(strings)
  • no external runtime dependencies
  • static-site compatible

3. Technology Direction

  • HTML5 Canvas 2D API for rendering
  • plain JavaScript (no frameworks, no libraries)
  • requestAnimationFrame for the simulation loop
  • separate mousedown/mousemove/mouseup and touchstart/touchmove/touchend handlers for drag-to-launch (mouse: canvas+document; touch: canvas start, document move/end/cancel)
  • localStorage for language preference (managed by i18n.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.css
  • site/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 > threshold skip
  • trail buffer capped at 45 points per orb
  • devicePixelRatio scaling 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)

  1. Page shell, header, breadcrumb, canvas, controls, pager, footer
  2. Canvas sizing and resize handler
  3. Physics loop: force accumulation, position update, fixed-orb skip
  4. Preset definitions (Solar, Chaos, Binary, Sandbox)
  5. Absorption detection and momentum conservation
  6. Radius formula and color system
  7. Orb rendering: glow hierarchy (5 passes)
  8. Gradient trails
  9. Absorption pulse (double ring) and merge flash
  10. Drag-to-launch (mouse + touch), launch burst ring
  11. Drag preview (ghost circle, velocity arrow)
  12. i18n via initLanguage(strings) and languagechange event
  13. Hint text with fade
  14. Reduced-motion handling and boundary cull
  15. Solar star fixed-position fix

16. Traceability

  • URS interaction requirements → Sections 11, 12
  • URS visual requirements → Sections 7, 12
  • URS technical constraints → Sections 2, 3, 14
  • FS physics model → Sections 9, 10
  • FS preset definitions → Section 10
  • FS rendering behavior → Section 12
  • FS controls → Sections 11, 13
Terug naar home