Files
hyperframes/packages/shader-transitions
Miguel Ángel 0f528ff2e8 feat(shader-transitions): add playSeamTransitionLoop to preview a seam on a loop (#4307)
* 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
2026-09-26 00:08:47 +00:00
..
2026-09-25 12:51:35 +00:00

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

License

MIT