From ebc13d13f37c431d304f913c9319ba6e53ad5960 Mon Sep 17 00:00:00 2001 From: alowpoly Date: Wed, 29 Jul 2026 11:46:34 -0300 Subject: [PATCH 1/2] docs(morph): add interactive cube-to-sphere example --- README.md | 29 +- packages/morph/README.md | 34 +- pnpm-lock.yaml | 3 + website/astro.config.mjs | 6 +- website/package.json | 1 + .../public/morph/morph-targets/LICENSE.txt | 21 + .../assets/lighting-state-000.webp | Bin 0 -> 31080 bytes .../assets/solid-triangles-000.webp | Bin 0 -> 4190 bytes website/public/morph/morph-targets/model.css | 2 + .../public/morph/morph-targets/runtime.json | 1 + website/src/components/MorphTargetsDemo.astro | 472 ++++++++++++++++++ website/src/content/docs/api/headless.mdx | 2 +- website/src/content/docs/guides/morph.mdx | 85 +++- 13 files changed, 597 insertions(+), 59 deletions(-) create mode 100644 website/public/morph/morph-targets/LICENSE.txt create mode 100644 website/public/morph/morph-targets/assets/lighting-state-000.webp create mode 100644 website/public/morph/morph-targets/assets/solid-triangles-000.webp create mode 100644 website/public/morph/morph-targets/model.css create mode 100644 website/public/morph/morph-targets/runtime.json create mode 100644 website/src/components/MorphTargetsDemo.astro diff --git a/README.md b/README.md index 44b52c70..92f281c3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ npm install @layoutit/polycss-react # Vue npm install @layoutit/polycss-vue -# Prepared retained models +# Morph npm install @layoutit/polycss-morph ``` @@ -156,13 +156,12 @@ const html = await exportPolySceneSnapshot(scene.host); If any referenced asset cannot be inlined, the function throws `PolySceneSnapshotError` with `code: "ASSET_INLINE_FAILED"`. -### Prepared retained models +### PolyCSS Morph -`@layoutit/polycss-morph` is an imperative, framework-agnostic package for -models prepared ahead of time and updated through one retained PolyCSS DOM -graph. +Use `@layoutit/polycss-morph` for models built ahead of time and updated through +a stable PolyCSS DOM graph. -Preparation is explicitly Node-only: +Preparation runs in Node: ```ts import { preparePolyMorphModel } from "@layoutit/polycss-morph/prepare"; @@ -203,13 +202,15 @@ updates do not rebuild topology or redraw prepared image resources. Prepared playback uses a two-phase sample: apply `sample.update`, then call `runtime.commit(sample)` only after the retained mount accepts the update. -On browsers that pass PolyCSS's solid-triangle support check, Morph uses -`corner-shape` when available and a CSS border triangle otherwise; Firefox uses -the larger border-triangle variant. Preparation also emits packed alpha-atlas -pages for WebKit/Safari. Every polygon receives a slice sized to its local-2D -bounding rect. Mount creates object URLs from the already-verified package -bytes, selects the fallback once, and revokes those URLs at teardown; it never -refetches, generates, or redraws the atlas. +Morph chooses the triangle paint path once when it mounts. It uses +`corner-shape` where available, a larger CSS border triangle in Firefox, and +prepared alpha-atlas pages in WebKit/Safari. Every polygon receives a slice +sized to its local-2D bounding rect. Mount creates object URLs from the +already-verified package bytes, selects the fallback once, and revokes those +URLs at teardown; it never refetches, generates, or redraws the atlas. + +See the [PolyCSS Morph guide](https://polycss.com/guides/morph/), including the +CC0 Animated Morph Sphere example. ### Polygon Data Model @@ -291,7 +292,7 @@ Each visible polygon is emitted as one leaf element; the renderer chooses the le | `@layoutit/polycss` | Vanilla custom elements and imperative `createPolyScene` API. | | `@layoutit/polycss-react` | React components, hooks, controls, and core re-exports. | | `@layoutit/polycss-vue` | Vue 3 components, composables, controls, and core re-exports. | -| `@layoutit/polycss-morph` | Prepared retained-model contracts, Node preparation, browser loading, sparse deformation, controls, springs, animation, skinning, and playback. | +| `@layoutit/polycss-morph` | Prepared-model loading, retained DOM animation, morph targets, skinning, and playback. | ## Made with PolyCSS diff --git a/packages/morph/README.md b/packages/morph/README.md index 9d4a54d1..9ba6c292 100644 --- a/packages/morph/README.md +++ b/packages/morph/README.md @@ -1,12 +1,12 @@ # @layoutit/polycss-morph -Prepared retained-model deformation and playback for PolyCSS. +Prepare, load, and animate retained DOM models with PolyCSS. ```bash npm install @layoutit/polycss-morph ``` -## Boundary +## Package entries Morph has two public entries: @@ -19,9 +19,8 @@ Morph has two public entries: packages, mounts one retained PolyCSS graph, and exposes imperative, caller-driven runtimes. -The generic Node preparer directly creates `static-prepared` and -`morph-regions` models. The browser runtime executes all four validated -profiles: +The Node preparer creates `static-prepared` and `morph-regions` models. The +browser runtime supports all four profiles: | Profile | Runtime contract | |---|---| @@ -71,6 +70,9 @@ const frame = deformation.sample({ mounted.apply({ leaves: frame.leafUpdates }); ``` +See the CC0 [Animated Morph Sphere](https://polycss.com/guides/morph/#example-animated-morph-sphere) +for a complete rendered animation and deformation example. + The browser API is intentionally imperative: - load or validate a model; @@ -92,17 +94,15 @@ mounted model keeps the same leaf elements for its lifetime. Runtime updates do not rebuild topology, add or remove leaves, construct image resources, or redraw prepared image resources. -The browser resolves prepared triangles once during mount. On browsers that -pass PolyCSS's solid-triangle support check, Morph uses `corner-shape` when -available and a CSS border triangle otherwise; Firefox uses the larger -border-triangle variant. WebKit/Safari instead uses each leaf's prepared -polygon-sized atlas slice. Mount creates object URLs from the loader's -already-verified image bytes and revokes them at teardown; it does not refetch -package resources. Atlas pages are generated with Node built-ins, so Morph has -no Sharp or other native image dependency. +Morph chooses the triangle paint path once when it mounts. It uses +`corner-shape` where available, a larger CSS border triangle in Firefox, and +each leaf's prepared polygon-sized atlas slice in WebKit/Safari. Mount creates +object URLs from the loader's already-verified image bytes and revokes them at +teardown; it does not refetch package resources. Atlas pages are generated with +Node built-ins, so Morph has no Sharp or other native image dependency. -## Consumer adapters +## Application ownership -Product-specific source cadence, schemas, input ordering, presentation, and -oracle tooling stay in the consuming product. Product adapters own their -prepared packages, mounting paths, presentation, and oracle evidence. +Morph owns the prepared model format and sparse DOM updates. Your application +owns input, timing, presentation, model-specific preparation, and product +behavior. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d3f8cb5..f3c02df3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,6 +272,9 @@ importers: '@layoutit/polycss-fonts': specifier: workspace:^ version: link:../packages/fonts + '@layoutit/polycss-morph': + specifier: workspace:^ + version: link:../packages/morph '@layoutit/polycss-react': specifier: workspace:^ version: link:../packages/react diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 088c151c..a917bf1c 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -35,6 +35,10 @@ export default defineConfig({ find: /^@layoutit\/polycss-fonts$/, replacement: repoPath('../packages/fonts/src/index.ts'), }, + { + find: /^@layoutit\/polycss-morph$/, + replacement: repoPath('../packages/morph/src/index.ts'), + }, { find: /^@layoutit\/polycss$/, replacement: repoPath('../packages/polycss/src/index.ts'), @@ -97,7 +101,7 @@ export default defineConfig({ { label: 'Performance', slug: 'guides/performance' }, { label: 'Projections', slug: 'guides/projections' }, { label: 'Animation', slug: 'guides/animation' }, - { label: 'Prepared Morph Models', slug: 'guides/morph' }, + { label: 'Morph', slug: 'guides/morph' }, ], }, { diff --git a/website/package.json b/website/package.json index 1611daf0..139c012f 100644 --- a/website/package.json +++ b/website/package.json @@ -16,6 +16,7 @@ "@astrojs/starlight": "^0.38.2", "@layoutit/polycss": "workspace:^", "@layoutit/polycss-fonts": "workspace:^", + "@layoutit/polycss-morph": "workspace:^", "@layoutit/polycss-react": "workspace:^", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/website/public/morph/morph-targets/LICENSE.txt b/website/public/morph/morph-targets/LICENSE.txt new file mode 100644 index 00000000..8ada2a5f --- /dev/null +++ b/website/public/morph/morph-targets/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright © 2010-2026 three.js authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/website/public/morph/morph-targets/assets/lighting-state-000.webp b/website/public/morph/morph-targets/assets/lighting-state-000.webp new file mode 100644 index 0000000000000000000000000000000000000000..56994ea0b6f526824f6c6f956310b2b25b2a277c GIT binary patch literal 31080 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

