stereoframe 0.2.5 → 0.2.7
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/dist/cli.js +252 -89
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFileSync as readFileSync2, existsSync as existsSync6 } from "node:fs";
|
|
5
|
+
import { join as join6, resolve as resolve8 } from "node:path";
|
|
6
|
+
import { basename as basename2 } from "node:path";
|
|
6
7
|
|
|
7
8
|
// src/blocks.ts
|
|
8
9
|
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
@@ -174,6 +175,205 @@ async function genModel(opts) {
|
|
|
174
175
|
return out;
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
// src/stage.ts
|
|
179
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
180
|
+
import { basename, join as join4, resolve as resolve4 } from "node:path";
|
|
181
|
+
|
|
182
|
+
// src/scaffold.ts
|
|
183
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
184
|
+
import { createRequire } from "node:module";
|
|
185
|
+
import { dirname, join as join3, resolve as resolve3 } from "node:path";
|
|
186
|
+
var TEMPLATE = `<!doctype html>
|
|
187
|
+
<html lang="en">
|
|
188
|
+
<head>
|
|
189
|
+
<meta charset="UTF-8" />
|
|
190
|
+
<style>
|
|
191
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
192
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; background: #000; }
|
|
193
|
+
body { font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
194
|
+
sf-scene { position: absolute; inset: 0; }
|
|
195
|
+
#title {
|
|
196
|
+
position: absolute; bottom: 100px; width: 100%; text-align: center;
|
|
197
|
+
font-size: 64px; font-weight: 700; color: #f4f4f5; letter-spacing: 0.04em;
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<sf-scene duration="5" width="1920" height="1080" background="#101225">
|
|
203
|
+
<sf-camera fov="38" position="0 0.8 6" look-at="0 0 0"></sf-camera>
|
|
204
|
+
<sf-light preset="soft"></sf-light>
|
|
205
|
+
|
|
206
|
+
<sf-mesh id="hero" geometry="icosahedron" args="1.4 2" color="#7dd3fc"
|
|
207
|
+
metalness="0.4" roughness="0.25"></sf-mesh>
|
|
208
|
+
|
|
209
|
+
<sf-animate target="#hero" verb="bounce-in" start="0.3" duration="0.8"></sf-animate>
|
|
210
|
+
<sf-animate target="#hero" verb="turntable" rpm="10"></sf-animate>
|
|
211
|
+
<sf-animate target="#hero" verb="float" amplitude="0.12" period="3"></sf-animate>
|
|
212
|
+
<sf-animate target="#title" verb="fade-in" start="1.2" duration="0.8" rise="30"></sf-animate>
|
|
213
|
+
</sf-scene>
|
|
214
|
+
|
|
215
|
+
<div id="title" class="clip" data-start="1.2">hello stereoframe</div>
|
|
216
|
+
|
|
217
|
+
<script type="module">
|
|
218
|
+
import "./assets/stereoframe.js";
|
|
219
|
+
</script>
|
|
220
|
+
</body>
|
|
221
|
+
</html>
|
|
222
|
+
`;
|
|
223
|
+
function resolveRuntimeBundle() {
|
|
224
|
+
const require2 = createRequire(import.meta.url);
|
|
225
|
+
return require2.resolve("stereoframe-runtime");
|
|
226
|
+
}
|
|
227
|
+
function scaffoldProject(name, cwd = process.cwd()) {
|
|
228
|
+
const dir = resolve3(cwd, name);
|
|
229
|
+
if (existsSync3(join3(dir, "index.html"))) {
|
|
230
|
+
throw new Error(`refusing to overwrite existing project at ${dir}`);
|
|
231
|
+
}
|
|
232
|
+
mkdirSync3(join3(dir, "assets"), { recursive: true });
|
|
233
|
+
writeFileSync2(join3(dir, "index.html"), TEMPLATE);
|
|
234
|
+
writeFileSync2(join3(dir, ".gitignore"), `renders/
|
|
235
|
+
`);
|
|
236
|
+
copyFileSync2(resolveRuntimeBundle(), join3(dir, "assets", "stereoframe.js"));
|
|
237
|
+
return dir;
|
|
238
|
+
}
|
|
239
|
+
function updateRuntime(projectDir) {
|
|
240
|
+
if (!existsSync3(join3(resolve3(projectDir), "index.html"))) {
|
|
241
|
+
throw new Error(`no index.html in ${projectDir} — not a stereoframe project`);
|
|
242
|
+
}
|
|
243
|
+
const target = join3(resolve3(projectDir), "assets", "stereoframe.js");
|
|
244
|
+
mkdirSync3(dirname(target), { recursive: true });
|
|
245
|
+
copyFileSync2(resolveRuntimeBundle(), target);
|
|
246
|
+
return target;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/stage.ts
|
|
250
|
+
var PRESETS = ["reveal", "hero-orbit", "turntable"];
|
|
251
|
+
function head(bg) {
|
|
252
|
+
return `<!doctype html>
|
|
253
|
+
<html lang="en">
|
|
254
|
+
<head>
|
|
255
|
+
<meta charset="UTF-8" />
|
|
256
|
+
<style>
|
|
257
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
258
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; background: ${bg}; }
|
|
259
|
+
body { font-family: "Helvetica Neue", Arial, sans-serif; }
|
|
260
|
+
sf-scene { position: absolute; inset: 0; }
|
|
261
|
+
#title {
|
|
262
|
+
position: absolute; bottom: 110px; width: 100%; text-align: center;
|
|
263
|
+
font-size: 70px; font-weight: 700; letter-spacing: 0.04em; color: #f4f1e8;
|
|
264
|
+
text-shadow: 0 4px 30px rgba(0,0,0,0.55);
|
|
265
|
+
}
|
|
266
|
+
</style>
|
|
267
|
+
</head>
|
|
268
|
+
<body>`;
|
|
269
|
+
}
|
|
270
|
+
var tail = `
|
|
271
|
+
<script type="module">
|
|
272
|
+
import "./assets/stereoframe.js";
|
|
273
|
+
</script>
|
|
274
|
+
</body>
|
|
275
|
+
</html>
|
|
276
|
+
`;
|
|
277
|
+
function titleBlock(title, t1) {
|
|
278
|
+
if (!title)
|
|
279
|
+
return { anim: "", dom: "" };
|
|
280
|
+
return {
|
|
281
|
+
anim: ` <sf-animate target="#title" verb="fade-in" start="${t1}" duration="1.2" rise="34"></sf-animate>
|
|
282
|
+
`,
|
|
283
|
+
dom: ` <div id="title" class="clip" data-start="${t1}">${title}</div>
|
|
284
|
+
`
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function reveal(model, d, bg, title) {
|
|
288
|
+
const t1 = Math.max(1.2, d * 0.4);
|
|
289
|
+
const t = titleBlock(title, t1);
|
|
290
|
+
return `${head(bg)}
|
|
291
|
+
<sf-scene duration="${d}" width="1920" height="1080" background="${bg}"
|
|
292
|
+
environment="room" exposure="0.92"
|
|
293
|
+
samples="2" bloom="0.22" bloom-threshold="0.86" vignette="0.45"
|
|
294
|
+
chromatic-aberration="0.14" grain="0.03" contrast="1.05" saturation="1.05">
|
|
295
|
+
<sf-camera fov="34" position="-3 -0.8 5.2" look-at="0 0.1 0"></sf-camera>
|
|
296
|
+
<sf-light type="hemisphere" color="#2a3450" intensity="0.5"></sf-light>
|
|
297
|
+
<sf-light type="directional" color="#ffffff" intensity="2.6" position="4 6 5"></sf-light>
|
|
298
|
+
<sf-light type="directional" color="#9db8ff" intensity="1.5" position="-5 2 -4"></sf-light>
|
|
299
|
+
<sf-model id="m" src="assets/${model}" fit="2.6"></sf-model>
|
|
300
|
+
<sf-animate target="#m" verb="bounce-in" start="0.2" duration="1" ease="back.out"></sf-animate>
|
|
301
|
+
<sf-animate target="#m" verb="turntable" rpm="2.4" start="0.5"></sf-animate>
|
|
302
|
+
<sf-animate target="camera" verb="camera-path" look="none"
|
|
303
|
+
points="-3 -0.8 5.2, -1.8 0 4.4, -0.7 0.5 3.9, 0.25 0.7 3.7"
|
|
304
|
+
start="0" duration="${d}" ease="power2.inOut"></sf-animate>
|
|
305
|
+
${t.anim} </sf-scene>
|
|
306
|
+
${t.dom}${tail}`;
|
|
307
|
+
}
|
|
308
|
+
function heroOrbit(model, d, bg, title) {
|
|
309
|
+
const t1 = Math.max(1.2, d * 0.4);
|
|
310
|
+
const t = titleBlock(title, t1);
|
|
311
|
+
return `${head(bg)}
|
|
312
|
+
<sf-scene duration="${d}" width="1920" height="1080" background="${bg}"
|
|
313
|
+
environment="room" exposure="1.0"
|
|
314
|
+
samples="2" bloom="0.12" bloom-threshold="0.9" vignette="0.32"
|
|
315
|
+
grain="0.025" contrast="1.03" saturation="1.05">
|
|
316
|
+
<sf-camera fov="34" position="0 0.6 5" look-at="0 0 0"></sf-camera>
|
|
317
|
+
<sf-light preset="studio"></sf-light>
|
|
318
|
+
<sf-model id="m" src="assets/${model}" fit="2.6"></sf-model>
|
|
319
|
+
<sf-animate target="#m" verb="turntable" rpm="1.4"></sf-animate>
|
|
320
|
+
<sf-animate target="camera" verb="orbit" around="0 0 0" radius="5"
|
|
321
|
+
from="-38deg" to="38deg" height="0.6" start="0" duration="${d}"
|
|
322
|
+
ease="sine.inOut"></sf-animate>
|
|
323
|
+
${t.anim} </sf-scene>
|
|
324
|
+
${t.dom}${tail}`;
|
|
325
|
+
}
|
|
326
|
+
function turntable(model, d, bg, title) {
|
|
327
|
+
const t1 = Math.max(1.2, d * 0.4);
|
|
328
|
+
const t = titleBlock(title, t1);
|
|
329
|
+
return `${head(bg)}
|
|
330
|
+
<sf-scene duration="${d}" width="1920" height="1080" background="${bg}"
|
|
331
|
+
environment="room" exposure="1.02"
|
|
332
|
+
samples="2" bloom="0.12" bloom-threshold="0.9" vignette="0.3"
|
|
333
|
+
grain="0.02" contrast="1.03">
|
|
334
|
+
<sf-camera fov="33" position="0 1.4 5.4" look-at="0 1 0"></sf-camera>
|
|
335
|
+
<sf-light preset="studio"></sf-light>
|
|
336
|
+
<sf-mesh geometry="cylinder" args="2 2.2 0.12" color="#1c1f26"
|
|
337
|
+
roughness="0.3" metalness="0.6" position="0 -0.06 0"></sf-mesh>
|
|
338
|
+
<sf-model id="m" src="assets/${model}" fit="2.3" fit-ground></sf-model>
|
|
339
|
+
<sf-animate target="#m" verb="turntable" rpm="5"></sf-animate>
|
|
340
|
+
<sf-animate target="camera" verb="dolly" toward="0 1 0" distance="0.5"
|
|
341
|
+
start="0" duration="${d}" ease="sine.inOut"></sf-animate>
|
|
342
|
+
${t.anim} </sf-scene>
|
|
343
|
+
${t.dom}${tail}`;
|
|
344
|
+
}
|
|
345
|
+
var TEMPLATES = {
|
|
346
|
+
reveal,
|
|
347
|
+
"hero-orbit": heroOrbit,
|
|
348
|
+
turntable
|
|
349
|
+
};
|
|
350
|
+
var DEFAULT_BG = {
|
|
351
|
+
reveal: "#0a0a0e",
|
|
352
|
+
"hero-orbit": "#16181c",
|
|
353
|
+
turntable: "#15171b"
|
|
354
|
+
};
|
|
355
|
+
function stageModel(opts) {
|
|
356
|
+
const modelPath = resolve4(opts.model);
|
|
357
|
+
if (!existsSync4(modelPath))
|
|
358
|
+
throw new Error(`model not found: ${opts.model}`);
|
|
359
|
+
if (!/\.(glb|gltf)$/i.test(modelPath)) {
|
|
360
|
+
throw new Error("stage expects a .glb or .gltf model");
|
|
361
|
+
}
|
|
362
|
+
const dir = resolve4(opts.projectDir);
|
|
363
|
+
mkdirSync4(join4(dir, "assets"), { recursive: true });
|
|
364
|
+
const modelFile = basename(modelPath);
|
|
365
|
+
copyFileSync3(modelPath, join4(dir, "assets", modelFile));
|
|
366
|
+
copyFileSync3(resolveRuntimeBundle(), join4(dir, "assets", "stereoframe.js"));
|
|
367
|
+
if (!existsSync4(join4(dir, ".gitignore")))
|
|
368
|
+
writeFileSync3(join4(dir, ".gitignore"), `renders/
|
|
369
|
+
`);
|
|
370
|
+
const d = opts.duration && opts.duration > 0 ? opts.duration : 8;
|
|
371
|
+
const bg = opts.background ?? DEFAULT_BG[opts.preset];
|
|
372
|
+
const html = TEMPLATES[opts.preset](modelFile, d, bg, opts.title);
|
|
373
|
+
writeFileSync3(join4(dir, "index.html"), html);
|
|
374
|
+
return dir;
|
|
375
|
+
}
|
|
376
|
+
|
|
177
377
|
// src/lint.ts
|
|
178
378
|
import { ASSET_ATTRS, EASE_NAMES, ELEMENT_NAMES, VERB_NAMES } from "stereoframe-runtime/vocab";
|
|
179
379
|
function readAttr(attrs, name) {
|
|
@@ -371,16 +571,16 @@ function lintHtml(html, opts) {
|
|
|
371
571
|
|
|
372
572
|
// src/render.ts
|
|
373
573
|
import { spawn } from "node:child_process";
|
|
374
|
-
import { mkdirSync as
|
|
375
|
-
import { dirname, resolve as
|
|
574
|
+
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
575
|
+
import { dirname as dirname2, resolve as resolve6 } from "node:path";
|
|
376
576
|
|
|
377
577
|
// src/session.ts
|
|
378
578
|
import puppeteer from "puppeteer";
|
|
379
579
|
|
|
380
580
|
// src/serve.ts
|
|
381
581
|
import { createServer } from "node:http";
|
|
382
|
-
import { createReadStream, existsSync as
|
|
383
|
-
import { extname, join as
|
|
582
|
+
import { createReadStream, existsSync as existsSync5, statSync } from "node:fs";
|
|
583
|
+
import { extname, join as join5, normalize, resolve as resolve5 } from "node:path";
|
|
384
584
|
var MIME = {
|
|
385
585
|
".html": "text/html; charset=utf-8",
|
|
386
586
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -408,7 +608,7 @@ var MIME = {
|
|
|
408
608
|
".otf": "font/otf"
|
|
409
609
|
};
|
|
410
610
|
function serveProject(projectDir, fixedPort = 0) {
|
|
411
|
-
const root =
|
|
611
|
+
const root = resolve5(projectDir);
|
|
412
612
|
const server = createServer((req, res) => {
|
|
413
613
|
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
|
|
414
614
|
if (urlPath === "/favicon.ico") {
|
|
@@ -416,15 +616,15 @@ function serveProject(projectDir, fixedPort = 0) {
|
|
|
416
616
|
return;
|
|
417
617
|
}
|
|
418
618
|
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
419
|
-
let filePath =
|
|
619
|
+
let filePath = join5(root, rel);
|
|
420
620
|
if (!filePath.startsWith(root)) {
|
|
421
621
|
res.writeHead(403).end("forbidden");
|
|
422
622
|
return;
|
|
423
623
|
}
|
|
424
|
-
if (
|
|
425
|
-
filePath =
|
|
624
|
+
if (existsSync5(filePath) && statSync(filePath).isDirectory()) {
|
|
625
|
+
filePath = join5(filePath, "index.html");
|
|
426
626
|
}
|
|
427
|
-
if (!
|
|
627
|
+
if (!existsSync5(filePath)) {
|
|
428
628
|
res.writeHead(404).end(`not found: ${urlPath}`);
|
|
429
629
|
return;
|
|
430
630
|
}
|
|
@@ -513,10 +713,10 @@ async function renderProject(opts) {
|
|
|
513
713
|
const fps = opts.fps ?? 30;
|
|
514
714
|
const crf = opts.draft ? 28 : opts.crf ?? 18;
|
|
515
715
|
const preset = opts.draft ? "veryfast" : "medium";
|
|
516
|
-
const projectDir =
|
|
716
|
+
const projectDir = resolve6(opts.projectDir);
|
|
517
717
|
const stamp = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19);
|
|
518
|
-
const out =
|
|
519
|
-
|
|
718
|
+
const out = resolve6(projectDir, opts.out ?? `renders/render_${stamp}.mp4`);
|
|
719
|
+
mkdirSync5(dirname2(out), { recursive: true });
|
|
520
720
|
const session = await openSession(projectDir, { echoErrors: true });
|
|
521
721
|
try {
|
|
522
722
|
const { page, info } = session;
|
|
@@ -580,80 +780,13 @@ async function renderProject(opts) {
|
|
|
580
780
|
}
|
|
581
781
|
}
|
|
582
782
|
|
|
583
|
-
// src/scaffold.ts
|
|
584
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
585
|
-
import { createRequire } from "node:module";
|
|
586
|
-
import { dirname as dirname2, join as join4, resolve as resolve5 } from "node:path";
|
|
587
|
-
var TEMPLATE = `<!doctype html>
|
|
588
|
-
<html lang="en">
|
|
589
|
-
<head>
|
|
590
|
-
<meta charset="UTF-8" />
|
|
591
|
-
<style>
|
|
592
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
593
|
-
html, body { width: 1920px; height: 1080px; overflow: hidden; background: #000; }
|
|
594
|
-
body { font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
595
|
-
sf-scene { position: absolute; inset: 0; }
|
|
596
|
-
#title {
|
|
597
|
-
position: absolute; bottom: 100px; width: 100%; text-align: center;
|
|
598
|
-
font-size: 64px; font-weight: 700; color: #f4f4f5; letter-spacing: 0.04em;
|
|
599
|
-
}
|
|
600
|
-
</style>
|
|
601
|
-
</head>
|
|
602
|
-
<body>
|
|
603
|
-
<sf-scene duration="5" width="1920" height="1080" background="#101225">
|
|
604
|
-
<sf-camera fov="38" position="0 0.8 6" look-at="0 0 0"></sf-camera>
|
|
605
|
-
<sf-light preset="soft"></sf-light>
|
|
606
|
-
|
|
607
|
-
<sf-mesh id="hero" geometry="icosahedron" args="1.4 2" color="#7dd3fc"
|
|
608
|
-
metalness="0.4" roughness="0.25"></sf-mesh>
|
|
609
|
-
|
|
610
|
-
<sf-animate target="#hero" verb="bounce-in" start="0.3" duration="0.8"></sf-animate>
|
|
611
|
-
<sf-animate target="#hero" verb="turntable" rpm="10"></sf-animate>
|
|
612
|
-
<sf-animate target="#hero" verb="float" amplitude="0.12" period="3"></sf-animate>
|
|
613
|
-
<sf-animate target="#title" verb="fade-in" start="1.2" duration="0.8" rise="30"></sf-animate>
|
|
614
|
-
</sf-scene>
|
|
615
|
-
|
|
616
|
-
<div id="title" class="clip" data-start="1.2">hello stereoframe</div>
|
|
617
|
-
|
|
618
|
-
<script type="module">
|
|
619
|
-
import "./assets/stereoframe.js";
|
|
620
|
-
</script>
|
|
621
|
-
</body>
|
|
622
|
-
</html>
|
|
623
|
-
`;
|
|
624
|
-
function resolveRuntimeBundle() {
|
|
625
|
-
const require2 = createRequire(import.meta.url);
|
|
626
|
-
return require2.resolve("stereoframe-runtime");
|
|
627
|
-
}
|
|
628
|
-
function scaffoldProject(name, cwd = process.cwd()) {
|
|
629
|
-
const dir = resolve5(cwd, name);
|
|
630
|
-
if (existsSync4(join4(dir, "index.html"))) {
|
|
631
|
-
throw new Error(`refusing to overwrite existing project at ${dir}`);
|
|
632
|
-
}
|
|
633
|
-
mkdirSync4(join4(dir, "assets"), { recursive: true });
|
|
634
|
-
writeFileSync2(join4(dir, "index.html"), TEMPLATE);
|
|
635
|
-
writeFileSync2(join4(dir, ".gitignore"), `renders/
|
|
636
|
-
`);
|
|
637
|
-
copyFileSync2(resolveRuntimeBundle(), join4(dir, "assets", "stereoframe.js"));
|
|
638
|
-
return dir;
|
|
639
|
-
}
|
|
640
|
-
function updateRuntime(projectDir) {
|
|
641
|
-
if (!existsSync4(join4(resolve5(projectDir), "index.html"))) {
|
|
642
|
-
throw new Error(`no index.html in ${projectDir} — not a stereoframe project`);
|
|
643
|
-
}
|
|
644
|
-
const target = join4(resolve5(projectDir), "assets", "stereoframe.js");
|
|
645
|
-
mkdirSync4(dirname2(target), { recursive: true });
|
|
646
|
-
copyFileSync2(resolveRuntimeBundle(), target);
|
|
647
|
-
return target;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
783
|
// src/validate.ts
|
|
651
|
-
import { resolve as
|
|
784
|
+
import { resolve as resolve7 } from "node:path";
|
|
652
785
|
async function validateProject(projectDir) {
|
|
653
786
|
const findings = [];
|
|
654
787
|
let session;
|
|
655
788
|
try {
|
|
656
|
-
session = await openSession(
|
|
789
|
+
session = await openSession(resolve7(projectDir));
|
|
657
790
|
} catch (err) {
|
|
658
791
|
return [
|
|
659
792
|
{
|
|
@@ -733,8 +866,8 @@ async function validateProject(projectDir) {
|
|
|
733
866
|
findings.push({
|
|
734
867
|
rule: "non_idempotent_seek",
|
|
735
868
|
severity: "error",
|
|
736
|
-
message: `seeking t=${mid.toFixed(2)} twice produced different
|
|
737
|
-
fixHint: "Look for accumulated state, wall-clock reads, or unseeded randomness in escape-hatch code."
|
|
869
|
+
message: `seeking t=${mid.toFixed(2)} twice produced a different frame — some state is not a pure function of seek time, so capture would glitch.`,
|
|
870
|
+
fixHint: "Look for accumulated state, trails, wall-clock reads, or unseeded randomness in escape-hatch code."
|
|
738
871
|
});
|
|
739
872
|
}
|
|
740
873
|
return findings;
|
|
@@ -778,6 +911,12 @@ USAGE
|
|
|
778
911
|
--draft fast low-quality render for iteration
|
|
779
912
|
stereoframe preview [dir] serve with looping wall-clock playback
|
|
780
913
|
--port <n> fixed port (default: random)
|
|
914
|
+
stereoframe stage <model.glb> auto-direct a GLB into a cinematic motion graphic
|
|
915
|
+
--preset <name> reveal | hero-orbit | turntable (default reveal)
|
|
916
|
+
--dir <dir> output project dir (default: <model name>)
|
|
917
|
+
--duration <s> seconds (default 8)
|
|
918
|
+
--title "<text>" optional title overlay
|
|
919
|
+
--bg <color> background color (preset default otherwise)
|
|
781
920
|
stereoframe gen "<prompt>" generate a 3D model (GLB) from text via Meshy
|
|
782
921
|
--dir <dir> project dir (default .)
|
|
783
922
|
--out <path> output path (default assets/<slug>.glb)
|
|
@@ -803,11 +942,11 @@ async function main() {
|
|
|
803
942
|
return;
|
|
804
943
|
}
|
|
805
944
|
case "lint": {
|
|
806
|
-
const htmlPath =
|
|
807
|
-
if (!
|
|
808
|
-
throw new Error(`no index.html in ${
|
|
945
|
+
const htmlPath = join6(resolve8(dir), "index.html");
|
|
946
|
+
if (!existsSync6(htmlPath))
|
|
947
|
+
throw new Error(`no index.html in ${resolve8(dir)}`);
|
|
809
948
|
const findings = lintHtml(readFileSync2(htmlPath, "utf8"), {
|
|
810
|
-
fileExists: (rel) =>
|
|
949
|
+
fileExists: (rel) => existsSync6(join6(resolve8(dir), rel))
|
|
811
950
|
});
|
|
812
951
|
reportFindings("lint", findings, options.get("json") === true);
|
|
813
952
|
return;
|
|
@@ -835,6 +974,30 @@ async function main() {
|
|
|
835
974
|
console.log("press ctrl-c to stop");
|
|
836
975
|
return;
|
|
837
976
|
}
|
|
977
|
+
case "stage": {
|
|
978
|
+
const model = positional[0];
|
|
979
|
+
if (!model)
|
|
980
|
+
throw new Error("usage: stereoframe stage <model.glb> [--preset reveal|hero-orbit|turntable]");
|
|
981
|
+
const preset = typeof options.get("preset") === "string" ? options.get("preset") : "reveal";
|
|
982
|
+
if (!PRESETS.includes(preset)) {
|
|
983
|
+
throw new Error(`unknown preset: ${preset}
|
|
984
|
+
|
|
985
|
+
presets: ${PRESETS.join(", ")}`);
|
|
986
|
+
}
|
|
987
|
+
const stem = basename2(model).replace(/\.(glb|gltf)$/i, "");
|
|
988
|
+
const outDir = typeof options.get("dir") === "string" ? options.get("dir") : `${stem}-${preset}`;
|
|
989
|
+
const created = stageModel({
|
|
990
|
+
model,
|
|
991
|
+
projectDir: outDir,
|
|
992
|
+
preset,
|
|
993
|
+
duration: options.has("duration") ? Number(options.get("duration")) : undefined,
|
|
994
|
+
background: typeof options.get("bg") === "string" ? options.get("bg") : undefined,
|
|
995
|
+
title: typeof options.get("title") === "string" ? options.get("title") : undefined
|
|
996
|
+
});
|
|
997
|
+
console.log(`staged ${model} (${preset}) → ${created}`);
|
|
998
|
+
console.log(`next: cd ${outDir} && stereoframe render`);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
838
1001
|
case "gen": {
|
|
839
1002
|
const prompt = positional.join(" ").trim();
|
|
840
1003
|
if (!prompt)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stereoframe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Declarative, deterministic 3D video on three.js — scaffold, lint, validate, and render compositions built for AI agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test": "bun test"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"stereoframe-runtime": "0.2.
|
|
28
|
+
"stereoframe-runtime": "0.2.7",
|
|
29
29
|
"puppeteer": "^24.0.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|