# Pendulum Wave — `pendulum-wave@1.0.0`

A row of pendulums released together that drift through waves, snakes and chaos and line up again, as a perfect loop.

The classic pendulum-wave desk toy: a row of balls on V-shaped strings under a bar. Each one swings slightly faster than its neighbour, so the row ripples into travelling waves, a snake, splits into two and three groups, looks like chaos, and then falls back into one straight line exactly as the loop ends. Choose chrome, brass or two-colour gradient balls, how many there are, how fast the pattern plays and the camera view.

- Kind: object; status: published; digest sha256:baf4c5022b18a4b44ce8d0a0f1f3c5b6ecc118cc8baadbe9d28f5637383aa907
- Role: Hero object: a satisfying pendulum-wave apparatus that loops exactly.
- Implementation: three; entry `src/pendulum-wave.js`
- Files: https://aipixelwizard.com/registry/pendulum-wave/1.0.0/<path> (`LICENSE`, `asset.json`, `demo.js`, `index.html`, `src/pendulum-wave.js`)
- Manifest JSON: https://aipixelwizard.com/api/v1/visuals/pendulum-wave/versions/1.0.0
- Runnable demo: https://aipixelwizard.com/registry/pendulum-wave/1.0.0/index.html (open over http; it imports sibling packages by relative path)
- Dependencies (exact): `three@0.185.1` (source) — Geometry, physical materials and shadows.; `chrome-material@1.1.0` (demo) — Demo uses the shared studio (environment light, cove, floor) and the chrome material for the balls.; `apw-runtime@1.0.0` (demo) — Demo harness.

## Exports
- `createPendulumWave({ count?, swings?, spacing?, longest?, realism?, barHeight?, bobRadius?, vSpread?, bobMaterial?, bobColors?, bobFinish?, frameMaterial?, stringColor?, frame? }) => { group, lengths, update(progress, amplitude?), dispose() }` — The apparatus as a THREE.Group (row along X, swings toward ±Z, stands on y = 0). update(progress 0…1) poses every pendulum.
- `pendulumAngles(progress, { count?, swings?, amplitude? }) => number[]` — Swing angles in radians; pendulum i makes swings + i whole swings per loop, so progress 0 and 1 match.
- `pendulumLengths({ count?, swings?, longest?, realism? }) => number[]` — String lengths, longest first, graded like the real apparatus.
- `gradeColors(count, stops) => THREE.Color[]` — Colours graded across the row through two or more stops.

## Parameters
- `balls` (select one of chrome | brass | gradient, default "chrome") — Balls
- `colorA` (color, default "#ff6b5e") — First ball colour: Used by the two-colour gradient.
- `colorB` (color, default "#6b5cff") — Last ball colour: Used by the two-colour gradient.
- `count` (integer 8–24, default 15) — Pendulums
- `swings` (integer 6–30, default 12) — Pattern speed: Swings the longest pendulum makes in one loop. More swings play the pattern faster.
- `amplitude` (number 8–35, default 24) — Swing: How far the balls swing, in degrees.
- `view` (select one of three | front | along, default "three") — Camera
- `background` (color, default "#0b0b10") — Background

## Composition
- Inputs: Loop progress 0…1 (frame / total frames) and the swing amplitude in radians. Needs scene.environment for metal balls.
- Outputs: A THREE.Group: the A-frame stand, the bar and the pendulums.
- Coordinates: Row along X centred on the origin (width ≈ count × 0.26), bar at y = 2.5, balls swing toward ±Z, the stand's feet at y = 0 and z = ±0.62.
- Lighting: Image-based (scene.environment) for chrome and brass; a shadow-casting directional light gives the swinging floor shadows.
- Timing: update(progress) is a pure function of progress; frames render in any order, and any clip length loops.
- Works with: chrome-material, orbit-camera, kinetic-title, gradient-background
- Keep Pattern speed whole: the loop is exact only for whole numbers of swings.
- At short loops the swings are faster than real pendulums of these lengths; lengths still grade like the real apparatus.

## Output and loop
- 1080×1080, 30 fps, 12 s, procedural
- Seamless loop: yes — Pendulum i makes a whole number of swings per loop (Pattern speed + i), all released together, so at the end every ball is back in line where it started; the camera drift is one whole sine cycle.
- Verified 2026-10-05 in Chrome 154.0.8037.97 headless on Linux 7.0.0-38-generic (x64), Node 22.14.0 (WebGL 2 via SwiftShader (software)); deterministic: yes; loop check: Frame 0 vs end state (progress 1): mean |Δ| 0.0000, max |Δ| 0 of 255.

## Use it
New project: Create a Three.js scene with an environment map (createStudioEnvironment from Chrome Material), add createPendulumWave().group and call update(frame / totalFrames) every frame.

Existing project: Copy src/pendulum-wave.js. Add wave.group to your scene, scale it to fit, and call wave.update(progress, amplitude) from your frame loop with progress running 0…1 over your loop.

Reuse ideas:
- Satisfying loop: render the chrome version on black for a reel; the line-up at the end makes a natural cut point.
- Brand colours: set the two gradient colours to a brand palette for an idle or loading loop.
- Science explainer: pair it with Kinetic Title to label the moments when the row splits into two and three groups.

## Limitations
- Not a physics simulation: the swings are exact closed-form motion, which is what makes the loop perfect, but the balls never collide or lose energy.
- Each pendulum swings in one plane only, as on the real bifilar apparatus.

## License
MIT. Copyright (c) 2026 AI Pixel Wizard. Released under the MIT License. See LICENSE in this folder.
- three.js 0.185.1: MIT (https://github.com/mrdoob/three.js)

Provenance: Adapted from the owner's threejs-sketches/audio-musical/rhythm-pendulums (MIT), rebuilt for AI Pixel Wizard in October 2026 as the real bifilar pendulum-wave apparatus with exact whole-swing timing, the shared studio and the frame/time contract.