2f39+H6iKpmXI;<>%li@=QyO;q7{5 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 F@PF5`)sz4L literal 0 HcmV?d00001 diff --git a/website/public/morph/morph-targets/assets/solid-triangles-000.webp b/website/public/morph/morph-targets/assets/solid-triangles-000.webp new file mode 100644 index 0000000000000000000000000000000000000000..ef352713243ee0985b37a6ea1dbb400d86b2d92d GIT binary patch literal 4190 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 + +

+
+ +
+ + +
+ +

+ Loading model… +

+

Drag to orbit · 768 DOM polygons · MIT

+
+ + + + diff --git a/website/src/content/docs/api/headless.mdx b/website/src/content/docs/api/headless.mdx index 41881197..06a050b3 100644 --- a/website/src/content/docs/api/headless.mdx +++ b/website/src/content/docs/api/headless.mdx @@ -568,4 +568,4 @@ The root package exports the matching element classes for manual registration or | `@layoutit/polycss/elements` | Side-effect: registers the PolyCSS custom elements | | `@layoutit/polycss-core` | Pure parsers / math, zero DOM: `parseObj`, `parseGltf`, `parseVox`, `parseStl`, `loadMesh`, types | | `@layoutit/polycss-fonts` | Font parsing, Google font loading, and text-to-polygon mesh generation | -| `@layoutit/polycss-morph` | Browser-safe prepared-model contracts, loading, retained mounting, sparse deformation, controls, springs, animation, skinning, and playback; Node preparation is exported separately from `/prepare` | +| `@layoutit/polycss-morph` | Prepared-model loading, retained DOM animation, morph targets, skinning, and playback; Node preparation is exported separately from `/prepare` | diff --git a/website/src/content/docs/guides/morph.mdx b/website/src/content/docs/guides/morph.mdx index 877abfb8..3a28500b 100644 --- a/website/src/content/docs/guides/morph.mdx +++ b/website/src/content/docs/guides/morph.mdx @@ -1,31 +1,33 @@ --- -title: Prepared Morph Models -description: Prepare retained PolyCSS models in Node, then load and update them through a stable browser DOM graph. +title: PolyCSS Morph +description: Prepare, load, and animate retained DOM models with PolyCSS. --- +import MorphTargetsDemo from "../../../components/MorphTargetsDemo.astro"; + `@layoutit/polycss-morph` is an imperative, framework-agnostic layer for models -whose topology, paint resources, and update plans are prepared ahead of time. +whose geometry and paint resources are built ahead of time, then updated +through a stable DOM graph. ```bash npm install @layoutit/polycss-morph ``` -Use ordinary `Polygon[]` loading for ordinary meshes. Use Morph when a model -needs a retained leaf graph plus sparse deformation, semantic controls, -springs, joint skinning, or an externally prepared playback stream. +Use the regular `Polygon[]` path when a mesh does not need prepared updates. +Use Morph when a model needs stable DOM elements plus morph targets, semantic +controls, springs, joint skinning, or prepared playback. -## Two explicit entries +## Package entries The Node and browser dependency graphs are separate: -| Entry | Environment | Owns | +| Entry | Environment | Purpose | |---|---|---| -| `@layoutit/polycss-morph/prepare` | Node | Strict config and glTF/GLB intake, normalization, topology, canonical solid-triangle plans, packed fallback pages with one local-size slice per polygon, provenance, content hashes, and atomic package writes. | -| `@layoutit/polycss-morph` | Browser or shared code | Contracts, validation, bounded loading, retained mounting, deformation, controls, springs, animation sampling, joint skinning, and prepared playback. | +| `@layoutit/polycss-morph/prepare` | Node | Turn glTF/GLB sources into deterministic model packages with CSS triangle plans and browser fallbacks. | +| `@layoutit/polycss-morph` | Browser or shared code | Validate, load, mount, and update prepared models. | -The generic preparer directly authors `static-prepared` and `morph-regions` -models. Dedicated tooling or a product adapter can author validated -`joint-skin` and `prepared-playback` models. +The Node preparer creates `static-prepared` and `morph-regions` models. Custom +tooling can create validated `joint-skin` and `prepared-playback` models. ## Prepare @@ -75,7 +77,40 @@ leaf elements as indexed write targets. Morph deduplicates requested values and invalidates the writers at destroy; the application retains DOM teardown ownership. -## Executable profiles +## Example: Cube to sphere + +This 768-polygon model from the +[three.js morph-target example](https://github.com/mrdoob/three.js/blob/7763535f6f944a32105af7d9d57d83a92f310d51/examples/webgl_morphtargets.html) +exposes two prepared targets. **Spherify** moves the cube toward a sphere; +**Swirl** applies the source twist target. Both sliders may be combined while +the same DOM leaves remain mounted. Drag anywhere on the stage to orbit the +model. + + + +The page samples only when a slider changes. Geometry follows the slider +directly; releasing it commits the nearest prepared lighting state with at +most two ancestor custom-property writes: + +```ts +const deformation = createPolyMorphDeformationRuntime(model); +const mounted = mountPolyMorphModel(host, loaded.model, { + resources: loaded.resources, +}); + +function update(spherify: number, twist: number) { + const frame = deformation.sample({ + tick: tick++, + morphWeights: { spherify, twist }, + }); + mounted.apply({ leaves: frame.leafUpdates }); +} +``` + +Morph owns no scheduler. Applications decide whether weights come from direct +input, state, an animation sampler, or a spring. + +## Profiles | Profile | Behavior | |---|---| @@ -84,7 +119,7 @@ ownership. | `joint-skin` | Hierarchical joint transforms and normalized weighted skinning. | | `prepared-playback` | Source-ordered retained model, shape, transform, visibility, opacity, and image-row changes. | -## Runtime invariant +## Runtime behavior Mount once, then sample and apply: @@ -95,16 +130,14 @@ Mount once, then sample and apply: - sparse samples visit only affected leaves; - runtime updates do not reconstruct topology or redraw prepared image resources. -On browsers that pass PolyCSS's solid-triangle support check, Morph uses -`corner-shape` when available and a CSS border triangle otherwise; Firefox uses -the larger border-triangle variant. WebKit/Safari instead uses each leaf's -prepared polygon-sized alpha-atlas slice. Mount uses the loader's verified image -bytes and revokes their object URLs at teardown; the browser never refetches, -generates, or redraws those pages. Dedicated tooling may also author other -image-backed strategies, including prepared-playback consumers. +Morph chooses the triangle paint path once when it mounts. It uses +`corner-shape` where available, a larger CSS border triangle in Firefox, and +each leaf's prepared polygon-sized alpha-atlas slice in WebKit/Safari. Mount +uses the loader's verified image bytes and revokes their object URLs at +teardown; the browser never refetches, generates, or redraws those pages. -## Consumer boundary +## Application ownership -A consuming product keeps its source-specific schemas, cadence, input ordering, -presentation, and oracle tooling. Product adapters own their prepared packages, -mounting paths, presentation, and oracle evidence. +Morph owns the prepared model format and sparse DOM updates. Your application +owns input, timing, presentation, model-specific preparation, and product +behavior. From b9114b7274c715e8e8d695077cf517c95ed02fa5 Mon Sep 17 00:00:00 2001 From: alowpoly Date: Wed, 29 Jul 2026 11:47:17 -0300 Subject: [PATCH 2/2] docs(morph): fix example links --- README.md | 2 +- packages/morph/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 92f281c3..6859c5f9 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ already-verified package bytes, selects the fallback once, and revokes those URLs at teardown; it never refetches, generates, or redraws the atlas. See the [PolyCSS Morph guide](https://polycss.com/guides/morph/), including the -CC0 Animated Morph Sphere example. +interactive cube-to-sphere example. ### Polygon Data Model diff --git a/packages/morph/README.md b/packages/morph/README.md index 9ba6c292..95d18435 100644 --- a/packages/morph/README.md +++ b/packages/morph/README.md @@ -70,8 +70,8 @@ const frame = deformation.sample({ mounted.apply({ leaves: frame.leafUpdates }); ``` -See the CC0 [Animated Morph Sphere](https://polycss.com/guides/morph/#example-animated-morph-sphere) -for a complete rendered animation and deformation example. +See the [cube-to-sphere example](https://polycss.com/guides/morph/#example-cube-to-sphere) +for a complete rendered deformation example. The browser API is intentionally imperative: