Webgl

Substrate
@wabbit/tome-webglv0.7.1

Tome's WebGL/WebGPU layer — shared background canvas, opt-in page transitions, inline experience blocks, and a TSL shader utility library, driven by Payload's block system.

Installnpm install @wabbit/tome-webgl

Overview

@wabbit/tome-webgl

Tome's WebGL/WebGPU layer: shared background canvas, opt-in page transitions, inline experience blocks, and a TSL shader utility library. Consumers register experiences once (React-free), mount a WebglCanvasProvider in their layout, and let Payload drive scene selection through the block system.

Install

pnpm add @wabbit/tome-webgl

| Peer | Range | Notes | |---|---|---| | react / react-dom | >=19.0.0 | | | three | >=0.180 | see the compatibility policy (A7) below | | @react-three/fiber | >=8 | | | @react-three/drei | >=9 | see the compatibility matrix (A5) below | | @react-three/postprocessing | >=2 | optional — 'classic' renderer only | | @react-three/rapier | >=1 | optional | | @react-spring/three | >=9 | optional |

@wabbit/tome-blocks-core is a regular (non-peer) dependency.

Public API (subpaths)

| Subpath | Purpose | React-free? | |---|---|---| | . | Root — WebglCanvasProvider, WebglBackgroundCanvas, WebglInlineBlock, WebGLHero (full-bleed hero, generalized from bickley-site-core's WebGLHero). Runtime R3F components. | No | | . (hooks) | useExperienceCanvas (resolved canvas policy/registry/coordination API), useActiveBackground (active background key + transition progress), useOptionalWebglCanvasContext (context value or null outside a provider), useCanvasInteractive (whether the background canvas is interactive on the current route), useTransitionPhase (+ TransitionPhase/TransitionRole types — the per-frame transition-phase channel, read via a ref inside useFrame, not as a render value) | No | | ./registry | createExperienceRegistry, ExperienceMeta, WebglCanvasPolicy. No React / three / R3F imports — safe in payload.config.ts. | Yes | | ./block | createWebglInlineBlock — Payload block factory; createTransitionField — React-free Payload select field for an admin-set transition name (built-ins always included, plus any extra custom names). No React imports — safe in payload.config.ts. | Yes | | ./transitions | TransitionDefinition, createTransitionController, builtInTransitions (GLSL set), registerTransition. Client-only ('use client'). | No | | ./tsl | builtInTransitionsTSL (node-shader set) + TSL helper Fns. Imports three/tsl — client/runtime only, never from payload.config.ts. | No | | ./fallback | useWebglSupported (WebGL2/WebGL1 availability, optimistic on SSR), useReducedMotion (prefers-reduced-motion live), ExperiencePoster (static poster component shown when WebGL is unavailable or reduced-motion is active — consumer supplies the poster src, the layer provides the layout wrapper). | No |

./registry and ./block are the two subpaths safe to import inside payload.config.ts or any server-only module. Everything else requires a client boundary.

---

Renderer decision tree (A6)

WebglCanvasPolicy.renderer controls the WebGL/WebGPU backend for the shared background canvas. Inline experiences (WebglInlineBlock) manage their own <Canvas> and are not affected.

| Value | Backend | TSL support | Browser support | |---|---|---|---| | 'classic' (default) | THREE.WebGLRenderer (R3F default) | None — NodeMaterial throws | Same as three.js baseline | | 'node-webgl' | THREE.WebGPURenderer with forceWebGL: true | Full — TSL/NodeMaterial compiles | Same as classic (WebGL2) | | 'node-webgpu' | THREE.WebGPURenderer auto-detect | Full — runs native WebGPU, falls back to WebGL2 | WebGPU browsers + WebGL2 fallback |

Decision:

  • Not using TSL → 'classic'.
  • Any shared-canvas scene uses TSL (including builtInTransitionsTSL) → 'node-webgl'.
  • Running TSL in production and ready to validate native WebGPU → 'node-webgpu'. Validate node-webgl through a full release cycle first — adapter-request timing and tab-startup behavior have documented caveats at 0.6.0.

`autoUpgradeRendererForTsl` (default false): when false, mounting a TSL transition while renderer === 'classic' throws an actionable error — the misconfiguration is surfaced immediately and cannot silently no-op. Set true to consent to an implicit coerce to node-webgl at runtime (and the WebGPU bundle pull that entails) with a console warning.

Postprocessing: policy.postprocessing is only supported on 'classic'. Mounting @react-three/postprocessing against a node renderer throws — the EffectComposer is WebGLRenderer-bound. Native node-renderer postprocessing via three's RenderPipeline is tracked for 0.7.x.

// payload-side — React-free
import { createExperienceRegistry } from '@wabbit/tome-webgl/registry'
import type { WebglCanvasPolicy } from '@wabbit/tome-webgl/registry'

const policy: WebglCanvasPolicy = {
  renderer: 'node-webgl', // any scene uses TSL
  autoUpgradeRendererForTsl: false,
  maxDpr: 1.5,
  singleLiveCanvas: true,
}

---

Authoring a TSL transition (A6)

TransitionDefinition is a discriminated union. The GLSL arm compiles to THREE.ShaderMaterial (works on any renderer); the TSL arm compiles to a node material (requires 'node-webgl' or 'node-webgpu').

import type { TransitionDefinition, TransitionNodeContext } from '@wabbit/tome-webgl/transitions'
import { vec4, float, abs } from 'three/tsl'
import type { Node } from 'three/webgpu' // type-only; erased at runtime

// GLSL arm — unchanged from 0.4.x
const glslFade: TransitionDefinition = {
  name: 'my-fade',
  fragmentShader: /* glsl */ `
    uniform vec3 uColor;
    uniform float uProgress;
    varying vec2 vUv;
    void main() {
      float a = 1.0 - abs(2.0 * uProgress - 1.0);
      gl_FragColor = vec4(uColor, a);
    }
  `,
}

// TSL arm — new in 0.5.0
const tslFade: TransitionDefinition = {
  name: 'my-fade-tsl',
  nodeShader: ({ color, progress }: TransitionNodeContext): Node => {
    // ctx nodes: color (vec3), progress (float 0..1), resolution (vec2 px), uv (vec2)
    // Must return vec4(color, alpha) where alpha is a cover mask peaking at progress 0.5.
    const a: any = float(1).sub((progress as any).mul(2).sub(1).abs())
    return vec4(color as any, a) as any
  },
}

The curtain model is identical across both arms (v0.3.1, OPAQUE): progress 0 = no curtain (outgoing visible), progress 0.5 = full cover (scene swap happens here), progress 1 = no curtain (incoming revealed). Alpha must peak at 1.0 when progress === 0.5.

`builtInTransitionsTSL` (from ./tsl) provides ready-made TSL equivalents of the six built-in GLSL transitions: fade, slide-left, slide-right, slide-up, slide-down, spiral. Pass them as the transitions map in createTransitionController:

import { createTransitionController } from '@wabbit/tome-webgl/transitions'
import { builtInTransitionsTSL } from '@wabbit/tome-webgl/tsl'

const controller = createTransitionController({
  transitions: { ...builtInTransitionsTSL },
  default: 'spiral',
  durationMs: 600,
})

// on route change:
await controller.trigger(nextExperienceKey)           // default transition
await controller.trigger(nextExperienceKey, 'fade')   // explicit

TSL helpers in `./tsl`: the subpath also exports React-free node-graph helpers ported from bickley-site-core for use inside inline experiences: curlNoise / snoiseVec3 (particle noise), voronoi3D, scaffoldFloat, and lighting builders wrappedDiffuseTwoLight / blendVolumeNormal / fresnelRim / pulseEnvelope / pulseWavefront. These are three/tsl Fn wrappers — import them at client/runtime only.

---

drei compatibility matrix (A5)

A node renderer (WebGPURenderer, either mode) is not a THREE.WebGLRenderer. Several @react-three/drei helpers assume WebGLRenderer internals and break or silently degrade. Everything @wabbit/tome-webgl itself depends on — controls, useProgress, useGLTF, Html — is safe on all renderer modes.

| Helper | classic | node-webgl | node-webgpu | Note | |---|---|---|---|---| | useProgress, useGLTF, <Html> | ✅ | ✅ | ✅ | No renderer contact (loaders / DOM). | | OrbitControls, CameraControls, KeyboardControls / useKeyboardControls | ✅ | ✅ | ✅ | Input/control only; renderer-agnostic. | | useFBO | ✅ | ✅ | ✅ | WebGLRenderTarget extends RenderTarget; node renderers accept it. | | <Text> (troika) | ✅ | ✅ | ⚠️ | SDF ShaderMaterial → under node-webgpu logs "Material ShaderMaterial is not compatible" → may render invisible. Confirm at runtime. | | <Environment> | ✅ | ❌ | ❌ | Sync readRenderTargetPixels is WebGLRenderer-only (env-map processing). | | <Sky> | ✅ | ❌ | ❌ | Raw ShaderMaterial → not in the node material library. | | <MeshReflectorMaterial> | ✅ | ❌ | ❌ | Per-frame gl.state.buffers.depth.setMask() — gl.state (WebGLState) doesn't exist on the base Renderer. | | <Caustics> | ✅ | ❌ | ❌ | ShaderMaterial-based materials → not compatible. | | <AccumulativeShadows> / <RandomizedLight> | ✅ | ❌ | ❌ | SoftShadowMaterial (ShaderMaterial) + onBeforeCompile lightmap — both unsupported. | | <Cloud> / <Clouds> | ✅ | ⚠️ | ❌ | onBeforeCompile opacity injection never runs on node renderers → flat sprites; custom ShaderMaterial path → breaks. | | <MeshTransmissionMaterial> | ✅ | ⚠️ | ⚠️ | MeshPhysical-based (no compat error) but onBeforeCompile transmission/chromatic GLSL is ignored → renders as plain MeshPhysical. | | <Preload> | ✅ | ⚠️ | ⚠️ | gl.compile() is async (returns a Promise) on node renderers; drei doesn't await → precompile silently no-ops. |

Root causes:

  • Sync readRenderTargetPixels — WebGLRenderer-only method; absent on base Renderer.
  • gl.state (WebGLState) — not present on the base Renderer interface.
  • onBeforeCompile — never called by the node Renderer (material compilation goes through the node pipeline, not the legacy hook).
  • Raw ShaderMaterial — not in the node material library; the renderer cannot compile it.
  • gl.compile() — returns a Promise on node renderers, not void; callers that don't await silently no-op.

Items marked ⚠️ need runtime confirmation (tracked by the 0.6.0 render-parity harness, A4).

---

Three.js compatibility policy (A7)

Peer floor: three >= 0.180 (minimum version exporting the TSL/MaterialX surface required by this package). The package is developed and tested against three / @types/three 0.183. The three/tsl API moves per-minor — consumers should stay within one minor of the tested version (0.183.x). @types/three must match the installed three minor exactly; a mismatch produces type errors that do not reflect runtime behavior.

Exports

  • @wabbit/tome-webgl
  • @wabbit/tome-webgl/block
  • @wabbit/tome-webgl/registry
  • @wabbit/tome-webgl/transitions
  • @wabbit/tome-webgl/tsl
  • @wabbit/tome-webgl/fallback

Changelog

v0.7.1patch

Updated dependencies [404d325] - @wabbit/tome-blocks-core@0.17.0

  • Updated dependencies [404d325] - @wabbit/tome-blocks-core@0.17.0
v0.7.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • 0836ef5: dist now raw-Node loadable: relative specifiers get explicit extensions post-build. `build` gains `&& node ../../scripts/fix-dist-extensions.mjs --strict` as its last step, joining the 13 packages that already ran it. tsup builds `bundle: false` and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM `ERR_MODULE_NOT_FOUND`; CJS worse, `require('./x')` finds the ESM `.js` twin and Node 22+ `require(esm)` then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, `generate:types`, `generate:importmap`, ops scripts, codegen tools. No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable. Two supporting changes made the wiring possible, both in repo scripts rather than package source. `fix-dist-extensions.mjs` now skips bundler-asset specifiers (`.css`, `.module.css`, `.scss`, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since `--strict` exited 1 on any package with a relative stylesheet import. Dotted MODULE names (`./config.meta`, `./x.variants`, `./y.demo`) are deliberately NOT treated as assets and still get `.js`/`.cjs` appended. `assert-node-loadable.mjs` gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under `node_modules` is a peer SKIP (next@15 has no exports map, so `next/image` fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as `SyntaxError: Unexpected token '.'` raised from inside the stylesheet). Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a `directory import` missing `/index`). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.
  • 73081e6: Manifest metadata: `homepage`, `bugs`, `engines`. All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change. - `homepage` deep-links to that package README on GitHub (`.../tree/main/packages/<dir>#readme`). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want. - `bugs.url` points at the repo issue tracker, so a paying customer has a place to report a defect that is not email. - `engines.node` is `>=22`, matching the root `engines` and `.nvmrc` set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for `node --test`, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version. The forcing function ships with the change: `scripts/assert-manifest-metadata.mjs` (root `pnpm assert:manifest-metadata`, wired into `platform-discipline.yml` beside `assert:license-metadata`) fails when any publishable manifest lacks `description`, `repository.directory` matching its own folder, `homepage`, `bugs`, `engines.node` equal to the repo floor, `license`, `files` or `sideEffects`. It reported 138 violations before this change and 0 after.
  • Updated dependencies [57875ba]
  • Updated dependencies [b01ca1f]
  • Updated dependencies [0836ef5]
  • Updated dependencies [73081e6]
  • Updated dependencies [090e984]
  • Updated dependencies [73081e6] - @wabbit/tome-blocks-core@0.16.0
v0.6.13patch

Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.

  • Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.
v0.6.12patch

Updated dependencies [510036f] - @wabbit/tome-blocks-core@0.15.0

  • Updated dependencies [510036f] - @wabbit/tome-blocks-core@0.15.0
v0.6.11patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.6.10patch

Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0

  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.6.9patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.6.8patch

36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.

  • 36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.
  • aef2725: Monolith decompositions (behavior- and markup-preserving; public APIs unchanged; markup identity mechanically verified per file): forms' FieldRenderer 633→84 via a field-control registry + shared FieldChrome (consent/checkbox byte-identical branches merged) and TomeForm 656→451 via four extracted hooks (the ordering-critical resolver sync deliberately stays inline, documented); rpg's CharacterSheet 841→130 across panels + three editing hooks + persistence hook (the StrictMode XP-ledger charRef guard preserved verbatim); gallery's GalleryIndex 1032→431 (BlockThumb/BlockCard/Toolbar/useFilteredCatalog siblings, T2's debounce+memo preserved); webgl's WebglCanvasProvider 938→546 (useTransitionOrchestrator + useCanvasRenderer extracted; settle thresholds hoisted to named consts); admin's mergeAdminComponents 828→404 orchestrator + four helpers (all docblocks relocated, 717 tests unmodified) and Nav's config-reading now typed (6 of 8 `as any` casts eliminated); marketing-starter's PricingPlans extracts its GSAP toggle timeline hook + a memoized card. rpg additionally trusts the denormalized `xpTotal` on sheet load/save hot paths (full recompute stays at the XP-recording reconciliation point).
  • Updated dependencies [26dfa07]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [a93f478]
  • Updated dependencies [5f78397]
  • Updated dependencies [5f78397]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0
