Skip to content

Commit e9376e9

Browse files
committed
release: prep for v0.1.0 pre-launch
1 parent 9242363 commit e9376e9

6 files changed

Lines changed: 135 additions & 32 deletions

File tree

.claude/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"name": "playground",
66
"runtimeExecutable": "npm",
77
"runtimeArgs": ["run", "dev", "-w", "playground"],
8-
"port": 5173
8+
"port": 5173,
9+
"autoPort": true
910
}
1011
]
1112
}

CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2026-07-08
9+
10+
### Added
11+
12+
- **Language (`.posecode` DSL)**:
13+
- Human-readable text-based kinematic motion definition syntax.
14+
- Joint and rotation specifications (e.g. `hips: flex 80`, `knees: flex 95`).
15+
- Support for `step` definitions with transition duration, easing curves (`ease-in-out`, `ease-out`, `ease-in`), and visual cues.
16+
- Starting poses (`supine`, `prone`, `seated`, `standing`).
17+
- Spatial choreography: `turn: <deg>` and `travel: <x> <z>` coordinates.
18+
- Hand / finger curl articulation (single-DOF curls + `fingers` group).
19+
- Scene props integration (`prop chair|wall|bar|box|dip-bars`).
20+
- **Parser (`posecode-parser`)**:
21+
- Full TypeScript parser converting `.posecode` source text to a validated, range-of-motion-clamped intermediate representation (IR).
22+
- Robust error recovery (never throws, instead reporting structured errors and warnings).
23+
- Clinical range-of-motion safety clamping.
24+
- **Renderer (`posecode-render`)**:
25+
- High-performance, client-side Three.js-based 3D mannequin rendering.
26+
- Forward kinematics plus ground-lock Cyclic Coordinate Descent (CCD) Inverse Kinematics (IK).
27+
- Range-of-motion constrained reach-to-target IK for limbs.
28+
- Motion aliveness layer simulating breathing and subtle aliveness.
29+
- **Playground (`playground`)**:
30+
- Live browser editor at `posecode.org/play` with split-pane view (code editor + 3D viewport).
31+
- Real-time linting, compilation warnings, and shareable link generator using URL-safe compression (`posecode-share`).
32+
- "Copy LLM prompt" tool to instantly get an interactive system prompt instructing models on how to write valid `.posecode`.
33+
- "New" button to clear the editor for pasting external model outputs.
34+
- Complete favicon, brand assets, and customized bare figure iconography.
35+
- **MCP Server (`posecode-mcp`)**:
36+
- Model Context Protocol server exposing tools to external AI agents:
37+
- `posecode_authoring_guide`: returns the grammar, joints, actions, and examples.
38+
- `validate_posecode`: parses a document and returns errors plus range-of-motion safety clamps.
39+
- `render_posecode`: compiles and returns a shareable preview link to the playground.
40+
- **Embed Component (`posecode-embed`)**:
41+
- Framework-free `<posecode-player>` Web Component for embedding movements anywhere using a single script tag.
42+
- Supports loading from a URL-safe share token, external `.posecode` file source, or inline DSL text.
43+
- Lazy loading (Three.js loads only when the player scrolls into view).
44+
- Accessibility first (supports `prefers-reduced-motion` and keyboard/mouse controls).
45+
- Isolated Shadow DOM styling.
46+
- **Eval Harness (`posecode-eval`)**:
47+
- Headless test and validation runner to measure kinematic fidelity.
48+
- Real parser → FK → ground-lock pipeline evaluation.
49+
- Geometric invariant assertions (e.g. shin angles, torso pitch, spine curl) scored programmatically to prevent regressions.

