oddlyalive 0.2.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/CONTRIBUTING.md +62 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/SECURITY.md +29 -0
- package/assets/photoreal/PROVENANCE.md +19 -0
- package/assets/photoreal/baseball.png +0 -0
- package/assets/photoreal/basketball.png +0 -0
- package/assets/photoreal/crystal.png +0 -0
- package/assets/photoreal/kicking-cleat.png +0 -0
- package/assets/photoreal/letter-charm-square.png +0 -0
- package/assets/photoreal/letter-charm.png +0 -0
- package/assets/photoreal/sneaker.png +0 -0
- package/assets/photoreal/soccer-ball.png +0 -0
- package/bin/oddlyalive.js +283 -0
- package/docs/ARCHITECTURE.md +55 -0
- package/docs/DEMO-VIDEOS.md +122 -0
- package/docs/QUICKSTART.md +115 -0
- package/docs/ROADMAP.md +33 -0
- package/examples/ball-lab/PROVENANCE.md +4 -0
- package/examples/ball-lab/app.js +33 -0
- package/examples/ball-lab/index.html +56 -0
- package/examples/ball-lab/scene.json +68 -0
- package/examples/crystal-mobile/PROVENANCE.md +4 -0
- package/examples/crystal-mobile/app.js +26 -0
- package/examples/crystal-mobile/index.html +56 -0
- package/examples/crystal-mobile/scene.json +69 -0
- package/examples/football-kick/PROVENANCE.md +4 -0
- package/examples/football-kick/app.js +33 -0
- package/examples/football-kick/index.html +56 -0
- package/examples/football-kick/scene.json +44 -0
- package/examples/gallery.css +379 -0
- package/examples/index.html +109 -0
- package/examples/shared/example.css +320 -0
- package/examples/shared/player.js +85 -0
- package/examples/shoe-splash/PROVENANCE.md +4 -0
- package/examples/shoe-splash/app.js +26 -0
- package/examples/shoe-splash/index.html +56 -0
- package/examples/shoe-splash/scene.json +38 -0
- package/examples/string-touch/PROVENANCE.md +11 -0
- package/examples/string-touch/app.js +79 -0
- package/examples/string-touch/index.html +70 -0
- package/examples/string-touch/scene.json +73 -0
- package/examples/string-touch/styles.css +273 -0
- package/package.json +82 -0
- package/schemas/scene.schema.json +336 -0
- package/scripts/render-demo-videos.sh +77 -0
- package/scripts/serve.js +88 -0
- package/scripts/update-visual-fixtures.mjs +60 -0
- package/scripts/verify-demo-videos.mjs +138 -0
- package/skills/oddlyalive/SKILL.md +68 -0
- package/skills/oddlyalive/agents/openai.yaml +4 -0
- package/skills/oddlyalive/references/object-models.md +25 -0
- package/skills/oddlyalive/references/recipes.md +27 -0
- package/skills/oddlyalive/references/scene-schema.md +84 -0
- package/src/crystal-renderer.js +338 -0
- package/src/gesture.js +58 -0
- package/src/index.js +44 -0
- package/src/rigid-balls.js +304 -0
- package/src/rigid-renderer.js +760 -0
- package/src/rigid-scene.js +223 -0
- package/src/scene-registry.js +17 -0
- package/src/scene.js +206 -0
- package/src/state-hash.js +17 -0
- package/src/strands.js +773 -0
- package/src/surface-wave.js +209 -0
- package/src/svg-renderer.js +348 -0
- package/src/wave-renderer.js +463 -0
- package/src/wave-scene.js +163 -0
- package/tests/cli.test.js +101 -0
- package/tests/determinism.test.js +57 -0
- package/tests/fixtures/visual-baselines.json +27 -0
- package/tests/rigid-balls.test.js +77 -0
- package/tests/scene.test.js +78 -0
- package/tests/surface-wave.test.js +39 -0
- package/tests/visual-fixtures.test.js +43 -0
- package/videos/oddlyalive-demos/BRIEF.md +40 -0
- package/videos/oddlyalive-demos/DEMO-PLAN.md +13 -0
- package/videos/oddlyalive-demos/README.md +45 -0
- package/videos/oddlyalive-demos/app.js +212 -0
- package/videos/oddlyalive-demos/hyperframes.json +9 -0
- package/videos/oddlyalive-demos/index.html +246 -0
- package/videos/oddlyalive-demos/meta.json +5 -0
- package/videos/oddlyalive-demos/package-lock.json +506 -0
- package/videos/oddlyalive-demos/package.json +19 -0
- package/videos/oddlyalive-demos/prepare-assets.mjs +16 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
font-family:
|
|
4
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
5
|
+
--page: #121211;
|
|
6
|
+
--page-glow: #302c26;
|
|
7
|
+
--paper: #efe7d5;
|
|
8
|
+
--ink: #20201d;
|
|
9
|
+
--muted: #716c62;
|
|
10
|
+
--accent: #ee6540;
|
|
11
|
+
--line: #3b3832;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
* {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
html,
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
min-height: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
background:
|
|
26
|
+
radial-gradient(circle at 50% 16%, var(--page-glow), transparent 42rem),
|
|
27
|
+
var(--page);
|
|
28
|
+
color: #f4f0e8;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
button,
|
|
32
|
+
input {
|
|
33
|
+
font: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main {
|
|
37
|
+
width: min(1180px, calc(100vw - 40px));
|
|
38
|
+
margin: 0 auto;
|
|
39
|
+
padding: 26px 0 32px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
header,
|
|
43
|
+
footer {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
header {
|
|
50
|
+
min-height: 42px;
|
|
51
|
+
margin-bottom: 20px;
|
|
52
|
+
color: #9b968c;
|
|
53
|
+
font-size: 11px;
|
|
54
|
+
letter-spacing: 0.12em;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
header nav {
|
|
59
|
+
display: flex;
|
|
60
|
+
gap: 20px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
header a {
|
|
64
|
+
color: inherit;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
header a:hover {
|
|
69
|
+
color: #fff;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.wordmark {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
gap: 10px;
|
|
75
|
+
align-items: center;
|
|
76
|
+
color: #f4f0e8;
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
font-weight: 750;
|
|
79
|
+
letter-spacing: -0.02em;
|
|
80
|
+
text-transform: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.mark {
|
|
84
|
+
width: 13px;
|
|
85
|
+
height: 13px;
|
|
86
|
+
border: 2px solid var(--accent);
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
box-shadow: inset 4px 0 0 var(--accent);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stage {
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
border: 1px solid var(--line);
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
background: #1b1a18;
|
|
96
|
+
box-shadow: 0 30px 90px rgb(0 0 0 / 44%);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.poster {
|
|
100
|
+
position: relative;
|
|
101
|
+
aspect-ratio: 16 / 9;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
background: var(--paper);
|
|
104
|
+
color: var(--ink);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.poster::after {
|
|
108
|
+
position: absolute;
|
|
109
|
+
inset: 0;
|
|
110
|
+
z-index: 8;
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
content: "";
|
|
113
|
+
opacity: 0.12;
|
|
114
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.15'/%3E%3C/svg%3E");
|
|
115
|
+
mix-blend-mode: multiply;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.poster-copy {
|
|
119
|
+
position: absolute;
|
|
120
|
+
top: 36px;
|
|
121
|
+
left: 40px;
|
|
122
|
+
z-index: 5;
|
|
123
|
+
width: 290px;
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.eyebrow {
|
|
128
|
+
margin: 0 0 9px;
|
|
129
|
+
color: var(--muted);
|
|
130
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
131
|
+
font-size: 8px;
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
letter-spacing: 0.15em;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
h1 {
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-size: clamp(34px, 4vw, 60px);
|
|
139
|
+
font-weight: 900;
|
|
140
|
+
letter-spacing: -0.075em;
|
|
141
|
+
line-height: 0.84;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
h1 em {
|
|
145
|
+
color: var(--accent);
|
|
146
|
+
font-style: normal;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.description {
|
|
150
|
+
width: 235px;
|
|
151
|
+
margin: 14px 0 0;
|
|
152
|
+
color: var(--muted);
|
|
153
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
154
|
+
font-size: clamp(7px, 0.76vw, 10px);
|
|
155
|
+
line-height: 1.55;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.corner-note {
|
|
159
|
+
position: absolute;
|
|
160
|
+
right: 38px;
|
|
161
|
+
top: 34px;
|
|
162
|
+
z-index: 5;
|
|
163
|
+
margin: 0;
|
|
164
|
+
color: var(--muted);
|
|
165
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
166
|
+
font-size: 8px;
|
|
167
|
+
line-height: 1.5;
|
|
168
|
+
text-align: right;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.motion {
|
|
172
|
+
position: absolute;
|
|
173
|
+
inset: 0;
|
|
174
|
+
z-index: 3;
|
|
175
|
+
width: 100%;
|
|
176
|
+
height: 100%;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.folio {
|
|
180
|
+
position: absolute;
|
|
181
|
+
right: 38px;
|
|
182
|
+
bottom: 26px;
|
|
183
|
+
z-index: 5;
|
|
184
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
185
|
+
font-size: 8px;
|
|
186
|
+
font-weight: 700;
|
|
187
|
+
letter-spacing: 0.18em;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.loading {
|
|
191
|
+
position: absolute;
|
|
192
|
+
right: 38px;
|
|
193
|
+
bottom: 46px;
|
|
194
|
+
z-index: 10;
|
|
195
|
+
padding: 5px 7px;
|
|
196
|
+
background: var(--ink);
|
|
197
|
+
color: var(--paper);
|
|
198
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
199
|
+
font-size: 8px;
|
|
200
|
+
letter-spacing: 0.12em;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
footer {
|
|
204
|
+
gap: 30px;
|
|
205
|
+
padding-top: 17px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.controls {
|
|
209
|
+
display: flex;
|
|
210
|
+
flex: 1;
|
|
211
|
+
gap: 12px;
|
|
212
|
+
align-items: center;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
button {
|
|
216
|
+
min-width: 96px;
|
|
217
|
+
padding: 8px 10px;
|
|
218
|
+
border: 1px solid #4a463f;
|
|
219
|
+
border-radius: 3px;
|
|
220
|
+
background: transparent;
|
|
221
|
+
color: #d8d3c9;
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
224
|
+
font-size: 9px;
|
|
225
|
+
letter-spacing: 0.09em;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
button:hover {
|
|
229
|
+
border-color: var(--accent);
|
|
230
|
+
color: #fff;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
input[type="range"] {
|
|
234
|
+
width: min(430px, 40vw);
|
|
235
|
+
accent-color: var(--accent);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.metrics {
|
|
239
|
+
display: grid;
|
|
240
|
+
grid-template-columns: repeat(4, auto);
|
|
241
|
+
gap: 22px;
|
|
242
|
+
margin: 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.metrics div {
|
|
246
|
+
display: grid;
|
|
247
|
+
gap: 3px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.metrics dt {
|
|
251
|
+
color: #777269;
|
|
252
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
253
|
+
font-size: 8px;
|
|
254
|
+
letter-spacing: 0.12em;
|
|
255
|
+
text-transform: uppercase;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.metrics dd {
|
|
259
|
+
margin: 0;
|
|
260
|
+
color: #d8d3c9;
|
|
261
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
262
|
+
font-size: 10px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.theme-crystal {
|
|
266
|
+
--paper: #f4eedc;
|
|
267
|
+
--accent: #cc5f4a;
|
|
268
|
+
--page-glow: #293337;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.theme-ball {
|
|
272
|
+
--paper: #e9e1cf;
|
|
273
|
+
--accent: #df672e;
|
|
274
|
+
--page-glow: #342c24;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.theme-kick {
|
|
278
|
+
--paper: #dbe5c9;
|
|
279
|
+
--accent: #f05d3d;
|
|
280
|
+
--page-glow: #263121;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.theme-wave {
|
|
284
|
+
--paper: #e7efe9;
|
|
285
|
+
--accent: #f06a43;
|
|
286
|
+
--page-glow: #20333a;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@media (max-width: 760px) {
|
|
290
|
+
main {
|
|
291
|
+
width: min(100% - 20px, 1180px);
|
|
292
|
+
padding-top: 12px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
header nav,
|
|
296
|
+
.corner-note,
|
|
297
|
+
.description,
|
|
298
|
+
.metrics {
|
|
299
|
+
display: none;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.poster-copy {
|
|
303
|
+
top: 20px;
|
|
304
|
+
left: 22px;
|
|
305
|
+
width: 210px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
h1 {
|
|
309
|
+
font-size: clamp(26px, 7vw, 42px);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
footer {
|
|
313
|
+
padding-top: 11px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.controls,
|
|
317
|
+
input[type="range"] {
|
|
318
|
+
width: 100%;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function mountPlayer({
|
|
2
|
+
scene,
|
|
3
|
+
simulation,
|
|
4
|
+
renderer,
|
|
5
|
+
metricLabel = "signal",
|
|
6
|
+
metricValue = "—"
|
|
7
|
+
}) {
|
|
8
|
+
const loading = document.querySelector("#loading");
|
|
9
|
+
const toggle = document.querySelector("#toggle");
|
|
10
|
+
const scrubber = document.querySelector("#scrubber");
|
|
11
|
+
const fingerprint = document.querySelector("#fingerprint");
|
|
12
|
+
const metricName = document.querySelector("#metric-name");
|
|
13
|
+
const metric = document.querySelector("#metric");
|
|
14
|
+
const lastFrame = simulation.frames.length - 1;
|
|
15
|
+
const durationMs = scene.timing.duration * 1000;
|
|
16
|
+
|
|
17
|
+
scrubber.max = String(lastFrame);
|
|
18
|
+
fingerprint.textContent = simulation.fingerprint;
|
|
19
|
+
metricName.textContent = metricLabel;
|
|
20
|
+
metric.textContent = metricValue;
|
|
21
|
+
loading?.remove();
|
|
22
|
+
|
|
23
|
+
let playing = true;
|
|
24
|
+
let origin = performance.now();
|
|
25
|
+
let pausedFrame = 0;
|
|
26
|
+
|
|
27
|
+
function paint(frame) {
|
|
28
|
+
const safeFrame = Math.max(0, Math.min(lastFrame, Math.round(frame)));
|
|
29
|
+
pausedFrame = safeFrame;
|
|
30
|
+
renderer.renderFrame(safeFrame);
|
|
31
|
+
scrubber.value = String(safeFrame);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function play() {
|
|
35
|
+
if (playing) return;
|
|
36
|
+
playing = true;
|
|
37
|
+
toggle.textContent = "PAUSE";
|
|
38
|
+
origin =
|
|
39
|
+
performance.now() -
|
|
40
|
+
(pausedFrame / scene.timing.fps) * 1000;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pause() {
|
|
44
|
+
playing = false;
|
|
45
|
+
toggle.textContent = "PLAY";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function seekFrame(frame) {
|
|
49
|
+
pause();
|
|
50
|
+
paint(frame);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function seekTime(time) {
|
|
54
|
+
seekFrame(time * scene.timing.fps);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function animate(now) {
|
|
58
|
+
if (playing) {
|
|
59
|
+
const elapsed = (now - origin) % durationMs;
|
|
60
|
+
paint((elapsed / 1000) * scene.timing.fps);
|
|
61
|
+
}
|
|
62
|
+
requestAnimationFrame(animate);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toggle.addEventListener("click", () => {
|
|
66
|
+
if (playing) pause();
|
|
67
|
+
else play();
|
|
68
|
+
});
|
|
69
|
+
scrubber.addEventListener("input", () => {
|
|
70
|
+
seekFrame(Number(scrubber.value));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
window.__oddlyAlivePlayer = {
|
|
74
|
+
scene,
|
|
75
|
+
simulation,
|
|
76
|
+
paint,
|
|
77
|
+
pause,
|
|
78
|
+
play,
|
|
79
|
+
seekFrame,
|
|
80
|
+
seekTime
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
paint(0);
|
|
84
|
+
requestAnimationFrame(animate);
|
|
85
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSurfaceWaveRenderer,
|
|
3
|
+
simulateScene,
|
|
4
|
+
validateScene
|
|
5
|
+
} from "../../src/index.js";
|
|
6
|
+
import { mountPlayer } from "../shared/player.js";
|
|
7
|
+
|
|
8
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
9
|
+
|
|
10
|
+
const response = await fetch("./scene.json");
|
|
11
|
+
if (!response.ok) throw new Error(`Could not load scene: ${response.status}`);
|
|
12
|
+
const scene = validateScene(await response.json());
|
|
13
|
+
const simulation = simulateScene(scene);
|
|
14
|
+
const renderer = createSurfaceWaveRenderer(
|
|
15
|
+
document.querySelector("#motion"),
|
|
16
|
+
simulation,
|
|
17
|
+
{ shoeHref: "../../assets/photoreal/sneaker.png" }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
mountPlayer({
|
|
21
|
+
scene,
|
|
22
|
+
simulation,
|
|
23
|
+
renderer,
|
|
24
|
+
metricLabel: "wave peak",
|
|
25
|
+
metricValue: `${simulation.diagnostics.maxWaveHeight.toFixed(1)} px`
|
|
26
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="OddlyAlive shoe and water-wave example" />
|
|
7
|
+
<title>Shoe Splash — OddlyAlive</title>
|
|
8
|
+
<link rel="stylesheet" href="../shared/example.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body class="theme-wave">
|
|
11
|
+
<main>
|
|
12
|
+
<header>
|
|
13
|
+
<a class="wordmark" href="../"><span class="mark"></span>OddlyAlive</a>
|
|
14
|
+
<nav>
|
|
15
|
+
<a href="../football-kick/">← Football Kick</a>
|
|
16
|
+
<a href="../">All examples →</a>
|
|
17
|
+
</nav>
|
|
18
|
+
</header>
|
|
19
|
+
<section class="stage">
|
|
20
|
+
<div class="poster">
|
|
21
|
+
<div class="poster-copy">
|
|
22
|
+
<p class="eyebrow">05 / COUPLED SYSTEM</p>
|
|
23
|
+
<h1>THROW<br /><em>THE SHOE.</em></h1>
|
|
24
|
+
<p class="description">
|
|
25
|
+
A rigid prop disturbs a damped wave surface. The water answers,
|
|
26
|
+
then buoyancy and drag make the shoe answer back.
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
<p class="corner-note">RIGID PROP<br />1D WAVE SURFACE<br />TWO-WAY COUPLING</p>
|
|
30
|
+
<svg
|
|
31
|
+
id="motion"
|
|
32
|
+
class="motion"
|
|
33
|
+
viewBox="0 0 960 540"
|
|
34
|
+
role="img"
|
|
35
|
+
aria-label="A shoe falling into water and making waves"
|
|
36
|
+
></svg>
|
|
37
|
+
<div class="folio">OA—005</div>
|
|
38
|
+
<div id="loading" class="loading">SOLVING WORLD…</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
<footer>
|
|
42
|
+
<div class="controls">
|
|
43
|
+
<button id="toggle" type="button">PAUSE</button>
|
|
44
|
+
<input id="scrubber" type="range" min="0" value="0" aria-label="Timeline" />
|
|
45
|
+
</div>
|
|
46
|
+
<dl class="metrics">
|
|
47
|
+
<div><dt>solver</dt><dd>coupled wave</dd></div>
|
|
48
|
+
<div><dt>step</dt><dd>240 Hz</dd></div>
|
|
49
|
+
<div><dt id="metric-name">wave peak</dt><dd id="metric">—</dd></div>
|
|
50
|
+
<div><dt>fingerprint</dt><dd id="fingerprint">—</dd></div>
|
|
51
|
+
</dl>
|
|
52
|
+
</footer>
|
|
53
|
+
</main>
|
|
54
|
+
<script type="module" src="./app.js"></script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../schemas/scene.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"type": "surface-wave",
|
|
5
|
+
"name": "OddlyAlive — Shoe Splash",
|
|
6
|
+
"seed": 31,
|
|
7
|
+
"canvas": { "width": 960, "height": 540 },
|
|
8
|
+
"timing": { "duration": 6.4, "fps": 60, "substeps": 4, "preRoll": 0 },
|
|
9
|
+
"surface": {
|
|
10
|
+
"x": 70,
|
|
11
|
+
"width": 820,
|
|
12
|
+
"restY": 330,
|
|
13
|
+
"samples": 161,
|
|
14
|
+
"waveSpeed": 172,
|
|
15
|
+
"damping": 2.4,
|
|
16
|
+
"coupling": 0.42
|
|
17
|
+
},
|
|
18
|
+
"world": {
|
|
19
|
+
"gravity": 720,
|
|
20
|
+
"airDrag": 0.06,
|
|
21
|
+
"waterDrag": 4.8,
|
|
22
|
+
"buoyancy": 29
|
|
23
|
+
},
|
|
24
|
+
"body": {
|
|
25
|
+
"id": "shoe",
|
|
26
|
+
"kind": "shoe",
|
|
27
|
+
"x": 500,
|
|
28
|
+
"y": 78,
|
|
29
|
+
"width": 174,
|
|
30
|
+
"height": 66,
|
|
31
|
+
"mass": 0.72,
|
|
32
|
+
"vx": -11,
|
|
33
|
+
"vy": 20,
|
|
34
|
+
"angle": -13,
|
|
35
|
+
"angularVelocity": 0.9,
|
|
36
|
+
"restitution": 0.18
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Provenance
|
|
2
|
+
|
|
3
|
+
The interaction model is an original extraction from TheVibeFounder’s 2026
|
|
4
|
+
city-postcard experiments. The public alpha preserves their fixed-step XPBD
|
|
5
|
+
constraints, pressure-bearing contact capsule, per-strand static/kinetic friction,
|
|
6
|
+
load-based peel, and deterministic material variation while removing all
|
|
7
|
+
city-specific artwork and layout assumptions.
|
|
8
|
+
|
|
9
|
+
The demo typography, SVG hardware, interface, and scene are original work for
|
|
10
|
+
OddlyAlive. No third-party image, video, font file, or embedded raster contributes
|
|
11
|
+
pixels.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSvgRenderer,
|
|
3
|
+
sampleGesture,
|
|
4
|
+
simulateScene,
|
|
5
|
+
validateScene
|
|
6
|
+
} from "../../src/index.js";
|
|
7
|
+
|
|
8
|
+
const loading = document.querySelector("#loading");
|
|
9
|
+
const svg = document.querySelector("#motion");
|
|
10
|
+
const toggle = document.querySelector("#toggle");
|
|
11
|
+
const scrubber = document.querySelector("#scrubber");
|
|
12
|
+
const debugButton = document.querySelector("#debug");
|
|
13
|
+
const grip = document.querySelector("#grip");
|
|
14
|
+
const fingerprint = document.querySelector("#fingerprint");
|
|
15
|
+
|
|
16
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
17
|
+
const rawScene = await fetch("./scene.json").then((response) => response.json());
|
|
18
|
+
const scene = validateScene(rawScene);
|
|
19
|
+
const simulation = simulateScene(scene);
|
|
20
|
+
const renderer = createSvgRenderer(svg, simulation, {
|
|
21
|
+
glyphCharmHref: "../../assets/photoreal/letter-charm-square.png"
|
|
22
|
+
});
|
|
23
|
+
const durationMs = scene.timing.duration * 1000;
|
|
24
|
+
const lastFrame = simulation.frames.length - 1;
|
|
25
|
+
|
|
26
|
+
scrubber.max = String(lastFrame);
|
|
27
|
+
grip.textContent = `${simulation.diagnostics.observedMaxStaticGrip}/${scene.contact.maxStaticGrip}`;
|
|
28
|
+
fingerprint.textContent = simulation.fingerprint;
|
|
29
|
+
loading.remove();
|
|
30
|
+
|
|
31
|
+
let playing = true;
|
|
32
|
+
let debug = false;
|
|
33
|
+
let origin = performance.now();
|
|
34
|
+
let pausedFrame = 0;
|
|
35
|
+
|
|
36
|
+
function paint(frame) {
|
|
37
|
+
const time = frame / scene.timing.fps;
|
|
38
|
+
renderer.renderFrame(frame, sampleGesture(scene.gesture.points, time));
|
|
39
|
+
scrubber.value = String(frame);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function animate(now) {
|
|
43
|
+
if (playing) {
|
|
44
|
+
const elapsed = (now - origin) % durationMs;
|
|
45
|
+
const frame = Math.min(
|
|
46
|
+
lastFrame,
|
|
47
|
+
Math.round((elapsed / 1000) * scene.timing.fps)
|
|
48
|
+
);
|
|
49
|
+
pausedFrame = frame;
|
|
50
|
+
paint(frame);
|
|
51
|
+
}
|
|
52
|
+
requestAnimationFrame(animate);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toggle.addEventListener("click", () => {
|
|
56
|
+
playing = !playing;
|
|
57
|
+
toggle.textContent = playing ? "PAUSE" : "PLAY";
|
|
58
|
+
if (playing) {
|
|
59
|
+
origin =
|
|
60
|
+
performance.now() -
|
|
61
|
+
(pausedFrame / scene.timing.fps) * 1000;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
scrubber.addEventListener("input", () => {
|
|
66
|
+
playing = false;
|
|
67
|
+
toggle.textContent = "PLAY";
|
|
68
|
+
pausedFrame = Number(scrubber.value);
|
|
69
|
+
paint(pausedFrame);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
debugButton.addEventListener("click", () => {
|
|
73
|
+
debug = !debug;
|
|
74
|
+
renderer.setDebug(debug);
|
|
75
|
+
debugButton.textContent = debug ? "HIDE TOUCH" : "SHOW TOUCH";
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
paint(0);
|
|
79
|
+
requestAnimationFrame(animate);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta
|
|
7
|
+
name="description"
|
|
8
|
+
content="OddlyAlive deterministic string-touch physics demo"
|
|
9
|
+
/>
|
|
10
|
+
<title>OddlyAlive — Make anything feel alive</title>
|
|
11
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main>
|
|
15
|
+
<header>
|
|
16
|
+
<a class="wordmark" href="../" aria-label="OddlyAlive home">
|
|
17
|
+
<span class="mark"></span>
|
|
18
|
+
OddlyAlive
|
|
19
|
+
</a>
|
|
20
|
+
<p>Deterministic object motion</p>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<section class="stage" aria-label="String touch physics demo">
|
|
24
|
+
<div class="poster">
|
|
25
|
+
<div class="poster-copy">
|
|
26
|
+
<div>
|
|
27
|
+
<p class="eyebrow">OPEN SOURCE PHYSICS · ALPHA 01</p>
|
|
28
|
+
<h1>MAKE<br />ANYTHING<br /><em>FEEL ALIVE.</em></h1>
|
|
29
|
+
</div>
|
|
30
|
+
<p class="caption">
|
|
31
|
+
No hand. No magnetic keyframes.<br />
|
|
32
|
+
Just pressure, friction, load and release.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
<svg
|
|
36
|
+
id="motion"
|
|
37
|
+
viewBox="0 0 960 540"
|
|
38
|
+
role="img"
|
|
39
|
+
aria-label="Letters hanging from flexible strings move under an invisible touch"
|
|
40
|
+
></svg>
|
|
41
|
+
<div class="folio">OA—001</div>
|
|
42
|
+
<div id="loading" class="loading">SIMULATING 240HZ PHYSICS…</div>
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<footer>
|
|
47
|
+
<div class="controls">
|
|
48
|
+
<button id="toggle" type="button">PAUSE</button>
|
|
49
|
+
<input
|
|
50
|
+
id="scrubber"
|
|
51
|
+
aria-label="Animation playhead"
|
|
52
|
+
type="range"
|
|
53
|
+
min="0"
|
|
54
|
+
max="384"
|
|
55
|
+
value="0"
|
|
56
|
+
/>
|
|
57
|
+
<button id="debug" type="button">SHOW TOUCH</button>
|
|
58
|
+
</div>
|
|
59
|
+
<dl class="metrics">
|
|
60
|
+
<div><dt>solver</dt><dd>XPBD</dd></div>
|
|
61
|
+
<div><dt>step</dt><dd>240Hz</dd></div>
|
|
62
|
+
<div><dt>grip</dt><dd id="grip">—</dd></div>
|
|
63
|
+
<div><dt>fingerprint</dt><dd id="fingerprint">—</dd></div>
|
|
64
|
+
</dl>
|
|
65
|
+
</footer>
|
|
66
|
+
</main>
|
|
67
|
+
|
|
68
|
+
<script type="module" src="./app.js"></script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|