Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions packages/engine/src/services/screenshotService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,17 +375,12 @@ export async function injectVideoFramesBatch(
let img = video.nextElementSibling as HTMLImageElement | null;
const isNewImage = !img || !img.classList.contains("__render_frame__");
const computedStyle = window.getComputedStyle(video);
// GSAP seeks re-apply tween values during an active tween, but do not
// re-apply tweens that have already completed. After an opacity fade-in
// finishes, GSAP's last set value is overwritten on subsequent frames
// by the `opacity: 0 !important` we apply at the bottom of this
// function to hide the native <video>. That leaves `computedOpacity`
// stuck at 0 even though the user's intent is opacity 1 (the tween's
// end state). The `|| 1` fallback treats computedOpacity === 0 as a
// hidden-native-video artifact and recovers opacity 1, matching the
// final on-screen state for the vast majority of compositions.
// For active tweens in the [0,1] exclusive range this is a no-op.
const computedOpacity = parseFloat(computedStyle.opacity) || 1;
// Read the GSAP-controlled opacity directly from the native <video>.
// We hide the <video> below with `visibility: hidden` only (never
// `opacity: 0`), so its computed opacity is preserved across seeks
// and accurately reflects the user's intent on every frame.
const opacityParsed = parseFloat(computedStyle.opacity);
const computedOpacity = Number.isNaN(opacityParsed) ? 1 : opacityParsed;
const sourceIsStatic = !computedStyle.position || computedStyle.position === "static";

if (isNewImage) {
Expand Down Expand Up @@ -454,8 +449,10 @@ export async function injectVideoFramesBatch(
);
img.style.opacity = String(computedOpacity);
img.style.visibility = "visible";
// Hide the native <video> with visibility only — never clobber inline
// opacity, so subsequent reads (and queryElementStacking) see the real
// GSAP-controlled value.
video.style.setProperty("visibility", "hidden", "important");
video.style.setProperty("opacity", "0", "important");
video.style.setProperty("pointer-events", "none", "important");
}
if (pendingDecodes.length > 0) {
Expand Down Expand Up @@ -489,10 +486,10 @@ export async function syncVideoFrameVisibility(
img.style.visibility = "visible";
}
} else {
// Inactive video: hide both
// Inactive video: hide both. Use visibility only (never opacity) so we
// never clobber GSAP-controlled inline opacity.
video.style.removeProperty("display");
video.style.setProperty("visibility", "hidden", "important");
video.style.setProperty("opacity", "0", "important");
video.style.setProperty("pointer-events", "none", "important");
if (hasImg) {
img.style.visibility = "hidden";
Expand Down
27 changes: 13 additions & 14 deletions packages/engine/src/services/videoFrameInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ export async function hideVideoElements(page: Page, videoIds: string[]): Promise
const el = document.getElementById(id) as HTMLVideoElement | null;
if (el) {
el.style.setProperty("visibility", "hidden", "important");
el.style.setProperty("opacity", "0", "important");
// Also hide the injected render frame image if present
const img = document.getElementById(`__render_frame_${id}__`);
if (img) img.style.setProperty("visibility", "hidden", "important");
}
Expand All @@ -168,7 +166,6 @@ export async function showVideoElements(page: Page, videoIds: string[]): Promise
const el = document.getElementById(id) as HTMLVideoElement | null;
if (el) {
el.style.removeProperty("visibility");
el.style.removeProperty("opacity");
const img = document.getElementById(`__render_frame_${id}__`);
if (img) img.style.removeProperty("visibility");
}
Expand Down Expand Up @@ -203,8 +200,10 @@ export async function queryVideoElementBounds(
}
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const zIndex = parseInt(style.zIndex) || 0;
const opacity = parseFloat(style.opacity) || 1;
const zIndexParsed = parseInt(style.zIndex);
const zIndex = Number.isNaN(zIndexParsed) ? 0 : zIndexParsed;
const opacityParsed = parseFloat(style.opacity);
const opacity = Number.isNaN(opacityParsed) ? 1 : opacityParsed;
const transform = style.transform || "none";
const visible =
style.visibility !== "hidden" &&
Expand Down Expand Up @@ -320,12 +319,12 @@ export async function queryElementStacking(
function resolveRadius(value: string, el: Element): number {
if (value.includes("%")) {
const pct = parseFloat(value) / 100;
const htmlEl = el as HTMLElement;
const w = htmlEl.offsetWidth || 0;
const h = htmlEl.offsetHeight || 0;
const w = el instanceof HTMLElement ? el.offsetWidth : 0;
const h = el instanceof HTMLElement ? el.offsetHeight : 0;
return pct * Math.min(w, h);
}
return parseFloat(value) || 0;
const parsed = parseFloat(value);
return Number.isNaN(parsed) ? 0 : parsed;
}

// Check element itself (replaced elements clip to own border-radius)
Expand Down Expand Up @@ -435,12 +434,12 @@ export async function queryElementStacking(
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const zIndex = getEffectiveZIndex(el);
// For HDR video elements, the frame injector sets `opacity: 0 !important`
// on the element itself. Start the opacity walk from the parent to get the
// real GSAP-animated opacity from wrapper divs.
const isHdrEl = hdrSet.has(id);
const opacityStartNode = isHdrEl ? el.parentElement : el;
const opacity = opacityStartNode ? getEffectiveOpacity(opacityStartNode) : 1;
// The frame injector now uses `visibility: hidden` (without `opacity: 0`)
// to hide native <video> elements, so the element's own computed opacity
// remains the GSAP-controlled value. Walk from the element itself to
// multiply through any ancestor opacity stacks.
const opacity = getEffectiveOpacity(el);
const visible =
style.visibility !== "hidden" &&
style.display !== "none" &&
Expand Down
940 changes: 438 additions & 502 deletions packages/producer/tests/style-7-prod/output/compiled.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/producer/tests/style-7-prod/output/output.mp4
Git LFS file not shown
Loading