diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44e211e..3f60b0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: - name: Typecheck run: npm run typecheck + - name: Build publishable packages + run: npm run build:packages + - name: Build playground run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index b515f74..fccf4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] - 2026-07-15 + ### Added +- **Canonical timing modes**: `flow`, `settle`, `drive`, `snap`, and `linear` + now describe continuous motion, deliberate rests, acceleration, and sharp + arrivals. The v0.1 easing spellings remain accepted as deprecated aliases. +- **Third-party validation CLI**: `npx posecode-parser@0.2.0 validate ` + recursively checks `.posecode` libraries, with `--strict` and `--json` modes + for CI. +- **Embed compatibility metadata**: the CDN bundle exports `version`, + `languageVersion`, and `validatePosecode()`. Player ready/error events now + include structured version and diagnostic details, while host elements expose + a machine-readable loading/ready/error state. +- **Release contract checks**: CI builds the publishable package chain so a + source-only language change cannot land without a valid CDN bundle. - **Realistic human figure**: the playground, landing hero, and `` embeds now render a fully rigged, textured human character (hands with articulated fingers, sneakers, face) instead of the procedural capsule mannequin. All solving (FK, ground-lock, pins, reach-IK) still runs on the driver skeleton, rebuilt to the character's exact proportions and retargeted bone-for-bone every frame; the procedural figure remains as an automatic fallback (and via `?figure=classic` / `character="off"`). - **Self-collision resolution**: a capsule-based de-penetration pass keeps forearms/hands out of the torso, head, and legs (and shins out of each other), clamped to healthy ROM, so limbs no longer pass through the body mid-movement. - **Solid props**: props now declare blocking faces (the wall's surface, the chair's backrest and seat edge, the box's near face) and a contact pass keeps the body out of them — translating the whole figure along the face normal, or bending the offending leg's hip clear (ROM-clamped). Limbs pinned/gripped/reached to a prop anchor stay exempt as declared support. A new `solid-props` eval invariant (independent geometry re-derivation) guards every prop movement against this bug class. diff --git a/README.md b/README.md index 14594d9..f0f3a59 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ A `.posecode` file describes movement as timed phases with targeted joint action | 1. Write `.posecode` | 2. Render the movement | | :--- | :--- | -| **`posecode`** `exercise "Body-weight squat"`
**`rig`** `humanoid`
**`pose`** `start = standing`

**`step`** `"Descend" 1.6s ease-in-out`:
  `hips: flex 80`
  `knees: flex 95`
  `ankles: dorsiflex 14`
  `ground-lock: feet`
  `cue "Sit the hips back"`

**`step`** `"Drive up" 1.2s ease-out`:
  `hips: flex 0`
  `knees: flex 0`
  `ankles: dorsiflex 0`
  `ground-lock: feet`

**`repeat`** `8` | Squat animation | +| **`posecode`** `exercise "Body-weight squat"`
**`rig`** `humanoid`
**`pose`** `start = standing`

**`step`** `"Descend" 1.6s settle`:
  `hips: flex 80`
  `knees: flex 95`
  `ankles: dorsiflex 14`
  `ground-lock: feet`
  `cue "Sit the hips back"`

**`step`** `"Drive up" 1.2s drive`:
  `hips: flex 0`
  `knees: flex 0`
  `ankles: dorsiflex 0`
  `ground-lock: feet`

