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
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
@@ -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
+ ![OddlyAlive string-touch recipe](docs/assets/oddlyalive-string-touch.gif)
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
@@ -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.