oddlyalive 0.2.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +62 -0
  3. package/LICENSE +21 -0
  4. package/README.md +158 -0
  5. package/SECURITY.md +29 -0
  6. package/assets/photoreal/PROVENANCE.md +19 -0
  7. package/assets/photoreal/baseball.png +0 -0
  8. package/assets/photoreal/basketball.png +0 -0
  9. package/assets/photoreal/crystal.png +0 -0
  10. package/assets/photoreal/kicking-cleat.png +0 -0
  11. package/assets/photoreal/letter-charm-square.png +0 -0
  12. package/assets/photoreal/letter-charm.png +0 -0
  13. package/assets/photoreal/sneaker.png +0 -0
  14. package/assets/photoreal/soccer-ball.png +0 -0
  15. package/bin/oddlyalive.js +283 -0
  16. package/docs/ARCHITECTURE.md +55 -0
  17. package/docs/DEMO-VIDEOS.md +122 -0
  18. package/docs/QUICKSTART.md +115 -0
  19. package/docs/ROADMAP.md +33 -0
  20. package/examples/ball-lab/PROVENANCE.md +4 -0
  21. package/examples/ball-lab/app.js +33 -0
  22. package/examples/ball-lab/index.html +56 -0
  23. package/examples/ball-lab/scene.json +68 -0
  24. package/examples/crystal-mobile/PROVENANCE.md +4 -0
  25. package/examples/crystal-mobile/app.js +26 -0
  26. package/examples/crystal-mobile/index.html +56 -0
  27. package/examples/crystal-mobile/scene.json +69 -0
  28. package/examples/football-kick/PROVENANCE.md +4 -0
  29. package/examples/football-kick/app.js +33 -0
  30. package/examples/football-kick/index.html +56 -0
  31. package/examples/football-kick/scene.json +44 -0
  32. package/examples/gallery.css +379 -0
  33. package/examples/index.html +109 -0
  34. package/examples/shared/example.css +320 -0
  35. package/examples/shared/player.js +85 -0
  36. package/examples/shoe-splash/PROVENANCE.md +4 -0
  37. package/examples/shoe-splash/app.js +26 -0
  38. package/examples/shoe-splash/index.html +56 -0
  39. package/examples/shoe-splash/scene.json +38 -0
  40. package/examples/string-touch/PROVENANCE.md +11 -0
  41. package/examples/string-touch/app.js +79 -0
  42. package/examples/string-touch/index.html +70 -0
  43. package/examples/string-touch/scene.json +73 -0
  44. package/examples/string-touch/styles.css +273 -0
  45. package/package.json +82 -0
  46. package/schemas/scene.schema.json +336 -0
  47. package/scripts/render-demo-videos.sh +77 -0
  48. package/scripts/serve.js +88 -0
  49. package/scripts/update-visual-fixtures.mjs +60 -0
  50. package/scripts/verify-demo-videos.mjs +138 -0
  51. package/skills/oddlyalive/SKILL.md +68 -0
  52. package/skills/oddlyalive/agents/openai.yaml +4 -0
  53. package/skills/oddlyalive/references/object-models.md +25 -0
  54. package/skills/oddlyalive/references/recipes.md +27 -0
  55. package/skills/oddlyalive/references/scene-schema.md +84 -0
  56. package/src/crystal-renderer.js +338 -0
  57. package/src/gesture.js +58 -0
  58. package/src/index.js +44 -0
  59. package/src/rigid-balls.js +304 -0
  60. package/src/rigid-renderer.js +760 -0
  61. package/src/rigid-scene.js +223 -0
  62. package/src/scene-registry.js +17 -0
  63. package/src/scene.js +206 -0
  64. package/src/state-hash.js +17 -0
  65. package/src/strands.js +773 -0
  66. package/src/surface-wave.js +209 -0
  67. package/src/svg-renderer.js +348 -0
  68. package/src/wave-renderer.js +463 -0
  69. package/src/wave-scene.js +163 -0
  70. package/tests/cli.test.js +101 -0
  71. package/tests/determinism.test.js +57 -0
  72. package/tests/fixtures/visual-baselines.json +27 -0
  73. package/tests/rigid-balls.test.js +77 -0
  74. package/tests/scene.test.js +78 -0
  75. package/tests/surface-wave.test.js +39 -0
  76. package/tests/visual-fixtures.test.js +43 -0
  77. package/videos/oddlyalive-demos/BRIEF.md +40 -0
  78. package/videos/oddlyalive-demos/DEMO-PLAN.md +13 -0
  79. package/videos/oddlyalive-demos/README.md +45 -0
  80. package/videos/oddlyalive-demos/app.js +212 -0
  81. package/videos/oddlyalive-demos/hyperframes.json +9 -0
  82. package/videos/oddlyalive-demos/index.html +246 -0
  83. package/videos/oddlyalive-demos/meta.json +5 -0
  84. package/videos/oddlyalive-demos/package-lock.json +506 -0
  85. package/videos/oddlyalive-demos/package.json +19 -0
  86. package/videos/oddlyalive-demos/prepare-assets.mjs +16 -0