**`repeat`** `8` | Squat animation | --- @@ -355,33 +355,31 @@ npm install posecode-render Example: ```ts -import { parsePosecode } from "posecode-parser"; -import { PosecodeRenderer } from "posecode-render"; +import { parse } from "posecode-parser"; +import { createViewer } from "posecode-render"; const source = ` -exercise "Lateral raise" -rig humanoid -pose start = standing +posecode exercise "Lateral raise" + rig humanoid + pose start = standing -step "Raise" 1.4s ease-in-out: - shoulders: abduct 90 + step "Raise" 1.4s settle: + shoulders: abduct 90 `; -const result = parsePosecode(source); +const { ir, errors, warnings } = parse(source); -if (!result.ok) { - console.error(result.errors); +if (!ir || errors.length > 0) { + console.error(errors); } else { - const renderer = new PosecodeRenderer({ - container: document.querySelector("#viewer"), - }); - - renderer.load(result.document); - renderer.play(); + console.warn(warnings); + const viewer = createViewer(document.querySelector("#viewer")); + viewer.load(ir); + viewer.play(); } ``` -> Adjust this example to match the current exported APIs before submission. +The `#viewer` element is an HTML ``. --- diff --git a/SECURITY.md b/SECURITY.md index b1c8cc2..bf3a42a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,8 @@ We actively support and fix security issues in the following versions of Posecod | Version | Supported | | ------- | --------- | -| v0.1.x | Yes | +| v0.2.x | Yes | +| v0.1.x | Security fixes only | --- diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 016f123..6436e78 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -3,7 +3,7 @@ "private": true, "displayName": "Posecode", "description": "Language support for the Posecode (.posecode) kinematic motion DSL: syntax highlighting, range-of-motion diagnostics, completion, and hover.", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "engines": { "vscode": "^1.85.0" diff --git a/package-lock.json b/package-lock.json index 2e8a11b..1adffd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "posecode-monorepo", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "posecode-monorepo", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "workspaces": [ "packages/*", @@ -26,7 +26,7 @@ }, "editors/vscode": { "name": "posecode-vscode", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "vscode-languageclient": "^9.0.1" @@ -7050,11 +7050,11 @@ "extraneous": true }, "packages/posecode-embed": { - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { - "posecode-parser": "0.1.0", - "posecode-render": "0.1.0", + "posecode-parser": "0.2.0", + "posecode-render": "0.2.0", "posecode-share": "0.1.0" }, "devDependencies": { @@ -7494,11 +7494,11 @@ } }, "packages/posecode-eval": { - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { - "posecode-parser": "0.1.0", - "posecode-render": "0.1.0", + "posecode-parser": "0.2.0", + "posecode-render": "0.2.0", "three": "^0.171.0" }, "devDependencies": { @@ -7506,17 +7506,17 @@ } }, "packages/posecode-language": { - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { - "posecode-parser": "0.1.0" + "posecode-parser": "0.2.0" } }, "packages/posecode-lsp": { - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { - "posecode-language": "0.1.0", + "posecode-language": "0.2.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, @@ -7959,11 +7959,11 @@ } }, "packages/posecode-mcp": { - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", - "posecode-parser": "0.1.0", + "posecode-parser": "0.2.0", "posecode-share": "0.1.0", "zod": "^3.24.1" }, @@ -8409,14 +8409,20 @@ } }, "packages/posecode-parser": { - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "zod": "^3.24.1" + }, + "bin": { + "posecode-parser": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.10.2" } }, "packages/posecode-render": { - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "three": "^0.171.0" @@ -8425,7 +8431,7 @@ "@types/three": "^0.171.0" }, "peerDependencies": { - "posecode-parser": "0.1.0" + "posecode-parser": "0.2.0" } }, "packages/posecode-share": { @@ -8434,7 +8440,7 @@ }, "playground": { "name": "posecode-playground", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@codemirror/autocomplete": "^6.18.4", "@codemirror/commands": "^6.7.1", @@ -8444,9 +8450,9 @@ "@codemirror/view": "^6.36.1", "@lezer/highlight": "^1.2.1", "@vercel/analytics": "^2.0.1", - "posecode-language": "0.1.0", - "posecode-parser": "0.1.0", - "posecode-render": "0.1.0", + "posecode-language": "0.2.0", + "posecode-parser": "0.2.0", + "posecode-render": "0.2.0", "posecode-share": "0.1.0", "three": "^0.171.0" }, diff --git a/package.json b/package.json index e41a33e..f395535 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posecode-monorepo", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Posecode: a kinematic motion protocol for LLMs. Describe 3D human movement as text; render it in the browser.", "license": "MIT", @@ -15,6 +15,7 @@ "coverage": "vitest run --coverage", "dev": "npm run dev -w playground", "build": "npm run build -w playground", + "build:packages": "npm run build -w posecode-parser && npm run build -w posecode-share && npm run build -w posecode-render && npm run build -w posecode-embed && npm run build -w posecode-mcp", "prepare:xbot": "node scripts/prepare-xbot.mjs", "eval": "tsx packages/posecode-eval/src/cli.ts", "video:cut2": "remotion render video/cut2/index.tsx PosecodeCut2 docs/launch-media/posecode-cut2-builder-16x9.mp4 --public-dir video/cut2/public --codec h264 --crf 16", diff --git a/packages/posecode-embed/README.md b/packages/posecode-embed/README.md index bfccf62..58d66ca 100644 --- a/packages/posecode-embed/README.md +++ b/packages/posecode-embed/README.md @@ -9,7 +9,7 @@ the movement as an animated 3D figure, right where a share link would have gone. ## Quick start (CDN, no build step) ```html - + @@ -22,10 +22,10 @@ the movement as an animated 3D figure, right where a share link would have gone. posecode exercise "Lateral raise" rig humanoid pose start = standing - step "Raise" 1.4s ease-out: + step "Raise" 1.4s settle: shoulders: abduct 90 elbows: flex 10 - step "Lower" 1.6s ease-in: + step "Lower" 1.6s drive: shoulders: abduct 0 elbows: flex 0 repeat 8 @@ -35,6 +35,10 @@ posecode exercise "Lateral raise" The script auto-registers the element and boots each player when it scrolls into view. That's it. +Pin a package version in production, as above, so a deployment always uses a +known parser/render pair. A `src` URL can be relative or absolute; cross-origin +movement files must be served with CORS permission. + ## With a bundler ```bash @@ -86,11 +90,33 @@ Boolean attributes accept `false` / `0` / `no` / `off` to turn them off, so ```js const player = document.querySelector("posecode-player"); -player.addEventListener("posecode:ready", () => player.viewer.pause()); -player.addEventListener("posecode:error", (e) => console.warn(e.detail.error)); +player.addEventListener("posecode:ready", (e) => { + console.log(e.detail.version, e.detail.languageVersion, e.detail.warnings); + player.viewer.pause(); +}); +player.addEventListener("posecode:error", (e) => { + console.warn(e.detail.code, e.detail.error, e.detail.errors); +}); player.toggle(); // play / pause player.viewer; // the underlying render Viewer (null until booted) ``` +The host element reflects `data-posecode-state="loading|ready|error"`, plus the +package and language versions, for integration tests and monitoring. Existing +listeners that only read `event.detail.error` remain compatible. + +CDN users can validate source without creating WebGL: + +```js +const result = Posecode.validatePosecode(source); +console.log(Posecode.version, Posecode.languageVersion, result.errors); +``` + +For a movement library in CI, run: + +```bash +npx posecode-parser@0.2.0 validate --strict ./movements +``` + MIT-licensed, part of [Posecode](https://github.com/posecode-dev/posecode). diff --git a/packages/posecode-embed/package.json b/packages/posecode-embed/package.json index 8327ecd..308ca0a 100644 --- a/packages/posecode-embed/package.json +++ b/packages/posecode-embed/package.json @@ -1,6 +1,6 @@ { "name": "posecode-embed", - "version": "0.1.0", + "version": "0.2.0", "description": "Embed a live 3D Posecode movement anywhere with one ...` works in a + // plain document, while doc/src attributes still boot immediately after. + this.#connectTimer = window.setTimeout(() => { + this.#connectTimer = null; + this.#startConnected(); + }, 0); + } + + #startConnected(): void { + if (!this.isConnected || this.#booted) return; + this.#source = this.textContent ?? ""; + const link = this.#root.querySelector("a.link") as HTMLAnchorElement | null; + if (link) link.href = this.#playgroundUrl(); // Boot immediately UNLESS the element is measurably off-screen, in which // case defer the heavy renderer until it scrolls into view. The bias is // toward booting: an embed must never stay blank because we couldn't @@ -75,6 +108,8 @@ export class PosecodePlayerElement extends HTMLElement { } disconnectedCallback(): void { + if (this.#connectTimer !== null) window.clearTimeout(this.#connectTimer); + this.#connectTimer = null; this.#io?.disconnect(); this.#io = null; this.#viewer?.dispose(); @@ -160,14 +195,18 @@ export class PosecodePlayerElement extends HTMLElement { src: this.getAttribute("src"), text: this.#source, }); - if (!resolved.ok) return this.#showMessage(resolved.error, true); + if (!resolved.ok) return this.#showError(resolved.error, "source"); // Keep the resolved source so the "Edit" link matches what's rendered. this.#source = resolved.source; - const { ir, errors } = parse(resolved.source); + const { ir, errors, warnings } = parse(resolved.source); if (!ir || errors.length > 0) { const detail = errors[0] ? `${errors[0].message} (line ${errors[0].line})` : ""; - return this.#showMessage(`Couldn't parse this movement. ${detail}`.trim(), true); + return this.#showError( + `Couldn't parse this movement. ${detail}`.trim(), + "parse", + errors, + ); } const opts = this.#options(); @@ -176,43 +215,55 @@ export class PosecodePlayerElement extends HTMLElement { matchMedia("(prefers-reduced-motion: reduce)").matches; // Lazy-import the heavy renderer only once we actually have work to show. - const { createViewer } = await import("posecode-render"); - const viewer = createViewer(this.#canvas, { - autoRotate: opts.autoRotate && !reduceMotion, - ...(opts.characterUrl ? { characterUrl: opts.characterUrl } : {}), - }); - this.#viewer = viewer; - viewer.onPhase(({ phaseName }) => { - this.#phaseEl.textContent = phaseName === "reset" ? "" : phaseName; - }); - viewer.load(ir); - viewer.setLoop(opts.loop); - viewer.setSpeed(opts.speed); + try { + const { createViewer } = await import("posecode-render"); + const viewer = createViewer(this.#canvas, { + autoRotate: opts.autoRotate && !reduceMotion, + ...(opts.characterUrl ? { characterUrl: opts.characterUrl } : {}), + }); + this.#viewer = viewer; + viewer.onPhase(({ phaseName }) => { + this.#phaseEl.textContent = phaseName === "reset" ? "" : phaseName; + }); + viewer.load(ir); + viewer.setLoop(opts.loop); + viewer.setSpeed(opts.speed); - const shouldPlay = opts.autoplay && !reduceMotion; - if (shouldPlay) viewer.play(); - this.#reflectPlaying(shouldPlay); + const shouldPlay = opts.autoplay && !reduceMotion; + if (shouldPlay) viewer.play(); + this.#reflectPlaying(shouldPlay); - // Update the Edit link now that we know the real source. - const link = this.#root.querySelector("a.link") as HTMLAnchorElement | null; - if (link) link.href = this.#playgroundUrl(); + // Update the Edit link now that we know the real source. + const link = this.#root.querySelector("a.link") as HTMLAnchorElement | null; + if (link) link.href = this.#playgroundUrl(); - this.dispatchEvent(new CustomEvent("posecode:ready", { bubbles: true })); + this.setAttribute("data-posecode-state", "ready"); + const detail: PosecodeReadyDetail = { version, languageVersion, warnings }; + this.dispatchEvent(new CustomEvent("posecode:ready", { bubbles: true, detail })); + } catch { + this.#viewer?.dispose(); + this.#viewer = null; + this.#showError("Couldn't start the Posecode renderer.", "render"); + } } #reflectPlaying(playing: boolean): void { this.#playBtn.textContent = playing ? PAUSE : PLAY; } - #showMessage(text: string, isError: boolean): void { + #showError(text: string, code: PosecodeErrorDetail["code"], errors?: ParseError[]): void { const msg = document.createElement("div"); - msg.className = isError ? "msg error" : "msg"; + msg.className = "msg error"; msg.textContent = text; this.#root.append(msg); - if (isError) { - this.dispatchEvent( - new CustomEvent("posecode:error", { bubbles: true, detail: { error: text } }), - ); - } + this.setAttribute("data-posecode-state", "error"); + const detail: PosecodeErrorDetail = { + error: text, + code, + version, + languageVersion, + ...(errors ? { errors } : {}), + }; + this.dispatchEvent(new CustomEvent("posecode:error", { bubbles: true, detail })); } } diff --git a/packages/posecode-embed/src/index.ts b/packages/posecode-embed/src/index.ts index cb23e75..05758b8 100644 --- a/packages/posecode-embed/src/index.ts +++ b/packages/posecode-embed/src/index.ts @@ -18,8 +18,8 @@ export { parseOptions, DEFAULT_OPTIONS } from "./options.js"; export type { PlayerOptions } from "./options.js"; export { resolveSource } from "./source.js"; export type { SourceInput, Resolved } from "./source.js"; - -export const version = "0.1.0"; +export { version, languageVersion, validatePosecode } from "./compat.js"; +export type { PosecodeReadyDetail, PosecodeErrorDetail } from "./element.js"; /** * Register the `` custom element. Safe to call more than once diff --git a/packages/posecode-embed/test/compat.test.ts b/packages/posecode-embed/test/compat.test.ts new file mode 100644 index 0000000..073c924 --- /dev/null +++ b/packages/posecode-embed/test/compat.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { languageVersion, validatePosecode, version } from "../src/compat.js"; + +const SPORTS_MOVE = `posecode exercise "Crossover" + rig humanoid + pose start = standing + step "Load" 0.4s settle: + knees: flex 68 + ground-lock: feet + step "Cross" 0.22s drive: + pelvis: rotate-out 14 + travel: -0.18 0 + ground-lock: feet + repeat 2 +`; + +describe("embed compatibility contract", () => { + it("validates current timing-mode documents without starting WebGL", () => { + const result = validatePosecode(SPORTS_MOVE); + expect(result.errors).toEqual([]); + expect(result.ir?.phases.map((phase) => phase.easing)).toEqual(["settle", "drive"]); + }); + + it("keeps exported package metadata aligned with the package manifest", () => { + const pkg = JSON.parse( + readFileSync(resolve(import.meta.dirname, "../package.json"), "utf8"), + ) as { version: string }; + expect(version).toBe(pkg.version); + expect(languageVersion).toBe("0.2"); + }); +}); diff --git a/packages/posecode-eval/package.json b/packages/posecode-eval/package.json index b6403ee..e22af74 100644 --- a/packages/posecode-eval/package.json +++ b/packages/posecode-eval/package.json @@ -1,6 +1,6 @@ { "name": "posecode-eval", - "version": "0.1.0", + "version": "0.2.0", "description": "Fidelity eval harness for Posecode: headless kinematic probing + geometric invariant scoring.", "license": "MIT", "type": "module", @@ -10,8 +10,8 @@ ".": "./src/index.ts" }, "dependencies": { - "posecode-parser": "0.1.0", - "posecode-render": "0.1.0", + "posecode-parser": "0.2.0", + "posecode-render": "0.2.0", "three": "^0.171.0" }, "devDependencies": { diff --git a/packages/posecode-language/package.json b/packages/posecode-language/package.json index a43d46e..0264b23 100644 --- a/packages/posecode-language/package.json +++ b/packages/posecode-language/package.json @@ -1,6 +1,6 @@ { "name": "posecode-language", - "version": "0.1.0", + "version": "0.2.0", "description": "Editor language service for .posecode: diagnostics, completions, and hovers built on posecode-parser. Powers both the playground editor and the LSP.", "license": "MIT", "type": "module", @@ -10,6 +10,6 @@ ".": "./src/index.ts" }, "dependencies": { - "posecode-parser": "0.1.0" + "posecode-parser": "0.2.0" } } diff --git a/packages/posecode-language/src/vocab.ts b/packages/posecode-language/src/vocab.ts index e628d2f..f0a5523 100644 --- a/packages/posecode-language/src/vocab.ts +++ b/packages/posecode-language/src/vocab.ts @@ -50,7 +50,7 @@ export const KEYWORD_DOCS: Record = { snap: "Timing mode: fast, near-immediate arrival — an accent.", linear: "Timing mode: constant velocity — intentionally mechanical.", repeat: "How many times the movement loops.", - "ground-lock": "Pins grouped or per-side effectors to the floor for this phase: `ground-lock: feet`, `ground-lock: foot_right`. Planted feet auto-level flat unless the ankle is plantarflexed (tiptoe).", + "ground-lock": "Pins grouped or per-side effectors to the floor: `feet`, `foot_left` (also written `left foot`), `hands`, or `forearms`. Planted feet auto-level unless the ankle is intentionally plantarflexed.", reach: "Drives an effector to a target via ROM-constrained IK: `reach: hand_left ankle_left`, `reach: hands floor`.", pin: "Moves the body so an effector sits on an anchor: `pin: hands bar` (hang, pull up, step up, dip).", diff --git a/packages/posecode-language/test/language.test.ts b/packages/posecode-language/test/language.test.ts index 352f0ea..be727e3 100644 --- a/packages/posecode-language/test/language.test.ts +++ b/packages/posecode-language/test/language.test.ts @@ -75,7 +75,7 @@ describe("getCompletions", () => { it("suggests effectors after `ground-lock: `", () => { expect(onLine(" ground-lock: ", 17)).toEqual( - expect.arrayContaining(["hands", "feet", "hand_left", "foot_right"]), + expect.arrayContaining(["hands", "feet", "hand_left", "hand_right", "foot_left", "foot_right"]), ); }); diff --git a/packages/posecode-lsp/package.json b/packages/posecode-lsp/package.json index 259fdbd..64f19ba 100644 --- a/packages/posecode-lsp/package.json +++ b/packages/posecode-lsp/package.json @@ -1,6 +1,6 @@ { "name": "posecode-lsp", - "version": "0.1.0", + "version": "0.2.0", "description": "Language Server Protocol implementation for the Posecode (.posecode) DSL: diagnostics, completion, and hover, powered by posecode-language.", "license": "MIT", "type": "module", @@ -15,7 +15,7 @@ "start": "tsx src/server.ts" }, "dependencies": { - "posecode-language": "0.1.0", + "posecode-language": "0.2.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12" }, diff --git a/packages/posecode-mcp/package.json b/packages/posecode-mcp/package.json index 926595b..2b67f16 100644 --- a/packages/posecode-mcp/package.json +++ b/packages/posecode-mcp/package.json @@ -1,6 +1,6 @@ { "name": "posecode-mcp", - "version": "0.1.1", + "version": "0.2.0", "description": "Model Context Protocol server for Posecode: let any LLM agent validate, ROM-check, and get a render link for a .posecode movement, natively.", "mcpName": "io.github.posecode-dev/posecode-mcp", "license": "MIT", @@ -40,7 +40,7 @@ ], "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", - "posecode-parser": "0.1.0", + "posecode-parser": "0.2.0", "posecode-share": "0.1.0", "zod": "^3.24.1" }, diff --git a/packages/posecode-mcp/server.json b/packages/posecode-mcp/server.json index 770664e..f788d26 100644 --- a/packages/posecode-mcp/server.json +++ b/packages/posecode-mcp/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/posecode-dev/posecode", "source": "github" }, - "version": "0.1.1", + "version": "0.2.0", "packages": [ { "registryType": "npm", "identifier": "posecode-mcp", - "version": "0.1.1", + "version": "0.2.0", "transport": { "type": "stdio" }, diff --git a/packages/posecode-mcp/src/guide.ts b/packages/posecode-mcp/src/guide.ts index 4f1dfab..3f1b74e 100644 --- a/packages/posecode-mcp/src/guide.ts +++ b/packages/posecode-mcp/src/guide.ts @@ -35,9 +35,9 @@ Output ONLY a \`.posecode\` document in a code block, no prose. posecode "" # kind = exercise | stretch | posture rig humanoid pose start = # neutral | standing | plank - step "" : # easing = linear | ease-in | ease-out | ease-in-out + step "" : # mode = flow | settle | drive | snap | linear : - ground-lock: # hands and/or feet pinned to the floor + ground-lock: # feet/hands/forearms, or one side: foot_left / left foot cue "" repeat \`\`\` diff --git a/packages/posecode-mcp/src/server.ts b/packages/posecode-mcp/src/server.ts index fe2df47..14d6805 100644 --- a/packages/posecode-mcp/src/server.ts +++ b/packages/posecode-mcp/src/server.ts @@ -22,7 +22,7 @@ export interface PosecodeServerOptions { export function createPosecodeServer(opts: PosecodeServerOptions = {}): McpServer { const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL; - const server = new McpServer({ name: "posecode", version: "0.1.0" }); + const server = new McpServer({ name: "posecode", version: "0.2.0" }); const sourceSchema = { source: z.string().describe("The full .posecode document text"), diff --git a/packages/posecode-parser/README.md b/packages/posecode-parser/README.md index 02f9a52..bf4cda6 100644 --- a/packages/posecode-parser/README.md +++ b/packages/posecode-parser/README.md @@ -23,7 +23,7 @@ const { ir, warnings, errors } = parse(` rig humanoid pose start = standing - step "Descend" 1.6s ease-in-out: + step "Descend" 1.6s settle: hips: flex 80 knees: flex 95 ground-lock: feet @@ -36,6 +36,25 @@ if (errors.length === 0 && ir) { } ``` +## Validate a movement library + +Version 0.2 includes a zero-config validator for local files and directories: + +```bash +npx posecode-parser@0.2.0 validate ./movements +npx posecode-parser@0.2.0 validate --strict ./movements +``` + +The command recurses through directories, prints file-and-line diagnostics, and +exits non-zero for parse errors. `--strict` also fails on ROM-clamp warnings; +`--json` emits machine-readable results for CI. + +## Timing modes + +Canonical modes are `flow`, `settle`, `drive`, `snap`, and `linear`. The v0.1 +aliases `ease-in`, `ease-out`, and `ease-in-out` remain accepted so existing +movement files keep working. + `parse()` never throws: malformed or out-of-range documents come back as structured `errors`/`warnings` instead. Every joint angle in `ir` is hard-clamped to a healthy range of motion, so a hallucinated `knee: flex 200` renders at its diff --git a/packages/posecode-parser/package.json b/packages/posecode-parser/package.json index 264e885..a47c8bd 100644 --- a/packages/posecode-parser/package.json +++ b/packages/posecode-parser/package.json @@ -1,6 +1,6 @@ { "name": "posecode-parser", - "version": "0.1.0", + "version": "0.2.0", "description": "Parse the .posecode kinematic motion DSL into a validated, ROM-clamped intermediate representation.", "license": "MIT", "type": "module", @@ -9,9 +9,15 @@ "exports": { ".": "./src/index.ts" }, + "bin": { + "posecode-parser": "dist/cli.js" + }, "dependencies": { "zod": "^3.24.1" }, + "devDependencies": { + "@types/node": "^22.10.2" + }, "repository": { "type": "git", "url": "git+https://github.com/posecode-dev/posecode.git", diff --git a/packages/posecode-parser/src/cli.ts b/packages/posecode-parser/src/cli.ts new file mode 100644 index 0000000..c105e2a --- /dev/null +++ b/packages/posecode-parser/src/cli.ts @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * Small, dependency-free validator for third-party movement libraries. + * + * Usage: + * posecode-parser validate movement.posecode ./movement-directory + * posecode-parser validate --strict --json ./movement-directory + */ + +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { extname, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { parse } from "./index.js"; +import type { ParseError, Warning } from "./types.js"; + +export interface FileValidation { + file: string; + errors: ParseError[]; + warnings: Warning[]; +} + +export interface ValidationSummary { + files: FileValidation[]; + fileCount: number; + errorCount: number; + warningCount: number; +} + +export function validatePaths(inputs: string[]): ValidationSummary { + const paths = [...new Set(inputs.flatMap(collectPosecodeFiles))].sort(); + const files = paths.map((file): FileValidation => { + const source = readFileSync(file, "utf8"); + const { errors, warnings } = parse(source); + return { file, errors, warnings }; + }); + + return { + files, + fileCount: files.length, + errorCount: files.reduce((total, file) => total + file.errors.length, 0), + warningCount: files.reduce((total, file) => total + file.warnings.length, 0), + }; +} + +function collectPosecodeFiles(input: string): string[] { + const path = resolve(input); + const stat = statSync(path); + if (stat.isFile()) return extname(path) === ".posecode" ? [path] : []; + if (!stat.isDirectory()) return []; + + return readdirSync(path, { withFileTypes: true }).flatMap((entry) => { + const child = resolve(path, entry.name); + if (entry.isDirectory()) return collectPosecodeFiles(child); + return entry.isFile() && extname(entry.name) === ".posecode" ? [child] : []; + }); +} + +export function renderValidation(summary: ValidationSummary): string { + const lines: string[] = []; + for (const file of summary.files) { + for (const error of file.errors) { + lines.push(`${file.file}:${error.line}: error: ${error.message}`); + } + for (const warning of file.warnings) { + lines.push( + `${file.file}:${warning.line}: warning: ${warning.joint} ${warning.action} ` + + `${warning.requested}° was clamped to ${warning.clamped}°`, + ); + } + } + lines.push( + `Validated ${summary.fileCount} .posecode file(s): ` + + `${summary.errorCount} error(s), ${summary.warningCount} warning(s).`, + ); + return lines.join("\n"); +} + +export function main(argv: string[]): number { + const args = argv[0] === "validate" ? argv.slice(1) : argv; + const json = args.includes("--json"); + const strict = args.includes("--strict"); + const inputs = args.filter((arg) => !arg.startsWith("--")); + + if (inputs.length === 0) { + console.error("Usage: posecode-parser validate [--strict] [--json] [...]"); + return 2; + } + + try { + const summary = validatePaths(inputs); + if (summary.fileCount === 0) { + console.error("No .posecode files found."); + return 2; + } + console.log(json ? JSON.stringify(summary, null, 2) : renderValidation(summary)); + return summary.errorCount > 0 || (strict && summary.warningCount > 0) ? 1 : 0; + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + return 2; + } +} + +const invokedPath = process.argv[1]; +if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/packages/posecode-parser/src/joints.ts b/packages/posecode-parser/src/joints.ts index 747547d..72aa4b5 100644 --- a/packages/posecode-parser/src/joints.ts +++ b/packages/posecode-parser/src/joints.ts @@ -143,7 +143,6 @@ const GROUND_LOCK_EFFECTOR_SET = new Set(GROUND_LOCK_EFFECTOR_NAMES); export function isGroundLockEffector(name: string): boolean { return GROUND_LOCK_EFFECTOR_SET.has(name); } - const EFFECTOR_SIDE_SET = new Set(EFFECTOR_SIDES); /** diff --git a/packages/posecode-parser/src/parser.ts b/packages/posecode-parser/src/parser.ts index 1bd5618..c4a0bdf 100644 --- a/packages/posecode-parser/src/parser.ts +++ b/packages/posecode-parser/src/parser.ts @@ -9,6 +9,9 @@ import { tokenize, TokenizeError, type Line, type Token } from "./tokenizer.js"; import type { ParseError } from "./types.js"; import { normalizeMode, MODES } from "./schema.js"; +import { GROUND_LOCK_EFFECTOR_NAMES } from "./joints.js"; + +const GROUND_LOCK_EFFECTORS = new Set(GROUND_LOCK_EFFECTOR_NAMES); export interface AstJointTarget { joint: string; @@ -110,9 +113,17 @@ export function parseToAst(source: string): ParseAstResult { }; let current: AstStep | null = null; + let invalidStepIndent: number | null = null; for (let i = 1; i < lines.length; i++) { const ln = lines[i]!; + // A malformed step header already explains why the phase cannot be parsed. + // Ignore its indented children so authors get one actionable diagnostic + // instead of a cascade of misleading "outside of a step" errors. + if (invalidStepIndent !== null) { + if (ln.indent > invalidStepIndent) continue; + invalidStepIndent = null; + } const head = word(ln.tokens[0]); const t = ln.tokens; @@ -175,6 +186,7 @@ export function parseToAst(source: string): ParseAstResult { : 'expected `step "" :`', }); current = null; + invalidStepIndent = ln.indent; break; } current = { @@ -219,10 +231,32 @@ function parseStepChild(ln: Line, current: AstStep | null): ParseError | null { if (head === "ground-lock") { if (!current) return { line: ln.line, message: "`ground-lock` outside of a step" }; - const effectors = t - .slice(1) - .filter((tok) => tok.type === "word") - .map((tok) => tok.value); + if (t[1]?.type !== "colon") { + return { line: ln.line, message: "expected `ground-lock: `" }; + } + const words = t.slice(2).filter((tok) => tok.type === "word").map((tok) => tok.value); + const effectors: string[] = []; + for (let i = 0; i < words.length; i++) { + const value = words[i]!; + if (value === "and") continue; + const next = words[i + 1]; + if ((value === "left" || value === "right") && next) { + const base = next === "hand" ? "hand" : next === "foot" ? "foot" : + next === "forearm" || next === "elbow" ? "elbow" : null; + if (base) { + effectors.push(`${base}_${value}`); + i++; + continue; + } + } + if (!GROUND_LOCK_EFFECTORS.has(value)) { + return { line: ln.line, message: `unknown ground-lock effector: "${value}"` }; + } + effectors.push(value); + } + if (effectors.length === 0) { + return { line: ln.line, message: "`ground-lock` requires at least one effector" }; + } current.groundLock = effectors; current.groundLockLine = ln.line; return null; diff --git a/packages/posecode-parser/src/types.ts b/packages/posecode-parser/src/types.ts index ca95148..242714e 100644 --- a/packages/posecode-parser/src/types.ts +++ b/packages/posecode-parser/src/types.ts @@ -7,7 +7,8 @@ * convention documented in `joints.ts` and `spec/SPEC.md`. */ -export const POSECODE_VERSION = "0.1"; +/** Version of the parsed Posecode language/IR contract. */ +export const POSECODE_VERSION = "0.2"; export type Axis = "x" | "y" | "z"; diff --git a/packages/posecode-parser/test/cli.test.ts b/packages/posecode-parser/test/cli.test.ts new file mode 100644 index 0000000..0efb424 --- /dev/null +++ b/packages/posecode-parser/test/cli.test.ts @@ -0,0 +1,48 @@ +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { renderValidation, validatePaths } from "../src/cli.js"; + +const SPORTS_MOVE = `posecode exercise "Jump shot" + rig humanoid + pose start = standing + step "Gather" 0.4s settle: + knees: flex 70 + ground-lock: feet + step "Explode" 0.3s drive: + knees: flex 10 + ankles: plantarflex 26 + travel: 0 0.12 + ground-lock: feet + step "Release" 0.2s snap: + shoulders: flex 160 + wrists: flex 34 + repeat 1 +`; + +describe("posecode validator", () => { + it("recursively validates third-party movement libraries using timing modes", () => { + const root = mkdtempSync(join(tmpdir(), "posecode-validator-")); + const nested = join(root, "basketball"); + mkdirSync(nested); + writeFileSync(join(nested, "jump-shot.posecode"), SPORTS_MOVE); + + const summary = validatePaths([root]); + + expect(summary).toMatchObject({ fileCount: 1, errorCount: 0, warningCount: 0 }); + }); + + it("prints file and line diagnostics for invalid timing modes", () => { + const root = mkdtempSync(join(tmpdir(), "posecode-validator-")); + const file = join(root, "broken.posecode"); + writeFileSync(file, SPORTS_MOVE.replace("0.3s drive", "0.3s turbo")); + + const summary = validatePaths([file]); + const output = renderValidation(summary); + + expect(summary.errorCount).toBe(1); + expect(output).toContain(`${file}:7: error:`); + expect(output).toContain('unknown timing mode "turbo"'); + }); +}); diff --git a/packages/posecode-render/package.json b/packages/posecode-render/package.json index 21efd27..50e8de4 100644 --- a/packages/posecode-render/package.json +++ b/packages/posecode-render/package.json @@ -1,6 +1,6 @@ { "name": "posecode-render", - "version": "0.1.0", + "version": "0.2.0", "description": "Render a Posecode IR as an animated 3D human figure (skinned character or procedural mannequin) with Three.js.", "license": "MIT", "type": "module", @@ -13,7 +13,7 @@ "three": "^0.171.0" }, "peerDependencies": { - "posecode-parser": "0.1.0" + "posecode-parser": "0.2.0" }, "devDependencies": { "@types/three": "^0.171.0" diff --git a/packages/posecode-render/src/index.ts b/packages/posecode-render/src/index.ts index cf3b280..3e09f12 100644 --- a/packages/posecode-render/src/index.ts +++ b/packages/posecode-render/src/index.ts @@ -1105,8 +1105,8 @@ function floorHandSidesOf( ): Set<"left" | "right"> { const sides = new Set<"left" | "right">(); const add = (effector: string): void => { - if (effector.endsWith("_left") || effector === "hands") sides.add("left"); - if (effector.endsWith("_right") || effector === "hands") sides.add("right"); + if (effector === "hands" || effector === "hand_left") sides.add("left"); + if (effector === "hands" || effector === "hand_right") sides.add("right"); }; for (const r of reaches) if (r.target === "floor") add(r.effector); for (const p of pins) if (p.anchor === "floor") add(p.effector); diff --git a/packages/posecode-render/test/render.test.ts b/packages/posecode-render/test/render.test.ts index 0bc3c10..ef5d544 100644 --- a/packages/posecode-render/test/render.test.ts +++ b/packages/posecode-render/test/render.test.ts @@ -446,6 +446,26 @@ describe("ground-lock (shared solver)", () => { expect(leftSole).toBeGreaterThan(0.1); }); + it("normalizes and plants a human-readable single-foot lock", () => { + const source = [ + 'posecode exercise "Layup"', + " rig humanoid", + " pose start = standing", + ' step "Plant" 1s settle:', + " knee_left: flex 86", + " hip_left: flex 62", + " ground-lock: left foot", + ].join("\n"); + const { ir, errors } = parse(source); + expect(errors).toEqual([]); + expect(ir?.phases[0]?.groundLock).toEqual(["foot_left"]); + + const m = posedRaw(source); + applyGroundLock(m, ir!.phases[0]!.groundLock); + const soleY = new THREE.Box3().setFromObject(m.bones.get("ankle_left")!).min.y; + expect(Math.abs(soleY)).toBeLessThan(0.01); + }); + it("is a no-op when no effectors are ground-locked", () => { const m = posedRaw( [ diff --git a/playground/embed-demo.html b/playground/embed-demo.html index 7ec4ccc..57f7860 100644 --- a/playground/embed-demo.html +++ b/playground/embed-demo.html @@ -35,11 +35,11 @@