v0.6.7patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.6.6patch

bed3f90: Docs-manifest emitter pipeline (W3 ship-readiness). `@wabbit/tome-blocks-core` now ships a standalone Node ESM CLI at `scripts/emit-docs-manifests.mjs` that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free `<pkg>/meta` block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: `node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>`. To let the emitter import block metadata uniformly without dragging Payload config into a build script, the `./meta` payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's `BlockMeta` was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed. Every supported-core package additionally adds `CHANGELOG.md` to its published `files` array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.

  • bed3f90: Docs-manifest emitter pipeline (W3 ship-readiness). `@wabbit/tome-blocks-core` now ships a standalone Node ESM CLI at `scripts/emit-docs-manifests.mjs` that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free `<pkg>/meta` block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: `node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>`. To let the emitter import block metadata uniformly without dragging Payload config into a build script, the `./meta` payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's `BlockMeta` was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed. Every supported-core package additionally adds `CHANGELOG.md` to its published `files` array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4
v0.6.5patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.6.4patch

Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0

  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.6.3patch

Updated dependencies [249b670] - @wabbit/tome-blocks-core@0.8.0

  • Updated dependencies [249b670] - @wabbit/tome-blocks-core@0.8.0
v0.6.2patch

Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0

  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.6.1patch

Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2
v0.6.0minor

