diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts
index da1a666be6a..38e0436e01a 100644
--- a/packages/core/src/runtime/init.test.ts
+++ b/packages/core/src/runtime/init.test.ts
@@ -609,7 +609,7 @@ describe("initSandboxRuntimeModular", () => {
}
});
- it("styles each hidden clip visible once, far scene included, so its cut does not do it first", () => {
+ it("styles a hidden clip visible once while playing toward its start, far scene included", () => {
document.body.innerHTML =
`
` +
`
` +
@@ -624,13 +624,28 @@ describe("initSandboxRuntimeModular", () => {
if (el === later && shown && !later.hasAttribute("data-hf-far-scene")) styledVisible++;
return original(el, pseudo);
});
+ const raf = createManualRaf();
+ vi.spyOn(performance, "now").mockImplementation(() => raf.now());
+ window.requestAnimationFrame =
+ raf.requestAnimationFrame as typeof window.requestAnimationFrame;
+ window.cancelAnimationFrame = raf.cancelAnimationFrame as typeof window.cancelAnimationFrame;
try {
initSandboxRuntimeModular();
- window.__player?.seek(1);
- const once = styledVisible;
- window.__player?.seek(2);
- window.__player?.seek(3);
- expect([once, styledVisible]).toEqual([1, 1]);
+ const counts: number[] = [];
+ const playFor = (ms: number) => {
+ for (let at = 0; at < ms; at += 100) raf.step(100);
+ counts.push(styledVisible);
+ };
+ for (const t of [6.5, 4]) {
+ window.__player?.seek(t);
+ counts.push(styledVisible);
+ }
+ window.__player?.play();
+ playFor(300);
+ playFor(1800);
+ playFor(1000);
+ window.__player?.pause();
+ expect(counts).toEqual([0, 0, 0, 1, 1]);
expect([later.style.visibility, later.hasAttribute("data-hf-clip-hidden")]).toEqual([
"hidden",
true,
diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts
index 1b034cf9e9c..20b57a36dbf 100644
--- a/packages/core/src/runtime/init.ts
+++ b/packages/core/src/runtime/init.ts
@@ -2889,14 +2889,19 @@ export function initSandboxRuntimeModular(): void {
resolver: timingResolverFor(true),
}),
);
- // A clip's first visible style pass is slow; each hidden clip gets it once on a seek, so its cut does not.
+ // A clip's first visible style pass is slow; playing, each hidden clip gets it once just before its start.
const clipsNeverStyledVisible = new Set
();
const clipsStyledVisible = new WeakSet();
- const styleHiddenClipsVisibleOnce = () => {
- const clips = Array.from(clipsNeverStyledVisible).filter(
- (el) => el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR),
- );
- clipsNeverStyledVisible.clear();
+ const styleClipsDueVisibleOnce = (t: number) => {
+ if (clipsNeverStyledVisible.size === 0 || !clock.isPlaying()) return;
+ const clips: StylableElement[] = [];
+ for (const el of clipsNeverStyledVisible) {
+ const start = resolveStartForElement(el, Number.NaN);
+ if (el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR) && !(start <= t + LOOKAHEAD_SECONDS))
+ continue;
+ clipsNeverStyledVisible.delete(el);
+ if (el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR) && start > t) clips.push(el);
+ }
if (clips.length === 0) return;
const farHosts = new Set(clips.flatMap((el) => el.closest(`[${FAR_SCENE_ATTR}]`) ?? []));
for (const host of farHosts) host.removeAttribute(FAR_SCENE_ATTR);
@@ -5175,7 +5180,7 @@ export function initSandboxRuntimeModular(): void {
const revision = opts?.revision ?? readCompositionTimingRevision();
if (revision !== tweensRenderedThrough.revision)
tweensRenderedThrough = { revision, time: Number.NEGATIVE_INFINITY };
- styleHiddenClipsVisibleOnce();
+ styleClipsDueVisibleOnce(t);
const firstRenders = t > tweensRenderedThrough.time;
// Only clips holding a tween that renders for the first time on this seek need their authored style read.
const firstRendered =