From 54a539816044c4095d3c07ed40fb444588bc4e4e Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Fri, 15 May 2026 22:10:24 -0700 Subject: [PATCH] feat(shader-transitions): make shader optional to support CSS crossfade mixing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow omitting the shader field in TransitionConfig to get a smooth CSS opacity crossfade instead of a WebGL effect. HyperShader manages all scene visibility regardless of transition type, so shader and CSS crossfade transitions can now be mixed freely in the same composition. When shader is omitted: - No WebGL program is compiled or cached for that transition - The existing applyFallbackTransition() path handles the crossfade - No texture prewarming needed — transition is marked ready immediately Tested: verified with a 3-scene composition (sdf-iris + CSS crossfade) rendered to MP4. Both transition types render correctly. engine/src/types.ts: HfTransitionMeta.shader is now optional to match --- packages/engine/src/types.ts | 4 +-- .../shader-transitions/src/hyper-shader.ts | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts index 963bcaa7d..437284a9b 100644 --- a/packages/engine/src/types.ts +++ b/packages/engine/src/types.ts @@ -44,8 +44,8 @@ export interface HfTransitionMeta { time: number; /** Transition duration (seconds) */ duration: number; - /** Shader identifier (e.g. "fade", "wipe") */ - shader: string; + /** Shader identifier. Undefined when the transition is a CSS crossfade. */ + shader?: string; /** GSAP easing string (e.g. "power2.inOut") */ ease: string; /** Scene id the transition starts from */ diff --git a/packages/shader-transitions/src/hyper-shader.ts b/packages/shader-transitions/src/hyper-shader.ts index 8bb3171d8..6779e1205 100644 --- a/packages/shader-transitions/src/hyper-shader.ts +++ b/packages/shader-transitions/src/hyper-shader.ts @@ -51,7 +51,8 @@ interface GsapTimeline { export interface TransitionConfig { time: number; - shader: ShaderName; + /** Omit to use a CSS crossfade instead of a WebGL shader. */ + shader?: ShaderName; duration?: number; ease?: string; } @@ -98,7 +99,7 @@ interface CachedTransition { duration: number; fromId: string; toId: string; - prog: WebGLProgram; + prog: WebGLProgram | null; // null for CSS-fallback transitions frames: CachedTransitionFrame[]; cacheKey: string; dirty: boolean; @@ -823,7 +824,7 @@ export function init(config: HyperShaderConfig): GsapTimeline { interface HfTransitionMeta { time: number; duration: number; - shader: string; + shader?: string; // undefined = CSS crossfade (no WebGL required) ease: string; fromScene: string; toScene: string; @@ -900,6 +901,7 @@ export function init(config: HyperShaderConfig): GsapTimeline { const programs = new Map(); for (const t of transitions) { + if (!t.shader) continue; // CSS-only transitions have no WebGL program if (!programs.has(t.shader)) { try { programs.set(t.shader, createProgram(gl, getFragSource(t.shader))); @@ -1162,7 +1164,7 @@ export function init(config: HyperShaderConfig): GsapTimeline { renderShader( gl, quadBuf, - state.prog, + state.prog!, // non-null: fallback path returns before reaching here interpolatedFromTex, interpolatedToTex, state.progress, @@ -1291,8 +1293,11 @@ export function init(config: HyperShaderConfig): GsapTimeline { const toId = scenes[i + 1]; if (!fromId || !toId) continue; - const prog = programs.get(t.shader); - if (!prog) continue; + // CSS-only transition when shader is omitted — uses the fallback opacity + // crossfade path. No WebGL program or texture prewarming needed. + const isCssFallback = !t.shader; + const prog = isCssFallback ? null : (programs.get(t.shader!) ?? null); + if (!isCssFallback && !prog) continue; // shader requested but not compiled const dur = t.duration ?? DEFAULT_DURATION; const ease = t.ease ?? DEFAULT_EASE; @@ -1307,10 +1312,10 @@ export function init(config: HyperShaderConfig): GsapTimeline { prog, frames: [], cacheKey: "", - dirty: true, - ready: false, - fallback: false, - persisted: false, + dirty: !isCssFallback, + ready: isCssFallback, // CSS fallback needs no prewarming + fallback: isCssFallback, + persisted: isCssFallback, textureReady: false, texturePromise: null, textureGeneration: 0,