* feat(shader-transitions): add playSeamTransitionLoop to preview a seam on a loop Plays a catalog transition's real WebGL effect between two already-decoded frames on a caller-supplied canvas, looping 0->1->0 every second. The caller supplies the two seam frames itself, so this takes two CanvasImageSource frames directly rather than resolving any DOM scene. Reuses the existing webgl.ts primitives hyper-shader.ts's full composition engine already relies on; no GSAP timeline, no scene capture, no multi-transition scheduling here, just setup + a render loop. shaderName is the catalog block's own name. 12 of the 14 shader-tagged catalog blocks match their shaders/registry.ts key verbatim; two are aliased internally (domain-warp-dissolve->domain-warp, chromatic-radial-split->chromatic-split) since those registry keys are also engine/producer's canonical TRANSITIONS[...] names and weren't renamed to match the catalog. Every other catalog block (including the CSS/motion-only ones) has no WebGL implementation and throws, so a caller can fall back to its own render for those. Re-exported from @hyperframes/studio (new workspace dependency on shader-transitions) so a consumer of studio and core needs no deep import. * fix(shader-transitions): keep the WebGL context alive across stop() - stop() called WEBGL_lose_context.loseContext(), which permanently kills the canvas's WebGL context -- a later playSeamTransitionLoop() call on the same canvas (a popover reusing its element across hovers) would pass the null-context guard with the same dead context object and silently render nothing. Delete the program, textures and buffer this call created instead, matching the granular-release idiom hyper-shader.ts already uses for its own reusable contexts. - ready never resolved if stop() ran before the first frame (a hover that ends within one frame, well under 16ms), hanging any caller awaiting it forever along with everything it closed over. stop() now resolves ready too; resolving an already-settled promise is a no-op for the normal path. Also: resolveReady() only needs calling once, on the first frame, not every tick. * test(shader-transitions): extract shared running-loop setup, dedup per fallow * fix(shader-transitions): pass accent colors, fix leaks and validation - renderShader's colors argument was undefined, so u_accent/ u_accent_dark/u_accent_bright stayed at WebGL's zero default. 7 of the 14 shader blocks (domain-warp, ridged-burn, sdf-iris, ripple-waves, gravitational-lens, thermal-distortion, light-leak) use those uniforms for their glow/burn colour, so the preview silently rendered without it for half the catalog -- the "plays the real effect" claim was false for those seven. Exported webgl.ts's DEFAULT_ACCENT_COLORS (hyper-shader.ts's own fallback, previously only inlined twice there) and pass it through. - Each linkProgram() call leaked its compiled vertex and fragment shader objects (created, attached, linked, never deleted) -- a real WebGL browser never frees them on its own, and this call is on a hover-repeat path via a reused canvas. Fixed in webgl.ts's linkProgram itself, which every caller in this package routes through, not just this new one. - loopMs was never validated: 0 divided to NaN progress, negative values ran the shader backwards, Infinity froze it at 0 -- all silently, no caller-visible error. Now throws synchronously for a non-finite or non-positive value, matching the function's other synchronous-throw contract (unknown shader, no WebGL context). - getFragSource() and the catalog-alias lookup both did an unguarded plain-object index (shaders[name], CATALOG_SHADER_ALIASES[shaderName]), which returns Object.prototype members for names like "constructor" instead of undefined -- an unusual shaderName could reach the shader compiler with a garbled error or an unintended match. Both now use Object.hasOwn. Tests: second half of the loop (progress descending, not climbing past 1), both catalog aliases (only one was covered before), all four invalid-loopMs shapes, the accent-color uniforms actually being set, and webgl.test.ts's new deleteShader assertion. All mutation-verified non-vacuous. * test(shader-transitions): dedup the two timing tests per fallow * fix(shader-transitions): apply default accent colors and validate loop options renderShader now defaults colors to DEFAULT_ACCENT_COLORS instead of the caller deciding, so playSeamTransitionLoop no longer renders 7 of 14 shader blocks with a zeroed-out accent. Validates width/height alongside loopMs, switches the catalog alias map to a Map so a bogus name never resolves an Object.prototype member, and fixes linkProgram to only delete the shader it created itself. * fix(shader-transitions): free GL resources when seam-loop setup throws A bad fromSource/toSource reaching uploadTextureSource previously left whatever quadBuf/program/textures had already been allocated dangling, since playSeamTransitionLoop throws before returning a handle a caller could call stop() on. Trims three over-limit JSDoc blocks and drops a redundant prototype-pollution rationale now that getFragSource guards it. * chore(shader-transitions): trim seam-loop comments to the comment ratchet * test(shader-transitions): unknown-shader throw covers names on Object.prototype
@hyperframes/shader-transitions
WebGL shader transitions for HyperFrames compositions. Renders GPU-accelerated scene-to-scene transitions using fragment shaders, driven by GSAP timelines.
Install
npm install @hyperframes/shader-transitions
Or load directly via CDN:
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions/dist/index.global.js"></script>
Usage
import { init } from "@hyperframes/shader-transitions";
const tl = init({
bgColor: "#0a0a0a",
accentColor: "#ff6b2b",
scenes: ["scene-1", "scene-2", "scene-3"],
transitions: [
{ time: 3, shader: "domain-warp", duration: 0.8 },
{ time: 8, shader: "light-leak", duration: 0.7 },
],
});
The init() function pre-captures animated scene samples for every transition, composites cached samples with the selected shader during playback, and returns a GSAP timeline. Scene animations keep advancing through shader transitions without running DOM captures in the playback loop. If WebGL is unavailable, it falls back to normal timeline playback without shader compositing.
When the browser exposes Chrome's experimental CanvasDrawElement API, scene
capture uses native HTML-in-canvas via drawElementImage(). Other browsers keep
using the existing html2canvas fallback. You can feature-detect the native path
with isHtmlInCanvasCaptureSupported().
With an existing timeline
Pass your own GSAP timeline to layer transitions onto it:
const tl = gsap.timeline({ paused: true });
// ... add your scene animations ...
init({
bgColor: "#000",
scenes: ["intro", "demo", "outro"],
transitions: [
{ time: 5, shader: "cinematic-zoom" },
{ time: 12, shader: "glitch", duration: 0.5 },
],
timeline: tl,
});
Available shaders
| Shader | Description |
|---|---|
domain-warp |
Organic noise-based warp with glowing edge |
ridged-burn |
Ridged noise burn with sparks and heat glow |
whip-pan |
Horizontal motion blur simulating a fast camera pan |
sdf-iris |
Circular iris wipe with glowing ring edge |
ripple-waves |
Concentric ripple distortion radiating from center |
gravitational-lens |
Warping gravity well with chromatic aberration |
cinematic-zoom |
Radial zoom blur with chromatic fringing |
chromatic-split |
RGB channel separation expanding from center |
glitch |
Digital glitch with block displacement and scanlines |
swirl-vortex |
Spiral rotation with noise-based warping |
thermal-distortion |
Heat shimmer rising from the bottom |
flash-through-white |
Flash to white then reveal the next scene |
cross-warp-morph |
Noise-driven morph blending both scenes |
light-leak |
Warm cinematic light leak with lens flare |
API
init(config): GsapTimeline
| Option | Type | Required | Description |
|---|---|---|---|
bgColor |
string |
yes | Fallback background color (hex) for scene capture. Use the composition's body/canvas background — individual scenes set their own background-color via CSS. |
accentColor |
string |
no | Accent color (hex) for shader glow effects |
scenes |
string[] |
yes | Element IDs of each scene, in order |
transitions |
TransitionConfig[] |
yes | Transition definitions (see below) |
timeline |
GsapTimeline |
no | Existing timeline to attach transitions to |
compositionId |
string |
no | Override the data-composition-id for timeline registration |
previewCaptureFps |
number |
no | Browser preview pre-capture samples per transition second. Defaults to 30; rendering uses deterministic per-frame compositing instead. |
Browser preview capture scale and transition-prep loading UI ownership are controlled by <hyperframes-player> (shader-capture-scale, shader-loading) instead of composition code. Direct non-player previews keep the built-in full-fidelity loading fallback.
Browser previews store captured transition snapshots in IndexedDB using a key derived from composition ID, scene DOM/style signatures, transition timing, capture FPS, scale, and dimensions. On refresh, matching snapshots are reloaded into WebGL textures instead of being captured again. Runtime scene or stylesheet edits mark only adjacent transition caches dirty; recapture is deferred until playback so editing stays responsive.
TransitionConfig
| Option | Type | Default | Description |
|---|---|---|---|
time |
number |
— | Start time in seconds |
shader |
ShaderName |
— | Shader name from the table above |
duration |
number |
0.7 |
Transition duration in seconds |
ease |
string |
"power2.inOut" |
GSAP easing function |
SHADER_NAMES
Array of all available shader name strings, useful for validation or building UIs.
import { SHADER_NAMES } from "@hyperframes/shader-transitions";
// ["domain-warp", "ridged-burn", "whip-pan", ...]
Distribution
| Format | File | Use case |
|---|---|---|
| ESM | dist/index.js |
Bundlers (Vite, webpack, etc.) |
| CJS | dist/index.cjs |
Node.js / require() |
| IIFE | dist/index.global.js |
<script> tag, CDN (global: HyperShader) |
All formats include source maps. TypeScript definitions included.
Related packages
@hyperframes/core-- types, parsers, runtime@hyperframes/engine-- rendering enginehyperframes-- CLI
License
MIT