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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to OddlyAlive are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/) for its public package versions.
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.2.0-alpha.2] - 2026-07-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Photographic, provenance-tracked object cutouts shared by the public gallery
|
|
13
|
+
and the deterministic HyperFrames demo composition.
|
|
14
|
+
- Five 6.4-second recipe demos and a 32-second high-quality overview render.
|
|
15
|
+
- Automated visual-contract fixtures covering all recipe fingerprints,
|
|
16
|
+
renderer sources, composition sources, and approved image assets.
|
|
17
|
+
- GitHub Actions CI, contribution guidance, security policy, issue forms, and
|
|
18
|
+
pull-request template.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Rebuilt the strand, crystal, rigid-body, football, and wave renderers around
|
|
23
|
+
hybrid photographic materials and simulated SVG deformation.
|
|
24
|
+
- Consolidated demo assets under `assets/photoreal/` with explicit provenance.
|
|
25
|
+
- Set the canonical package homepage to the public OddlyAlive project showcase at
|
|
26
|
+
`https://thevibefounder.com/os/oddlyalive/`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Included the photographic asset library and correct PNG response types in
|
|
31
|
+
generated recipe projects.
|
|
32
|
+
- Kept generated HyperFrames bundles, render outputs, media caches, snapshots,
|
|
33
|
+
and Studio thumbnails out of source control and npm package contents.
|
|
34
|
+
|
|
35
|
+
## [0.2.0-alpha.1] - 2026-07-18
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Initial public alpha with deterministic strand, rigid-circle, and coupled
|
|
40
|
+
surface-wave solvers.
|
|
41
|
+
- JSON scene schema, CLI, five browser recipes, diagnostics, and Codex skill.
|
|
42
|
+
|
|
43
|
+
[Unreleased]: https://github.com/thevibefounder/OddlyAlive/compare/v0.2.0-alpha.2...HEAD
|
|
44
|
+
[0.2.0-alpha.2]: https://github.com/thevibefounder/OddlyAlive/compare/v0.2.0-alpha.1...v0.2.0-alpha.2
|
|
45
|
+
[0.2.0-alpha.1]: https://github.com/thevibefounder/OddlyAlive/releases/tag/v0.2.0-alpha.1
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Contributing to OddlyAlive
|
|
2
|
+
|
|
3
|
+
Thanks for helping make deterministic object motion more useful and easier to
|
|
4
|
+
understand.
|
|
5
|
+
|
|
6
|
+
## Development setup
|
|
7
|
+
|
|
8
|
+
OddlyAlive requires Node.js 20 or newer. The demo-video pipeline additionally
|
|
9
|
+
requires FFmpeg.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm ci
|
|
13
|
+
npm start
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run the complete repository gate before opening a pull request:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run check
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For changes to the HyperFrames demo composition:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd videos/oddlyalive-demos
|
|
26
|
+
npm ci
|
|
27
|
+
npm run check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Project invariants
|
|
31
|
+
|
|
32
|
+
- AI may describe scene intent; deterministic simulation authors the motion.
|
|
33
|
+
- Simulations use fixed timesteps and may not depend on wall-clock time.
|
|
34
|
+
- Seeded material variation and diagnostic fingerprints must remain
|
|
35
|
+
reproducible.
|
|
36
|
+
- Choose a physical model that fits the object family. Do not describe a
|
|
37
|
+
recipe as physically universal.
|
|
38
|
+
- Keep solvers renderer-agnostic. SVG is the reference renderer, not the
|
|
39
|
+
physics representation.
|
|
40
|
+
- Disclose whether example artwork is original, generated, licensed, or only
|
|
41
|
+
used as reference.
|
|
42
|
+
|
|
43
|
+
## Visual fixture changes
|
|
44
|
+
|
|
45
|
+
`tests/fixtures/visual-baselines.json` protects every shipped recipe's
|
|
46
|
+
simulation fingerprint and the approved bytes that define its appearance.
|
|
47
|
+
When an intentional visual change has been reviewed in the browser, update the
|
|
48
|
+
fixture explicitly:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run fixtures:update
|
|
52
|
+
npm run test:visual
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Include before/after screenshots or a short capture in the pull request. A
|
|
56
|
+
baseline update without visual evidence should not be merged.
|
|
57
|
+
|
|
58
|
+
## Pull requests
|
|
59
|
+
|
|
60
|
+
Keep changes focused, add or update tests for behavioral changes, and explain
|
|
61
|
+
which solver or renderer family is affected. Generated MP4 files belong in
|
|
62
|
+
release assets, not Git history.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheVibeFounder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# OddlyAlive
|
|
2
|
+
|
|
3
|
+
**Make anything feel alive.**
|
|
4
|
+
|
|
5
|
+
[Project page](https://thevibefounder.com/os/oddlyalive/) ·
|
|
6
|
+
[Source](https://github.com/thevibefounder/OddlyAlive)
|
|
7
|
+
|
|
8
|
+
OddlyAlive is an open-source toolkit for natural, deterministic object motion.
|
|
9
|
+
Describe the physical setup once; the solver produces every frame.
|
|
10
|
+
|
|
11
|
+
The toolkit began with the tactile strand interaction developed for
|
|
12
|
+
TheVibeFounder’s city-postcard series. It now ships three small, inspectable
|
|
13
|
+
solver families: flexible strands, rigid balls, and a rigid prop coupled to a
|
|
14
|
+
damped wave surface.
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
No runtime dependencies. No cloud account. No animation keyframes.
|
|
19
|
+
|
|
20
|
+
## Start in 60 seconds
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/thevibefounder/OddlyAlive.git
|
|
24
|
+
cd OddlyAlive
|
|
25
|
+
npm start
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Open the printed local URL and choose a recipe. Nothing is uploaded; the
|
|
29
|
+
simulation and SVG rendering run locally.
|
|
30
|
+
|
|
31
|
+
For the full install, customization, and scene-authoring path, read
|
|
32
|
+
[`docs/QUICKSTART.md`](docs/QUICKSTART.md).
|
|
33
|
+
|
|
34
|
+
## Try the repository
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then open the printed local URL. The gallery contains five SVG-rendered physics
|
|
41
|
+
examples with local high-resolution object cutouts, simulated at a fixed 240Hz
|
|
42
|
+
and rendered at 60fps.
|
|
43
|
+
|
|
44
|
+
## Built-in recipes
|
|
45
|
+
|
|
46
|
+
| Recipe | Physical idea | Good for |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `string-touch` | XPBD strands + pressure/friction contact | strings, charms, garlands, hanging type |
|
|
49
|
+
| `crystal-mobile` | weighted terminal masses on strands | crystals, pendants, mobiles |
|
|
50
|
+
| `ball-lab` | rigid circles + material response | balls, marbles, loose round props |
|
|
51
|
+
| `football-kick` | one impulse + gravity/spin/friction | kicks, hits, throws |
|
|
52
|
+
| `shoe-splash` | rigid prop + damped 1D surface | graphic splashes, floating props, ripples |
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx oddlyalive list
|
|
56
|
+
npx oddlyalive play crystal-mobile
|
|
57
|
+
npx oddlyalive play football-kick
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Create a self-contained starter:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx oddlyalive new my-motion \
|
|
64
|
+
--preset football-kick
|
|
65
|
+
|
|
66
|
+
cd my-motion
|
|
67
|
+
npm start
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Inspect or simulate any scene without opening a browser:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx oddlyalive inspect scene.json
|
|
74
|
+
npx oddlyalive simulate scene.json --output result.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`schemas/scene.schema.json` provides editor autocomplete and catches malformed
|
|
78
|
+
recipes before simulation.
|
|
79
|
+
|
|
80
|
+
## Why this exists
|
|
81
|
+
|
|
82
|
+
Most prompt-to-animation systems ask a model to guess transforms frame by frame.
|
|
83
|
+
OddlyAlive uses the model only to select and configure physical intent:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
prompt → scene recipe → constraints + gesture → simulation → renderer
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The same scene gives the same frames on every run. That makes the result
|
|
90
|
+
seek-safe, testable, renderable in the browser, and suitable for video pipelines.
|
|
91
|
+
|
|
92
|
+
## Alpha scope
|
|
93
|
+
|
|
94
|
+
Available now:
|
|
95
|
+
|
|
96
|
+
- XPBD strand fields with length, bend, and anchor constraints
|
|
97
|
+
- `FREE → STICK → SLIP → COOLDOWN` friction per strand
|
|
98
|
+
- Elliptical touch capsule with pressure
|
|
99
|
+
- Deterministic seeded material variation
|
|
100
|
+
- SVG reference renderer
|
|
101
|
+
- JSON scene schema and diagnostics
|
|
102
|
+
- Deterministic circle collisions, restitution, friction, spin and impulses
|
|
103
|
+
- Weighted strand payloads for pendants and crystals
|
|
104
|
+
- Damped 1D wave surface coupled to a falling rigid prop
|
|
105
|
+
- Five one-command browser demos and project presets
|
|
106
|
+
- Bundled Codex skill
|
|
107
|
+
|
|
108
|
+
Planned object families:
|
|
109
|
+
|
|
110
|
+
- articulated walking bodies with joints and inverse kinematics
|
|
111
|
+
- articulated flowers, rackets, and hinged mobiles
|
|
112
|
+
- cloth, banners, and garlands
|
|
113
|
+
- a prompt compiler for local models and cloud agents
|
|
114
|
+
|
|
115
|
+
## Library usage
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import {
|
|
119
|
+
createStringTouchScene,
|
|
120
|
+
simulateScene
|
|
121
|
+
} from "oddlyalive";
|
|
122
|
+
|
|
123
|
+
const scene = createStringTouchScene({
|
|
124
|
+
payload: { text: "HELLO PHYSICS" }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = simulateScene(scene);
|
|
128
|
+
console.log(result.diagnostics);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
See `examples/` for the complete browser gallery and
|
|
132
|
+
`docs/ARCHITECTURE.md` for the separation between AI, physics, and rendering.
|
|
133
|
+
Contributions are welcome; start with [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
134
|
+
|
|
135
|
+
## Rebuild the demo videos
|
|
136
|
+
|
|
137
|
+
With Node 20+ and FFmpeg installed:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run demos:render
|
|
141
|
+
npm run demos:verify
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This produces a 32-second reel, five 6.4-second recipe clips, and the README
|
|
145
|
+
GIF from the same deterministic simulations. See
|
|
146
|
+
[`docs/DEMO-VIDEOS.md`](docs/DEMO-VIDEOS.md) for output paths, preview commands,
|
|
147
|
+
and the review-before-publish protocol.
|
|
148
|
+
|
|
149
|
+
## Status
|
|
150
|
+
|
|
151
|
+
`0.2.0-alpha.2` is a focused technical preview. The five listed recipes are
|
|
152
|
+
real and deterministic; full fluid dynamics, cloth, articulated walking, and
|
|
153
|
+
the broader “animate any object” prompt compiler remain roadmap items.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT. Example artwork is original or generated specifically for this project;
|
|
158
|
+
see [`assets/photoreal/PROVENANCE.md`](assets/photoreal/PROVENANCE.md).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
OddlyAlive is currently an alpha. Security fixes are applied to the newest
|
|
6
|
+
`0.2.x` prerelease only.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `0.2.x` alpha | Yes |
|
|
11
|
+
| Earlier versions | No |
|
|
12
|
+
|
|
13
|
+
## Reporting a vulnerability
|
|
14
|
+
|
|
15
|
+
Please use GitHub's private vulnerability reporting for this repository:
|
|
16
|
+
|
|
17
|
+
1. Open the repository's **Security** tab.
|
|
18
|
+
2. Choose **Report a vulnerability**.
|
|
19
|
+
3. Include reproduction steps, affected versions, impact, and any suggested
|
|
20
|
+
mitigation.
|
|
21
|
+
|
|
22
|
+
Do not open a public issue for an unpatched vulnerability. Maintainers will
|
|
23
|
+
acknowledge a report as soon as practical and coordinate disclosure after a
|
|
24
|
+
fix is available.
|
|
25
|
+
|
|
26
|
+
OddlyAlive executes local scene recipes and serves a local development site.
|
|
27
|
+
Reports involving path traversal, unintended network access, malicious scene
|
|
28
|
+
input, generated-project integrity, or package-install behavior are especially
|
|
29
|
+
useful.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Photoreal example assets
|
|
2
|
+
|
|
3
|
+
These transparent PNG object cutouts were generated specifically for
|
|
4
|
+
OddlyAlive with OpenAI's image-generation tooling on 2026-07-19. They do not
|
|
5
|
+
contain stock photography, brand marks, or third-party source pixels.
|
|
6
|
+
|
|
7
|
+
The generation prompts requested isolated studio-product photographs on a
|
|
8
|
+
solid chroma background: a soccer ball, basketball, baseball, white canvas
|
|
9
|
+
sneaker, faceted champagne crystal pendant, white soccer cleat with lower leg,
|
|
10
|
+
and two ivory-enamel/brass letter-charm shapes. The chroma backgrounds were
|
|
11
|
+
removed locally with a soft matte and despill pass. Only the final transparent
|
|
12
|
+
cutouts are distributed; chroma intermediates and review renders are excluded.
|
|
13
|
+
|
|
14
|
+
The files are used solely as appearance layers. OddlyAlive's deterministic
|
|
15
|
+
solvers still calculate the bodies, constraints, collisions, net response,
|
|
16
|
+
surface waves, and every output frame.
|
|
17
|
+
|
|
18
|
+
The project distributes these generated assets under the repository's MIT
|
|
19
|
+
license to the extent permitted by applicable law.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { simulateScene, validateScene } from "../src/index.js";
|
|
14
|
+
import { startServer } from "../scripts/serve.js";
|
|
15
|
+
|
|
16
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const commands = new Set(["help", "inspect", "list", "new", "play", "simulate"]);
|
|
18
|
+
const presets = new Map([
|
|
19
|
+
["string-touch", "deterministic XPBD strand field"],
|
|
20
|
+
["crystal-mobile", "weighted payloads on flexible strands"],
|
|
21
|
+
["ball-lab", "rigid-body material comparison"],
|
|
22
|
+
["football-kick", "single-impulse flight, bounce and roll"],
|
|
23
|
+
["shoe-splash", "rigid prop coupled to a damped wave surface"]
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function flagValue(args, flag, fallback = undefined) {
|
|
27
|
+
const index = args.indexOf(flag);
|
|
28
|
+
return index === -1 ? fallback : args[index + 1];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function printHelp() {
|
|
32
|
+
console.log(`OddlyAlive — Make anything feel alive.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
oddlyalive play [gallery|preset] [--port 4173] [--no-open]
|
|
36
|
+
oddlyalive new <directory> [--preset string-touch] [--text "HELLO"]
|
|
37
|
+
oddlyalive inspect <scene.json>
|
|
38
|
+
oddlyalive simulate <scene.json> [--output result.json]
|
|
39
|
+
oddlyalive list
|
|
40
|
+
|
|
41
|
+
Start here:
|
|
42
|
+
npx oddlyalive play
|
|
43
|
+
npx oddlyalive play football-kick
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadScene(path) {
|
|
48
|
+
if (!path) throw new TypeError("A scene.json path is required.");
|
|
49
|
+
return validateScene(JSON.parse(readFileSync(resolve(path), "utf8")));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function diagnosticSummary(result) {
|
|
53
|
+
const { scene, diagnostics, fingerprint } = result;
|
|
54
|
+
const common = {
|
|
55
|
+
ok: true,
|
|
56
|
+
name: scene.name,
|
|
57
|
+
type: scene.type,
|
|
58
|
+
seed: scene.seed,
|
|
59
|
+
duration: scene.timing.duration,
|
|
60
|
+
fps: scene.timing.fps,
|
|
61
|
+
simulationHz: scene.timing.fps * scene.timing.substeps,
|
|
62
|
+
frameCount: Math.round(scene.timing.duration * scene.timing.fps) + 1,
|
|
63
|
+
fingerprint
|
|
64
|
+
};
|
|
65
|
+
if (scene.type === "strand-field") {
|
|
66
|
+
return {
|
|
67
|
+
...common,
|
|
68
|
+
particleCount: scene.field.columns * scene.field.rows,
|
|
69
|
+
configuredStaticGripCap: scene.contact.maxStaticGrip,
|
|
70
|
+
observedMaxStaticGrip: diagnostics.observedMaxStaticGrip,
|
|
71
|
+
observedMaxCombinedContact: diagnostics.observedMaxCombinedContact,
|
|
72
|
+
firstGripTime: diagnostics.firstGripTime,
|
|
73
|
+
lastGripTime: diagnostics.lastGripTime,
|
|
74
|
+
observedMaxStretch: diagnostics.observedMaxStretch,
|
|
75
|
+
maxStretchPerProjection: scene.material.maxStretch
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (scene.type === "rigid-balls") {
|
|
79
|
+
return {
|
|
80
|
+
...common,
|
|
81
|
+
bodyCount: scene.bodies.length,
|
|
82
|
+
collisionCount: diagnostics.collisionCount,
|
|
83
|
+
firstImpactTime: diagnostics.firstImpactTime,
|
|
84
|
+
maxSpeed: diagnostics.maxSpeed,
|
|
85
|
+
maxPenetration: diagnostics.maxPenetration,
|
|
86
|
+
appliedImpulses: diagnostics.appliedImpulses,
|
|
87
|
+
finalRestingBodies: diagnostics.finalRestingBodies
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...common,
|
|
92
|
+
surfaceSamples: scene.surface.samples,
|
|
93
|
+
impactCount: diagnostics.impactCount,
|
|
94
|
+
firstImpactTime: diagnostics.firstImpactTime,
|
|
95
|
+
maxWaveHeight: diagnostics.maxWaveHeight,
|
|
96
|
+
maxSubmersion: diagnostics.maxSubmersion,
|
|
97
|
+
maxBodySpeed: diagnostics.maxBodySpeed
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createStarter(args) {
|
|
102
|
+
const targetArgument = args[0]?.startsWith("-") ? undefined : args[0];
|
|
103
|
+
if (!targetArgument) {
|
|
104
|
+
throw new TypeError("Provide a directory name: oddlyalive new my-motion");
|
|
105
|
+
}
|
|
106
|
+
const target = resolve(targetArgument);
|
|
107
|
+
const preset = flagValue(args, "--preset", "string-touch");
|
|
108
|
+
const text = flagValue(args, "--text");
|
|
109
|
+
if (!presets.has(preset)) {
|
|
110
|
+
throw new RangeError(`Unknown preset: ${preset}`);
|
|
111
|
+
}
|
|
112
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
113
|
+
throw new Error(`Target directory is not empty: ${target}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mkdirSync(target, { recursive: true });
|
|
117
|
+
cpSync(join(packageRoot, "src"), join(target, "src"), { recursive: true });
|
|
118
|
+
cpSync(
|
|
119
|
+
join(packageRoot, "examples", preset),
|
|
120
|
+
join(target, "examples", preset),
|
|
121
|
+
{ recursive: true }
|
|
122
|
+
);
|
|
123
|
+
cpSync(
|
|
124
|
+
join(packageRoot, "examples", "shared"),
|
|
125
|
+
join(target, "examples", "shared"),
|
|
126
|
+
{ recursive: true }
|
|
127
|
+
);
|
|
128
|
+
cpSync(join(packageRoot, "schemas"), join(target, "schemas"), {
|
|
129
|
+
recursive: true
|
|
130
|
+
});
|
|
131
|
+
cpSync(join(packageRoot, "assets"), join(target, "assets"), {
|
|
132
|
+
recursive: true
|
|
133
|
+
});
|
|
134
|
+
mkdirSync(join(target, "scripts"), { recursive: true });
|
|
135
|
+
cpSync(
|
|
136
|
+
join(packageRoot, "scripts", "serve.js"),
|
|
137
|
+
join(target, "scripts", "serve.js")
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const scenePath = join(target, "examples", preset, "scene.json");
|
|
141
|
+
const scene = JSON.parse(readFileSync(scenePath, "utf8"));
|
|
142
|
+
if (text) {
|
|
143
|
+
scene.name = `${text} — OddlyAlive`;
|
|
144
|
+
if (scene.type === "strand-field") {
|
|
145
|
+
scene.payload.text = `${text} • `;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
writeFileSync(scenePath, `${JSON.stringify(scene, null, 2)}\n`);
|
|
149
|
+
const exampleIndexPath = join(target, "examples", preset, "index.html");
|
|
150
|
+
const exampleIndex = readFileSync(exampleIndexPath, "utf8")
|
|
151
|
+
.replaceAll('href="../"', 'href="./"')
|
|
152
|
+
.replace(
|
|
153
|
+
/<nav>[\s\S]*?<\/nav>/,
|
|
154
|
+
'<nav><a href="./">LOCAL RECIPE</a></nav>'
|
|
155
|
+
);
|
|
156
|
+
writeFileSync(exampleIndexPath, exampleIndex);
|
|
157
|
+
|
|
158
|
+
const packageName = basename(target)
|
|
159
|
+
.toLowerCase()
|
|
160
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
161
|
+
.replace(/^-+|-+$/g, "");
|
|
162
|
+
writeFileSync(
|
|
163
|
+
join(target, "package.json"),
|
|
164
|
+
`${JSON.stringify(
|
|
165
|
+
{
|
|
166
|
+
name: packageName || "oddlyalive-motion",
|
|
167
|
+
private: true,
|
|
168
|
+
type: "module",
|
|
169
|
+
scripts: {
|
|
170
|
+
start: `node ./scripts/serve.js --path /examples/${preset}/`
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
null,
|
|
174
|
+
2
|
|
175
|
+
)}\n`
|
|
176
|
+
);
|
|
177
|
+
writeFileSync(
|
|
178
|
+
join(target, "README.md"),
|
|
179
|
+
`# ${text ?? scene.name}\n\nGenerated from the OddlyAlive \`${preset}\` recipe.\n\n\`\`\`bash\nnpm start\n\`\`\`\n\nEdit \`examples/${preset}/scene.json\` to change the world, material, objects, or timing.\n`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
console.log(`Created ${target}`);
|
|
183
|
+
console.log(`Next:\n cd ${target}\n npm start`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function main() {
|
|
187
|
+
const args = process.argv.slice(2);
|
|
188
|
+
const command = args[0] ?? "help";
|
|
189
|
+
if (!commands.has(command)) {
|
|
190
|
+
throw new RangeError(`Unknown command: ${command}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (command === "help") {
|
|
194
|
+
printHelp();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (command === "list") {
|
|
199
|
+
console.log(
|
|
200
|
+
`Available recipes:\n${[...presets]
|
|
201
|
+
.map(([name, description]) => ` ${name.padEnd(16)} ${description}`)
|
|
202
|
+
.join("\n")}`
|
|
203
|
+
);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (command === "play") {
|
|
208
|
+
const preset =
|
|
209
|
+
args[1]?.startsWith("-") ? "gallery" : args[1] ?? "gallery";
|
|
210
|
+
if (preset !== "gallery" && !presets.has(preset)) {
|
|
211
|
+
throw new RangeError(`Unknown preset: ${preset}`);
|
|
212
|
+
}
|
|
213
|
+
const port = Number(flagValue(args, "--port", 4173));
|
|
214
|
+
await startServer({
|
|
215
|
+
root: packageRoot,
|
|
216
|
+
port,
|
|
217
|
+
pathname: preset === "gallery" ? "/examples/" : `/examples/${preset}/`,
|
|
218
|
+
shouldOpen: !args.includes("--no-open")
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (command === "new") {
|
|
224
|
+
createStarter(args.slice(1));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (command === "inspect") {
|
|
229
|
+
const scene = loadScene(args[1]);
|
|
230
|
+
const details =
|
|
231
|
+
scene.type === "strand-field"
|
|
232
|
+
? {
|
|
233
|
+
particles: scene.field.columns * scene.field.rows,
|
|
234
|
+
gesturePoints: scene.gesture.points.length,
|
|
235
|
+
configuredStaticGripCap: scene.contact.maxStaticGrip
|
|
236
|
+
}
|
|
237
|
+
: scene.type === "rigid-balls"
|
|
238
|
+
? {
|
|
239
|
+
bodies: scene.bodies.length,
|
|
240
|
+
scheduledImpulses: scene.impulses.length,
|
|
241
|
+
floorY: scene.world.floorY
|
|
242
|
+
}
|
|
243
|
+
: {
|
|
244
|
+
surfaceSamples: scene.surface.samples,
|
|
245
|
+
bodyKind: scene.body.kind,
|
|
246
|
+
surfaceY: scene.surface.restY
|
|
247
|
+
};
|
|
248
|
+
console.log(
|
|
249
|
+
JSON.stringify(
|
|
250
|
+
{
|
|
251
|
+
ok: true,
|
|
252
|
+
name: scene.name,
|
|
253
|
+
version: scene.version,
|
|
254
|
+
type: scene.type,
|
|
255
|
+
canvas: scene.canvas,
|
|
256
|
+
timing: scene.timing,
|
|
257
|
+
...details
|
|
258
|
+
},
|
|
259
|
+
null,
|
|
260
|
+
2
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (command === "simulate") {
|
|
267
|
+
const scene = loadScene(args[1]);
|
|
268
|
+
const result = simulateScene(scene, { includeFrames: false });
|
|
269
|
+
const summary = diagnosticSummary(result);
|
|
270
|
+
const output = flagValue(args, "--output");
|
|
271
|
+
if (output) {
|
|
272
|
+
writeFileSync(resolve(output), `${JSON.stringify(summary, null, 2)}\n`);
|
|
273
|
+
console.log(`Wrote ${resolve(output)}`);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
main().catch((error) => {
|
|
281
|
+
console.error(`OddlyAlive: ${error.message}`);
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
OddlyAlive separates creative intent from physical truth.
|
|
4
|
+
|
|
5
|
+
## Layers
|
|
6
|
+
|
|
7
|
+
1. **Prompt compiler** — converts language into a validated scene recipe. This
|
|
8
|
+
layer may run through Codex, another cloud model, or a local model.
|
|
9
|
+
2. **Scene schema** — declares bodies, constraints, material ranges, gestures,
|
|
10
|
+
duration, frame rate, and seed.
|
|
11
|
+
3. **Physics recipes** — select a solver appropriate to the object family.
|
|
12
|
+
4. **Simulation** — advances at a fixed timestep and emits renderer-neutral
|
|
13
|
+
transforms plus diagnostics.
|
|
14
|
+
5. **Render adapters** — map transforms to SVG, Canvas, WebGL, HyperFrames, or a
|
|
15
|
+
frame sequence.
|
|
16
|
+
|
|
17
|
+
## Alpha data flow
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
scene.json
|
|
21
|
+
│
|
|
22
|
+
├── validate by scene type
|
|
23
|
+
├── select strand / rigid / wave solver
|
|
24
|
+
├── simulate at 240Hz
|
|
25
|
+
└── sample states at output fps
|
|
26
|
+
│
|
|
27
|
+
├── SVG browser renderer
|
|
28
|
+
└── JSON diagnostics
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Determinism contract
|
|
32
|
+
|
|
33
|
+
- Advance simulation using `1 / (fps × substeps)`.
|
|
34
|
+
- Sample an authored gesture by simulation time, never wall-clock time.
|
|
35
|
+
- Seed all material variation from the scene.
|
|
36
|
+
- Store live state in `Float64Array`.
|
|
37
|
+
- Reset constraint lambdas at each substep.
|
|
38
|
+
- Hash rounded output state in tests.
|
|
39
|
+
- A renderer may interpolate states but must never modify simulation state.
|
|
40
|
+
|
|
41
|
+
## Object-family boundary
|
|
42
|
+
|
|
43
|
+
No individual solver is a universal physics engine. Recipes currently use:
|
|
44
|
+
|
|
45
|
+
- strand XPBD for strings, garlands, stems, and ropes;
|
|
46
|
+
- weighted strand terminals for crystals and pendants;
|
|
47
|
+
- rigid-circle dynamics for balls and kickable round props;
|
|
48
|
+
- a damped 1D surface coupled to one rigid prop for stylized water ripples.
|
|
49
|
+
|
|
50
|
+
Future recipes should use cloth XPBD for connected surfaces and articulated
|
|
51
|
+
joints plus inverse kinematics for walking bodies, rackets, flowers, and
|
|
52
|
+
mechanisms.
|
|
53
|
+
|
|
54
|
+
The prompt compiler selects a recipe. It must not force unrelated objects through
|
|
55
|
+
the strand model simply because that solver already exists.
|