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.
- package/CHANGELOG.md +45 -0
- package/CONTRIBUTING.md +62 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/SECURITY.md +29 -0
- package/assets/photoreal/PROVENANCE.md +19 -0
- package/assets/photoreal/baseball.png +0 -0
- package/assets/photoreal/basketball.png +0 -0
- package/assets/photoreal/crystal.png +0 -0
- package/assets/photoreal/kicking-cleat.png +0 -0
- package/assets/photoreal/letter-charm-square.png +0 -0
- package/assets/photoreal/letter-charm.png +0 -0
- package/assets/photoreal/sneaker.png +0 -0
- package/assets/photoreal/soccer-ball.png +0 -0
- package/bin/oddlyalive.js +283 -0
- package/docs/ARCHITECTURE.md +55 -0
- package/docs/DEMO-VIDEOS.md +122 -0
- package/docs/QUICKSTART.md +115 -0
- package/docs/ROADMAP.md +33 -0
- package/examples/ball-lab/PROVENANCE.md +4 -0
- package/examples/ball-lab/app.js +33 -0
- package/examples/ball-lab/index.html +56 -0
- package/examples/ball-lab/scene.json +68 -0
- package/examples/crystal-mobile/PROVENANCE.md +4 -0
- package/examples/crystal-mobile/app.js +26 -0
- package/examples/crystal-mobile/index.html +56 -0
- package/examples/crystal-mobile/scene.json +69 -0
- package/examples/football-kick/PROVENANCE.md +4 -0
- package/examples/football-kick/app.js +33 -0
- package/examples/football-kick/index.html +56 -0
- package/examples/football-kick/scene.json +44 -0
- package/examples/gallery.css +379 -0
- package/examples/index.html +109 -0
- package/examples/shared/example.css +320 -0
- package/examples/shared/player.js +85 -0
- package/examples/shoe-splash/PROVENANCE.md +4 -0
- package/examples/shoe-splash/app.js +26 -0
- package/examples/shoe-splash/index.html +56 -0
- package/examples/shoe-splash/scene.json +38 -0
- package/examples/string-touch/PROVENANCE.md +11 -0
- package/examples/string-touch/app.js +79 -0
- package/examples/string-touch/index.html +70 -0
- package/examples/string-touch/scene.json +73 -0
- package/examples/string-touch/styles.css +273 -0
- package/package.json +82 -0
- package/schemas/scene.schema.json +336 -0
- package/scripts/render-demo-videos.sh +77 -0
- package/scripts/serve.js +88 -0
- package/scripts/update-visual-fixtures.mjs +60 -0
- package/scripts/verify-demo-videos.mjs +138 -0
- package/skills/oddlyalive/SKILL.md +68 -0
- package/skills/oddlyalive/agents/openai.yaml +4 -0
- package/skills/oddlyalive/references/object-models.md +25 -0
- package/skills/oddlyalive/references/recipes.md +27 -0
- package/skills/oddlyalive/references/scene-schema.md +84 -0
- package/src/crystal-renderer.js +338 -0
- package/src/gesture.js +58 -0
- package/src/index.js +44 -0
- package/src/rigid-balls.js +304 -0
- package/src/rigid-renderer.js +760 -0
- package/src/rigid-scene.js +223 -0
- package/src/scene-registry.js +17 -0
- package/src/scene.js +206 -0
- package/src/state-hash.js +17 -0
- package/src/strands.js +773 -0
- package/src/surface-wave.js +209 -0
- package/src/svg-renderer.js +348 -0
- package/src/wave-renderer.js +463 -0
- package/src/wave-scene.js +163 -0
- package/tests/cli.test.js +101 -0
- package/tests/determinism.test.js +57 -0
- package/tests/fixtures/visual-baselines.json +27 -0
- package/tests/rigid-balls.test.js +77 -0
- package/tests/scene.test.js +78 -0
- package/tests/surface-wave.test.js +39 -0
- package/tests/visual-fixtures.test.js +43 -0
- package/videos/oddlyalive-demos/BRIEF.md +40 -0
- package/videos/oddlyalive-demos/DEMO-PLAN.md +13 -0
- package/videos/oddlyalive-demos/README.md +45 -0
- package/videos/oddlyalive-demos/app.js +212 -0
- package/videos/oddlyalive-demos/hyperframes.json +9 -0
- package/videos/oddlyalive-demos/index.html +246 -0
- package/videos/oddlyalive-demos/meta.json +5 -0
- package/videos/oddlyalive-demos/package-lock.json +506 -0
- package/videos/oddlyalive-demos/package.json +19 -0
- 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
|
package/docs/ROADMAP.md
ADDED
|
@@ -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,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,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,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
|
+
});
|