feat(webgl): renderer robustness + TSL/WebGPU hardening (0.6.0) Hardens the node-renderer path introduced for TSL and proves TSL↔GLSL parity. Additive and non-breaking — `classic` + GLSL stays the default; existing consumers are unaffected. - **WebGPU adapter init hardening** — `node-webgpu` now races `renderer.init()` against a timeout and, on adapter-request hang/rejection, downgrades to a WebGL2 (`forceWebGL:true`) renderer with a one-time warn instead of hanging the canvas. `node-webgl` stays deterministic. - **postprocessing hard-gate** — `policy.postprocessing` under a node renderer now throws an actionable error (`@react-three/postprocessing` is WebGL-bound and crashes on `WebGPURenderer`). The flag was previously inert, so no consumer breaks. Native node-renderer postprocessing via three's `RenderPipeline` is tracked for 0.7.x. - **renderer-init fallback** — a new `RendererInitBoundary` renders a static poster if all renderer paths fail, instead of a blank/crashed canvas. - **docs** — a README with the renderer decision tree (`classic`/`node-webgl`/`node-webgpu`), a TSL transition authoring guide, and a `@react-three/drei` compatibility matrix under node renderers (`<Environment>`/`<Sky>`/reflectors break; controls/`useProgress`/`useGLTF`/`Html` are safe). `node-webgpu` remains "validate `node-webgl` in production first." TSL↔GLSL render parity for the built-in spiral was verified pixel-identical on the WebGL2/`node-webgl` path (render-parity harness in `test/parity/`).

  • feat(webgl): renderer robustness + TSL/WebGPU hardening (0.6.0) Hardens the node-renderer path introduced for TSL and proves TSL↔GLSL parity. Additive and non-breaking — `classic` + GLSL stays the default; existing consumers are unaffected. - **WebGPU adapter init hardening** — `node-webgpu` now races `renderer.init()` against a timeout and, on adapter-request hang/rejection, downgrades to a WebGL2 (`forceWebGL:true`) renderer with a one-time warn instead of hanging the canvas. `node-webgl` stays deterministic. - **postprocessing hard-gate** — `policy.postprocessing` under a node renderer now throws an actionable error (`@react-three/postprocessing` is WebGL-bound and crashes on `WebGPURenderer`). The flag was previously inert, so no consumer breaks. Native node-renderer postprocessing via three's `RenderPipeline` is tracked for 0.7.x. - **renderer-init fallback** — a new `RendererInitBoundary` renders a static poster if all renderer paths fail, instead of a blank/crashed canvas. - **docs** — a README with the renderer decision tree (`classic`/`node-webgl`/`node-webgpu`), a TSL transition authoring guide, and a `@react-three/drei` compatibility matrix under node renderers (`<Environment>`/`<Sky>`/reflectors break; controls/`useProgress`/`useGLTF`/`Html` are safe). `node-webgpu` remains "validate `node-webgl` in production first." TSL↔GLSL render parity for the built-in spiral was verified pixel-identical on the WebGL2/`node-webgl` path (render-parity harness in `test/parity/`).