packages/posecode-mcp/server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
33
"name": "io.github.posecode-dev/posecode-mcp",
44
"description": "Author, ROM-validate, and render 3D human movement (.posecode) with a shareable playground link.",
55
"repository": {

playground/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,26 @@
3838
<meta name="twitter:image" content="https://posecode.org/og.png" />
3939
<link rel="preconnect" href="https://fonts.googleapis.com" />
4040
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
41+
<!-- Load fonts without blocking render: fetch as a low-priority stylesheet
42+
(media="print") then flip to "all" once loaded. display=swap paints
43+
text in the fallback immediately, so first paint isn't font-gated. -->
44+
<link
45+
rel="preload"
46+
as="style"
47+
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap"
48+
/>
4149
<link
4250
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap"
4351
rel="stylesheet"
52+
media="print"
53+
onload="this.media='all'"
4454
/>
55+
<noscript>
56+
<link
57+
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap"
58+
rel="stylesheet"
59+
/>
60+
</noscript>
4561
<link rel="stylesheet" href="./src/landing.css" />
4662
<script type="application/ld+json">
4763
{

playground/play.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,26 @@
2626
<meta name="twitter:card" content="summary_large_image" />
2727
<link rel="preconnect" href="https://fonts.googleapis.com" />
2828
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
29+
<!-- Load fonts without blocking render: fetch as a low-priority stylesheet
30+
(media="print") then flip to "all" once loaded. display=swap paints
31+
text in the fallback immediately, so first paint isn't font-gated. -->
32+
<link
33+
rel="preload"
34+
as="style"
35+
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap"
36+
/>
2937
<link
3038
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap"
3139
rel="stylesheet"
40+
media="print"
41+
onload="this.media='all'"
3242
/>
43+
<noscript>
44+
<link
45+
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap"
46+
rel="stylesheet"
47+
/>
48+
</noscript>
3349
<link rel="stylesheet" href="./src/style.css" />
3450
</head>
3551
<body>

playground/src/main.ts

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import { parse } from "posecode-parser";
11-
import { createViewer } from "posecode-render";
11+
import type { Viewer } from "posecode-render";
1212
import { buildNiceShareHash, resolveSharedSource } from "./nice-share.js";
1313
import { createPosecodeEditor, type PosecodeEditor } from "./editor.js";
1414
import { PRESETS } from "./presets.js";
@@ -36,9 +36,11 @@ const shareBtn = $<HTMLButtonElement>("share");
3636
const tabEditor = $<HTMLButtonElement>("tab-editor");
3737
const tabViewer = $<HTMLButtonElement>("tab-viewer");
3838

39-
const viewer = createViewer(canvas);
40-
// Exposed for capture/e2e tooling (frame capture drives README GIFs).
41-
(window as unknown as Record<string, unknown>).__posecodeViewer = viewer;
39+
// Three.js is heavy (~530 kB). Like the landing page, we load the renderer
40+
// *after* the editor shell paints (dynamic import → its own chunk) so first
41+
// paint and interactivity aren't blocked by it. Until `boot()` resolves the
42+
// import, `viewer` is null and viewer-dependent work is skipped/deferred.
43+
let viewer: Viewer | null = null;
4244
let scrubbing = false;
4345
let repeat = 1;
4446
let rep = 1;
@@ -59,32 +61,35 @@ function paintScrub(): void {
5961
scrub.style.setProperty("--pct", `${Number(scrub.value) / 10}%`);
6062
}
6163

62-
viewer.onPhase(({ phaseName, cue }) => {
63-
phaseEl.textContent = phaseName === "reset" ? "" : phaseName;
64-
cueEl.textContent = cue ?? "";
65-
highlightChip(phaseName);
66-
// Light up the step block driving this phase (cleared between loops / on reset).
67-
const range = phaseName === "reset" ? undefined : phaseRanges.get(phaseName);
68-
editorApi?.highlightPhase(range ? range.from : null, range?.to);
69-
});
70-
viewer.onTick((time, duration) => {
71-
if (!scrubbing) {
72-
scrub.value = String(Math.round((time / (duration || 1)) * 1000));
73-
paintScrub();
74-
}
75-
clock.textContent = `${time.toFixed(1)}s`;
76-
});
77-
viewer.onLoop(() => {
78-
rep = (rep % repeat) + 1;
79-
updateReps();
80-
});
64+
/** Wire the viewer's playback callbacks. Runs once, after the renderer loads. */
65+
function wireViewer(v: Viewer): void {
66+
v.onPhase(({ phaseName, cue }) => {
67+
phaseEl.textContent = phaseName === "reset" ? "" : phaseName;
68+
cueEl.textContent = cue ?? "";
69+
highlightChip(phaseName);
70+
// Light up the step block driving this phase (cleared between loops / on reset).
71+
const range = phaseName === "reset" ? undefined : phaseRanges.get(phaseName);
72+
editorApi?.highlightPhase(range ? range.from : null, range?.to);
73+
});
74+
v.onTick((time, duration) => {
75+
if (!scrubbing) {
76+
scrub.value = String(Math.round((time / (duration || 1)) * 1000));
77+
paintScrub();
78+
}
79+
clock.textContent = `${time.toFixed(1)}s`;
80+
});
81+
v.onLoop(() => {
82+
rep = (rep % repeat) + 1;
83+
updateReps();
84+
});
85+
}
8186

8287
function updateReps(): void {
8388
reps.textContent = repeat > 1 ? `rep ${rep} / ${repeat}` : "";
8489
}
8590

8691
function buildRibbonAndMarkers(): void {
87-
const tl = viewer.getTimeline();
92+
const tl = viewer?.getTimeline();
8893
ribbon.innerHTML = "";
8994
markers.innerHTML = "";
9095
if (!tl) return;
@@ -94,7 +99,7 @@ function buildRibbonAndMarkers(): void {
9499
chip.textContent = seg.name;
95100
chip.dataset.name = seg.name;
96101
chip.title = seg.cue ?? "";
97-
chip.addEventListener("click", () => viewer.seek(seg.start + 1e-3));
102+
chip.addEventListener("click", () => viewer?.seek(seg.start + 1e-3));
98103
ribbon.append(chip);
99104

100105
if (seg.start > 0) {
@@ -154,13 +159,15 @@ function recompile(): void {
154159
// error state. Show a hint instead of parse errors and stop the playback.
155160
warnings.innerHTML =
156161
'<div class="row hint">Blank editor. Paste a movement from your AI chat, or pick one from the library.</div>';
157-
viewer.pause();
162+
viewer?.pause();
158163
setPlaying(false);
159164
return;
160165
}
161166
const { ir, errors, warnings: warns } = parse(source);
162167
renderWarnings(warnings, errors, warns);
163-
if (ir) {
168+
// Before the renderer finishes loading, we still parse + surface warnings;
169+
// the viewer-dependent work re-runs once `boot()` calls recompile() again.
170+
if (ir && viewer) {
164171
viewer.load(ir);
165172
viewer.setLoop(loop.checked);
166173
viewer.play();
@@ -353,18 +360,19 @@ setMobileView("viewer");
353360

354361
// --- Transport ---
355362
playpause.addEventListener("click", () => {
356-
setPlaying(viewer.toggle());
363+
if (viewer) setPlaying(viewer.toggle());
357364
});
358365
scrub.addEventListener("input", () => {
366+
if (!viewer) return;
359367
scrubbing = true;
360368
paintScrub();
361369
viewer.seek((Number(scrub.value) / 1000) * viewer.duration);
362370
});
363371
scrub.addEventListener("change", () => {
364372
scrubbing = false;
365373
});
366-
loop.addEventListener("change", () => viewer.setLoop(loop.checked));
367-
speed.addEventListener("change", () => viewer.setSpeed(Number(speed.value)));
374+
loop.addEventListener("change", () => viewer?.setLoop(loop.checked));
375+
speed.addEventListener("change", () => viewer?.setSpeed(Number(speed.value)));
368376

369377
// --- Button label feedback ---
370378
// Swap a button's label (the inner `.lbl` span when present, else the button
@@ -476,4 +484,17 @@ editorApi = createPosecodeEditor($("editor"), {
476484
doc: initialDoc,
477485
onChange: scheduleRecompile,
478486
});
487+
// Parse + surface warnings immediately so the editor is useful at first paint,
488+
// even though the 3D figure appears a beat later once the renderer chunk loads.
479489
recompile();
490+
491+
// Boot the renderer after first paint: dynamic import keeps Three.js off the
492+
// critical path (its own chunk), mirroring the landing page. Once the viewer
493+
// exists we wire its callbacks and recompile so the current doc animates.
494+
void import("posecode-render").then(({ createViewer }) => {
495+
viewer = createViewer(canvas);
496+
// Exposed for capture/e2e tooling (frame capture drives README GIFs).
497+
(window as unknown as Record<string, unknown>).__posecodeViewer = viewer;
498+
wireViewer(viewer);
499+
recompile();
500+
});

0 commit comments

Comments
 (0)