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,101 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+
8
+ test("the CLI creates a dependency-free strand starter", async () => {
9
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "oddlyalive-"));
10
+ const target = join(temporaryRoot, "my-motion");
11
+ const result = spawnSync(
12
+ process.execPath,
13
+ [
14
+ "./bin/oddlyalive.js",
15
+ "new",
16
+ target,
17
+ "--text",
18
+ "HELLO MOTION"
19
+ ],
20
+ { cwd: new URL("..", import.meta.url), encoding: "utf8" }
21
+ );
22
+
23
+ assert.equal(result.status, 0, result.stderr);
24
+ const scene = JSON.parse(
25
+ await readFile(join(target, "examples/string-touch/scene.json"), "utf8")
26
+ );
27
+ const packageJson = JSON.parse(
28
+ await readFile(join(target, "package.json"), "utf8")
29
+ );
30
+ assert.match(scene.payload.text, /HELLO MOTION/);
31
+ assert.equal(
32
+ packageJson.scripts.start,
33
+ "node ./scripts/serve.js --path /examples/string-touch/"
34
+ );
35
+ await rm(temporaryRoot, { recursive: true, force: true });
36
+ });
37
+
38
+ test("the CLI can create every built-in recipe", async () => {
39
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "oddlyalive-recipes-"));
40
+ const presets = [
41
+ "string-touch",
42
+ "crystal-mobile",
43
+ "ball-lab",
44
+ "football-kick",
45
+ "shoe-splash"
46
+ ];
47
+ const requiredAssets = {
48
+ "string-touch": ["letter-charm-square.png"],
49
+ "crystal-mobile": ["crystal.png"],
50
+ "ball-lab": ["baseball.png", "basketball.png", "soccer-ball.png"],
51
+ "football-kick": ["kicking-cleat.png", "soccer-ball.png"],
52
+ "shoe-splash": ["sneaker.png"]
53
+ };
54
+ for (const preset of presets) {
55
+ const target = join(temporaryRoot, preset);
56
+ const result = spawnSync(
57
+ process.execPath,
58
+ ["./bin/oddlyalive.js", "new", target, "--preset", preset],
59
+ { cwd: new URL("..", import.meta.url), encoding: "utf8" }
60
+ );
61
+ assert.equal(result.status, 0, `${preset}: ${result.stderr}`);
62
+ const scene = JSON.parse(
63
+ await readFile(join(target, `examples/${preset}/scene.json`), "utf8")
64
+ );
65
+ assert.equal(typeof scene.type, "string");
66
+ await readFile(join(target, "examples/shared/player.js"), "utf8");
67
+ await readFile(join(target, "schemas/scene.schema.json"), "utf8");
68
+ for (const asset of requiredAssets[preset]) {
69
+ await readFile(join(target, "assets/photoreal", asset));
70
+ }
71
+ await readFile(join(target, "assets/photoreal/PROVENANCE.md"), "utf8");
72
+ await readFile(join(target, "scripts/serve.js"), "utf8");
73
+ const html = await readFile(
74
+ join(target, `examples/${preset}/index.html`),
75
+ "utf8"
76
+ );
77
+ assert.doesNotMatch(
78
+ html,
79
+ /href="\.\.\/(string-touch|crystal-mobile|ball-lab|football-kick|shoe-splash)\//
80
+ );
81
+ }
82
+ await rm(temporaryRoot, { recursive: true, force: true });
83
+ });
84
+
85
+ test("the CLI lists the complete recipe gallery", () => {
86
+ const result = spawnSync(
87
+ process.execPath,
88
+ ["./bin/oddlyalive.js", "list"],
89
+ { cwd: new URL("..", import.meta.url), encoding: "utf8" }
90
+ );
91
+ assert.equal(result.status, 0, result.stderr);
92
+ for (const preset of [
93
+ "string-touch",
94
+ "crystal-mobile",
95
+ "ball-lab",
96
+ "football-kick",
97
+ "shoe-splash"
98
+ ]) {
99
+ assert.match(result.stdout, new RegExp(preset));
100
+ }
101
+ });
@@ -0,0 +1,57 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createStringTouchScene,
5
+ simulateScene
6
+ } from "../src/index.js";
7
+
8
+ const compactScene = createStringTouchScene({
9
+ timing: {
10
+ duration: 4.8,
11
+ fps: 30,
12
+ substeps: 8,
13
+ preRoll: 0.5
14
+ },
15
+ field: {
16
+ columns: 13,
17
+ rows: 10,
18
+ originX: 145,
19
+ originY: 108,
20
+ spacingX: 48,
21
+ spacingY: 28
22
+ },
23
+ contact: {
24
+ maxStaticGrip: 6,
25
+ captureStartRow: 3,
26
+ captureEndRow: 9
27
+ }
28
+ });
29
+
30
+ test("the same scene produces the same simulation fingerprint", () => {
31
+ const first = simulateScene(compactScene, { includeFrames: false });
32
+ const second = simulateScene(compactScene, { includeFrames: false });
33
+ assert.equal(first.fingerprint, second.fingerprint);
34
+ assert.equal(
35
+ first.diagnostics.observedMaxStaticGrip,
36
+ second.diagnostics.observedMaxStaticGrip
37
+ );
38
+ assert.ok(first.diagnostics.observedMaxStaticGrip > 0);
39
+ assert.ok(
40
+ first.diagnostics.observedMaxStaticGrip <=
41
+ compactScene.contact.maxStaticGrip
42
+ );
43
+ assert.ok(first.diagnostics.firstGripTime !== null);
44
+ assert.ok(
45
+ first.diagnostics.observedMaxStretch <
46
+ compactScene.material.maxStretch + 0.02
47
+ );
48
+ });
49
+
50
+ test("changing the material seed changes the fingerprint", () => {
51
+ const first = simulateScene(compactScene, { includeFrames: false });
52
+ const second = simulateScene(
53
+ { ...compactScene, seed: compactScene.seed + 1 },
54
+ { includeFrames: false }
55
+ );
56
+ assert.notEqual(first.fingerprint, second.fingerprint);
57
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "version": 1,
3
+ "description": "Approved deterministic motion and visual-source baselines. Update only after visual review.",
4
+ "sceneFingerprints": {
5
+ "string-touch": "4c56b4e5",
6
+ "crystal-mobile": "04e45087",
7
+ "ball-lab": "c3b9e206",
8
+ "football-kick": "2179d5a7",
9
+ "shoe-splash": "438ccfca"
10
+ },
11
+ "fileSha256": {
12
+ "assets/photoreal/baseball.png": "0d8e6be0dc67ad577a40772d6d6b17cd70732d15f57b1184500c9676abd387b6",
13
+ "assets/photoreal/basketball.png": "5c5c8e714525066a46fa8f694f1ada3312f202b6bd8e9059015c4b6acf92f6af",
14
+ "assets/photoreal/crystal.png": "92ea76d9d9fb24899510764670eb0d88887a70822d6689fe5df4f374a2c62903",
15
+ "assets/photoreal/kicking-cleat.png": "9151599c10eb26e3250e9f7c4921e6e4b44dd8286a19a61fa6ae09a79dfa673f",
16
+ "assets/photoreal/letter-charm-square.png": "b2bccab5e482bfa86c0f71ae70f6d5cef44ab1d892135384c27a6f763bd73cae",
17
+ "assets/photoreal/letter-charm.png": "e20eb93043feb7bf307a432db53cf00af0997c3d17d6e59c070f835375b87364",
18
+ "assets/photoreal/sneaker.png": "a6736fbdc0120cbd95cdb6157522e980226c9dc53e7d9af31e1817df180c6da9",
19
+ "assets/photoreal/soccer-ball.png": "38e85510f7380e8334f2f6562aad55240132af21787de8746d2698e000bcf542",
20
+ "src/crystal-renderer.js": "e63ead4940827c4a47f685ce1657b2e0f12ef721ad533f3659fd5d5edcbc8059",
21
+ "src/rigid-renderer.js": "39e1fe69fa84a3c9ec499554b8551655443988d18c6f357db6e86109288976b3",
22
+ "src/svg-renderer.js": "c5848d44d7560a23b32db26503b231f2482a41dbcbb6f1bab01e6a3e615ece3a",
23
+ "src/wave-renderer.js": "5c8d1d2aff3dbfe8aede895ddf1cce8317b31a962ccc70ff569bfe2125cc384d",
24
+ "videos/oddlyalive-demos/app.js": "393414f946090b67a2dec2a2bf254d4b8fd4a2b74d4fdeb1022da4d005841a68",
25
+ "videos/oddlyalive-demos/index.html": "86bf5c2502a67c9e70154ec11f9bcb79bb96344f8cba85c6b354ad5b0e8a5107"
26
+ }
27
+ }
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createRigidBallsScene,
5
+ simulateScene,
6
+ validateScene
7
+ } from "../src/index.js";
8
+
9
+ test("rigid ball worlds are deterministic and collide within bounds", () => {
10
+ const scene = validateScene(createRigidBallsScene());
11
+ const first = simulateScene(scene, { includeFrames: false });
12
+ const second = simulateScene(scene, { includeFrames: false });
13
+
14
+ assert.equal(first.fingerprint, second.fingerprint);
15
+ assert.ok(first.diagnostics.collisionCount > 0);
16
+ assert.ok(first.diagnostics.firstImpactTime > 0.5);
17
+ assert.ok(first.diagnostics.firstImpactTime < 1.5);
18
+ assert.ok(first.diagnostics.maxPenetration < 10);
19
+ assert.equal(first.diagnostics.appliedImpulses, 0);
20
+ });
21
+
22
+ test("a scheduled kick is applied exactly once and changes the world", () => {
23
+ const scene = createRigidBallsScene({
24
+ bodies: [
25
+ {
26
+ id: "ball",
27
+ kind: "football",
28
+ label: "FOOTBALL",
29
+ x: 180,
30
+ y: 392,
31
+ radius: 38,
32
+ mass: 0.43,
33
+ restitution: 0.62,
34
+ friction: 0.58,
35
+ vx: 0,
36
+ vy: 0,
37
+ angle: 0,
38
+ angularVelocity: 0,
39
+ color: "#f6f3e9"
40
+ }
41
+ ],
42
+ world: { floorY: 430, left: 70, right: 910, groundFriction: 2.4 },
43
+ impulses: [
44
+ {
45
+ time: 0.62,
46
+ body: "ball",
47
+ impulseX: 300,
48
+ impulseY: -235,
49
+ angularImpulse: 4.6
50
+ }
51
+ ]
52
+ });
53
+ const kicked = simulateScene(scene, { includeFrames: false });
54
+ const still = simulateScene(
55
+ createRigidBallsScene({ ...scene, impulses: [] }),
56
+ { includeFrames: false }
57
+ );
58
+
59
+ assert.equal(kicked.diagnostics.appliedImpulses, 1);
60
+ assert.notEqual(kicked.fingerprint, still.fingerprint);
61
+ assert.ok(kicked.diagnostics.maxSpeed > 500);
62
+ });
63
+
64
+ test("rigid pre-roll advances the world before output frame zero", () => {
65
+ const withoutPreRoll = simulateScene(
66
+ createRigidBallsScene({
67
+ timing: { duration: 1, fps: 60, substeps: 4, preRoll: 0 }
68
+ })
69
+ );
70
+ const withPreRoll = simulateScene(
71
+ createRigidBallsScene({
72
+ timing: { duration: 1, fps: 60, substeps: 4, preRoll: 0.25 }
73
+ })
74
+ );
75
+
76
+ assert.ok(withPreRoll.frames[0].y[0] > withoutPreRoll.frames[0].y[0]);
77
+ });
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createRigidBallsScene,
5
+ createStringTouchScene,
6
+ createSurfaceWaveScene,
7
+ sampleGesture,
8
+ validateScene
9
+ } from "../src/index.js";
10
+
11
+ test("the default scene validates and merges payload overrides", () => {
12
+ const scene = validateScene(
13
+ createStringTouchScene({ payload: { text: "HELLO" } })
14
+ );
15
+ assert.equal(scene.payload.text, "HELLO");
16
+ assert.equal(scene.field.columns, 23);
17
+ assert.equal(scene.timing.fps * scene.timing.substeps, 240);
18
+ });
19
+
20
+ test("the registry validates every implemented scene family", () => {
21
+ assert.equal(validateScene(createRigidBallsScene()).type, "rigid-balls");
22
+ assert.equal(validateScene(createSurfaceWaveScene()).type, "surface-wave");
23
+ assert.throws(
24
+ () => validateScene({ type: "cloth" }),
25
+ /Unsupported scene type/
26
+ );
27
+ });
28
+
29
+ test("rigid impulse references and wave sample counts are guarded", () => {
30
+ assert.throws(
31
+ () =>
32
+ validateScene(
33
+ createRigidBallsScene({
34
+ impulses: [
35
+ {
36
+ time: 0.2,
37
+ body: "missing",
38
+ impulseX: 1,
39
+ impulseY: 1
40
+ }
41
+ ]
42
+ })
43
+ ),
44
+ /unknown body/
45
+ );
46
+ assert.throws(
47
+ () =>
48
+ validateScene(
49
+ createSurfaceWaveScene({ surface: { samples: 4 } })
50
+ ),
51
+ /at least 16/
52
+ );
53
+ });
54
+
55
+ test("gesture sampling is smooth and bounded at both ends", () => {
56
+ const scene = createStringTouchScene();
57
+ const before = sampleGesture(scene.gesture.points, -10);
58
+ const middle = sampleGesture(scene.gesture.points, 1.3);
59
+ const after = sampleGesture(scene.gesture.points, 20);
60
+ assert.equal(before.pressure, 0);
61
+ assert.ok(middle.pressure > 0.9);
62
+ assert.equal(after.pressure, 0);
63
+ });
64
+
65
+ test("invalid pressure and unordered gesture points are rejected", () => {
66
+ assert.throws(
67
+ () =>
68
+ validateScene({
69
+ gesture: {
70
+ points: [
71
+ { time: 1, x: 0, y: 0, pressure: 0 },
72
+ { time: 0, x: 1, y: 1, pressure: 1.2 }
73
+ ]
74
+ }
75
+ }),
76
+ /pressure|ordered/
77
+ );
78
+ });
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createSurfaceWaveScene,
5
+ simulateScene,
6
+ validateScene
7
+ } from "../src/index.js";
8
+
9
+ test("the coupled wave is deterministic and responds to impact", () => {
10
+ const scene = validateScene(createSurfaceWaveScene());
11
+ const first = simulateScene(scene, { includeFrames: false });
12
+ const second = simulateScene(scene, { includeFrames: false });
13
+
14
+ assert.equal(first.fingerprint, second.fingerprint);
15
+ assert.ok(first.diagnostics.impactCount > 0);
16
+ assert.ok(first.diagnostics.firstImpactTime > 0.5);
17
+ assert.ok(first.diagnostics.firstImpactTime < 1.5);
18
+ assert.ok(first.diagnostics.maxWaveHeight > 1);
19
+ assert.ok(first.diagnostics.maxWaveHeight < 100);
20
+ assert.ok(first.diagnostics.maxSubmersion < scene.body.height);
21
+ });
22
+
23
+ test("surface parameters alter the deterministic result", () => {
24
+ const original = simulateScene(createSurfaceWaveScene(), {
25
+ includeFrames: false
26
+ });
27
+ const calmer = simulateScene(
28
+ createSurfaceWaveScene({
29
+ surface: { damping: 5.5, coupling: 0.22 }
30
+ }),
31
+ { includeFrames: false }
32
+ );
33
+
34
+ assert.notEqual(original.fingerprint, calmer.fingerprint);
35
+ assert.ok(
36
+ calmer.diagnostics.maxWaveHeight <
37
+ original.diagnostics.maxWaveHeight
38
+ );
39
+ });
@@ -0,0 +1,43 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import test from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import { simulateScene, validateScene } from "../src/index.js";
9
+
10
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const baseline = JSON.parse(
12
+ await readFile(resolve(root, "tests/fixtures/visual-baselines.json"), "utf8")
13
+ );
14
+ const expectedScenes = [
15
+ "string-touch",
16
+ "crystal-mobile",
17
+ "ball-lab",
18
+ "football-kick",
19
+ "shoe-splash"
20
+ ];
21
+
22
+ test("approved recipe motion fingerprints remain stable", async () => {
23
+ assert.deepEqual(Object.keys(baseline.sceneFingerprints), expectedScenes);
24
+ for (const name of expectedScenes) {
25
+ const scene = validateScene(
26
+ JSON.parse(
27
+ await readFile(resolve(root, `examples/${name}/scene.json`), "utf8")
28
+ )
29
+ );
30
+ const actual = simulateScene(scene, { includeFrames: false }).fingerprint;
31
+ assert.equal(actual, baseline.sceneFingerprints[name], `${name} changed`);
32
+ }
33
+ });
34
+
35
+ test("approved visual source bytes remain stable", async () => {
36
+ assert.ok(Object.keys(baseline.fileSha256).length >= 14);
37
+ for (const [path, expected] of Object.entries(baseline.fileSha256)) {
38
+ const actual = createHash("sha256")
39
+ .update(await readFile(resolve(root, path)))
40
+ .digest("hex");
41
+ assert.equal(actual, expected, `${path} changed`);
42
+ }
43
+ });
@@ -0,0 +1,40 @@
1
+ ---
2
+ workflow: general-video
3
+ flow: automation
4
+ storyboard: no
5
+ message: "OddlyAlive turns ordinary objects into deterministic, physically believable motion."
6
+ destination: github-and-social
7
+ aspect: 1920x1080
8
+ language: en
9
+ audience: creative-coders
10
+ length: 32s
11
+ angle: product-demo
12
+ ---
13
+
14
+ ## Intent
15
+
16
+ Create a local-first demo package for the OddlyAlive open-source alpha. Each
17
+ recipe must show the real solver and renderer at work, preserve the gallery's
18
+ paper-and-ink identity, and make the physical behavior understandable without
19
+ voice-over.
20
+
21
+ ## Assets
22
+
23
+ - `../../src/` — the current OddlyAlive engine, bundled into the render so the
24
+ browser runtime has no external dependency.
25
+ - `../../assets/photoreal/` — locally frozen transparent object cutouts shared
26
+ with the browser examples; provenance is recorded beside the assets.
27
+
28
+ ## Customizations
29
+
30
+ - Five faithful 6.4-second recipe demos.
31
+ - One 32-second combined reel.
32
+ - One lightweight GIF preview for the repository README.
33
+ - Silent by design so the demos work in GitHub, npm, and social previews.
34
+
35
+ ## Notes
36
+
37
+ - No stock footage or third-party artwork; locally generated photographic
38
+ cutouts provide material detail while SVG owns deformation and physics.
39
+ - No live network data.
40
+ - Do not commit, push, release, or publish until the user reviews the package.
@@ -0,0 +1,13 @@
1
+ # OddlyAlive demo plan
2
+
3
+ | Time | Recipe | Physical idea | Visual proof |
4
+ | --- | --- | --- | --- |
5
+ | 00:00–00:06.4 | String Touch | Local contact, grip, slip, peel, and settle | Only nearby strands catch; every strand releases independently |
6
+ | 00:06.4–00:12.8 | Crystal Mobile | Coupled pendulum-like strands with weighted payloads | Crystals lag, overshoot, and settle at different rates |
7
+ | 00:12.8–00:19.2 | Ball Lab | Mass, restitution, friction, and collision | Three familiar balls bounce and roll with distinct character |
8
+ | 00:19.2–00:25.6 | Football Kick | A timed impulse under gravity | The ball launches, spins, bounces, and loses energy naturally |
9
+ | 00:25.6–00:32.0 | Shoe Splash | Rigid-body entry coupled to a wave surface | The shoe displaces water, floats, and leaves propagating waves |
10
+
11
+ Each scene uses the current OddlyAlive implementation at 60 fps with four
12
+ solver substeps per frame. The reel uses hard cuts so no transition hides the
13
+ start or end state of a simulation.
@@ -0,0 +1,45 @@
1
+ # OddlyAlive demo composition
2
+
3
+ This HyperFrames project renders the current five OddlyAlive recipes from the
4
+ real deterministic engine.
5
+
6
+ ## Local preview
7
+
8
+ ```bash
9
+ npm ci
10
+ npm run build:bundle
11
+ npm run dev
12
+ ```
13
+
14
+ ## Validate
15
+
16
+ ```bash
17
+ npm run check
18
+ ```
19
+
20
+ ## Render the 32-second reel
21
+
22
+ ```bash
23
+ npm run render -- \
24
+ -o renders/oddlyalive-demos-overview.mp4 \
25
+ --fps 60 \
26
+ --quality high \
27
+ --strict
28
+ ```
29
+
30
+ The root-level `npm run demos:render` command is the preferred path because it
31
+ also cuts the five clips, rebuilds the README GIF, and verifies every encode.
32
+
33
+ ## Source map
34
+
35
+ - `BRIEF.md` — intent and delivery constraints
36
+ - `DEMO-PLAN.md` — exact five-scene sequence
37
+ - `index.html` — visual system and timed clips
38
+ - `app.js` — scene configuration, simulation, and seek-safe timeline
39
+ - `../../src/` — current OddlyAlive source bundled by esbuild
40
+ - `../../assets/photoreal/` — canonical high-resolution object cutouts
41
+ - `prepare-assets.mjs` — copies canonical cutouts into the ignored local render cache
42
+ - `app.bundle.js` — generated by `npm run build:bundle` and ignored by Git
43
+ - `renders/`, `review/`, `snapshots/` — generated and ignored by Git
44
+
45
+ No Git or publishing command is part of this project.