@@ -0,0 +1,122 @@
1
+ # Demo videos
2
+
3
+ The demo package proves the current alpha with the real OddlyAlive solvers.
4
+ There are no hand-authored motion keyframes or stock clips. Locally generated,
5
+ high-resolution cutouts provide material appearance; SVG strings, shadows,
6
+ nets, water, and deformation remain driven by deterministic simulation.
7
+
8
+ ## Outputs
9
+
10
+ | File | Duration | Purpose |
11
+ | --- | ---: | --- |
12
+ | `videos/oddlyalive-demos/renders/oddlyalive-demos-overview.mp4` | 32.0s | Five-recipe reel |
13
+ | `videos/oddlyalive-demos/renders/demos/01-string-touch.mp4` | 6.4s | Strand contact and peel |
14
+ | `videos/oddlyalive-demos/renders/demos/02-crystal-mobile.mp4` | 6.4s | Weighted hanging payloads |
15
+ | `videos/oddlyalive-demos/renders/demos/03-ball-lab.mp4` | 6.4s | Material response comparison |
16
+ | `videos/oddlyalive-demos/renders/demos/04-football-kick.mp4` | 6.4s | Timed impulse and spin |
17
+ | `videos/oddlyalive-demos/renders/demos/05-shoe-splash.mp4` | 6.4s | Body-to-surface coupling |
18
+ | `docs/assets/oddlyalive-string-touch.gif` | 6.4s | Lightweight README preview |
19
+
20
+ The MP4s are intentionally ignored by Git. Review them locally first; if
21
+ approved, upload them as release assets or to the chosen media host instead of
22
+ inflating the source repository.
23
+
24
+ ## One-command rebuild
25
+
26
+ Install Node.js 20+ and FFmpeg, then run:
27
+
28
+ ```bash
29
+ npm run demos:render
30
+ ```
31
+
32
+ That command:
33
+
34
+ 1. installs the pinned demo-project dependencies;
35
+ 2. bundles the current OddlyAlive engine for a network-free browser render;
36
+ 3. runs HyperFrames lint, runtime, layout, motion, and contrast checks;
37
+ 4. renders the 1920×1080, 60 fps master;
38
+ 5. cuts five frame-accurate 6.4-second MP4s;
39
+ 6. rebuilds the README GIF; and
40
+ 7. decodes and verifies every MP4.
41
+
42
+ Set `HF_WORKERS=1` on a low-memory machine:
43
+
44
+ ```bash
45
+ HF_WORKERS=1 npm run demos:render
46
+ ```
47
+
48
+ ## Preview without rendering
49
+
50
+ ```bash
51
+ cd videos/oddlyalive-demos
52
+ npm ci
53
+ npm run build:bundle
54
+ npm run dev
55
+ ```
56
+
57
+ The preview is seek-safe: scrubbing to any time paints the corresponding
58
+ precomputed simulation frame. It does not use `requestAnimationFrame`,
59
+ `performance.now()`, network fetches, or random values.
60
+
61
+ ## Verify existing outputs
62
+
63
+ ```bash
64
+ npm run demos:verify
65
+ ```
66
+
67
+ The verifier checks:
68
+
69
+ - H.264 video with no audio stream;
70
+ - 1920×1080 frame size;
71
+ - 60 fps;
72
+ - yuv420p pixel format;
73
+ - 32.0 seconds / 1,920 frames for the reel;
74
+ - 6.4 seconds / 384 frames for each recipe; and
75
+ - a complete FFmpeg decode with zero reported errors.
76
+
77
+ It also prints a SHA-256 checksum for every file so a reviewed master can be
78
+ identified exactly.
79
+
80
+ ## Approved `0.2.0-alpha.2` media
81
+
82
+ The 2026-07-21 release-candidate encodes were decoded, visually inspected, and
83
+ approved with these SHA-256 checksums:
84
+
85
+ | File | SHA-256 |
86
+ | --- | --- |
87
+ | `oddlyalive-demos-overview.mp4` | `2b885811737841364721a16b38c7417810d1f13bcb785caeca5318bf2bfb7ad8` |
88
+ | `01-string-touch.mp4` | `a4816683c1edb8cc9b9c7a8d0937ffb36c5ddb47899df5bae1aff13bd27d373c` |
89
+ | `02-crystal-mobile.mp4` | `e148c455f480e52bfb18c48f691bc98d46acd5dcb7f383d101833644ab3d0834` |
90
+ | `03-ball-lab.mp4` | `38fcb06e7a0f6c571ef7d49055aa38242581b5aef6805cbabb054c0b3012d2e5` |
91
+ | `04-football-kick.mp4` | `54b00a6adaeab87140403dbd3478a635c840424c56597e15ff3c2e2558088929` |
92
+ | `05-shoe-splash.mp4` | `a7b061d1a9f192a2c944ef8001eafa4b6a4db7c9e2be0fc0a150b70cc408bc0b` |
93
+ | `oddlyalive-string-touch.gif` | `da5d2db730703fdd7b99293f00a7e4e86de42137faf8252f13ac158bad5a00c2` |
94
+
95
+ If any file differs, treat it as a new candidate and repeat decode and visual
96
+ review before upload.
97
+
98
+ ## Review-before-publish protocol
99
+
100
+ Do not push or publish merely because the build passed.
101
+
102
+ 1. Watch the full reel at normal speed.
103
+ 2. Watch every recipe clip twice: once for design, once for physical behavior.
104
+ 3. Check the first and last frame of each clip.
105
+ 4. Run `npm run demos:verify`.
106
+ 5. Review `git diff` and confirm generated MP4s remain ignored.
107
+ 6. Only after explicit approval, commit the source/docs/GIF.
108
+ 7. Upload approved MP4s as release assets separately.
109
+ 8. Update GitHub or npm only as a distinct, confirmed action.
110
+
111
+ ## Design contract
112
+
113
+ - 1920×1080 landscape, 60 fps
114
+ - 6.4 seconds per recipe
115
+ - silent by design
116
+ - paper, ink, and coral OddlyAlive identity
117
+ - photographic object cutouts with procedural SVG physics and deformation
118
+ - hard cuts between recipes so transitions never hide the physics
119
+ - fixed-step solvers at four 240Hz substeps per displayed frame
120
+
121
+ The composition source and exact scene sequence live in
122
+ `videos/oddlyalive-demos/`.
@@ -0,0 +1,115 @@
1
+ # OddlyAlive quick start
2
+
3
+ OddlyAlive turns a small JSON scene into deterministic simulation frames and
4
+ renders them as procedural SVG. The same input produces the same output on
5
+ every run.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 20 or newer
10
+ - npm
11
+ - A modern browser
12
+ - FFmpeg only if you want to rebuild the demo videos
13
+
14
+ ## 1. Run the gallery
15
+
16
+ ```bash
17
+ git clone https://github.com/thevibefounder/OddlyAlive.git
18
+ cd OddlyAlive
19
+ npm start
20
+ ```
21
+
22
+ Open the local URL printed in the terminal. Choose any of the five recipes:
23
+
24
+ - `string-touch`
25
+ - `crystal-mobile`
26
+ - `ball-lab`
27
+ - `football-kick`
28
+ - `shoe-splash`
29
+
30
+ The gallery runs entirely on your machine. It does not require an API key,
31
+ cloud account, or hosted service.
32
+
33
+ ## 2. Run one recipe directly
34
+
35
+ ```bash
36
+ npx oddlyalive play string-touch
37
+ npx oddlyalive play football-kick
38
+ npx oddlyalive play shoe-splash
39
+ ```
40
+
41
+ Use `npx oddlyalive list` to see every built-in recipe.
42
+
43
+ ## 3. Create your own local project
44
+
45
+ ```bash
46
+ npx oddlyalive new my-motion --preset crystal-mobile
47
+ cd my-motion
48
+ npm start
49
+ ```
50
+
51
+ The generated folder contains a complete starter, including `scene.json`.
52
+ Change the physical parameters there and refresh the browser.
53
+
54
+ For example, a rigid ball is controlled by familiar material properties:
55
+
56
+ ```json
57
+ {
58
+ "radius": 38,
59
+ "mass": 0.43,
60
+ "restitution": 0.62,
61
+ "friction": 0.58,
62
+ "vx": 0,
63
+ "vy": 0
64
+ }
65
+ ```
66
+
67
+ The scene schema is at `schemas/scene.schema.json`. Point your editor at it for
68
+ autocomplete and validation.
69
+
70
+ ## 4. Inspect or simulate without a browser
71
+
72
+ ```bash
73
+ npx oddlyalive inspect scene.json
74
+ npx oddlyalive simulate scene.json --output result.json
75
+ ```
76
+
77
+ `inspect` validates the scene and prints its structure. `simulate` runs the
78
+ fixed-step solver and writes deterministic frame data.
79
+
80
+ ## 5. Use OddlyAlive as a library
81
+
82
+ ```js
83
+ import {
84
+ createStringTouchScene,
85
+ simulateScene
86
+ } from "oddlyalive";
87
+
88
+ const scene = createStringTouchScene({
89
+ payload: { text: "HELLO PHYSICS" }
90
+ });
91
+
92
+ const result = simulateScene(scene);
93
+ console.log(result.fingerprint);
94
+ console.log(result.diagnostics);
95
+ ```
96
+
97
+ The renderers are exported too, so a browser project can mount the result into
98
+ an SVG element without recreating the physics.
99
+
100
+ ## 6. Verify the repository
101
+
102
+ ```bash
103
+ npm test
104
+ npm run check
105
+ ```
106
+
107
+ The tests lock validation behavior and deterministic fingerprints across all
108
+ three solver families.
109
+
110
+ ## Where to go next
111
+
112
+ - `docs/ARCHITECTURE.md` — solver, scene, and renderer boundaries
113
+ - `docs/ROADMAP.md` — planned object families
114
+ - `skills/oddlyalive/SKILL.md` — instructions for Codex and compatible agents
115
+ - `docs/DEMO-VIDEOS.md` — reproducible video build and review workflow
@@ -0,0 +1,33 @@
1
+ # Roadmap
2
+
3
+ ## v0.1 — Touch the strings
4
+
5
+ - Extract the deterministic strand solver.
6
+ - Publish the JSON scene contract.
7
+ - Ship one-command CLI, SVG demo, diagnostics, and agent skill.
8
+ - Reproduce local catch, pressure, friction, staggered peel, and living settle.
9
+
10
+ ## v0.2 — Object recipes
11
+
12
+ - [x] Add rigid-circle adapter and `football-kick`.
13
+ - [x] Add weighted `crystal-mobile`.
14
+ - [x] Add `ball-lab` material comparison.
15
+ - [x] Add `shoe-splash` coupled wave recipe.
16
+ - [x] Ship a unified five-example gallery.
17
+ - [ ] Add articulated walking and `flower-touch`.
18
+ - [ ] Add `pluck`, `drag`, `shake`, and reusable `drop` gesture primitives.
19
+ - [x] Add automated visual regression fixtures for every recipe.
20
+
21
+ ## v0.3 — Prompt compiler
22
+
23
+ - Translate natural language into bounded scene parameters.
24
+ - Support local model providers and cloud agent workflows.
25
+ - Reject impossible or ambiguous physical mappings with useful guidance.
26
+ - Keep AI output behind schema validation and deterministic simulation.
27
+
28
+ ## v0.4 — Render everywhere
29
+
30
+ - HyperFrames adapter and MP4 recipe.
31
+ - Canvas/WebGL renderer for larger particle counts.
32
+ - Transparent PNG sequence and animated SVG export.
33
+ - Optional interactive editor for contact paths and material envelopes.
@@ -0,0 +1,4 @@
1
+ # Provenance
2
+
3
+ The ball artwork is original procedural SVG built from circles, paths and
4
+ polygons. No external image assets are used.
@@ -0,0 +1,33 @@
1
+ import {
2
+ createRigidBallRenderer,
3
+ simulateScene,
4
+ validateScene
5
+ } from "../../src/index.js";
6
+ import { mountPlayer } from "../shared/player.js";
7
+
8
+ await new Promise((resolve) => requestAnimationFrame(resolve));
9
+
10
+ const response = await fetch("./scene.json");
11
+ if (!response.ok) throw new Error(`Could not load scene: ${response.status}`);
12
+ const scene = validateScene(await response.json());
13
+ const simulation = simulateScene(scene);
14
+ const renderer = createRigidBallRenderer(
15
+ document.querySelector("#motion"),
16
+ simulation,
17
+ {
18
+ mode: "lab",
19
+ ballAssets: {
20
+ baseball: "../../assets/photoreal/baseball.png",
21
+ basketball: "../../assets/photoreal/basketball.png",
22
+ football: "../../assets/photoreal/soccer-ball.png"
23
+ }
24
+ }
25
+ );
26
+
27
+ mountPlayer({
28
+ scene,
29
+ simulation,
30
+ renderer,
31
+ metricLabel: "collisions",
32
+ metricValue: String(simulation.diagnostics.collisionCount)
33
+ });
@@ -0,0 +1,56 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="OddlyAlive rigid ball material lab" />
7
+ <title>Ball Lab — OddlyAlive</title>
8
+ <link rel="stylesheet" href="../shared/example.css" />
9
+ </head>
10
+ <body class="theme-ball">
11
+ <main>
12
+ <header>
13
+ <a class="wordmark" href="../"><span class="mark"></span>OddlyAlive</a>
14
+ <nav>
15
+ <a href="../crystal-mobile/">← Crystal Mobile</a>
16
+ <a href="../football-kick/">Football Kick →</a>
17
+ </nav>
18
+ </header>
19
+ <section class="stage">
20
+ <div class="poster">
21
+ <div class="poster-copy">
22
+ <p class="eyebrow">03 / RIGID BODY LAB</p>
23
+ <h1>THREE BALLS.<br /><em>THREE LIVES.</em></h1>
24
+ <p class="description">
25
+ Diameter is only the silhouette. Mass, restitution, friction and
26
+ spin determine whether an object snaps, rebounds or settles.
27
+ </p>
28
+ </div>
29
+ <p class="corner-note">BASEBALL · 145 G<br />BASKETBALL · 625 G<br />FOOTBALL · 430 G</p>
30
+ <svg
31
+ id="motion"
32
+ class="motion"
33
+ viewBox="0 0 960 540"
34
+ role="img"
35
+ aria-label="A baseball, basketball and football falling and bouncing differently"
36
+ ></svg>
37
+ <div class="folio">OA—003</div>
38
+ <div id="loading" class="loading">SOLVING WORLD…</div>
39
+ </div>
40
+ </section>
41
+ <footer>
42
+ <div class="controls">
43
+ <button id="toggle" type="button">PAUSE</button>
44
+ <input id="scrubber" type="range" min="0" value="0" aria-label="Timeline" />
45
+ </div>
46
+ <dl class="metrics">
47
+ <div><dt>solver</dt><dd>rigid circles</dd></div>
48
+ <div><dt>step</dt><dd>240 Hz</dd></div>
49
+ <div><dt id="metric-name">impacts</dt><dd id="metric">—</dd></div>
50
+ <div><dt>fingerprint</dt><dd id="fingerprint">—</dd></div>
51
+ </dl>
52
+ </footer>
53
+ </main>
54
+ <script type="module" src="./app.js"></script>
55
+ </body>
56
+ </html>
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "../../schemas/scene.schema.json",
3
+ "version": 1,
4
+ "type": "rigid-balls",
5
+ "name": "OddlyAlive — Ball Lab",
6
+ "seed": 21,
7
+ "canvas": { "width": 960, "height": 540 },
8
+ "timing": { "duration": 6.4, "fps": 60, "substeps": 4, "preRoll": 0 },
9
+ "world": {
10
+ "gravity": 920,
11
+ "airDrag": 0.08,
12
+ "floorY": 456,
13
+ "left": 84,
14
+ "right": 876,
15
+ "groundFriction": 1.8
16
+ },
17
+ "bodies": [
18
+ {
19
+ "id": "baseball",
20
+ "kind": "baseball",
21
+ "label": "BASEBALL",
22
+ "x": 250,
23
+ "y": 104,
24
+ "radius": 29,
25
+ "mass": 0.145,
26
+ "restitution": 0.52,
27
+ "friction": 0.38,
28
+ "vx": 34,
29
+ "vy": 0,
30
+ "angle": -8,
31
+ "angularVelocity": 1.2,
32
+ "color": "#f4efe4"
33
+ },
34
+ {
35
+ "id": "basketball",
36
+ "kind": "basketball",
37
+ "label": "BASKETBALL",
38
+ "x": 480,
39
+ "y": 72,
40
+ "radius": 48,
41
+ "mass": 0.625,
42
+ "restitution": 0.76,
43
+ "friction": 0.58,
44
+ "vx": -18,
45
+ "vy": 0,
46
+ "angle": 4,
47
+ "angularVelocity": -0.5,
48
+ "color": "#e66b2d"
49
+ },
50
+ {
51
+ "id": "football",
52
+ "kind": "football",
53
+ "label": "FOOTBALL",
54
+ "x": 710,
55
+ "y": 124,
56
+ "radius": 39,
57
+ "mass": 0.43,
58
+ "restitution": 0.63,
59
+ "friction": 0.5,
60
+ "vx": -42,
61
+ "vy": 0,
62
+ "angle": 11,
63
+ "angularVelocity": 0.8,
64
+ "color": "#f6f3e9"
65
+ }
66
+ ],
67
+ "impulses": []
68
+ }
@@ -0,0 +1,4 @@
1
+ # Provenance
2
+
3
+ All geometry is original SVG authored for OddlyAlive. No external raster or
4
+ third-party illustration contributes pixels to this example.
@@ -0,0 +1,26 @@
1
+ import {
2
+ createCrystalRenderer,
3
+ simulateScene,
4
+ validateScene
5
+ } from "../../src/index.js";
6
+ import { mountPlayer } from "../shared/player.js";
7
+
8
+ await new Promise((resolve) => requestAnimationFrame(resolve));
9
+
10
+ const response = await fetch("./scene.json");
11
+ if (!response.ok) throw new Error(`Could not load scene: ${response.status}`);
12
+ const scene = validateScene(await response.json());
13
+ const simulation = simulateScene(scene);
14
+ const renderer = createCrystalRenderer(
15
+ document.querySelector("#motion"),
16
+ simulation,
17
+ { pendantHref: "../../assets/photoreal/crystal.png" }
18
+ );
19
+
20
+ mountPlayer({
21
+ scene,
22
+ simulation,
23
+ renderer,
24
+ metricLabel: "peak grip",
25
+ metricValue: `${simulation.diagnostics.observedMaxStaticGrip}/${scene.contact.maxStaticGrip}`
26
+ });
@@ -0,0 +1,56 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="OddlyAlive weighted crystal mobile example" />
7
+ <title>Crystal Mobile — OddlyAlive</title>
8
+ <link rel="stylesheet" href="../shared/example.css" />
9
+ </head>
10
+ <body class="theme-crystal">
11
+ <main>
12
+ <header>
13
+ <a class="wordmark" href="../"><span class="mark"></span>OddlyAlive</a>
14
+ <nav>
15
+ <a href="../string-touch/">← String Touch</a>
16
+ <a href="../ball-lab/">Ball Lab →</a>
17
+ </nav>
18
+ </header>
19
+ <section class="stage">
20
+ <div class="poster">
21
+ <div class="poster-copy">
22
+ <p class="eyebrow">02 / WEIGHTED STRANDS</p>
23
+ <h1>CRYSTAL<br /><em>MOBILE.</em></h1>
24
+ <p class="description">
25
+ The same touch becomes a different gesture when every cord carries
26
+ a faceted weight. Mass creates lag, overswing and a longer memory.
27
+ </p>
28
+ </div>
29
+ <p class="corner-note">13 CORDS<br />HEAVY TERMINALS<br />XPBD · 240 HZ</p>
30
+ <svg
31
+ id="motion"
32
+ class="motion"
33
+ viewBox="0 0 960 540"
34
+ role="img"
35
+ aria-label="A mobile of weighted crystals responding to an invisible touch"
36
+ ></svg>
37
+ <div class="folio">OA—002</div>
38
+ <div id="loading" class="loading">SOLVING WORLD…</div>
39
+ </div>
40
+ </section>
41
+ <footer>
42
+ <div class="controls">
43
+ <button id="toggle" type="button">PAUSE</button>
44
+ <input id="scrubber" type="range" min="0" value="0" aria-label="Timeline" />
45
+ </div>
46
+ <dl class="metrics">
47
+ <div><dt>solver</dt><dd>strand XPBD</dd></div>
48
+ <div><dt>step</dt><dd>240 Hz</dd></div>
49
+ <div><dt id="metric-name">grip</dt><dd id="metric">—</dd></div>
50
+ <div><dt>fingerprint</dt><dd id="fingerprint">—</dd></div>
51
+ </dl>
52
+ </footer>
53
+ </main>
54
+ <script type="module" src="./app.js"></script>
55
+ </body>
56
+ </html>
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "../../schemas/scene.schema.json",
3
+ "version": 1,
4
+ "type": "strand-field",
5
+ "name": "OddlyAlive — Crystal Mobile",
6
+ "seed": 17,
7
+ "canvas": {
8
+ "width": 960,
9
+ "height": 540
10
+ },
11
+ "timing": {
12
+ "duration": 6.4,
13
+ "fps": 60,
14
+ "substeps": 4,
15
+ "preRoll": 2
16
+ },
17
+ "field": {
18
+ "columns": 13,
19
+ "rows": 14,
20
+ "originX": 390,
21
+ "originY": 96,
22
+ "spacingX": 38,
23
+ "spacingY": 24
24
+ },
25
+ "material": {
26
+ "gravity": 480,
27
+ "linearDrag": 0.76,
28
+ "quadraticDrag": 0.0026,
29
+ "lengthCompliance": 0.0000011,
30
+ "compressionCompliance": 0.00052,
31
+ "bendCompliance": 0.00072,
32
+ "maxStretch": 1.045
33
+ },
34
+ "contact": {
35
+ "radiusX": 58,
36
+ "radiusY": 37,
37
+ "maxStaticGrip": 6,
38
+ "captureStartRow": 5,
39
+ "captureEndRow": 12
40
+ },
41
+ "gesture": {
42
+ "type": "touch-path",
43
+ "points": [
44
+ { "time": 0.48, "x": -60, "y": 320, "pressure": 0 },
45
+ { "time": 0.68, "x": 250, "y": 328, "pressure": 0.72 },
46
+ { "time": 1.14, "x": 430, "y": 360, "pressure": 1 },
47
+ { "time": 1.72, "x": 665, "y": 391, "pressure": 0.98 },
48
+ { "time": 2.14, "x": 900, "y": 379, "pressure": 0.94 },
49
+ { "time": 2.38, "x": 934, "y": 368, "pressure": 0.88 },
50
+ { "time": 2.72, "x": 770, "y": 404, "pressure": 0.86 },
51
+ { "time": 3.12, "x": 590, "y": 414, "pressure": 0.65 },
52
+ { "time": 3.58, "x": 390, "y": 386, "pressure": 0.32 },
53
+ { "time": 4.08, "x": 155, "y": 352, "pressure": 0.04 },
54
+ { "time": 4.34, "x": 30, "y": 348, "pressure": 0 }
55
+ ]
56
+ },
57
+ "payload": {
58
+ "text": "✦",
59
+ "fontFamily": "ui-monospace, SFMono-Regular, Menlo, monospace",
60
+ "fontSize": 12,
61
+ "terminalMass": 6
62
+ },
63
+ "render": {
64
+ "background": "#f4eedc",
65
+ "ink": "#292720",
66
+ "accent": "#cc5f4a",
67
+ "filament": "rgba(42, 40, 34, 0.45)"
68
+ }
69
+ }
@@ -0,0 +1,4 @@
1
+ # Provenance
2
+
3
+ The football and field are original procedural SVG. The motion is solver output
4
+ from one scheduled impulse; no authored flight path is used.
@@ -0,0 +1,33 @@
1
+ import {
2
+ createRigidBallRenderer,
3
+ simulateScene,
4
+ validateScene
5
+ } from "../../src/index.js";
6
+ import { mountPlayer } from "../shared/player.js";
7
+
8
+ await new Promise((resolve) => requestAnimationFrame(resolve));
9
+
10
+ const response = await fetch("./scene.json");
11
+ if (!response.ok) throw new Error(`Could not load scene: ${response.status}`);
12
+ const scene = validateScene(await response.json());
13
+ const simulation = simulateScene(scene);
14
+ const renderer = createRigidBallRenderer(
15
+ document.querySelector("#motion"),
16
+ simulation,
17
+ {
18
+ mode: "kick",
19
+ labels: false,
20
+ ballAssets: {
21
+ football: "../../assets/photoreal/soccer-ball.png"
22
+ },
23
+ kickActorHref: "../../assets/photoreal/kicking-cleat.png"
24
+ }
25
+ );
26
+
27
+ mountPlayer({
28
+ scene,
29
+ simulation,
30
+ renderer,
31
+ metricLabel: "impulses",
32
+ metricValue: String(simulation.diagnostics.appliedImpulses)
33
+ });