v0.5.0minor

feat(webgl): TSL-variant mode (dual-mode transitions + `./tsl` subpath) Additive, non-breaking TSL lane alongside the GLSL/`classic` default. Existing consumers are unaffected until they opt in. - **`./transitions`** — `TransitionDefinition` becomes a discriminated union: the GLSL `fragmentShader` arm is source-compatible with every existing definition, plus a new TSL `nodeShader: (ctx: TransitionNodeContext) => Node` arm. Adds the `TransitionNodeContext` / `TransitionNodeBuilder` contract. - **`./registry`** — `WebglCanvasPolicy` gains `autoUpgradeRendererForTsl`; `node-webgpu` promoted from "reserved" to supported (validate `node-webgl` in production first). - **runtime** — `TransitionCompositor` branches on the resolved definition: GLSL builds `THREE.ShaderMaterial` unchanged; TSL lazily imports `three/webgpu` and builds a `NodeMaterial` `colorNode`. `WebglCanvasProvider` enforces the renderer auto-upgrade contract — a TSL transition under `renderer: 'classic'` throws an actionable error by default, or coerces to `node-webgl` (with a one-time warn) when `autoUpgradeRendererForTsl` is set. Never a silent no-op or GLSL fallback. - **`./tsl`** (new subpath) — React-free TSL `Fn` helper library (`curlNoise`/`snoiseVec3`, `voronoi3D`, `scaffoldFloat`, `wrappedDiffuseTwoLight`/`blendVolumeNormal`/`fresnelRim`/`pulseEnvelope`/`pulseWavefront`) plus `builtInTransitionsTSL` node-builders for the six curtains. Imports `three/tsl` only; never touches React. - **peer** — `three` floor raised `>=0.171` → `>=0.180` (the floor that exports the required TSL/MaterialX surface). `three/webgpu` + `three/tsl` are dynamic-import-only, so the `classic` GLSL path never pulls the WebGPU bundle.

  • feat(webgl): TSL-variant mode (dual-mode transitions + `./tsl` subpath) Additive, non-breaking TSL lane alongside the GLSL/`classic` default. Existing consumers are unaffected until they opt in. - **`./transitions`** — `TransitionDefinition` becomes a discriminated union: the GLSL `fragmentShader` arm is source-compatible with every existing definition, plus a new TSL `nodeShader: (ctx: TransitionNodeContext) => Node` arm. Adds the `TransitionNodeContext` / `TransitionNodeBuilder` contract. - **`./registry`** — `WebglCanvasPolicy` gains `autoUpgradeRendererForTsl`; `node-webgpu` promoted from "reserved" to supported (validate `node-webgl` in production first). - **runtime** — `TransitionCompositor` branches on the resolved definition: GLSL builds `THREE.ShaderMaterial` unchanged; TSL lazily imports `three/webgpu` and builds a `NodeMaterial` `colorNode`. `WebglCanvasProvider` enforces the renderer auto-upgrade contract — a TSL transition under `renderer: 'classic'` throws an actionable error by default, or coerces to `node-webgl` (with a one-time warn) when `autoUpgradeRendererForTsl` is set. Never a silent no-op or GLSL fallback. - **`./tsl`** (new subpath) — React-free TSL `Fn` helper library (`curlNoise`/`snoiseVec3`, `voronoi3D`, `scaffoldFloat`, `wrappedDiffuseTwoLight`/`blendVolumeNormal`/`fresnelRim`/`pulseEnvelope`/`pulseWavefront`) plus `builtInTransitionsTSL` node-builders for the six curtains. Imports `three/tsl` only; never touches React. - **peer** — `three` floor raised `>=0.171` → `>=0.180` (the floor that exports the required TSL/MaterialX surface). `three/webgpu` + `three/tsl` are dynamic-import-only, so the `classic` GLSL path never pulls the WebGPU bundle.
