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,212 @@
1
+ import { gsap } from "gsap";
2
+ import {
3
+ createCrystalRenderer,
4
+ createRigidBallRenderer,
5
+ createStringTouchScene,
6
+ createSurfaceWaveRenderer,
7
+ createSurfaceWaveScene,
8
+ createRigidBallsScene,
9
+ createSvgRenderer,
10
+ simulateScene,
11
+ validateScene
12
+ } from "../../src/index.js";
13
+
14
+ const FPS = 60;
15
+ const SCENE_DURATION = 6.4;
16
+ const PHOTO_ASSETS = {
17
+ baseball: "./assets/images/photoreal/baseball.png",
18
+ basketball: "./assets/images/photoreal/basketball.png",
19
+ football: "./assets/images/photoreal/soccer-ball.png",
20
+ sneaker: "./assets/images/photoreal/sneaker.png",
21
+ crystal: "./assets/images/photoreal/crystal.png",
22
+ kickingCleat: "./assets/images/photoreal/kicking-cleat.png",
23
+ letterCharm: "./assets/images/photoreal/letter-charm-square.png"
24
+ };
25
+
26
+ const crystalScene = createStringTouchScene({
27
+ name: "OddlyAlive — Crystal Mobile",
28
+ seed: 17,
29
+ field: {
30
+ columns: 13,
31
+ rows: 14,
32
+ originX: 390,
33
+ originY: 96,
34
+ spacingX: 38,
35
+ spacingY: 24
36
+ },
37
+ material: {
38
+ gravity: 480,
39
+ linearDrag: 0.76,
40
+ quadraticDrag: 0.0026,
41
+ lengthCompliance: 0.0000011,
42
+ compressionCompliance: 0.00052,
43
+ bendCompliance: 0.00072,
44
+ maxStretch: 1.045
45
+ },
46
+ contact: {
47
+ radiusX: 58,
48
+ radiusY: 37,
49
+ maxStaticGrip: 6,
50
+ captureStartRow: 5,
51
+ captureEndRow: 12
52
+ },
53
+ gesture: {
54
+ type: "touch-path",
55
+ points: [
56
+ { time: 0.48, x: -60, y: 320, pressure: 0 },
57
+ { time: 0.68, x: 250, y: 328, pressure: 0.72 },
58
+ { time: 1.14, x: 430, y: 360, pressure: 1 },
59
+ { time: 1.72, x: 665, y: 391, pressure: 0.98 },
60
+ { time: 2.14, x: 900, y: 379, pressure: 0.94 },
61
+ { time: 2.38, x: 934, y: 368, pressure: 0.88 },
62
+ { time: 2.72, x: 770, y: 404, pressure: 0.86 },
63
+ { time: 3.12, x: 590, y: 414, pressure: 0.65 },
64
+ { time: 3.58, x: 390, y: 386, pressure: 0.32 },
65
+ { time: 4.08, x: 155, y: 352, pressure: 0.04 },
66
+ { time: 4.34, x: 30, y: 348, pressure: 0 }
67
+ ]
68
+ },
69
+ payload: {
70
+ text: "✦",
71
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
72
+ fontSize: 12,
73
+ terminalMass: 6
74
+ },
75
+ render: {
76
+ background: "#f4eedc",
77
+ ink: "#292720",
78
+ accent: "#cc5f4a",
79
+ filament: "rgba(42, 40, 34, 0.45)"
80
+ }
81
+ });
82
+
83
+ const kickScene = createRigidBallsScene({
84
+ name: "OddlyAlive — Football Kick",
85
+ seed: 27,
86
+ world: {
87
+ gravity: 920,
88
+ airDrag: 0.08,
89
+ floorY: 430,
90
+ left: 70,
91
+ right: 910,
92
+ groundFriction: 2.4
93
+ },
94
+ bodies: [
95
+ {
96
+ id: "ball",
97
+ kind: "football",
98
+ label: "FOOTBALL",
99
+ x: 180,
100
+ y: 392,
101
+ radius: 38,
102
+ mass: 0.43,
103
+ restitution: 0.62,
104
+ friction: 0.58,
105
+ vx: 0,
106
+ vy: 0,
107
+ angle: 0,
108
+ angularVelocity: 0,
109
+ color: "#f6f3e9"
110
+ }
111
+ ],
112
+ impulses: [
113
+ {
114
+ time: 0.62,
115
+ body: "ball",
116
+ impulseX: 300,
117
+ impulseY: -235,
118
+ angularImpulse: 4.6
119
+ }
120
+ ]
121
+ });
122
+
123
+ const demos = [
124
+ {
125
+ id: "string-touch",
126
+ scene: createStringTouchScene({
127
+ render: { accent: "#8b351f" }
128
+ }),
129
+ makeRenderer: (svg, simulation) =>
130
+ createSvgRenderer(svg, simulation, {
131
+ glyphCharmHref: PHOTO_ASSETS.letterCharm
132
+ })
133
+ },
134
+ {
135
+ id: "crystal-mobile",
136
+ scene: crystalScene,
137
+ makeRenderer: (svg, simulation) =>
138
+ createCrystalRenderer(svg, simulation, {
139
+ pendantHref: PHOTO_ASSETS.crystal
140
+ })
141
+ },
142
+ {
143
+ id: "ball-lab",
144
+ scene: createRigidBallsScene(),
145
+ makeRenderer: (svg, simulation) =>
146
+ createRigidBallRenderer(svg, simulation, {
147
+ mode: "lab",
148
+ ballAssets: PHOTO_ASSETS
149
+ })
150
+ },
151
+ {
152
+ id: "football-kick",
153
+ scene: kickScene,
154
+ makeRenderer: (svg, simulation) =>
155
+ createRigidBallRenderer(svg, simulation, {
156
+ mode: "kick",
157
+ labels: false,
158
+ ballAssets: PHOTO_ASSETS,
159
+ kickActorHref: PHOTO_ASSETS.kickingCleat
160
+ })
161
+ },
162
+ {
163
+ id: "shoe-splash",
164
+ scene: createSurfaceWaveScene(),
165
+ makeRenderer: (svg, simulation) =>
166
+ createSurfaceWaveRenderer(svg, simulation, {
167
+ shoeHref: PHOTO_ASSETS.sneaker
168
+ })
169
+ }
170
+ ];
171
+
172
+ window.__timelines = window.__timelines || {};
173
+ const timeline = gsap.timeline({ paused: true });
174
+ const diagnostics = [];
175
+ window.__timelines["oddlyalive-demos"] = timeline;
176
+
177
+ demos.forEach((demo, index) => {
178
+ const scene = validateScene(demo.scene);
179
+ const simulation = simulateScene(scene);
180
+ const svg = document.querySelector(`#motion-${demo.id}`);
181
+ const renderer = demo.makeRenderer(svg, simulation);
182
+ const proxy = { frame: 0 };
183
+ const lastFrame = simulation.frames.length - 1;
184
+ const render = () => renderer.renderFrame(Math.round(proxy.frame));
185
+
186
+ diagnostics.push({ id: demo.id, fingerprint: simulation.fingerprint });
187
+ render();
188
+ timeline.to(
189
+ proxy,
190
+ {
191
+ frame: lastFrame,
192
+ duration: SCENE_DURATION,
193
+ ease: "none",
194
+ onUpdate: render
195
+ },
196
+ index * SCENE_DURATION
197
+ );
198
+ });
199
+
200
+ timeline.eventCallback("onUpdate", () => {
201
+ const frame = Math.min(
202
+ Math.round(timeline.time() * FPS),
203
+ Math.round(timeline.duration() * FPS) - 1
204
+ );
205
+ document.documentElement.dataset.frame = String(frame);
206
+ });
207
+
208
+ window.__oddlyAliveDemo = {
209
+ fps: FPS,
210
+ duration: timeline.duration(),
211
+ demos: diagnostics
212
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
3
+ "registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
4
+ "paths": {
5
+ "blocks": "compositions",
6
+ "components": "compositions/components",
7
+ "assets": "assets"
8
+ }
9
+ }
@@ -0,0 +1,246 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-resolution="landscape">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=1920, height=1080">
6
+ <style>
7
+ :root {
8
+ --shell: #171714;
9
+ --ink: #20201d;
10
+ --accent: #8b351f;
11
+ --paper: #f0e7d3;
12
+ }
13
+
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ html,
21
+ body {
22
+ margin: 0;
23
+ width: 1920px;
24
+ height: 1080px;
25
+ overflow: hidden;
26
+ background: var(--shell);
27
+ }
28
+
29
+ body {
30
+ color: var(--ink);
31
+ font-family: "Inter", sans-serif;
32
+ }
33
+
34
+ #root {
35
+ position: relative;
36
+ width: 1920px;
37
+ height: 1080px;
38
+ overflow: hidden;
39
+ background: var(--shell);
40
+ }
41
+
42
+ .clip {
43
+ position: absolute;
44
+ inset: 0;
45
+ overflow: hidden;
46
+ padding: 46px;
47
+ }
48
+
49
+ .poster {
50
+ position: relative;
51
+ width: 100%;
52
+ height: 100%;
53
+ overflow: hidden;
54
+ background: var(--scene-paper);
55
+ border: 2px solid rgb(255 255 255 / 9%);
56
+ box-shadow:
57
+ 0 18px 70px rgb(0 0 0 / 28%),
58
+ inset 0 0 90px rgb(80 64 36 / 5%);
59
+ }
60
+
61
+ .poster::after {
62
+ position: absolute;
63
+ inset: 0;
64
+ z-index: 4;
65
+ pointer-events: none;
66
+ content: "";
67
+ opacity: 0.32;
68
+ background-image:
69
+ repeating-radial-gradient(
70
+ circle at 20% 30%,
71
+ rgb(25 23 18 / 9%) 0 0.7px,
72
+ transparent 0.8px 4px
73
+ );
74
+ background-size: 9px 11px;
75
+ mix-blend-mode: multiply;
76
+ }
77
+
78
+ .motion {
79
+ position: absolute;
80
+ inset: 0;
81
+ z-index: 1;
82
+ width: 100%;
83
+ height: 100%;
84
+ }
85
+
86
+ .eyebrow,
87
+ .scene-number,
88
+ .footer,
89
+ .recipe-chip {
90
+ position: absolute;
91
+ z-index: 5;
92
+ font-family: "JetBrains Mono", monospace;
93
+ font-weight: 700;
94
+ letter-spacing: 0.16em;
95
+ text-transform: uppercase;
96
+ }
97
+
98
+ .eyebrow {
99
+ top: 58px;
100
+ left: 62px;
101
+ font-size: 18px;
102
+ color: var(--accent);
103
+ }
104
+
105
+ h1 {
106
+ position: absolute;
107
+ top: 116px;
108
+ left: 58px;
109
+ z-index: 5;
110
+ max-width: 780px;
111
+ font-size: 82px;
112
+ font-stretch: condensed;
113
+ font-weight: 900;
114
+ letter-spacing: -0.062em;
115
+ line-height: 0.9;
116
+ text-transform: uppercase;
117
+ }
118
+
119
+ .scene-number {
120
+ top: 60px;
121
+ right: 62px;
122
+ font-size: 17px;
123
+ }
124
+
125
+ .recipe-chip {
126
+ right: 62px;
127
+ bottom: 58px;
128
+ padding: 12px 16px 10px;
129
+ color: var(--scene-paper);
130
+ font-size: 14px;
131
+ background: var(--ink);
132
+ }
133
+
134
+ .footer {
135
+ bottom: 62px;
136
+ left: 62px;
137
+ max-width: 720px;
138
+ font-size: 15px;
139
+ line-height: 1.5;
140
+ }
141
+
142
+ .footer strong {
143
+ color: var(--accent);
144
+ }
145
+
146
+ .kick .eyebrow,
147
+ .kick h1,
148
+ .kick .scene-number {
149
+ color: #f3efdf;
150
+ }
151
+
152
+ .kick .recipe-chip {
153
+ color: #182217;
154
+ background: #f3efdf;
155
+ }
156
+
157
+ .kick .footer,
158
+ .kick .footer strong {
159
+ color: #f7f0d8;
160
+ }
161
+
162
+ #shoe-splash .footer {
163
+ color: #11110f;
164
+ }
165
+
166
+ #shoe-splash .footer strong {
167
+ color: #2d0c06;
168
+ }
169
+ </style>
170
+ </head>
171
+ <body>
172
+ <div data-hf-id="hf-18cd" id="root" data-composition-id="oddlyalive-demos" data-start="0" data-duration="32" data-width="1920" data-height="1080" data-fps="60">
173
+ <section data-hf-id="hf-z3d1" id="string-touch" class="clip" data-start="0" data-duration="6.4" data-track-index="1" style="--scene-paper: #f0e7d3">
174
+ <div data-hf-id="hf-mhuf" class="poster">
175
+ <svg data-hf-id="hf-2fmn" id="motion-string-touch" class="motion" viewBox="0 0 960 540" aria-label="String Touch physics demo" />
176
+ <p data-hf-id="hf-xjgn" class="eyebrow">OddlyAlive / Recipe 01</p>
177
+ <h1 data-hf-id="hf-6fv6">String<br data-hf-id="hf-x5px">Touch</h1>
178
+ <p data-hf-id="hf-a8yy" class="scene-number">01 / 05</p>
179
+ <p data-hf-id="hf-b4yf" class="footer">
180
+ Local contact → static grip → slip → peel → <strong data-hf-id="hf-7ufq">alive settle</strong>
181
+ </p>
182
+ <p data-hf-id="hf-3e1h" class="recipe-chip">strand-field</p>
183
+ </div>
184
+ </section>
185
+
186
+ <section data-hf-id="hf-g8rr" id="crystal-mobile" class="clip" data-start="6.4" data-duration="6.4" data-track-index="1" style="--scene-paper: #f4eedc">
187
+ <div data-hf-id="hf-uod3" class="poster">
188
+ <svg data-hf-id="hf-4jl5" id="motion-crystal-mobile" class="motion" viewBox="0 0 960 540" aria-label="Crystal Mobile physics demo" />
189
+ <p data-hf-id="hf-x55m" class="eyebrow">OddlyAlive / Recipe 02</p>
190
+ <h1 data-hf-id="hf-9dqe">Crystal<br data-hf-id="hf-rsrt">Mobile</h1>
191
+ <p data-hf-id="hf-ibv5" class="scene-number">02 / 05</p>
192
+ <p data-hf-id="hf-56s8" class="footer">
193
+ Weighted payloads → coupled motion → <strong data-hf-id="hf-tbbi">irregular settle</strong>
194
+ </p>
195
+ <p data-hf-id="hf-40sp" class="recipe-chip">strand-field</p>
196
+ </div>
197
+ </section>
198
+
199
+ <section data-hf-id="hf-k891" id="ball-lab" class="clip" data-start="12.8" data-duration="6.4" data-track-index="1" style="--scene-paper: #eee4cf">
200
+ <div data-hf-id="hf-ua22" class="poster">
201
+ <svg data-hf-id="hf-pqdz" id="motion-ball-lab" class="motion" viewBox="0 0 960 540" aria-label="Ball Lab physics demo" />
202
+ <p data-hf-id="hf-wqul" class="eyebrow">OddlyAlive / Recipe 03</p>
203
+ <h1 data-hf-id="hf-nhn6">Ball<br data-hf-id="hf-szow">Lab</h1>
204
+ <p data-hf-id="hf-jfks" class="scene-number">03 / 05</p>
205
+ <p data-hf-id="hf-fc1i" class="footer">
206
+ Mass + restitution + friction → <strong data-hf-id="hf-12x9">distinct material feel</strong>
207
+ </p>
208
+ <p data-hf-id="hf-1yh8" class="recipe-chip">rigid-balls</p>
209
+ </div>
210
+ </section>
211
+
212
+ <section data-hf-id="hf-8gjv" id="football-kick" class="clip kick" data-start="19.2" data-duration="6.4" data-track-index="1" style="--scene-paper: #2f675f">
213
+ <div data-hf-id="hf-tvr1" class="poster">
214
+ <svg data-hf-id="hf-w50h" id="motion-football-kick" class="motion" viewBox="0 0 960 540" aria-label="Football Kick physics demo" />
215
+ <p data-hf-id="hf-zizs" class="eyebrow">OddlyAlive / Recipe 04</p>
216
+ <h1 data-hf-id="hf-gdlt">Football<br data-hf-id="hf-sldv">Kick</h1>
217
+ <p data-hf-id="hf-qvgj" class="scene-number">04 / 05</p>
218
+ <p data-hf-id="hf-4gc8" class="footer">
219
+ Timed impulse → spin → bounce → <strong data-hf-id="hf-wvbs">natural energy loss</strong>
220
+ </p>
221
+ <p data-hf-id="hf-zvms" class="recipe-chip">rigid-balls</p>
222
+ </div>
223
+ </section>
224
+
225
+ <section data-hf-id="hf-kx1q" id="shoe-splash" class="clip" data-start="25.6" data-duration="6.4" data-track-index="1" style="--scene-paper: #dceae5">
226
+ <div data-hf-id="hf-wnw8" class="poster">
227
+ <svg data-hf-id="hf-l761" id="motion-shoe-splash" class="motion" viewBox="0 0 960 540" aria-label="Shoe Splash physics demo" />
228
+ <p data-hf-id="hf-z4or" class="eyebrow">OddlyAlive / Recipe 05</p>
229
+ <h1 data-hf-id="hf-v3n2">Shoe<br data-hf-id="hf-qluq">Splash</h1>
230
+ <p data-hf-id="hf-jriu" class="scene-number">05 / 05</p>
231
+ <p data-hf-id="hf-9o3c" class="footer">
232
+ Entry + buoyancy + wave coupling → <strong data-hf-id="hf-tzkj">responsive surface</strong>
233
+ </p>
234
+ <p data-hf-id="hf-mmxr" class="recipe-chip">surface-wave</p>
235
+ </div>
236
+ </section>
237
+ </div>
238
+
239
+ <script>
240
+ window.__timelines = window.__timelines || {};
241
+ window.__timelines["oddlyalive-demos"] =
242
+ window.__timelines["oddlyalive-demos"] || null;
243
+ </script>
244
+ <script src="./app.bundle.js"></script>
245
+ </body>
246
+ </html>
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "oddlyalive-demos",
3
+ "name": "oddlyalive-demos",
4
+ "createdAt": "2026-07-19T03:53:32.152Z"
5
+ }