3. From inline text: reads like the language itself

posecode exercise "Lateral raise" rig humanoid pose start = standing - step "Raise" 1.4s ease-out: + step "Raise" 1.4s settle: shoulders: abduct 90 elbows: flex 10 cue "Lift out to shoulder height, soft elbows" - step "Lower" 1.6s ease-in: + step "Lower" 1.6s drive: shoulders: abduct 0 elbows: flex 0 repeat 8 @@ -55,13 +55,13 @@

4. Graceful error (an invalid token)

const deadlift = `posecode exercise "Deadlift" rig humanoid pose start = standing - step "Lower" 1.8s ease-in-out: + step "Lower" 1.8s settle: pelvis: hinge 75 knees: flex 25 shoulders: extend 60 ground-lock: feet cue "Push the hips back and hinge with a flat back" - step "Lift" 1.4s ease-out: + step "Lift" 1.4s drive: pelvis: hinge 0 knees: flex 0 shoulders: extend 0 diff --git a/playground/index.html b/playground/index.html index fc9a183..df4a700 100644 --- a/playground/index.html +++ b/playground/index.html @@ -183,7 +183,7 @@

@@ -339,7 +339,7 @@

Common questions

Consult a qualified professional for physiotherapy or exercise prescription.

Grammar

rig humanoid prop <type> # optional: chair | wall | bar | box | dip-bars (repeatable) pose start = <pose> # neutral | standing | plank | supine | prone | seated - step "<Phase name>" <Ns> <easing>: # easing = linear | ease-in | ease-out | ease-in-out + step "<Phase name>" <Ns> <mode>: # mode = flow | settle | drive | snap | linear <joint>: <action> <degrees> reach: <effector> <target> # optional: drive a hand/foot to a target via IK - ground-lock: <effectors> # groups (hands/forearms/feet) or per-side aliases such as foot_right + ground-lock: <effectors> # groups, foot_left/foot_right, or natural left foot/right foot turn: <degrees> # optional: face this yaw by phase end (standing only) travel: <x> <z> # optional: move to this x z (metres) by phase end cue "<short coaching cue>" @@ -152,19 +152,26 @@

Rules

  1. Add a one-line cue per phase. Use ground-lock for whatever touches the

floor (feet when standing; hands and feet in a plank).

  1. repeat the rep count.
+

Timing modes

+
  • flow: carry momentum through an interior pose; use for continuous dance,
+

locomotion, and multi-part sports motion.

+
  • settle: decelerate into a real rest; use at a squat bottom, landing, hold,
+

or final pose.

+
  • drive: accelerate from rest; use for a jump, push, lift, or recoil.
  • snap: arrive quickly and stop sharply; use for a strike or release.
  • linear: constant timing; best for deliberate holds or mechanical motion.
+

The older names ease-in, ease-out, and ease-in-out still parse for compatibility, but do not author new documents with them.

Example

posecode exercise "Body-weight squat"
   rig humanoid
   pose start = standing
 
-  step "Descend" 1.6s ease-in-out:
+  step "Descend" 1.6s settle:
     hips: flex 80
     knees: flex 95
     ankles: dorsiflex 14
     ground-lock: feet
     cue "Sit the hips back, chest proud, knees track over the toes"
 
-  step "Drive up" 1.2s ease-out:
+  step "Drive up" 1.2s drive:
     hips: flex 0
     knees: flex 0
     ankles: dorsiflex 0
@@ -178,14 +185,14 @@ 

Hip-hinge example

rig humanoid pose start = standing - step "Lower" 1.8s ease-in-out: + step "Lower" 1.8s settle: pelvis: hinge 95 knees: flex 25 shoulders: flex 90 ground-lock: feet cue "Hips back, flat back: let the arms hang to the bar" - step "Lift" 1.4s ease-out: + step "Lift" 1.4s drive: pelvis: hinge 0 knees: flex 0 shoulders: flex 0 @@ -196,7 +203,7 @@

Hip-hinge example

Reaching, props, lying poses & hands

  • Reach a target: reach: <effector> <target> drives a hand or foot to a

world point via IK. Effectors: hand_left hand_right foot_left foot_right, or hands / feet for both sides at once. Targets: a body landmark bone (ankle_left, knee_right…), floor, or a prop anchor (bar, seat, wall). The solve is ROM-constrained: the arm/leg can never exceed the safe joint limits chasing a target, so an out-of-reach target just yields the closest healthy pose. Author the gross pose (e.g. a pelvis: hinge), then let reach finish the hand placement. Example, touch your toes:

-

``posecode step "Fold" 2.5s ease-in-out: pelvis: hinge 95 knees: flex 12 reach: hand_left ankle_left reach: hand_right ankle_right ground-lock: feet cue "Hinge and reach toward the ankles" ``

+

``posecode step "Fold" 2.5s settle: pelvis: hinge 95 knees: flex 12 reach: hand_left ankle_left reach: hand_right ankle_right ground-lock: feet cue "Hinge and reach toward the ankles" ``

  • Props: prop chair | wall | bar | box | dip-bars (top level). The chair

sits behind the figure (sit-to-stand, box squat), the wall behind that (wall sit), the bar overhead, the box in front (step-ups), and the dip bars either side at hip-press height (pin: hands bars + elbow flex = triceps dips).

  • Pins: pin: <effector> <anchor> moves the whole BODY so the effector sits
@@ -213,8 +220,8 @@

Authoring by domain

singular joint (knee_right: flex 95) and skip ground-lock so the standing leg stays planted.

  • Desk / posture: slow stretch documents with ground-lock: feet;

contrast a "collapsed" phase with a "tall" reset.

-
  • Sports / martial arts: short, snappy phases (0.3–0.6s) with ease-out
-

on the strike; chamber → extend → re-chamber → return.

+
  • Sports / martial arts: short phases (0.2–0.6s); use flow through a
+

gather/chamber, drive for takeoff, and snap on the strike or release.

Dance / choreography

Posecode shines for showing the movement in your head. Build a phrase as a sequence of phases, one per count or musical beat, and let later phases inherit unset joints:

  • Turnout: hips: rotate-out 25–30.
  • Port de bras (arm positions): first shoulders: flex 30, elbows: flex 35;
diff --git a/playground/public/moves/box-step.html b/playground/public/moves/box-step.html index 023fa5b..8f9c142 100644 --- a/playground/public/moves/box-step.html +++ b/playground/public/moves/box-step.html @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -135,20 +135,20 @@

Box step

How to do it

  1. - Step forward-right0.9s · flow - Step the right foot forward and out to the corner + Right foot forward0.9s · flow + Step forward onto the right foot with an easy contralateral arm swing
  2. - Close the feet0.7s · flow - Bring the left foot to meet it, standing tall on the corner + Left foot side0.85s · flow + Transfer left to the side, keeping the torso lifted
  3. - Step back-left0.9s · flow - Step the left foot back to the start on the diagonal + Right foot back0.9s · flow + Step back onto the right foot to trace the third side of the box
  4. - Close home0.7s · settle - Close the feet together, back home and ready to repeat + Left foot closes home0.85s · settle + Close the left foot and settle over both feet, completing the square
@@ -159,43 +159,50 @@

The .posecode source

rig humanoid pose start = standing - step "Step forward-right" 0.9s flow: - hip_right: flex 35 - knee_right: flex 40 - ankle_right: plantarflex 40 - shoulder_left: flex 25 - shoulder_right: extend 15 - travel: -0.35 0.35 + step "Right foot forward" 0.9s flow: + hip_right: flex 30 + knee_right: flex 32 + ankle_right: plantarflex 24 + shoulder_left: flex 22 + shoulder_right: extend 12 + travel: 0 0.32 pin: foot_left floor - cue "Step the right foot forward and out to the corner" + cue "Step forward onto the right foot with an easy contralateral arm swing" - step "Close the feet" 0.7s flow: + step "Left foot side" 0.85s flow: hip_right: flex 0 knee_right: flex 0 ankle_right: plantarflex 0 - shoulders: flex 0 - travel: -0.35 0.35 - ground-lock: feet - cue "Bring the left foot to meet it, standing tall on the corner" - - step "Step back-left" 0.9s flow: - hip_left: flex 35 - knee_left: flex 40 - ankle_left: plantarflex 40 - shoulder_right: flex 25 - shoulder_left: extend 15 - travel: 0 0 + hip_left: abduct 24 + knee_left: flex 22 + ankle_left: plantarflex 20 + shoulder_left: flex 0 + shoulder_right: flex 20 + travel: -0.32 0.32 pin: foot_right floor - cue "Step the left foot back to the start on the diagonal" + cue "Transfer left to the side, keeping the torso lifted" - step "Close home" 0.7s settle: - hip_left: flex 0 + step "Right foot back" 0.9s flow: + hip_left: abduct 0 knee_left: flex 0 ankle_left: plantarflex 0 + hip_right: extend 18 + knee_right: flex 30 + ankle_right: plantarflex 24 + shoulder_right: flex 0 + shoulder_left: flex 22 + travel: -0.32 0 + pin: foot_left floor + cue "Step back onto the right foot to trace the third side of the box" + + step "Left foot closes home" 0.85s settle: + hip_right: extend 0 + knee_right: flex 0 + ankle_right: plantarflex 0 shoulders: flex 0 travel: 0 0 ground-lock: feet - cue "Close the feet together, back home and ready to repeat" + cue "Close the left foot and settle over both feet, completing the square" repeat 4
diff --git a/playground/public/moves/chasse.html b/playground/public/moves/chasse.html index b477e73..3d90205 100644 --- a/playground/public/moves/chasse.html +++ b/playground/public/moves/chasse.html @@ -8,11 +8,11 @@ - + - + @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -135,20 +135,28 @@

Chassé

How to do it

  1. - Reach & push0.8s · settle - Reach the lead foot out and push off to travel sideways + Right foot reaches0.65s · flow + Reach the right foot sideways and push away from the left leg
  2. - Gallop close0.6s · drive - Chase the trailing foot to the lead: feet meet in the air + Left foot chases0.5s · flow + Let the left foot chase under the body without breaking the sideways flow
  3. - Reach & push back0.8s · settle - Change direction and travel back the other way + Right foot reaches again0.65s · flow + Reach right once more and finish the outward chassé
  4. - Gallop home0.6s · drive - Close home, ready to chassé again + Left foot reaches back0.65s · flow + Reverse cleanly and reach the left foot back across the floor +
  5. +
  6. + Right foot chases0.5s · flow + Let the right foot chase under the body on the return +
  7. +
  8. + Left foot closes home0.7s · settle + Close home over both feet and let the momentum resolve
@@ -159,47 +167,73 @@

The .posecode source

rig humanoid pose start = standing - step "Reach & push" 0.8s settle: - hip_right: flex 30 - knee_right: flex 25 - ankle_right: plantarflex 25 + step "Right foot reaches" 0.65s flow: + hip_right: abduct 26 + knee_right: flex 20 + ankle_right: plantarflex 24 ankle_left: plantarflex 20 shoulders: abduct 60 elbows: flex 18 - travel: 0.5 0 + travel: 0.34 0 pin: foot_left floor reach: foot_right floor - cue "Reach the lead foot out and push off to travel sideways" + cue "Reach the right foot sideways and push away from the left leg" - step "Gallop close" 0.6s drive: - hip_right: flex 0 + step "Left foot chases" 0.5s flow: + hip_right: abduct 0 knee_right: flex 0 ankle_right: plantarflex 0 - ankle_left: plantarflex 0 - travel: 0.9 0 - ground-lock: feet - cue "Chase the trailing foot to the lead: feet meet in the air" + hip_left: abduct 18 + knee_left: flex 22 + ankle_left: plantarflex 24 + travel: 0.66 0 + pin: foot_right floor + cue "Let the left foot chase under the body without breaking the sideways flow" + + step "Right foot reaches again" 0.65s flow: + hip_left: abduct 0 + knee_left: flex 0 + ankle_left: plantarflex 18 + hip_right: abduct 26 + knee_right: flex 20 + ankle_right: plantarflex 24 + travel: 1 0 + pin: foot_left floor + reach: foot_right floor + cue "Reach right once more and finish the outward chassé" - step "Reach & push back" 0.8s settle: - hip_left: flex 30 - knee_left: flex 25 - ankle_left: plantarflex 25 + step "Left foot reaches back" 0.65s flow: + hip_right: abduct 0 + knee_right: flex 0 ankle_right: plantarflex 20 - travel: 0.4 0 + hip_left: abduct 26 + knee_left: flex 20 + ankle_left: plantarflex 24 + travel: 0.66 0 pin: foot_right floor reach: foot_left floor - cue "Change direction and travel back the other way" + cue "Reverse cleanly and reach the left foot back across the floor" - step "Gallop home" 0.6s drive: - hip_left: flex 0 + step "Right foot chases" 0.5s flow: + hip_left: abduct 0 knee_left: flex 0 ankle_left: plantarflex 0 - ankle_right: plantarflex 0 + hip_right: abduct 18 + knee_right: flex 22 + ankle_right: plantarflex 24 + travel: 0.34 0 + pin: foot_left floor + cue "Let the right foot chase under the body on the return" + + step "Left foot closes home" 0.7s settle: + hip_right: abduct 0 + knee_right: flex 0 + ankles: plantarflex 0 shoulders: abduct 0 elbows: flex 0 travel: 0 0 ground-lock: feet - cue "Close home, ready to chassé again" + cue "Close home over both feet and let the momentum resolve" repeat 4 diff --git a/playground/public/moves/dance-phrase.html b/playground/public/moves/dance-phrase.html index b7b4354..421fdd4 100644 --- a/playground/public/moves/dance-phrase.html +++ b/playground/public/moves/dance-phrase.html @@ -147,7 +147,7 @@

How to do it

Press up to relevé and lift the arms into a frame overhead
  • - 7-8 - close2s · settle + 7-8 - close2.2s · settle Lower the heels and resolve, arms floating back to the sides
  • @@ -170,7 +170,7 @@

    The .posecode source

    step "3-4 - demi-plié" 2s flow: hips: flex 18 knees: flex 50 - ankles: plantarflex 50 + ankles: dorsiflex 12 shoulders: flex 28 elbows: flex 30 ground-lock: feet @@ -186,7 +186,7 @@

    The .posecode source

    ground-lock: feet cue "Press up to relevé and lift the arms into a frame overhead" - step "7-8 - close" 2s settle: + step "7-8 - close" 2.2s settle: ankles: plantarflex 0 hips: rotate-out 0 shoulders: flex 0 diff --git a/playground/public/moves/demi-plie.html b/playground/public/moves/demi-plie.html index 83f9c8c..7df79f3 100644 --- a/playground/public/moves/demi-plie.html +++ b/playground/public/moves/demi-plie.html @@ -135,11 +135,11 @@

    Demi-plié

    How to do it

    1. - Plié2s · flow + Plié2.2s · settle Turn out from the hips and bend the knees over the toes: heels down
    2. - Straighten2s · settle + Straighten2.2s · settle Press the floor away and rise to a tall first position
    @@ -151,7 +151,7 @@

    The .posecode source

    rig humanoid pose start = standing - step "Plié" 2s flow: + step "Plié" 2.2s settle: hips: flex 20 hips: rotate-out 30 knees: flex 55 @@ -162,7 +162,7 @@

    The .posecode source

    ground-lock: feet cue "Turn out from the hips and bend the knees over the toes: heels down" - step "Straighten" 2s settle: + step "Straighten" 2.2s settle: hips: flex 0 hips: rotate-out 0 knees: flex 0 diff --git a/playground/public/moves/forward-lunge.html b/playground/public/moves/forward-lunge.html index f45d22f..a396349 100644 --- a/playground/public/moves/forward-lunge.html +++ b/playground/public/moves/forward-lunge.html @@ -156,7 +156,7 @@

    The .posecode source

    knee_right: flex 95 hip_left: extend 15 knee_left: flex 80 - ankle_right: plantarflex 50 + ankle_left: plantarflex 50 spine: extend 4 travel: 0 0.3 pin: foot_left floor @@ -168,7 +168,7 @@

    The .posecode source

    knee_right: flex 0 hip_left: extend 0 knee_left: flex 0 - ankle_right: plantarflex 0 + ankle_left: plantarflex 0 spine: flex 0 travel: 0 0 ground-lock: feet diff --git a/playground/public/moves/grapevine.html b/playground/public/moves/grapevine.html index 6ce88a9..6f49fce 100644 --- a/playground/public/moves/grapevine.html +++ b/playground/public/moves/grapevine.html @@ -8,11 +8,11 @@ - + - + @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -135,20 +135,36 @@

    Grapevine

    How to do it

    1. - Step out0.7s · flow - Step the leading foot out to the side + Left step side0.65s · flow + Step the left foot to the side and begin travelling
    2. - Cross behind0.7s · flow - Cross the trailing foot behind and keep travelling + Right crosses behind0.65s · flow + Cross the right foot behind without stopping the sideways momentum
    3. - Step out again0.7s · flow - Step out to the side once more + Left step side again0.65s · flow + Step left again and keep the shoulders quiet
    4. - Return home1s · settle - Travel back the other way to the starting spot + Right touch0.55s · flow + Touch the right foot in lightly before reversing +
    5. +
    6. + Right step side0.65s · flow + Step the right foot to the side to travel back +
    7. +
    8. + Left crosses behind0.65s · flow + Cross the left foot behind and continue through the beat +
    9. +
    10. + Right step side again0.65s · flow + Step right and arrive back near the starting place +
    11. +
    12. + Close home0.65s · settle + Close the left foot under the body and settle the phrase
    @@ -159,40 +175,77 @@

    The .posecode source

    rig humanoid pose start = standing - step "Step out" 0.7s flow: - hip_left: abduct 30 + step "Left step side" 0.65s flow: + hip_left: abduct 28 shoulders: abduct 40 - travel: 0.4 0 + travel: 0.3 0 pin: foot_right floor reach: foot_left floor - cue "Step the leading foot out to the side" + cue "Step the left foot to the side and begin travelling" - step "Cross behind" 0.7s flow: + step "Right crosses behind" 0.65s flow: hip_left: abduct 0 hip_right: rotate-in 20 knee_right: flex 25 ankle_right: plantarflex 25 - travel: 0.8 0 + travel: 0.6 0 pin: foot_left floor reach: foot_right floor - cue "Cross the trailing foot behind and keep travelling" + cue "Cross the right foot behind without stopping the sideways momentum" - step "Step out again" 0.7s flow: + step "Left step side again" 0.65s flow: hip_right: rotate-in 0 knee_right: flex 0 ankle_right: plantarflex 0 - hip_left: abduct 30 - travel: 1.2 0 + hip_left: abduct 28 + travel: 0.9 0 pin: foot_right floor reach: foot_left floor - cue "Step out to the side once more" + cue "Step left again and keep the shoulders quiet" - step "Return home" 1s settle: + step "Right touch" 0.55s flow: hip_left: abduct 0 + knee_right: flex 16 + ankle_right: plantarflex 20 + travel: 1 0 + ground-lock: feet + cue "Touch the right foot in lightly before reversing" + + step "Right step side" 0.65s flow: + knee_right: flex 0 + ankle_right: plantarflex 0 + hip_right: abduct 28 + travel: 0.7 0 + pin: foot_left floor + reach: foot_right floor + cue "Step the right foot to the side to travel back" + + step "Left crosses behind" 0.65s flow: + hip_right: abduct 0 + hip_left: rotate-in 20 + knee_left: flex 25 + ankle_left: plantarflex 25 + travel: 0.4 0 + pin: foot_right floor + reach: foot_left floor + cue "Cross the left foot behind and continue through the beat" + + step "Right step side again" 0.65s flow: + hip_left: rotate-in 0 + knee_left: flex 0 + ankle_left: plantarflex 0 + hip_right: abduct 28 + travel: 0.1 0 + pin: foot_left floor + reach: foot_right floor + cue "Step right and arrive back near the starting place" + + step "Close home" 0.65s settle: + hip_right: abduct 0 shoulders: abduct 0 travel: 0 0 ground-lock: feet - cue "Travel back the other way to the starting spot" + cue "Close the left foot under the body and settle the phrase" repeat 3 diff --git a/playground/public/moves/index.html b/playground/public/moves/index.html index 4c0b6c0..929724c 100644 --- a/playground/public/moves/index.html +++ b/playground/public/moves/index.html @@ -176,8 +176,8 @@

    Fitness

    Lats · Bar · Advanced - Triceps dips (chair) - Triceps · Chair · Intermediate + Triceps dips (bars) + Triceps · Bars · Intermediate Bent-over row (hinge) diff --git a/playground/public/moves/jumping-jacks.html b/playground/public/moves/jumping-jacks.html index 0b5b11e..a11568f 100644 --- a/playground/public/moves/jumping-jacks.html +++ b/playground/public/moves/jumping-jacks.html @@ -149,6 +149,7 @@

    The .posecode source

    angles, not 3D transforms.

    posecode exercise "Jumping jacks"
       rig humanoid
    +  clip "jumping-jacks"
       pose start = standing
     
       step "Out" 0.5s settle:
    diff --git a/playground/public/moves/pirouette.html b/playground/public/moves/pirouette.html
    index 47337d2..a6946e2 100644
    --- a/playground/public/moves/pirouette.html
    +++ b/playground/public/moves/pirouette.html
    @@ -8,11 +8,11 @@
         
         
         
    -    
    +    
         
         
         
    -    
    +    
         
         
         
    @@ -99,7 +99,7 @@
     @media(max-width:600px){.wrap{padding:0 18px}}
     
         
    -    
    +    
         
    @@ -143,8 +143,12 @@ 

    How to do it

    Push up to relevé, pull the arms in, and spin a full turn
  • - Land1s · settle - Land softly through the feet, arms floating back to the sides + Land through plié0.9s · settle + Finish the turn and absorb the landing through a soft plié +
  • +
  • + Recover1.1s · settle + Lengthen to standing and let the arms float back to the sides
  • @@ -158,7 +162,7 @@

    The .posecode source

    step "Prep - plié" 1.2s flow: hips: rotate-out 25 knees: flex 45 - ankles: plantarflex 45 + ankles: dorsiflex 12 shoulders: flex 40 elbows: flex 35 ground-lock: feet @@ -177,15 +181,28 @@

    The .posecode source

    pin: foot_left floor cue "Push up to relevé, pull the arms in, and spin a full turn" - step "Land" 1s settle: + step "Land through plié" 0.9s settle: + hip_right: flex 0 + hip_right: rotate-out 0 + knee_right: flex 28 + ankle_right: dorsiflex 8 + knee_left: flex 28 + ankle_left: dorsiflex 8 + shoulders: abduct 55 + shoulders: flex 0 + elbows: flex 18 + ground-lock: feet + cue "Finish the turn and absorb the landing through a soft plié" + + step "Recover" 1.1s settle: hips: flex 0 hips: rotate-out 0 knees: flex 0 - ankles: plantarflex 0 - shoulders: flex 0 + ankles: dorsiflex 0 + shoulders: abduct 0 elbows: flex 0 ground-lock: feet - cue "Land softly through the feet, arms floating back to the sides" + cue "Lengthen to standing and let the arms float back to the sides" repeat 3
    diff --git a/playground/public/moves/port-de-bras.html b/playground/public/moves/port-de-bras.html index 79bc3c2..a35cb60 100644 --- a/playground/public/moves/port-de-bras.html +++ b/playground/public/moves/port-de-bras.html @@ -8,11 +8,11 @@ - + - + @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -143,12 +143,16 @@

    How to do it

    Open the arms out to the sides: second position, wrists lifted
  • - Fifth en haut2.2s · flow + Fifth en haut2.4s · flow Float the arms into a rounded frame overhead: fifth position
  • - Lower2s · settle - Lower through second back to the start, eyes following the hands + Open down through second2.2s · flow + Open from overhead through a wide second position, shoulders released +
  • +
  • + Close low2s · settle + Float the arms down to the start, eyes following the hands
  • @@ -173,19 +177,27 @@

    The .posecode source

    ground-lock: feet cue "Open the arms out to the sides: second position, wrists lifted" - step "Fifth en haut" 2.2s flow: + step "Fifth en haut" 2.4s flow: shoulders: abduct 0 shoulders: flex 160 elbows: flex 20 ground-lock: feet cue "Float the arms into a rounded frame overhead: fifth position" - step "Lower" 2s settle: + step "Open down through second" 2.2s flow: + shoulders: flex 0 + shoulders: abduct 85 + elbows: flex 16 + ground-lock: feet + cue "Open from overhead through a wide second position, shoulders released" + + step "Close low" 2s settle: + shoulders: abduct 0 shoulders: flex 0 elbows: flex 0 hips: rotate-out 0 ground-lock: feet - cue "Lower through second back to the start, eyes following the hands" + cue "Float the arms down to the start, eyes following the hands" repeat 3 diff --git a/playground/public/moves/releve.html b/playground/public/moves/releve.html index 7b34a76..eb9867f 100644 --- a/playground/public/moves/releve.html +++ b/playground/public/moves/releve.html @@ -135,11 +135,11 @@

    Relevé

    How to do it

    1. - Rise1.6s · settle + Rise1.8s · settle Turn out and rise onto the balls of the feet: lengthen up tall
    2. - Lower1.8s · drive + Lower2s · settle Lower the heels with control, staying lifted through the spine
    @@ -151,7 +151,7 @@

    The .posecode source

    rig humanoid pose start = standing - step "Rise" 1.6s settle: + step "Rise" 1.8s settle: ankles: plantarflex 30 hips: rotate-out 25 knees: extend 0 @@ -161,7 +161,7 @@

    The .posecode source

    ground-lock: feet cue "Turn out and rise onto the balls of the feet: lengthen up tall" - step "Lower" 1.8s drive: + step "Lower" 2s settle: ankles: plantarflex 0 hips: rotate-out 0 shoulders: abduct 0 diff --git a/playground/public/moves/superman.html b/playground/public/moves/superman.html index 6f84623..f550732 100644 --- a/playground/public/moves/superman.html +++ b/playground/public/moves/superman.html @@ -152,7 +152,7 @@

    The .posecode source

    pose start = prone step "Lift" 1.5s settle: - shoulders: abduct 130 + shoulders: flex 140 spine: extend 20 chest: extend 15 neck: extend 25 @@ -161,7 +161,7 @@

    The .posecode source

    cue "Lift the arms, chest, and legs off the floor" step "Lower" 1.5s drive: - shoulders: abduct 0 + shoulders: flex 0 spine: flex 0 chest: flex 0 neck: flex 0 diff --git a/playground/public/moves/tendu.html b/playground/public/moves/tendu.html index 497820a..682835e 100644 --- a/playground/public/moves/tendu.html +++ b/playground/public/moves/tendu.html @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -135,11 +135,11 @@

    Tendu

    How to do it

    1. - Point front1.6s · flow + Brush and point1.8s · settle Brush the right foot forward to a fully pointed tendu, leg turned out
    2. - Close1.6s · settle + Close through the floor1.8s · settle Draw the foot back to first position, heel down
    @@ -151,7 +151,7 @@

    The .posecode source

    rig humanoid pose start = standing - step "Point front" 1.6s flow: + step "Brush and point" 1.8s settle: hip_right: flex 22 hip_right: rotate-out 30 knee_right: extend 0 @@ -161,7 +161,7 @@

    The .posecode source

    pin: foot_left floor cue "Brush the right foot forward to a fully pointed tendu, leg turned out" - step "Close" 1.6s settle: + step "Close through the floor" 1.8s settle: hip_right: flex 0 hip_right: rotate-out 0 ankle_right: plantarflex 0 diff --git a/playground/public/moves/triceps-dips.html b/playground/public/moves/triceps-dips.html index ec36fb0..ce86b2a 100644 --- a/playground/public/moves/triceps-dips.html +++ b/playground/public/moves/triceps-dips.html @@ -8,11 +8,11 @@ - + - + @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -123,7 +123,7 @@

    Fitness · Upper arms

    Triceps dips

    -

    Target Triceps  ·  Equipment Chair +

    Target Triceps  ·  Equipment Bars  ·  Level Intermediate  ·  Reps 8

    Triceps dips is a intermediate-level fitness movement targeting the triceps, written in Posecode, a small open-source language diff --git a/playground/public/moves/waltz-box.html b/playground/public/moves/waltz-box.html index 39a48c1..dffbfa5 100644 --- a/playground/public/moves/waltz-box.html +++ b/playground/public/moves/waltz-box.html @@ -8,11 +8,11 @@ - + - + @@ -99,7 +99,7 @@ @media(max-width:600px){.wrap{padding:0 18px}} - + @@ -135,20 +135,28 @@

    Waltz box step

    How to do it

    1. - 1 - forward, rise1s · flow - Step forward onto the right foot and rise, arms in a soft frame + 1 - right foot forward0.95s · flow + Step forward onto the right foot and begin the waltz rise
    2. - 2 - side, lower1s · flow - Side step to the corner and lower through the heels + 2 - left foot side0.95s · flow + Step left to the side and continue rising through count two
    3. - 3 - back, rise1s · flow - Step back onto the left foot and rise again + 3 - close and lower1.05s · flow + Close the right foot to the left and lower softly through count three
    4. - 4 - close home1s · settle - Close to the start, completing the box + 4 - left foot back0.95s · flow + Step back onto the left foot and begin the second rise +
    5. +
    6. + 5 - right foot side0.95s · flow + Step right to the side, staying buoyant through count five +
    7. +
    8. + 6 - close and lower1.05s · settle + Close the left foot and lower with control to complete the six-count box
    @@ -159,41 +167,63 @@

    The .posecode source

    rig humanoid pose start = standing - step "1 - forward, rise" 1s flow: - hip_right: flex 25 - knee_right: flex 20 - ankles: plantarflex 12 + step "1 - right foot forward" 0.95s flow: + hip_right: flex 24 + knee_right: flex 24 + ankle_right: plantarflex 18 shoulders: abduct 55 elbows: flex 20 - travel: 0 0.4 + travel: 0 0.32 pin: foot_left floor - cue "Step forward onto the right foot and rise, arms in a soft frame" + cue "Step forward onto the right foot and begin the waltz rise" - step "2 - side, lower" 1s flow: + step "2 - left foot side" 0.95s flow: hip_right: flex 0 knee_right: flex 0 + ankle_right: plantarflex 0 + hip_left: abduct 22 + knee_left: flex 16 + ankles: plantarflex 22 + travel: -0.32 0.32 + pin: foot_right floor + cue "Step left to the side and continue rising through count two" + + step "3 - close and lower" 1.05s flow: + hip_left: abduct 0 + knee_left: flex 0 ankles: plantarflex 0 - travel: -0.4 0.4 + travel: -0.32 0.32 ground-lock: feet - cue "Side step to the corner and lower through the heels" + cue "Close the right foot to the left and lower softly through count three" - step "3 - back, rise" 1s flow: - hip_left: flex 25 - knee_left: flex 20 - ankles: plantarflex 12 - travel: -0.4 0 + step "4 - left foot back" 0.95s flow: + hip_left: extend 18 + knee_left: flex 24 + ankle_left: plantarflex 18 + travel: -0.32 0 pin: foot_right floor - cue "Step back onto the left foot and rise again" + cue "Step back onto the left foot and begin the second rise" - step "4 - close home" 1s settle: - hip_left: flex 0 + step "5 - right foot side" 0.95s flow: + hip_left: extend 0 knee_left: flex 0 + ankle_left: plantarflex 0 + hip_right: abduct 22 + knee_right: flex 16 + ankles: plantarflex 22 + travel: 0 0 + pin: foot_left floor + cue "Step right to the side, staying buoyant through count five" + + step "6 - close and lower" 1.05s settle: + hip_right: abduct 0 + knee_right: flex 0 ankles: plantarflex 0 shoulders: abduct 0 elbows: flex 0 travel: 0 0 ground-lock: feet - cue "Close to the start, completing the box" + cue "Close the left foot and lower with control to complete the six-count box" repeat 3 diff --git a/playground/public/sitemap.xml b/playground/public/sitemap.xml index 093445f..6b95e08 100644 --- a/playground/public/sitemap.xml +++ b/playground/public/sitemap.xml @@ -2,463 +2,463 @@ https://posecode.org/ - 2026-07-12 + 2026-07-15 weekly 1.0 https://posecode.org/play - 2026-07-12 + 2026-07-15 weekly 0.9 https://posecode.org/moves/ - 2026-07-12 + 2026-07-15 weekly 0.8 https://posecode.org/spec.html - 2026-07-12 + 2026-07-15 weekly 0.6 https://posecode.org/llm-guide.html - 2026-07-12 + 2026-07-15 weekly 0.6 https://posecode.org/moves/squat.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/dance-phrase.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/deadlift.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/shoulder-abduction.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/front-kick.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/good-morning.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/chest-opener.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/plank-hold.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/mountain-climber.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/crunch.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/bicycle-crunch.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/supine-leg-raise.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/superman.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/forward-lunge.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/calf-raise.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/jumping-jacks.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/box-step-taps.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/pull-up.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/step-up.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/triceps-dips.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/quad-stretch.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/hip-flexion.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/knee-flexion.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/spine-rotation.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/elbow-forearm.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/heel-raises.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/hamstring-curl.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/hip-abduction.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/shoulder.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/neck.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/posture.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/twist.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/shoulder-rolls.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/neck-side-stretch.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/overhead-reach.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/jab-cross.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/horse-stance.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/bow.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/arm-circles.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/high-knee-march.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/demi-plie.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/releve.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/tendu.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/port-de-bras.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/bent-over-row.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/biceps.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/lateral.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/fold.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/chair.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/sidebend.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/touch-toes.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/cross-body-reach.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/glute-bridge.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/dead-bug.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/cobra.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/seated-forward-fold.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/sit-to-stand.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/box-squat.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/wall-sit.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/dead-hang.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/hanging-knee-raise.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/make-a-fist.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/pinch-grip.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/finger-spell.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/hand-wave.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/pirouette.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/box-step.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/grapevine.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/waltz-box.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/chasse.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/walk-cycle.html - 2026-07-12 + 2026-07-15 weekly 0.5 https://posecode.org/moves/quarter-turns.html - 2026-07-12 + 2026-07-15 weekly 0.5 diff --git a/playground/public/spec.html b/playground/public/spec.html index 976ead3..5422e85 100644 --- a/playground/public/spec.html +++ b/playground/public/spec.html @@ -8,11 +8,11 @@ - + - + @@ -121,10 +121,10 @@

    Reference

    -

    Posecode Protocol Specification v0.1

    +

    Posecode Protocol Specification v0.2

    Posecode is a small text language for describing a single person's kinematic movement so it can be rendered as an animated 3D figure in a web browser.

    It is to human movement what Mermaid is to diagrams: an LLM (or a human) writes a compact, readable document; a client-side parser + renderer turns it into a moving mannequin. The model never produces 3D matrices: it expresses the *semantic phases* of a movement, which it already understands.

    -
    • Version keyword: documents declare nothing; this is posecode 0.1.
    • File extension: .posecode
    • Compute model: generation is pure text (server-cheap); all 3D math runs
    +
    • Version keyword: documents declare nothing; this is posecode 0.2.
    • Compatibility: v0.2 parsers continue to accept the v0.1 easing aliases.
    • File extension: .posecode
    • Compute model: generation is pure text (server-cheap); all 3D math runs

    on the client (Three.js). See the project research §6.


    1. Grammar

    @@ -138,8 +138,8 @@

    1. Grammar

    pose = "pose" "start" "=" WORD ; (* neutral|standing|plank|supine|prone|seated *) clip = "clip" STRING ; (* optional mocap clip; renderer may retarget & blend *) repeat = "repeat" NUMBER ; -step = "step" STRING DURATION easing ":" { child } ; -easing = "linear" | "ease-in" | "ease-out" | "ease-in-out" ; +step = "step" STRING DURATION timingMode ":" { child } ; +timingMode = "flow" | "settle" | "drive" | "snap" | "linear" ; child = jointTarget | groundLock | reach | pin | turn | travel | cue ; jointTarget= joint ":" action [ NUMBER ] ; groundLock = "ground-lock" ":" effector { "," effector } ; @@ -150,6 +150,16 @@

    1. Grammar

    cue = "cue" STRING ; DURATION = NUMBER "s" ; (* e.g. 2s, 1.5s *)

    A step is one phase of the movement. Phases run in sequence; within a phase, all joint targets apply concurrently.

    +

    Ground locks accept the groups hands, forearms, and feet, plus the single-side forms hand_left|hand_right, elbow_left|elbow_right, and foot_left|foot_right. Human-readable left foot, right foot, left hand, and related forms normalize to the canonical side-specific names.

    +

    Timing modes describe how motion crosses the phase boundary:

    +
    + + + + + +
    ModeUse
    flowCarry velocity smoothly through the pose.
    settleDecelerate into a deliberate rest.
    driveAccelerate out of rest.
    snapArrive quickly and stop sharply.
    linearEven timing without a shaped acceleration curve.
    +

    Legacy v0.1 spellings remain valid and normalize to canonical modes: ease-indrive, ease-outsettle, and ease-in-outsettle. New documents should use the canonical v0.2 names.


    2. Joints

    @@ -195,8 +205,8 @@

    4. Range of Motion (safety)

    > These are general literature values, not medical advice. Consult a > qualified professional for physiotherapy or exercise prescription.


    5. Rendering model

    -
    1. Forward kinematics: each phase sets joint angles; the renderer slerps
    -

    bone rotations between phases with the phase's easing.

    +
    1. Forward kinematics: each phase sets joint angles; the renderer uses
    +

    C1-continuous quaternion splines between keyframes, shaped by the destination phase's timing mode.

    1. Grounding: the figure is dropped so its lowest point rests on the floor

    (a bounding-box drop), which grounds standing, plank, and the lying/seated poses alike.

    1. Ground-lock IK: effectors listed in ground-lock (hands, forearms,
    @@ -204,7 +214,7 @@

    5. Rendering model

    1. Reach-IK: a reach: line drives an effector (`hand_left|hand_right|

    foot_left|foot_right, or the groups hands/feet for both sides) to a world target via Cyclic Coordinate Descent (CCD) over the arm/leg chain. A target is a body landmark bone (e.g. ankle_left), the keyword floor, or a prop anchor (bar, seat, wall`). The solve is ROM-constrained: each iteration clamps every chain joint into its §4 Range-of-Motion limits (expressed as a per-axis box in the bone's local Euler frame), so a reach toward an unsafe or unreachable target settles on the closest *healthy* pose; solved angles obey the same hard limits as authored ones.

    1. Props: prop chair|wall|bar|box|dip-bars adds a scene object at a
    -

    fixed default placement (chair/wall behind, bar overhead, box in front, dip bars either side); its named anchors (seat, wall, bar, box, bars) become reach/pin targets.

    +

    fixed default placement (chair/wall behind, bar overhead, box in front, dip bars either side); its named anchors (seat, wall, bar, box, bars) become reach/pin targets. Props are solid: each prop declares blocking faces (the wall's surface, the chair's backrest and seat edge, the box's near face) and a contact pass removes any body overlap — either by translating the whole figure out along the face normal (a wall-sit slides down the wall's *surface*, feet walking forward, instead of the torso hinging through the slab) or by bending the offending limb's hip clear, ROM-clamped like every other solve. Limbs pinned, gripped, or reached to a prop anchor are that phase's declared support and are exempt (a foot standing on the box top is not "inside" the box).

    1. Pins: pin: <effector> <anchor> translates the whole figure so the

    effector sits on the anchor (effectors accept the same hands/feet groups as reach: pin: hands bar pins both). Where ground-lock keeps a foot on the floor and reach moves a limb to a target, a pin moves the body while the contact stays put, so the figure hangs from a bar, pulls up toward it, rises onto a box, or lowers into a dip as the joints work. Symmetric bar contacts resolve to side-specific anchors automatically (bar.left / bar.right, bars.left / bars.right). Contact post-processing also keeps floor-contacting soles level and gives bar-contacting wrists a stable overhand orientation; existing Posecode syntax remains unchanged.

    1. Spatial choreography: turn: <deg> rotates the figure's facing (yaw
    @@ -213,12 +223,12 @@

    5. Rendering model

    count surfaced to the UI.

    When a mocap clip is active, the renderer selects the take containing the most actual bone motion (rather than blindly choosing the longest embedded take), retargets and blends it, then restores solved terminal contacts on the visible character. Mocap therefore cannot overwrite a planted sole or pinned grip.

    Start poses: neutral, standing, plank, supine (face-up), prone (face-down), seated (long-sit on the floor).

    -

    IK note: Three.js's bundled CCDIKSolver targets SkinnedMesh; the Posecode mannequin is rigid capsule segments, so Posecode implements CCD directly over the Object3D bone hierarchy (posecode-render/ik.ts) for both ground-lock and reach. Two-person/dual-IK and collision detection remain deferred (research §5.2, §6.2).

    +

    IK note: Three.js's bundled CCDIKSolver targets SkinnedMesh; the Posecode mannequin is rigid capsule segments, so Posecode implements CCD directly over the Object3D bone hierarchy (posecode-render/ik.ts) for both ground-lock and reach. Self-collision (limb-vs-body) and body-vs-prop contact are solved; two-person/ dual-IK and figure-vs-figure collision remain deferred (research §5.2, §6.2).


    6. Intermediate Representation (IR)

    parse(source) returns { ir, warnings, errors }. The IR is renderer-agnostic; angles are in degrees.

    interface PosecodeIR {
    -  version: string;          // "0.1"
    +  version: string;          // "0.2"
       kind: string;             // "exercise" | "stretch" | "posture"
       name: string;
       rig: string;              // "humanoid"
    @@ -227,7 +237,7 @@ 

    6. Intermediate Representation (IR)

    phases: { name: string; durationSec: number; - easing: "linear" | "ease-in" | "ease-out" | "ease-in-out"; + easing: "flow" | "settle" | "drive" | "snap" | "linear"; targets: { boneId: string; euler: { x: number; y: number; z: number } }[]; groundLock: string[]; // ["hands","feet"] or ["foot_right"] cue?: string; diff --git a/playground/public/squat.posecode b/playground/public/squat.posecode index 97db8f2..fde149e 100644 --- a/playground/public/squat.posecode +++ b/playground/public/squat.posecode @@ -2,7 +2,7 @@ posecode exercise "Body-weight squat" rig humanoid pose start = standing - step "Descend" 1.6s ease-in-out: + step "Descend" 1.6s settle: hips: flex 80 knees: flex 95 ankles: dorsiflex 15 @@ -13,7 +13,7 @@ posecode exercise "Body-weight squat" ground-lock: feet cue "Sit the hips back, chest proud, knees track over the toes" - step "Drive up" 1.2s ease-out: + step "Drive up" 1.2s drive: hips: flex 0 knees: flex 0 ankles: plantarflex 0 diff --git a/playground/src/editor.ts b/playground/src/editor.ts index 6c6a3bf..a9d6968 100644 --- a/playground/src/editor.ts +++ b/playground/src/editor.ts @@ -75,6 +75,10 @@ const ACTIONS = new Set([ "plantarflex", ]); const ATOMS = new Set([ + "flow", + "settle", + "drive", + "snap", "linear", "ease-in", "ease-out", diff --git a/scripts/generate-content-pages.mjs b/scripts/generate-content-pages.mjs index b118633..ead5fdc 100644 --- a/scripts/generate-content-pages.mjs +++ b/scripts/generate-content-pages.mjs @@ -62,7 +62,11 @@ function slugTitle(label) { } async function main() { - const server = await createServer({ root: playgroundRoot, server: { middlewareMode: true } }); + const server = await createServer({ + root: playgroundRoot, + appType: "custom", + server: { middlewareMode: true, hmr: false, ws: false }, + }); let PRESETS; try { ({ PRESETS } = await server.ssrLoadModule("/src/presets.ts")); @@ -199,7 +203,7 @@ ${stepsHtml} const specHtml = pageShell({ title: "Posecode Language Specification: The .posecode Kinematic Motion DSL", description: - "The full Posecode v0.1 grammar, joints, actions, and range-of-motion tables: a small text language LLMs write to describe human movement as an animated 3D figure.", + "The full Posecode v0.2 grammar, timing modes, joints, actions, and range-of-motion tables: a small text language LLMs write to describe human movement as an animated 3D figure.", canonicalPath: "/spec.html", bodyHtml: `

    Reference

    \n${renderMarkdown(specMd)}`, }); diff --git a/spec/SPEC.md b/spec/SPEC.md index 1dd0d38..d459cc2 100644 --- a/spec/SPEC.md +++ b/spec/SPEC.md @@ -1,4 +1,4 @@ -# Posecode Protocol Specification v0.1 +# Posecode Protocol Specification v0.2 Posecode is a small text language for describing a single person's **kinematic movement** so it can be rendered as an animated 3D figure in a web browser. @@ -8,7 +8,8 @@ a compact, readable document; a client-side parser + renderer turns it into a moving mannequin. The model never produces 3D matrices: it expresses the *semantic phases* of a movement, which it already understands. -- **Version keyword:** documents declare nothing; this is `posecode 0.1`. +- **Version keyword:** documents declare nothing; this is `posecode 0.2`. +- **Compatibility:** v0.2 parsers continue to accept the v0.1 easing aliases. - **File extension:** `.posecode` - **Compute model:** generation is pure text (server-cheap); all 3D math runs on the client (Three.js). See the project research §6. @@ -29,8 +30,8 @@ prop = "prop" WORD ; (* chair|wall|bar|box|di pose = "pose" "start" "=" WORD ; (* neutral|standing|plank|supine|prone|seated *) clip = "clip" STRING ; (* optional mocap clip; renderer may retarget & blend *) repeat = "repeat" NUMBER ; -step = "step" STRING DURATION easing ":" { child } ; -easing = "linear" | "ease-in" | "ease-out" | "ease-in-out" ; +step = "step" STRING DURATION timingMode ":" { child } ; +timingMode = "flow" | "settle" | "drive" | "snap" | "linear" ; child = jointTarget | groundLock | reach | pin | turn | travel | cue ; jointTarget= joint ":" action [ NUMBER ] ; groundLock = "ground-lock" ":" effector { "," effector } ; @@ -45,6 +46,25 @@ DURATION = NUMBER "s" ; (* e.g. 2s, 1.5s *) A `step` is one **phase** of the movement. Phases run in sequence; within a phase, all joint targets apply concurrently. +Ground locks accept the groups `hands`, `forearms`, and `feet`, plus the +single-side forms `hand_left|hand_right`, `elbow_left|elbow_right`, and +`foot_left|foot_right`. Human-readable `left foot`, `right foot`, `left hand`, +and related forms normalize to the canonical side-specific names. + +Timing modes describe how motion crosses the phase boundary: + +| Mode | Use | +| --- | --- | +| `flow` | Carry velocity smoothly through the pose. | +| `settle` | Decelerate into a deliberate rest. | +| `drive` | Accelerate out of rest. | +| `snap` | Arrive quickly and stop sharply. | +| `linear` | Even timing without a shaped acceleration curve. | + +Legacy v0.1 spellings remain valid and normalize to canonical modes: +`ease-in` → `drive`, `ease-out` → `settle`, and `ease-in-out` → `settle`. +New documents should use the canonical v0.2 names. + --- ## 2. Joints @@ -119,8 +139,9 @@ research §5.1 normative tables. Selected ceilings (degrees): ## 5. Rendering model -1. **Forward kinematics**: each phase sets joint angles; the renderer slerps - bone rotations between phases with the phase's easing. +1. **Forward kinematics**: each phase sets joint angles; the renderer uses + C1-continuous quaternion splines between keyframes, shaped by the destination + phase's timing mode. 2. **Grounding**: the figure is dropped so its lowest point rests on the floor (a bounding-box drop), which grounds standing, plank, and the lying/seated poses alike. @@ -194,7 +215,7 @@ angles are in **degrees**. ```ts interface PosecodeIR { - version: string; // "0.1" + version: string; // "0.2" kind: string; // "exercise" | "stretch" | "posture" name: string; rig: string; // "humanoid" @@ -203,7 +224,7 @@ interface PosecodeIR { phases: { name: string; durationSec: number; - easing: "linear" | "ease-in" | "ease-out" | "ease-in-out"; + easing: "flow" | "settle" | "drive" | "snap" | "linear"; targets: { boneId: string; euler: { x: number; y: number; z: number } }[]; groundLock: string[]; // ["hands","feet"] or ["foot_right"] cue?: string; diff --git a/spec/llm-authoring.md b/spec/llm-authoring.md index 50f0450..d28f1f8 100644 --- a/spec/llm-authoring.md +++ b/spec/llm-authoring.md @@ -17,10 +17,10 @@ posecode "" # kind = exercise | stretch | posture rig humanoid prop # optional: chair | wall | bar | box | dip-bars (repeatable) pose start = # neutral | standing | plank | supine | prone | seated - step "" : # easing = linear | ease-in | ease-out | ease-in-out + step "" : # mode = flow | settle | drive | snap | linear : reach: # optional: drive a hand/foot to a target via IK - ground-lock: # groups (hands/forearms/feet) or per-side aliases such as foot_right + ground-lock: # groups, foot_left/foot_right, or natural left foot/right foot turn: # optional: face this yaw by phase end (standing only) travel: # optional: move to this x z (metres) by phase end cue "" @@ -57,6 +57,19 @@ wrists hips knees ankles`. Plural names move both sides symmetrically; use floor (feet when standing; hands and feet in a plank). 5. `repeat` the rep count. +## Timing modes + +- `flow`: carry momentum through an interior pose; use for continuous dance, + locomotion, and multi-part sports motion. +- `settle`: decelerate into a real rest; use at a squat bottom, landing, hold, + or final pose. +- `drive`: accelerate from rest; use for a jump, push, lift, or recoil. +- `snap`: arrive quickly and stop sharply; use for a strike or release. +- `linear`: constant timing; best for deliberate holds or mechanical motion. + +The older names `ease-in`, `ease-out`, and `ease-in-out` still parse for +compatibility, but do not author new documents with them. + ## Example ```posecode @@ -64,14 +77,14 @@ posecode exercise "Body-weight squat" rig humanoid pose start = standing - step "Descend" 1.6s ease-in-out: + step "Descend" 1.6s settle: hips: flex 80 knees: flex 95 ankles: dorsiflex 14 ground-lock: feet cue "Sit the hips back, chest proud, knees track over the toes" - step "Drive up" 1.2s ease-out: + step "Drive up" 1.2s drive: hips: flex 0 knees: flex 0 ankles: dorsiflex 0 @@ -91,14 +104,14 @@ posecode exercise "Deadlift" rig humanoid pose start = standing - step "Lower" 1.8s ease-in-out: + step "Lower" 1.8s settle: pelvis: hinge 95 knees: flex 25 shoulders: flex 90 ground-lock: feet cue "Hips back, flat back: let the arms hang to the bar" - step "Lift" 1.4s ease-out: + step "Lift" 1.4s drive: pelvis: hinge 0 knees: flex 0 shoulders: flex 0 @@ -120,7 +133,7 @@ posecode exercise "Deadlift" let `reach` finish the hand placement. Example, touch your toes: ```posecode - step "Fold" 2.5s ease-in-out: + step "Fold" 2.5s settle: pelvis: hinge 95 knees: flex 12 reach: hand_left ankle_left @@ -155,8 +168,8 @@ The same grammar covers many fields. A few patterns that read well: leg stays planted. - **Desk / posture**: slow `stretch` documents with `ground-lock: feet`; contrast a "collapsed" phase with a "tall" reset. -- **Sports / martial arts**: short, snappy phases (0.3–0.6s) with `ease-out` - on the strike; chamber → extend → re-chamber → return. +- **Sports / martial arts**: short phases (0.2–0.6s); use `flow` through a + gather/chamber, `drive` for takeoff, and `snap` on the strike or release. ### Dance / choreography
    Group (plural)BonesSingular forms