v0.4.1patch

8f58d28: fix(webgl): hoist node-renderer `useMemo` above early-return in `WebglBackgroundCanvas` 0.4.0 placed the new `glFactory` `useMemo` below the existing `if (!activeBackground && !isTransitioning) return null` guard. When the canvas mounted empty (no active scene yet) and then a scene arrived, hook count went 4 → 5 and React killed the tree with `Rendered more hooks than during the previous render` — observed on bickley-site-core first-paint with `policy={{ renderer: 'node-webgl' }}`. Fix: move the `useMemo` above the early return so the hook order is invariant across both paths. No behavior change — the factory was already memoized on `policy.renderer`. Inline comment added to the call site so the next contributor doesn't reintroduce the regression.

  • 8f58d28: fix(webgl): hoist node-renderer `useMemo` above early-return in `WebglBackgroundCanvas` 0.4.0 placed the new `glFactory` `useMemo` below the existing `if (!activeBackground && !isTransitioning) return null` guard. When the canvas mounted empty (no active scene yet) and then a scene arrived, hook count went 4 → 5 and React killed the tree with `Rendered more hooks than during the previous render` — observed on bickley-site-core first-paint with `policy={{ renderer: 'node-webgl' }}`. Fix: move the `useMemo` above the early return so the hook order is invariant across both paths. No behavior change — the factory was already memoized on `policy.renderer`. Inline comment added to the call site so the next contributor doesn't reintroduce the regression.
