mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +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 {
|
interface PageCompositeTransitionConfig {
|
||||||
time: number;
|
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;
|
duration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +121,10 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
|||||||
|
|
||||||
const programs = new Map<string, WebGLProgram>();
|
const programs = new Map<string, WebGLProgram>();
|
||||||
for (const t of transitions) {
|
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;
|
if (programs.has(t.shader)) continue;
|
||||||
try {
|
try {
|
||||||
programs.set(t.shader, createProgram(gl, getFragSource(t.shader)));
|
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++) {
|
for (let i = 0; i < transitions.length; i++) {
|
||||||
const t = transitions[i];
|
const t = transitions[i];
|
||||||
if (!t) continue;
|
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 fromSceneId = scenes[i];
|
||||||
const toSceneId = scenes[i + 1];
|
const toSceneId = scenes[i + 1];
|
||||||
const prog = programs.get(t.shader);
|
const prog = programs.get(t.shader);
|
||||||
|
|||||||
@@ -903,7 +903,11 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
|||||||
|
|
||||||
const programs = new Map<string, WebGLProgram>();
|
const programs = new Map<string, WebGLProgram>();
|
||||||
for (const t of transitions) {
|
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)) {
|
if (!programs.has(t.shader)) {
|
||||||
try {
|
try {
|
||||||
programs.set(t.shader, createProgram(gl, getFragSource(t.shader)));
|
programs.set(t.shader, createProgram(gl, getFragSource(t.shader)));
|
||||||
@@ -1302,11 +1306,19 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
|||||||
const toId = scenes[i + 1];
|
const toId = scenes[i + 1];
|
||||||
if (!fromId || !toId) continue;
|
if (!fromId || !toId) continue;
|
||||||
|
|
||||||
// CSS-only transition when shader is omitted — uses the fallback opacity
|
// shader omitted → CSS crossfade. shader present but program failed to
|
||||||
// crossfade path. No WebGL program or texture prewarming needed.
|
// compile (logged above) → degrade gracefully to CSS crossfade so the
|
||||||
const isCssFallback = !t.shader;
|
// opacity timeline still runs and scene progression isn't broken. Both
|
||||||
const prog = isCssFallback ? null : (programs.get(t.shader!) ?? null);
|
// paths land in the always-ready prog=null cache.
|
||||||
if (!isCssFallback && !prog) continue; // shader requested but not compiled
|
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 dur = t.duration ?? DEFAULT_DURATION;
|
||||||
const ease = t.ease ?? DEFAULT_EASE;
|
const ease = t.ease ?? DEFAULT_EASE;
|
||||||
@@ -1322,7 +1334,7 @@ export function init(config: HyperShaderConfig): GsapTimeline {
|
|||||||
frames: [],
|
frames: [],
|
||||||
cacheKey: "",
|
cacheKey: "",
|
||||||
dirty: !isCssFallback,
|
dirty: !isCssFallback,
|
||||||
ready: isCssFallback, // CSS fallback needs no prewarming
|
ready: isCssFallback,
|
||||||
fallback: isCssFallback,
|
fallback: isCssFallback,
|
||||||
persisted: isCssFallback,
|
persisted: isCssFallback,
|
||||||
textureReady: false,
|
textureReady: false,
|
||||||
@@ -2274,16 +2286,13 @@ function initEngineMode(
|
|||||||
const rawH = Number(root?.getAttribute("data-height"));
|
const rawH = Number(root?.getAttribute("data-height"));
|
||||||
const compWidth = Number.isFinite(rawW) && rawW > 0 ? rawW : 1920;
|
const compWidth = Number.isFinite(rawW) && rawW > 0 ? rawW : 1920;
|
||||||
const compHeight = Number.isFinite(rawH) && rawH > 0 ? rawH : 1080;
|
const compHeight = Number.isFinite(rawH) && rawH > 0 ? rawH : 1080;
|
||||||
// Page-side compositing only handles WebGL shader transitions. CSS
|
// Pass the full transitions array so transition[i] still pairs with
|
||||||
// crossfades are driven by GSAP opacity timelines elsewhere, so filter
|
// scenes[i]/scenes[i+1]. The compositor itself skips entries with
|
||||||
// them out — passing them in would break the compositor's required
|
// `shader === undefined` while preserving the index↔scene mapping.
|
||||||
// `shader` field and produce a dead transition window with no rendering.
|
// (CSS crossfades remain driven by the GSAP opacity timeline.)
|
||||||
const shaderTransitions = transitions.filter(
|
|
||||||
(t): t is TransitionConfig & { shader: ShaderName } => !!t.shader,
|
|
||||||
);
|
|
||||||
installPageSideCompositor({
|
installPageSideCompositor({
|
||||||
scenes,
|
scenes,
|
||||||
transitions: shaderTransitions,
|
transitions,
|
||||||
bgColor,
|
bgColor,
|
||||||
accentColors,
|
accentColors,
|
||||||
width: compWidth,
|
width: compWidth,
|
||||||
|
|||||||
Reference in New Issue
Block a user