mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(shader-transitions): address Copilot round-2 review
Three follow-up fixes from the Copilot review on commit 8cad2173:
1. Use strict `t.shader === undefined` instead of `!t.shader` (Copilot c4)
in both the WebGL program compile loop and the page-side compositor.
An empty-string `shader: ""` from a vanilla-JS caller (the IIFE bundle
is hand-loaded via <script> tags in user HTML) should reach the shader
registry and surface a loud "unknown shader" error, not silently
degrade to a crossfade.
2. Graceful degradation when shader compile fails (Copilot c5). The
previous `continue` dropped the transition from `cachedTransitions`,
which also dropped its scene-visibility timeline entries and broke
scene progression. Now: log a warning and downgrade to the CSS
crossfade fallback (prog=null, fallback=true) so the opacity timeline
still runs and the composition keeps playing.
3. Preserve index-to-scene-pair correlation when calling the page-side
compositor (Copilot c6). The earlier filter `transitions.filter(t =>
!!t.shader)` shifted indices, so a shader transition at original index
2 (sitting between CSS crossfades) would be paired with scenes[1] and
scenes[2] inside `installPageSideCompositor` instead of the correct
scenes[2] and scenes[3]. The compositor now accepts the full array,
makes `PageCompositeTransitionConfig.shader` optional, and skips
CSS-only entries internally while keeping `transitions[i]` aligned
with `scenes[i]`/`scenes[i+1]`.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -42,7 +42,14 @@ import { isHtmlInCanvasCaptureSupported } from "./capture.js";
|
||||
|
||||
interface PageCompositeTransitionConfig {
|
||||
time: number;
|
||||
shader: ShaderName;
|
||||
/**
|
||||
* Shader id. Undefined entries are CSS crossfades — the page-side
|
||||
* compositor skips them so the GSAP opacity timeline handles the blend,
|
||||
* but the entry stays in the array to preserve `transitions[i]` ↔
|
||||
* `scenes[i]`/`scenes[i+1]` index alignment for the surrounding shader
|
||||
* entries.
|
||||
*/
|
||||
shader?: ShaderName;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
@@ -114,6 +121,10 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
||||
|
||||
const programs = new Map<string, WebGLProgram>();
|
||||
for (const t of transitions) {
|
||||
// CSS crossfade entries (shader undefined) carry no program. Use a
|
||||
// strict undefined check so a misconfigured empty string still fails
|
||||
// loudly through the createProgram path below.
|
||||
if (t.shader === undefined) continue;
|
||||
if (programs.has(t.shader)) continue;
|
||||
try {
|
||||
programs.set(t.shader, createProgram(gl, getFragSource(t.shader)));
|
||||
@@ -127,6 +138,10 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
||||
for (let i = 0; i < transitions.length; i++) {
|
||||
const t = transitions[i];
|
||||
if (!t) continue;
|
||||
// CSS-only transitions stay on the GSAP opacity timeline; the page-
|
||||
// side compositor only handles shader entries. Index i is preserved
|
||||
// so subsequent shader transitions still pair with the right scenes.
|
||||
if (t.shader === undefined) continue;
|
||||
const fromSceneId = scenes[i];
|
||||
const toSceneId = scenes[i + 1];
|
||||
const prog = programs.get(t.shader);
|
||||
|
||||
@@ -903,7 +903,11 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
||||
|
||||
const programs = new Map<string, WebGLProgram>();
|
||||
for (const t of transitions) {
|
||||
if (!t.shader) continue; // CSS-only transitions have no WebGL program
|
||||
// Strict undefined check — an explicit empty string from a vanilla-JS
|
||||
// caller (the IIFE bundle is hand-loaded via <script> tags) should NOT
|
||||
// be silently coerced into a CSS crossfade. The shader registry will
|
||||
// throw a clear "unknown shader" error for it.
|
||||
if (t.shader === undefined) continue;
|
||||
if (!programs.has(t.shader)) {
|
||||
try {
|
||||
programs.set(t.shader, createProgram(gl, getFragSource(t.shader)));
|
||||
@@ -1302,11 +1306,19 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
||||
const toId = scenes[i + 1];
|
||||
if (!fromId || !toId) 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
|
||||
// shader omitted → CSS crossfade. shader present but program failed to
|
||||
// compile (logged above) → degrade gracefully to CSS crossfade so the
|
||||
// opacity timeline still runs and scene progression isn't broken. Both
|
||||
// paths land in the always-ready prog=null cache.
|
||||
const requestedShader = t.shader !== undefined;
|
||||
const compiledProg = requestedShader ? (programs.get(t.shader!) ?? null) : null;
|
||||
const isCssFallback = !requestedShader || compiledProg === null;
|
||||
if (requestedShader && compiledProg === null) {
|
||||
console.warn(
|
||||
`[HyperShader] Shader "${t.shader}" failed to compile — falling back to CSS crossfade.`,
|
||||
);
|
||||
}
|
||||
const prog = isCssFallback ? null : compiledProg;
|
||||
|
||||
const dur = t.duration ?? DEFAULT_DURATION;
|
||||
const ease = t.ease ?? DEFAULT_EASE;
|
||||
@@ -1322,7 +1334,7 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
||||
frames: [],
|
||||
cacheKey: "",
|
||||
dirty: !isCssFallback,
|
||||
ready: isCssFallback, // CSS fallback needs no prewarming
|
||||
ready: isCssFallback,
|
||||
fallback: isCssFallback,
|
||||
persisted: isCssFallback,
|
||||
textureReady: false,
|
||||
@@ -2274,16 +2286,13 @@ function initEngineMode(
|
||||
const rawH = Number(root?.getAttribute("data-height"));
|
||||
const compWidth = Number.isFinite(rawW) && rawW > 0 ? rawW : 1920;
|
||||
const compHeight = Number.isFinite(rawH) && rawH > 0 ? rawH : 1080;
|
||||
// Page-side compositing only handles WebGL shader transitions. CSS
|
||||
// crossfades are driven by GSAP opacity timelines elsewhere, so filter
|
||||
// them out — passing them in would break the compositor's required
|
||||
// `shader` field and produce a dead transition window with no rendering.
|
||||
const shaderTransitions = transitions.filter(
|
||||
(t): t is TransitionConfig & { shader: ShaderName } => !!t.shader,
|
||||
);
|
||||
// Pass the full transitions array so transition[i] still pairs with
|
||||
// scenes[i]/scenes[i+1]. The compositor itself skips entries with
|
||||
// `shader === undefined` while preserving the index↔scene mapping.
|
||||
// (CSS crossfades remain driven by the GSAP opacity timeline.)
|
||||
installPageSideCompositor({
|
||||
scenes,
|
||||
transitions: shaderTransitions,
|
||||
transitions,
|
||||
bgColor,
|
||||
accentColors,
|
||||
width: compWidth,
|
||||
|
||||
Reference in New Issue
Block a user