v0.4.0minor

7b07215: feat(webgl): opt-in node-aware renderer via `policy.renderer` `WebglCanvasPolicy` gains a `renderer` field. Default `'classic'` preserves current behavior (R3F default `THREE.WebGLRenderer`). New `'node-webgl'` opts the shared background canvas into `THREE.WebGPURenderer` with `forceWebGL: true` — required for TSL / `NodeMaterial` consumers. Hardware path stays WebGL2, so browser support is unchanged. A future `'node-webgpu'` value is reserved for 0.5.x once adapter-request timing + WebGPU detection bootstrap are handled in the provider. The WebGPU bundle (`three/webgpu`) is dynamically imported inside the gl factory, so consumers on `'classic'` pay no bundle cost. Inline experiences (`WebglInlineBlock`) bring their own `<Canvas>` and pick their own renderer — this change only affects the shared background canvas. Rationale: consumer-side TSL migrations (e.g. bickley-site-core's The Entity stack) were blocked because `THREE.WebGLRenderer` has no node-aware path. Three's only node-aware renderer is `WebGPURenderer`, which transparently runs on either WebGPU or WebGL2 backends. Shipping this opt-in unblocks consumer TSL work without forcing the change on consumers that aren't ready.

  • 7b07215: feat(webgl): opt-in node-aware renderer via `policy.renderer` `WebglCanvasPolicy` gains a `renderer` field. Default `'classic'` preserves current behavior (R3F default `THREE.WebGLRenderer`). New `'node-webgl'` opts the shared background canvas into `THREE.WebGPURenderer` with `forceWebGL: true` — required for TSL / `NodeMaterial` consumers. Hardware path stays WebGL2, so browser support is unchanged. A future `'node-webgpu'` value is reserved for 0.5.x once adapter-request timing + WebGPU detection bootstrap are handled in the provider. The WebGPU bundle (`three/webgpu`) is dynamically imported inside the gl factory, so consumers on `'classic'` pay no bundle cost. Inline experiences (`WebglInlineBlock`) bring their own `<Canvas>` and pick their own renderer — this change only affects the shared background canvas. Rationale: consumer-side TSL migrations (e.g. bickley-site-core's The Entity stack) were blocked because `THREE.WebGLRenderer` has no node-aware path. Three's only node-aware renderer is `WebGPURenderer`, which transparently runs on either WebGPU or WebGL2 backends. Shipping this opt-in unblocks consumer TSL work without forcing the change on consumers that aren't ready.
v0.3.2patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
v0.3.1patch

fab6fac: Opaque-curtain transition (replaces the FBO cross-blend that produced squashed spirals + camera jumps + no load-gate flash). Single live scene, hidden swap at full curtain cover, load-gated hold (Suspense done + drei `useProgress` idle + camera-idle via `SettleWatch`), reveal-on-direct-load (no outgoing → start at full cover, hold for load, uncover). Provider gains `pendingTargetRef` + `commitSwap` + `markIncomingReady` plumbing with an idempotency guard against legacy-store re-fires. Shaders rewritten to `uColor` + alpha (no scene textures); spiral now aspect-corrected. `TransitionDefinition` shape unchanged; `TransitionCompositorProps` adds `color`, `readyRef`, `revealOnly`. 34 tests green.

  • fab6fac: Opaque-curtain transition (replaces the FBO cross-blend that produced squashed spirals + camera jumps + no load-gate flash). Single live scene, hidden swap at full curtain cover, load-gated hold (Suspense done + drei `useProgress` idle + camera-idle via `SettleWatch`), reveal-on-direct-load (no outgoing → start at full cover, hold for load, uncover). Provider gains `pendingTargetRef` + `commitSwap` + `markIncomingReady` plumbing with an idempotency guard against legacy-store re-fires. Shaders rewritten to `uColor` + alpha (no scene textures); spiral now aspect-corrected. `TransitionDefinition` shape unchanged; `TransitionCompositorProps` adds `color`, `readyRef`, `revealOnly`. 34 tests green.
v0.3.0minor

a03fbdc: Add `useTransitionPhase()` — the hybrid choreography channel for background experiences. Pure FBO (0.2.0) cross-blends two static scene images. The hybrid model also lets each background scene choreograph its OWN entrance/exit _while_ it is being composited. During a transition the `TransitionCompositor` now publishes the live eased progress plus each scene's role (`'from'` outgoing / `'to'` incoming) through a React context; a scene reads it via `useTransitionPhase()` inside `useFrame` (a ref, never a render value — no per-frame React re-render) and animates its objects, while the transition shader blends the two animating images. - New export `useTransitionPhase()` returning `{ role: 'from' | 'to' | null, progress: MutableRefObject<number> }`. At rest: `{ role: null, progress.current === 1 }`. - New exported types `TransitionPhase`, `TransitionRole`. - Backward-compatible: a scene that ignores the hook renders static (pure-FBO behavior, unchanged from 0.2.0). The `BackgroundExperienceProps.transitionProgress` prop stays the coarse signal; the hook is the live channel. The choreography itself is consumer-owned (per-scene entrance/exit + which transitions per route are author decisions); the platform supplies the channel. Also in this release: - **Per-scene camera in the FBO compositor** — background experiences mount their own camera (e.g. drei `<PerspectiveCamera makeDefault>`); the compositor now renders each sub-scene to its target with ITS OWN camera (falls back to the main camera), so a scene's intended framing is preserved during a transition. - **Interactivity mode (mixed-by-route)** — new `interactive` flag + `setInteractive()` on the canvas context, a `useCanvasInteractive()` hook, and the background canvas now toggles `pointer-events` accordingly. A consumer's route director can make the global canvas interactive on showcase routes (controls + pointer events) and a passive backdrop elsewhere. Default false (passive) — unchanged from prior behavior.

  • a03fbdc: Add `useTransitionPhase()` — the hybrid choreography channel for background experiences. Pure FBO (0.2.0) cross-blends two static scene images. The hybrid model also lets each background scene choreograph its OWN entrance/exit _while_ it is being composited. During a transition the `TransitionCompositor` now publishes the live eased progress plus each scene's role (`'from'` outgoing / `'to'` incoming) through a React context; a scene reads it via `useTransitionPhase()` inside `useFrame` (a ref, never a render value — no per-frame React re-render) and animates its objects, while the transition shader blends the two animating images. - New export `useTransitionPhase()` returning `{ role: 'from' | 'to' | null, progress: MutableRefObject<number> }`. At rest: `{ role: null, progress.current === 1 }`. - New exported types `TransitionPhase`, `TransitionRole`. - Backward-compatible: a scene that ignores the hook renders static (pure-FBO behavior, unchanged from 0.2.0). The `BackgroundExperienceProps.transitionProgress` prop stays the coarse signal; the hook is the live channel. The choreography itself is consumer-owned (per-scene entrance/exit + which transitions per route are author decisions); the platform supplies the channel. Also in this release: - **Per-scene camera in the FBO compositor** — background experiences mount their own camera (e.g. drei `<PerspectiveCamera makeDefault>`); the compositor now renders each sub-scene to its target with ITS OWN camera (falls back to the main camera), so a scene's intended framing is preserved during a transition. - **Interactivity mode (mixed-by-route)** — new `interactive` flag + `setInteractive()` on the canvas context, a `useCanvasInteractive()` hook, and the background canvas now toggles `pointer-events` accordingly. A consumer's route director can make the global canvas interactive on showcase routes (controls + pointer events) and a passive backdrop elsewhere. Default false (passive) — unchanged from prior behavior.
v0.2.0minor

3ab7144: `@wabbit/tome-webgl/transitions` ships its first real render-to-target (FBO) implementation — the module is no longer a documented stub. What changed (see spec amendment A1, 2026-05-25): - **`registerTransition`** now writes to a module-level registry that the compositor resolves against (was a no-op `void definition`). Built-ins seed it; consumer registrations and per-controller `transitions` config merge over them. - **`createTransitionController`** is real: `trigger(toKey, name?)` routes a scene change through the mounted provider's FBO compositor and resolves when the transition completes (or is skipped — first scene / reduced motion / no canvas mounted). `list()` returns built-in ∪ registered ∪ config. - **`TransitionCompositor`** (new, internal to the runtime) renders the outgoing and incoming background scenes to two render targets via `createPortal`, then composites them with the active transition's fragment shader (`uFrom`/`uTo`/`uProgress`) on a clip-space full-screen quad. It mounts only during the transition window; steady state renders the single active scene directly, so the dual-render cost is bounded. - **`transitionProgress`** is frame-driven: the smooth `0→1` value lives in a ref advanced in `useFrame` and written straight to the shader uniform (no per-frame React re-render). The `BackgroundExperienceProps.transitionProgress` prop contract is preserved but coarse (`0` at start, `1` at completion). - **`singleLiveCanvas`** is now enforced (was stored-never-read): the background canvas pauses (`frameloop='never'`) when the document is hidden or no scene is active, and a new `claimLiveCanvas`/`releaseLiveCanvas` API on the provider lets an in-view inline canvas pause the background. `WebglInlineBlock` claims it automatically when mounted under a provider (graceful no-op without one). - **Reduced motion:** when `respectReducedMotion` is on and the user prefers reduced motion, transitions are skipped (the new scene swaps in directly). - New export: `useOptionalWebglCanvasContext` (non-throwing context read). New provider prop: `transition?: { enabled?, default?, durationMs? }`. Admin-configurable transitions (per-page + per-route, editor-controlled): - **`createTransitionField(opts?)`** (`@wabbit/tome-webgl/block`, React-free) — a Payload `select` field pre-populated with the built-in transition names (+ any `extra` custom names), with an `Auto` option. Registry-injection pattern like `experienceKey`. Drop it on a Page collection (per-page override) or inside a global route-map array. - **`resolveTransition({ pageTransition, routeMap, toPath, directional, fallback })`** + **`matchRouteTransition`** (`@wabbit/tome-webgl/transitions`, pure) — resolve a transition with precedence **per-page override → global route map → directional default → fallback** (`'auto'`/null = unset; route map supports exact + `prefix/*` longest-wildcard match). The directional default is consumer-supplied (sibling order / route depth is consumer route data), keeping the layer generic. - New React-free constants `BUILT_IN_TRANSITION_NAMES` / `AUTO_TRANSITION` (`@wabbit/tome-webgl/transitions`). Non-breaking: the v0 inline-block + background contract is unchanged; `setActiveBackground(key)` keeps its signature (now optionally animates). The FBO composite's visual correctness is gated at the proving consumer (bickley + Playwright), per amendment A1 — WebGL cannot render in node, so the in-package gate covers the pure logic (registry, controller, easing, progress math).

  • 3ab7144: `@wabbit/tome-webgl/transitions` ships its first real render-to-target (FBO) implementation — the module is no longer a documented stub. What changed (see spec amendment A1, 2026-05-25): - **`registerTransition`** now writes to a module-level registry that the compositor resolves against (was a no-op `void definition`). Built-ins seed it; consumer registrations and per-controller `transitions` config merge over them. - **`createTransitionController`** is real: `trigger(toKey, name?)` routes a scene change through the mounted provider's FBO compositor and resolves when the transition completes (or is skipped — first scene / reduced motion / no canvas mounted). `list()` returns built-in ∪ registered ∪ config. - **`TransitionCompositor`** (new, internal to the runtime) renders the outgoing and incoming background scenes to two render targets via `createPortal`, then composites them with the active transition's fragment shader (`uFrom`/`uTo`/`uProgress`) on a clip-space full-screen quad. It mounts only during the transition window; steady state renders the single active scene directly, so the dual-render cost is bounded. - **`transitionProgress`** is frame-driven: the smooth `0→1` value lives in a ref advanced in `useFrame` and written straight to the shader uniform (no per-frame React re-render). The `BackgroundExperienceProps.transitionProgress` prop contract is preserved but coarse (`0` at start, `1` at completion). - **`singleLiveCanvas`** is now enforced (was stored-never-read): the background canvas pauses (`frameloop='never'`) when the document is hidden or no scene is active, and a new `claimLiveCanvas`/`releaseLiveCanvas` API on the provider lets an in-view inline canvas pause the background. `WebglInlineBlock` claims it automatically when mounted under a provider (graceful no-op without one). - **Reduced motion:** when `respectReducedMotion` is on and the user prefers reduced motion, transitions are skipped (the new scene swaps in directly). - New export: `useOptionalWebglCanvasContext` (non-throwing context read). New provider prop: `transition?: { enabled?, default?, durationMs? }`. Admin-configurable transitions (per-page + per-route, editor-controlled): - **`createTransitionField(opts?)`** (`@wabbit/tome-webgl/block`, React-free) — a Payload `select` field pre-populated with the built-in transition names (+ any `extra` custom names), with an `Auto` option. Registry-injection pattern like `experienceKey`. Drop it on a Page collection (per-page override) or inside a global route-map array. - **`resolveTransition({ pageTransition, routeMap, toPath, directional, fallback })`** + **`matchRouteTransition`** (`@wabbit/tome-webgl/transitions`, pure) — resolve a transition with precedence **per-page override → global route map → directional default → fallback** (`'auto'`/null = unset; route map supports exact + `prefix/*` longest-wildcard match). The directional default is consumer-supplied (sibling order / route depth is consumer route data), keeping the layer generic. - New React-free constants `BUILT_IN_TRANSITION_NAMES` / `AUTO_TRANSITION` (`@wabbit/tome-webgl/transitions`). Non-breaking: the v0 inline-block + background contract is unchanged; `setActiveBackground(key)` keeps its signature (now optionally animates). The FBO composite's visual correctness is gated at the proving consumer (bickley + Playwright), per amendment A1 — WebGL cannot render in node, so the in-package gate covers the pure logic (registry, controller, easing, progress math).
  • @wabbit/tome-blocks-core@0.5.7