Documentation menu

Documentation / Realtime & 3D / webgl

Webgl

Package
@wabbit/tome-webgl
Version
v0.8.4
npm install @wabbit/tome-webgl
First install from our registry?
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
Realtime & 3DStable

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.

Overview

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.

Peer dependencies

PeerRangeNotes
react / react-dom>=19.0.0
three>=0.180see the compatibility policy below
@react-three/fiber>=8
@react-three/drei>=9see the compatibility matrix below
@react-three/postprocessing>=2optional — 'classic' renderer only
@react-three/rapier>=1optional
@react-spring/three>=9optional
@wabbit/tome-blocks-core>=0.16.6 <1.0.0optional — type-only, for @wabbit/tome-webgl/block

Public API (subpaths)

SubpathPurposeReact-free?
.Root — WebglCanvasProvider, WebglBackgroundCanvas, WebglInlineBlock, WebGLHero (full-bleed hero). Runtime R3F components. The root also re-exports createExperienceRegistry and the registry types (ExperienceMode, InlineExperienceProps, BackgroundExperienceProps, ExperienceMeta, ExperienceRegistry, WebglCanvasPolicy) and the @wabbit/tome-webgl/fallback hooks useWebglSupported and useReducedMotion, so a client component needs one import.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
@wabbit/tome-webgl/registrycreateExperienceRegistry, ExperienceMeta, WebglCanvasPolicy. No React / three / R3F imports — safe in payload.config.ts.Yes
@wabbit/tome-webgl/blockcreateWebglInlineBlock — 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
@wabbit/tome-webgl/transitionscreateTransitionController(config?) ({ trigger(toKey, name?), list() }; default fade, 600 ms), builtInTransitions (GLSL set: fade, slide-left/-right/-up/-down, spiral), registerTransition, getResolvedTransitions, resolveTransition / matchRouteTransition (pick a transition for a route change), BUILT_IN_TRANSITION_NAMES, AUTO_TRANSITION, and the helpers computeProgress, easeInOutCubic, QUAD_VERTEX_SHADER. Types: TransitionDefinition (GlslTransitionDefinition \TslTransitionDefinition), TransitionDefinitionBase, TransitionUniforms, TransitionNodeContext, TransitionNodeBuilder, TransitionControllerConfig, TransitionController, ResolveTransitionArgs, BuiltInTransitionName. Client-only ('use client').No
@wabbit/tome-webgl/tslbuiltInTransitionsTSL (node-shader set) + TSL helper Fns. Imports three/tsl — client/runtime only, never from payload.config.ts.No
@wabbit/tome-webgl/styles.cssBase stylesheet — inline block frame and WebGLHero overlay, themed by --tome-* tokens. Import once from the app root layout: import '@wabbit/tome-webgl/styles.css'.n/a (CSS)
@wabbit/tome-webgl/fallbackuseWebglSupported (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

@wabbit/tome-webgl/registry and @wabbit/tome-webgl/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

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

ValueBackendTSL supportBrowser support
'classic' (default)THREE.WebGLRenderer (R3F default)None — NodeMaterial throwsSame as three.js baseline
'node-webgl'THREE.WebGPURenderer with forceWebGL: trueFull — TSL/NodeMaterial compilesSame as classic (WebGL2)
'node-webgpu'THREE.WebGPURenderer auto-detectFull — runs native WebGPU, falls back to WebGL2WebGPU 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

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 @wabbit/tome-webgl/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 `@wabbit/tome-webgl/tsl`: the subpath also exports React-free node-graph helpers 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

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.

Helperclassicnode-webglnode-webgpuNote
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

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

Import path
@wabbit/tome-webgl
@wabbit/tome-webgl/block
@wabbit/tome-webgl/registry
@wabbit/tome-webgl/transitions
@wabbit/tome-webgl/tsl
@wabbit/tome-webgl/fallback
@wabbit/tome-webgl/styles.css

Changelog

0.8.xMinor

  • @wabbit/tome-blocks-core moves from dependencies to an optional peer (>=0.16.6 <1.0.0); it stays a workspace devDependency. webgl is a domain-layer package and uses blocks-core for a single import type in ./block, so the hard edge installed a blocks-layer package on every consumer for an import that is erased at build time (2026-09-24 audit A3 §3 / #10, now gated by assert:declared-imports).

    Sites that use ./block are Payload + blocks sites and already have blocks-core. A site that uses only the R3F runtime no longer installs it.

4 fixes (0.8.1 – 0.8.4)
  • 0.8.4
    The changelog, README and license text now use American spellings.

    No code or behavior changes.

  • 0.8.3

    README: subpath and file mentions are written package-qualified (for example @wabbit/tome-x/render) or as code text, so crawlers no longer resolve them as site URLs.

  • 0.8.2
    Published packages now contain compiled JavaScript and type declarations under a one-line license banner, and no longer include source maps.

    What you install: one compiled .js (ESM) and .cjs (CommonJS) file per source module, its .d.ts / .d.cts declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license: /*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.

    The .map files and the sourceMappingURL comments that pointed at them are gone, which roughly halves the size of each tarball.

    Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line.

    A 'use client' directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged.

    No API change, no runtime behavior change, and nothing to do on upgrade.

    In @wabbit/tome-blocks-gallery, the source snapshots extractGallerySource writes from an installed pack leave out the license banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 0.8.1

    styles.css now ships in dist/ and is exported as @wabbit/tome-webgl/styles.css; it was previously never copied by the build, so consumers importing the stylesheet got nothing.

0.7.xMinor

  • 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.

  • 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.

  • 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
1 fix (0.7.1)
  • 0.7.1
    Updated dependencies [404d325]
    • @wabbit/tome-blocks-core@0.17.0

0.6.xMinor

  • 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/).

23 fixes (0.6.1 – 0.6.13)
  • 0.6.13
    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.

  • 0.6.12
    Updated dependencies [510036f]
    • @wabbit/tome-blocks-core@0.15.0
  • 0.6.11
    Updated dependencies
    • @wabbit/tome-blocks-core@0.14.0
  • 0.6.10
    Updated dependencies [f4d55c9]
    • @wabbit/tome-blocks-core@0.13.0
  • 0.6.9
    Updated dependencies [e11d5a2]
    • @wabbit/tome-blocks-core@0.11.2
  • 0.6.8
    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.

  • 0.6.8

    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).

  • 0.6.8

    Updated dependencies [26dfa07]

  • 0.6.8

    Updated dependencies [36e537a]

  • 0.6.8

    Updated dependencies [36e537a]

  • 0.6.8

    Updated dependencies [36e537a]

  • 0.6.8

    Updated dependencies [a93f478]

  • 0.6.8

    Updated dependencies [5f78397]

  • 0.6.8

    Updated dependencies [5f78397]

  • 0.6.8
    Updated dependencies [aef2725]
    • @wabbit/tome-blocks-core@0.11.0
  • 0.6.7
    Updated dependencies
    • @wabbit/tome-blocks-core@0.10.0
  • 0.6.6
    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.

  • 0.6.6
    Updated dependencies [bed3f90]
    • @wabbit/tome-blocks-core@0.9.4
  • 0.6.5
    Updated dependencies
    • @wabbit/tome-blocks-core@0.9.2
  • 0.6.4
    Updated dependencies [c7d0afc]
    • @wabbit/tome-blocks-core@0.9.0
  • 0.6.3
    Updated dependencies [249b670]
    • @wabbit/tome-blocks-core@0.8.0
  • 0.6.2
    Updated dependencies [66c611c]
    • @wabbit/tome-blocks-core@0.7.0
  • 0.6.1
    Updated dependencies [4b2f368]
    • @wabbit/tome-blocks-core@0.6.2

0.5.xMinor

  • 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.

0.4.xMinor

  • 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. a consumer's node-aware rendering work) 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.

1 fix (0.4.1)
  • 0.4.1
    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 a consumer site's 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.

0.3.xMinor

  • 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.
2 fixes (0.3.1 – 0.3.2)
  • 0.3.2

    @wabbit/tome-blocks-core@0.5.9

  • 0.3.1
    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.

0.2.xMinor

  • @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 (a consumer site + 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

Full changelog across all packages