Documentation menu
Documentation / Realtime & 3D / webgl
Webgl
npm install @wabbit/tome-webglFirst install from our registry?
Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
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
| Peer | Range | Notes |
|---|---|---|
react / react-dom | >=19.0.0 | |
three | >=0.180 | see the compatibility policy below |
@react-three/fiber | >=8 | |
@react-three/drei | >=9 | see the compatibility matrix below |
@react-three/postprocessing | >=2 | optional — 'classic' renderer only |
@react-three/rapier | >=1 | optional |
@react-spring/three | >=9 | optional |
@wabbit/tome-blocks-core | >=0.16.6 <1.0.0 | optional — type-only, for @wabbit/tome-webgl/block |
Public API (subpaths)
| Subpath | Purpose | React-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/registry | createExperienceRegistry, ExperienceMeta, WebglCanvasPolicy. No React / three / R3F imports — safe in payload.config.ts. | Yes | |
@wabbit/tome-webgl/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 | |
@wabbit/tome-webgl/transitions | createTransitionController(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/tsl | builtInTransitionsTSL (node-shader set) + TSL helper Fns. Imports three/tsl — client/runtime only, never from payload.config.ts. | No | |
@wabbit/tome-webgl/styles.css | Base 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/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 |
@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.
| 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'. Validatenode-webglthrough 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') // explicitTSL 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.
| 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 aPromiseon node renderers, notvoid; 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-coremoves fromdependenciesto 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 singleimport typein./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 byassert:declared-imports).Sites that use
./blockare 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.ctsdeclarations, 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
.mapfiles and thesourceMappingURLcomments 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 snapshotsextractGallerySourcewrites 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.cssnow ships indist/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-dompeer 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
linkedblocks 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; devDepsreact/@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
^18devDependency pins on the two block-shaped packages were already fiction: the rootpnpm.overridespins@types/reactto19.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.
buildgains&& node ../../scripts/fix-dist-extensions.mjs --strictas its last step, joining the 13 packages that already ran it. tsup buildsbundle: falseand emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESMERR_MODULE_NOT_FOUND; CJS worse,require('./x')finds the ESM.jstwin 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.mjsnow 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--strictexited 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/.cjsappended.assert-node-loadable.mjsgained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands undernode_modulesis a peer SKIP (next@15 has no exports map, sonext/imagefails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it asSyntaxError: 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 importmissing/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.
homepagedeep-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.urlpoints at the repo issue tracker, so a paying customer has a place to report a defect that is not email.engines.nodeis>=22, matching the rootenginesand.nvmrcset the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use fornode --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(rootpnpm assert:manifest-metadata, wired intoplatform-discipline.ymlbesideassert:license-metadata) fails when any publishable manifest lacksdescription,repository.directorymatching its own folder,homepage,bugs,engines.nodeequal to the repo floor,license,filesorsideEffects. 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-webgpunow racesrenderer.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-webglstays deterministic. - postprocessing hard-gate —
policy.postprocessingunder a node renderer now throws an actionable error (@react-three/postprocessingis WebGL-bound and crashes onWebGPURenderer). The flag was previously inert, so no consumer breaks. Native node-renderer postprocessing via three'sRenderPipelineis tracked for 0.7.x. - renderer-init fallback — a new
RendererInitBoundaryrenders 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/dreicompatibility matrix under node renderers (<Environment>/<Sky>/reflectors break; controls/useProgress/useGLTF/Htmlare safe).
node-webgpuremains "validatenode-webglin production first." TSL↔GLSL render parity for the built-in spiral was verified pixel-identical on the WebGL2/node-webglpath (render-parity harness intest/parity/).- WebGPU adapter init hardening —
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
sideEffectsfield (38 added; motion/engine/forms already correct).Registration-bearing modules (render files'
registerRenderer,blocks/*/index.tsdefineBlockself-registration, widgetregister.tsfiles, 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 blanketfalseon 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 anycasts eliminated); marketing-starter's PricingPlans extracts its GSAP toggle timeline hook + a memoized card. rpg additionally trusts the denormalizedxpTotalon 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-corenow ships a standalone Node ESM CLI atscripts/emit-docs-manifests.mjsthat emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free<pkg>/metablock-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
./metapayload-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'sBlockMetawas 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.mdto its publishedfilesarray 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 +
./tslsubpath)Additive, non-breaking TSL lane alongside the GLSL/
classicdefault. Existing consumers are unaffected until they opt in.- `./transitions` —
TransitionDefinitionbecomes a discriminated union: the GLSLfragmentShaderarm is source-compatible with every existing definition, plus a new TSLnodeShader: (ctx: TransitionNodeContext) => Nodearm. Adds theTransitionNodeContext/TransitionNodeBuildercontract. - `./registry` —
WebglCanvasPolicygainsautoUpgradeRendererForTsl;node-webgpupromoted from "reserved" to supported (validatenode-webglin production first). - runtime —
TransitionCompositorbranches on the resolved definition: GLSL buildsTHREE.ShaderMaterialunchanged; TSL lazily importsthree/webgpuand builds aNodeMaterialcolorNode.WebglCanvasProviderenforces the renderer auto-upgrade contract — a TSL transition underrenderer: 'classic'throws an actionable error by default, or coerces tonode-webgl(with a one-time warn) whenautoUpgradeRendererForTslis set. Never a silent no-op or GLSL fallback. - `./tsl` (new subpath) — React-free TSL
Fnhelper library (curlNoise/snoiseVec3,voronoi3D,scaffoldFloat,wrappedDiffuseTwoLight/blendVolumeNormal/fresnelRim/pulseEnvelope/pulseWavefront) plusbuiltInTransitionsTSLnode-builders for the six curtains. Importsthree/tslonly; never touches React. - peer —
threefloor raised>=0.171→>=0.180(the floor that exports the required TSL/MaterialX surface).
three/webgpu+three/tslare dynamic-import-only, so theclassicGLSL path never pulls the WebGPU bundle.- `./transitions` —
0.4.xMinor
feat(webgl): opt-in node-aware renderer via
policy.rendererWebglCanvasPolicygains arendererfield. Default'classic'preserves current behavior (R3F defaultTHREE.WebGLRenderer).New
'node-webgl'opts the shared background canvas intoTHREE.WebGPURendererwithforceWebGL: true— required for TSL /NodeMaterialconsumers. 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.WebGLRendererhas no node-aware path. Three's only node-aware renderer isWebGPURenderer, 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
useMemoabove early-return inWebglBackgroundCanvas0.4.0 placed the new
glFactoryuseMemobelow the existingif (!activeBackground && !isTransitioning) return nullguard. When the canvas mounted empty (no active scene yet) and then a scene arrived, hook count went 4 → 5 and React killed the tree withRendered more hooks than during the previous render— observed on a consumer site's first-paint withpolicy={{ renderer: 'node-webgl' }}.Fix: move the
useMemoabove the early return so the hook order is invariant across both paths. No behavior change — the factory was already memoized onpolicy.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
TransitionCompositornow publishes the live eased progress plus each scene's role ('from'outgoing /'to'incoming) through a React context; a scene reads it viauseTransitionPhase()insideuseFrame(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.transitionProgressprop 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
interactiveflag +setInteractive()on the canvas context, auseCanvasInteractive()hook, and the background canvas now togglespointer-eventsaccordingly. 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.
- New export
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
useProgressidle + camera-idle viaSettleWatch), reveal-on-direct-load (no outgoing → start at full cover, hold for load, uncover). Provider gainspendingTargetRef+commitSwap+markIncomingReadyplumbing with an idempotency guard against legacy-store re-fires.Shaders rewritten to
uColor+ alpha (no scene textures); spiral now aspect-corrected.TransitionDefinitionshape unchanged;TransitionCompositorPropsaddscolor,readyRef,revealOnly. 34 tests green.
0.2.xMinor
@wabbit/tome-webgl/transitionsships 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-controllertransitionsconfig 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→1value lives in a ref advanced inuseFrameand written straight to the shader uniform (no per-frame React re-render). TheBackgroundExperienceProps.transitionProgressprop contract is preserved but coarse (0at start,1at 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 newclaimLiveCanvas/releaseLiveCanvasAPI on the provider lets an in-view inline canvas pause the background.WebglInlineBlockclaims it automatically when mounted under a provider (graceful no-op without one). - Reduced motion: when
respectReducedMotionis 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 Payloadselectfield pre-populated with the built-in transition names (+ anyextracustom names), with anAutooption. Registry-injection pattern likeexperienceKey. 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).- `registerTransition` now writes to a module-level registry that the compositor resolves against (was a no-op
@wabbit/tome-blocks-core@0.5.7