diff --git a/docs/media/deadlift.gif b/docs/media/deadlift.gif index 849df5f..ee3f3d7 100644 Binary files a/docs/media/deadlift.gif and b/docs/media/deadlift.gif differ diff --git a/docs/media/jumping-jacks.gif b/docs/media/jumping-jacks.gif index 5932842..c71e4e6 100644 Binary files a/docs/media/jumping-jacks.gif and b/docs/media/jumping-jacks.gif differ diff --git a/docs/media/lateral-raise.gif b/docs/media/lateral-raise.gif index d33088f..e19641f 100644 Binary files a/docs/media/lateral-raise.gif and b/docs/media/lateral-raise.gif differ diff --git a/docs/media/squat.gif b/docs/media/squat.gif index 0a064cc..f9a814b 100644 Binary files a/docs/media/squat.gif and b/docs/media/squat.gif differ diff --git a/playground/src/clips.ts b/playground/src/clips.ts new file mode 100644 index 0000000..f216180 --- /dev/null +++ b/playground/src/clips.ts @@ -0,0 +1,19 @@ +/** + * Showcase mocap-clip map: clip name (as written in a doc's `clip ""` + * directive) → retargeted animation asset URL. Shared by the playground viewer + * and the landing hero so both play the same Mixamo-sourced motion for the + * marketing surfaces, while the procedural DSL stays the source of truth for + * every un-clipped movement. + * + * The FBX binaries live in `public/clips/` (gitignored, served from storage/CDN + * in production). A movement plays its clip only when it declares `clip ""` + * AND the skinned character is active; anything missing falls back to procedural. + */ +export const SHOWCASE_CLIPS: Record = { + walk: "/clips/walk.fbx", + squat: "/clips/back-squat.fbx", + "bicycle-crunch": "/clips/bicycle-crunch.fbx", + "jab-cross": "/clips/jab-cross.fbx", + "jumping-jacks": "/clips/jumping-jacks.fbx", + shuffling: "/clips/shuffling.fbx", +}; diff --git a/playground/src/landing.ts b/playground/src/landing.ts index 547b0ce..daec286 100644 --- a/playground/src/landing.ts +++ b/playground/src/landing.ts @@ -8,6 +8,7 @@ import { parse } from "posecode-parser"; import { inject } from "@vercel/analytics"; import { PRESETS } from "./presets.js"; +import { SHOWCASE_CLIPS } from "./clips.js"; import llmPrompt from "../../spec/llm-authoring.md?raw"; inject(); @@ -32,6 +33,9 @@ function initHero(): void { // Realistic skinned figure without flashing the procedural fallback. characterUrl: "/models/character.glb", showProceduralWhileLoading: false, + // Marketing surface: the hero movement declares `clip "jumping-jacks"`, so + // it plays the retargeted Mixamo mocap for maximum polish on first paint. + clips: SHOWCASE_CLIPS, }); const phaseEl = document.getElementById("hero-phase"); viewer.onPhase(({ phaseName }) => { diff --git a/playground/src/main.ts b/playground/src/main.ts index 1b221b6..d7b3731 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -18,6 +18,7 @@ import { } from "./nice-share.js"; import type { PosecodeEditor } from "./editor.js"; import { PRESETS } from "./presets.js"; +import { SHOWCASE_CLIPS } from "./clips.js"; import { renderWarnings } from "./warnings.js"; import llmPrompt from "../../spec/llm-authoring.md?raw"; @@ -548,18 +549,7 @@ void import("posecode-render").then(({ createViewer }) => { // clips.ts). Only fetched when a loaded movement names the clip, so this // never slows the default page. Disabled with the classic figure, which has // no skinned mesh to retarget onto. - ...(classicFigure - ? {} - : { - clips: { - walk: "/clips/walk.fbx", - squat: "/clips/back-squat.fbx", - "bicycle-crunch": "/clips/bicycle-crunch.fbx", - "jab-cross": "/clips/jab-cross.fbx", - "jumping-jacks": "/clips/jumping-jacks.fbx", - shuffling: "/clips/shuffling.fbx", - }, - }), + ...(classicFigure ? {} : { clips: SHOWCASE_CLIPS }), }); // Exposed for capture/e2e tooling (frame capture drives README GIFs). (window as unknown as Record).__posecodeViewer = viewer; diff --git a/scripts/capture-gifs.mjs b/scripts/capture-gifs.mjs index ac5c96d..d917fb0 100644 --- a/scripts/capture-gifs.mjs +++ b/scripts/capture-gifs.mjs @@ -53,11 +53,13 @@ if (targets.length === 0) { const server = await createServer({ configFile: resolve(repoRoot, "playground/vite.config.ts"), - server: { port: 0 }, + server: { port: 0, host: "127.0.0.1" }, logLevel: "error", }); await server.listen(); -const port = server.config.server.port ?? server.httpServer.address().port; +// Use the ACTUAL bound port: with `port: 0`, config.server.port stays 0 (and +// `0 ?? …` keeps 0), so read the listening socket instead. +const port = server.httpServer.address().port; const origin = `http://127.0.0.1:${port}`; const browser = await chromium.launch({ executablePath: chromiumPath() }); @@ -66,7 +68,9 @@ page.on("pageerror", (e) => console.error("[page]", e.message)); for (const t of targets) { const [w, h] = t.size; - await page.goto(`${origin}/play/${t.id}`, { waitUntil: "load" }); + // `/play/` is a production rewrite (vercel); in the vite dev server used + // here it doesn't resolve, so use the SPA entry + hash which loads the doc. + await page.goto(`${origin}/play.html#doc=${t.id}`, { waitUntil: "load" }); await page.reload({ waitUntil: "load" }); await page.waitForFunction(() => window.__posecodeViewer?.duration > 0, null, { timeout: 60000,