Skip to content

perf(core): Safari stops re-serializing inline styles on every GSAP write - #5974

Open
miguel-heygen wants to merge 9 commits into
mainfrom
perf/safari-hidden-clip-attr
Open

miguel-heygen wants to merge 9 commits into
mainfrom
perf/safari-hidden-clip-attr

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

What this fixes

Safari spent about a fifth of a playing film's main thread re-serializing inline styles. The runtime always injects a stylesheet that hides a hidden clip's contents with [data-hf-clip-hidden][style*="visibility: hidden"] * (plus SVG filter variants). A [style] attribute selector above the matched element makes WebKit treat the style attribute as needed for invalidation, so every inline style write re-serializes the element's whole style to text (invalidateStyleAttribute → asTextAtom → asTextInternal). GSAP writes transform and opacity on every animated element every frame, so a GSAP-heavy film pays that on every write. Chrome serializes lazily and doesn't pay it.

The rule now matches the runtime's own mark only:

  • [data-hf-clip-hidden][data-hf-clip-hidden] * hides a hidden clip's contents. The attribute is doubled to keep the old rule's weight, so author rules that lost to it still lose. The visibility pass sets the mark and the inline visibility: hidden together (init.ts), as before.
  • The Studio preview's image-skip rule matches the same mark instead of the inline style.
  • Code that forces a hidden clip visible goes through one helper, revealClip in @hyperframes/core/runtime/clip-reveal. It sets the inline visibility and lifts the mark together and returns the call that puts the mark back, so a revealer cannot do one without the other:
    • the runtime's first-render reveal (showHiddenClips);
    • a shader transition's captured copy (showSceneAndClips);
    • the page-side compositor's scene copies in renders (prepareComposite), which are thrown away, so nothing is put back;
    • Studio's layer reveal, which puts the mark back only when it restores its own visibility;
    • Studio's gesture preview, which puts it back on release.
  • shader-transitions bundles the helper from core at build time (noExternal, as the player does), so it gains no install dependency. Its build grows by the helper alone: 255,276 → 255,415 bytes ESM, 255,962 → 256,101 CJS, 255,775 → 255,914 IIFE (+139 each; +87, +71, +76 gzipped). The mark name appears once and no other core module is pulled in.
  • The engine's 3D projection needs no change. It copies each original element's computed style onto its clone and rasterizes the clone in its own SVG image, where no runtime stylesheet applies. The originals carry the mark and the inline hide together, so the old and new rules match the same elements.

Compatibility path for published shader-transitions builds

Builds of @hyperframes/shader-transitions already on npm and the CDN reveal scenes by inline style only. They can't lift the mark. Under the mark-only rule, a film loading one of them (a pinned version, or the unpinned CDN URL before it serves this release) would render an empty incoming or outgoing scene for part of every shader transition across a scene boundary.

Decision (Miguel): keep published builds rendering, chosen over shipping with a minimum shader-transitions version and a lint warning on older pins, and over holding the change.

  • Builds from this change call declareClipRevealAware() on load.
  • Once composition scripts have run, the runtime checks for a shader build on the page (its HyperShader global, or __hf.transitions) that did not declare. If it finds one, it rewrites its two injected rules back to the inline-style form (keepInlineHideRulesForOlderShaders). Those films render as on main and keep Safari's old cost. Every other film gets the fast rules.
  • The flag is internal to the runtime, the same way as the existing __hfSuppressSceneMutations: no new authored attribute and no author-facing API.
  • When it can go: once shader-transitions versions older than this release are no longer supported for rendering. Removing it means deleting keepInlineHideRulesForOlderShaders, its call in entry.ts, olderShaderBuildOnPage and the inline-style selector.
  • A build loaded after the runtime starts (a script injected late) isn't detected and gets the fast rules. Shipped templates and guides load it with a plain script tag.

Behaviour change

An author's own script that sets visibility: visible inline on a timed clip element while the runtime hides it no longer reveals that clip's children; they stay hidden until the runtime shows the clip. The runtime owns a timed clip's visibility, and the old behaviour cost every GSAP film this tax in Safari. Revealers inside HyperFrames (Studio, shader transitions) go through revealClip.

Measured

WebKit 26.5 on a physical Mac (M4 Pro), the player playing at 960x540, page main thread from an Instruments Time Profiler trace, 6 s per run:

frame rate page main thread main thread per frame, p95 / max frames over 8 ms (of ~385) inline style serialization
two-minute GSAP film, 400 tiles, before 32.3 / 33.4 fps 298 / 310 ms/s 12 / 14 to 16 ms 94 / 96 19 to 20%
same, after 34.6 / 34.7 fps 191 / 193 ms/s 7 / 9 to 10 ms 7 / 4 0%
gsap-heavy film, before 60.0 fps 230 ms/s 6 / 8 ms 0 19%
same, after 60.1 fps 174 ms/s 5 / 8 ms 0 0%

On AC power, two runs per arm, the old and new builds served side by side and alternated in one session. Two earlier sessions on battery in Low Power Mode (WebKit capped at 30 fps for both builds) gave the same CPU picture: 318 to 329 ms/s before, 202 to 206 after. The long film's remaining limit is painting in WebKit's GPU process, which this PR does not touch.

Chrome, synthetic 4K film, after merging main

Chrome 152 headless on a 32-core Linux host with no GPU, so every frame is painted in software. The film is 3840x2160: 92 timed video tiles (about 31 playing at once), a scene of about 7,000 divs and 500 SVG paths that GSAP writes inline styles to every frame, and 40 later scenes, about 100 hidden clips in all. The player plays it as in Studio's preview for 10 s per run. The two builds alternate (main at this branch's merge base, then this head), three runs each, then one traced run each. The host was shared, with load average 10 to 25.

main this head
frame rate, runs 1 / 2 / 3 (median) 12.4 / 15.0 / 9.8 (12.4) 18.0 / 13.7 / 12.0 (13.7)
dropped frames per second, median 47.6 46.3
style recalculation share of main-thread time (traced run) 36% 12.5%
elements restyled per recalculation, median / max (traced run) 51 / 7,900 1 / 898

The CPU result is clear: an inline style write no longer restyles the hidden-clip rules' descendants, so most recalculations touch one element. Frame rate barely moves on this host, because software painting of 31 playing 4K-scaled videos dominates each frame. The noise between runs is larger than the gap.

Chrome, same 4K film, on a GPU host (Apple silicon)

The same film, builds and method on an Apple silicon Mac on AC power with Low Power Mode off: Chrome 152 headless, painting on the GPU (Metal through ANGLE, with GPU compositing, rasterization and video decode enabled). Three alternating runs each, then two traced runs each. The host was shared, with load average 8 to 16.

main this head
frame rate, runs 1 / 2 / 3 (median) 56.3 / 59.1 / 56.5 (56.5) 57.8 / 59.7 / 56.8 (57.8)
dropped frames per second, median 3.5 2.2
worst frame, runs 1 / 2 / 3 33 / 33 / 50 ms 33 / 33 / 33 ms
style recalculation share of main-thread time (traced runs) 48% / 46% 21% / 20%
style recalculation / main-thread busy time over 10 s (second traced run) 2.1 s / 4.6 s 0.57 s / 2.8 s
elements restyled per recalculation, median / max (traced runs) 7,842 / 7,898 and 7,740 / 7,898 842 / 898 both

With a GPU, both builds play close to the 60 fps headless Chrome draws at, and the gap between their medians (1.3 fps) is smaller than the spread between runs of one build (about 3 fps). This host shows no frame-rate gain beyond noise. The CPU result repeats: style recalculation falls from under half of the main thread to a fifth, and the main thread is busy about 40% less for the same 10 s of playback, which shows up as headroom rather than frames here.

Tests

  • timedClipHide.test.ts:
    • No rule the runtime injects (playback or Studio preview) selects on inline style. This test fails on main.
    • A clip's contents are hidden by the mark alone; the fixture has no inline hide, so it fails on main.
    • The contents rule keeps the weight of two attribute selectors, asserted on the selector because jsdom settles !important ties by order.
    • showHiddenClips and revealClip lift the mark and put it back.
    • Compatibility path: with an older shader build on the page (found by its global or by __hf.transitions), a scene it revealed by inline style shows its text and images, and a hidden one stays hidden. With a build that declared, the fast rules stay and none of them selects on inline style.
  • shader-transitions: the page-side compositor's copy of a marked scene and of its timed clips carries no mark and is visible, while the live scene keeps its mark. The html2canvas copy shows a marked scene's contents. Loading the package declares it.
  • Studio: one test each for the layer reveal and the gesture preview (lift, then put back).
  • Each new test was checked by breaking its code: compositor copy without revealClip, the compatibility path returning early, a single-attribute contents rule, and the package not declaring. Each break turned exactly its own test red.
  • Real Chrome (coreRuntimeBrowser.test.ts), in a render and in a preview:
    • An inline style write restyles only its own element, not the hidden-clip rules' descendants. Fails with the contents rule keyed on inline style.
    • A hidden scene paints nothing, even a child or SVG shape a rewound GSAP set made visible inline. Fails without !important on the contents rule.
    • A scene copy forced visible inline shows its contents with an older shader build on the page and stays hidden with a current one. Fails with the compatibility path off, and with it applied to declared builds.
    • Preview images in hidden scenes stay skipped until the scene shows. Fails when the skip rule ignores the mark.
    • A later scene's child shows again after a seek or render seek past the GSAP set that hides it, and filter tweens start from the stylesheet filter. Fails when showHiddenClips keeps the mark.
    • Main's filter-output tests (SVG shapes, HTML layers, the scene, pseudo-elements) pass on the mark-keyed rule. They fail when the rule drops the pseudo-element layers.

Render output

Seven existing producer fixtures, rendered to PNG at main and with this change (Chrome, one worker), every frame hashed:

  • Byte-identical: timed-descendant-visibility (270 frames), many-cuts (168), sub-comp-t0 (180), page-side-shader-compositor-render-compat (60).
  • Not comparable: style-1-prod and overlay-montage-prod differ between two renders of main itself (96 frames for the montage).
  • Changed: gsap-letters-render-compat, 35 of 120 frames, the same on every render. The cause is the removed selector itself, not a change in what is hidden: adding back a rule that never matches, [data-hf-never][style*="never"] * {}, restores main's bytes exactly. A [style] selector anywhere in the page changes how Chrome paints elements whose inline transform changes each frame; without it, the edge antialiasing of the rotating letters moves by under a pixel. Frame 77: 730 edge pixels of the "H" change, PSNR 43.9 dB (the fixture's floor is 30). The edges are no softer: 518 partially covered edge pixels against 608 on main, with the same total edge contrast.

CI note

The Windows engine-cli test lane timed out in three CLI network tests this change does not touch: the publicNetwork controlled-socket integration hook (30,000 ms limit), and in cloud/download "splits a large file into parallel ranged requests" (20,494 ms) and "streams the whole file in one request when the server ignores Range" (20,004 ms), both against a 20,000 ms limit. The same workflow passed on main at this branch's base. They are handed over as Windows flakes; the failed jobs are rerun at this head.

Before

gsap-letters-render-compat frame 77 at main, the "H" cropped and scaled 4x over black:

Before: the H in frame 77 at main, 4x

After

The same crop with this change: the stroke edges land a fraction of a pixel differently, no softer.

After: the H in frame 77 with this change, 4x

Render across a scene boundary (page-side compositor, MP4)

The page-side-shader-compositor-render-compat fixture with scene-b moved to start at 1.2 s, inside the 0.75 s to 1.6 s glitch transition, so the runtime marks the incoming scene for the transition's first half. Rendered to MP4 with page-side compositing on (the default), video frames compared by decoded-frame MD5:

runtime older vendored shader build shader build from this branch
main vs this head identical, 60 of 60 frames identical, 60 of 60 frames
main vs this head without the compatibility path frames 0 to 48 differ: the incoming scene loses its contents during the transition, and the encoder's lookahead carries that into earlier frames not rendered

Frames 26, 30 and 34 with the older shader build. Left to right: main, this head without the compatibility path (no "BETA"), this head:

Frames 26, 30, 34 with the older shader build: main, head without the compatibility path, head

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at ba20af48. The WebKit fix is right and well measured: dropping [style*=…] from every injected rule ends the per-write inline-style re-serialization. I'm not approving yet, because of one cross-version case that needs an answer.

Older pinned @hyperframes/shader-transitions against the new runtime rule. Compositions load shader-transitions from the CDN at a pinned version, for example packages/producer/tests/hdr-regression/src/index.html loads @hyperframes/shader-transitions@0.4.11/dist/index.global.js. The runtime, though, is always the current one. Here's how the pieces interact:

  • Released versions' showSceneAndClips set visibility: visible !important inline on the scene and on each [data-start] inside it, but don't call liftClipHiddenMark. They can't, since it's new.
  • The visibility pass still marks every timed clip it hides (init.ts:3029, toggleAttribute(CLIP_HIDDEN_ATTR, …)), including an incoming scene's inner clips that start later.
  • Old rule: [data-hf-clip-hidden][style*="visibility: hidden"] * stopped matching once the old capture rewrote the inline style to visible, so those clips' children were captured.
  • New rule: [data-hf-clip-hidden] * { visibility: hidden !important } still matches. The [data-start] elements themselves stay visible, because inline !important beats an author-sheet !important, but their plain descendants (text, divs, images without data-start) are now hidden in the captured frame.

So a composition pinned to a released shader-transitions, whose incoming scene holds an inner clip that starts after the scene, would lose that clip's content from the transition's captured texture. I derived this from the cascade and haven't rendered it. The hdr-regression fixture (0.4.11) passing doesn't settle it unless it has an inner clip starting after its scene.

Ways to close it, your call:

  1. Render a fixture pinned to the current published shader-transitions with an inner timed clip that starts inside the incoming scene, and compare it against main. If it's unchanged, I'm wrong about the reachability, and a note in the body is enough.
  2. If it does change: keep a compat path that doesn't reintroduce a [style] selector in the injected sheet. For example, the runtime could lift the mark on any element whose inline visibility is forced visible when it next runs its pass, or the release notes could require a minimum shader-transitions version with a lint warning on older pins.

What I checked, and it's fine:

  • The liftClipHiddenMark helper returns its restore function. showHiddenClips restores both the inline visibility and the marks. Studio's layer reveal puts the mark back only when it restores its own visibility, and the gesture preview puts it back on release.
  • noExternal: ["@hyperframes/core"] bundles the helper into shader-transitions, so there's no new install dependency (it's a devDependency).
  • The remaining [style*="visibility: hidden"] uses are closest()/querySelectorAll lookups (init.ts:3090, layout-audit.browser.js:716), not injected rules, so they don't cause the WebKit cost.
  • The "author sets visibility: visible inline" behaviour change is stated in the body. For authors, that's fine.

— Somu

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 124 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (4)

  • seqplainkeys-none-center-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqheldtwice-tlhold-px-r0-nested-z100: tracking 120.29, pressJump 312.3, drop 0.07, reload 0.07, render 0.03, renderKey -, undo true, teleport false / tracking 0.41, pressJump 0.11, drop 0.39, reload 0.39, render 0.02, renderKey -, undo true, teleport true / tracking 0.41, pressJump 0.11, drop 0.39, reload 0.39, render 0.02, renderKey -, undo true, teleport true

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 11c461b2. The WebKit diagnosis and the fix are right, and the measurements are careful. Dropping [style*=…] from every injected rule is the correct structural fix, and lifting the mark explicitly in showHiddenClips, capture.ts, the layer reveal and the gesture preview (each with a test that asserts both the lift and the put-back) is well done. One revealer was missed, and it sits on the default render path.

Blocker: the page-side shader compositor reveals scene copies without lifting the mark

packages/shader-transitions/src/engineModePageComposite.ts:287-302 (prepareComposite) does live.cloneNode(true), sets clone.style.visibility = "visible" and sets visibility = "visible" on every [data-start] inside the clone. It never calls liftClipHiddenMark. Its own comment says "Cf. forceSceneVisibleInClone", so it is a sibling of the capture.ts showSceneAndClips this PR fixes.

  • cloneNode(true) copies data-hf-clip-hidden. The copies go into layoutsubtree staging canvases appended to document.body (:236-247), so the runtime's contents rule, which hideTimedClipsUntilFirstPass installs in render as well as preview, applies to them.
  • Before: [data-hf-clip-hidden][style*="visibility: hidden"] * stopped matching once the copy's inline visibility was set to visible.
  • Now: [data-hf-clip-hidden] * { visibility: hidden !important } keeps matching. Every plain descendant of a marked copy (or of a marked [data-start] inside it) is hidden, and drawElementImage rasterizes the scene's box and background with its contents gone.

It is reachable by default:

  • enablePageSideCompositing: true is the default (packages/engine/src/config.ts:307).
  • Shipped guidance places transitions across scene boundaries (docs/guides/claude-design-hyperframes.md:264: "scene-3 ends at 8s, transition duration 0.5s -> time: 7.75"), with class="scene clip" scenes that carry data-start. So the visibility pass marks the incoming scene for the first half of the transition and the outgoing scene for the second half. The compositor's own test models this timing (engineModePageComposite.test.ts:150-163).

Result: in SDR MP4/GIF renders, part of every boundary-straddling shader transition blends from or to an empty scene background, and the content pops in or out at the clip boundary.

Why CI and the fixture sweep miss it: page-side-shader-compositor-render-compat has both scenes at data-start="0" data-duration="2" (src/index.html:84,89), so neither is ever marked. It also loads a vendored src/vendor/hyper-shader.global.js, not this package's source.

Fix: lift the mark on the copy and on its [data-start] descendants in prepareComposite. No put-back is needed, because the copies are thrown away. Add a compositor test with a marked scene so that removing the lift fails it.

This is separate from @somanshreddy's open question about older pinned shader-transitions builds. That one is version skew; this is the in-repo code at this head.

Nits (non-blocking)

  1. timedClipHide.ts:22: the hide rule's specificity drops from (0,2,0) to (0,1,0), so an author !important visibility: visible rule at (0,1,1) or above now beats it. I found no shipped template that does this. [data-hf-clip-hidden][data-hf-clip-hidden] * keeps the old weight without a [style] selector.
  2. packages/cli/src/commands/layout-audit.browser.js:716 still mirrors the old [style*="visibility: hidden"] rule, and its comment says it follows core's timedClipHide rule. Nothing breaks, but the mirror has drifted.
  3. timedClipHide.test.ts: the "hides … by the mark alone" fixture also carries inline visibility: hidden, so it passes on main too. The selector test is the one that tells old from new.

CI: every required check has a success run at 11c461b2.

Verdict: REQUEST CHANGES
Reasoning: The default page-side compositor path copies the mark into its scene copies without lifting it, so boundary-straddling shader transitions render empty scenes, and no fixture covers that case.

— Via

…eep the scene's contents

Every revealer now goes through one core helper that shows the clip and
lifts the runtime's hidden mark together; the page-side compositor's scene
copies were still carrying the mark. The contents rule keeps its old weight.
…sition scenes

Published shader builds reveal scenes by inline style without lifting the
hidden mark. When one is on the page, the runtime keeps the inline-style
rules; builds that lift the mark declare it on load and keep the fast rules.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Updated to 6284f104.

@vanceingalls, blocker: fixed at the root, not only in prepareComposite. Every revealer now goes through one helper, revealClip(el, priority?) in @hyperframes/core/runtime/clip-reveal. It sets the inline visibility and lifts the mark in one call and returns the put-back, so no revealer can do one without the other. It covers the runtime's showHiddenClips, the html2canvas copy, the page-side compositor's copies (the clone and each [data-start] inside it; no put-back since they're thrown away), Studio's layer reveal and the gesture preview. I re-audited every visibility = "visible" / setProperty("visibility", "visible") and every cloneNode across packages/. The only other clone that's shown is the engine's 3D projection, which rasterizes its clone in its own SVG image where no runtime sheet applies. The engine's layered capture reveals through a stylesheet, not inline style, so it matched the same elements under the old and new rules. The new compositor test stages a marked incoming scene with a marked timed child and fails when the lift is removed.

Nits:

  1. Weight kept: [data-hf-clip-hidden][data-hf-clip-hidden] *, with a test that fails on a single attribute. That also keeps the engine's render-id show rule ([attr="…"] *, one attribute) losing to it as before, rather than tying.
  2. layout-audit.browser.js now mirrors the mark-only selector.
  3. The "mark alone" fixture no longer carries an inline hide, so it fails on main.

@somanshreddy, older pinned builds: you were right, and it reaches the default render path. I rendered your case: the page-side compositor fixture with the incoming scene starting mid-transition, the older vendored build, MP4. Without a compatibility path the incoming scene renders without its contents during the transition's first half. The frames are in the body. Fixed by a compatibility path. Current builds call declareClipRevealAware() on load. Once composition scripts have run, the runtime checks whether a shader build that didn't declare is on the page (its HyperShader global or __hf.transitions). If so, it rewrites its two injected rules back to the inline-style form for that page only. With that, this head renders the boundary fixture byte-identical to main with both the older and the current shader build, and the render without the path shows the empty scene. The body names it as a compatibility path and says when it can be removed.

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Updated to 4ad55282. Main is merged in (no rebase, no force push).

Conflict, timedClipHide.ts. Main changed the same rules twice since this branch's base. Both changes are kept, keyed on the mark:

  • The filter-output rule now covers the hidden clip itself, its descendants and their ::before/::after. hideHiddenClipContents(clip) builds that list from the clip selector it is given, so the compatibility path for older shader builds rewrites it the same way.
  • Studio's capture page injects no image-skip rule. The preview's skip rule still matches the mark.
  • Main now shows hidden clips on every forward render step, not only after a jump. That path is showHiddenClips, which already lifts the mark through revealClip.

New real-Chrome tests in coreRuntimeBrowser.test.ts, listed under Tests in the body. Each one was checked by breaking the code it covers. The checks: the contents rule keyed on inline style again, the rule without !important, the compatibility path off, the compatibility path applied to declared builds, the skip rule ignoring the mark, showHiddenClips keeping the mark, and the filter rule dropping pseudo-element layers. Each break turned its own tests red, and the head is green three runs in a row.

The earlier findings (the page-side compositor copy, older pinned shader builds, the rule's weight, the layout-audit mirror and the mark-only fixture) were fixed at 6284f104 and are unchanged by the merge.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · head 4ad55282

All prior findings are resolved at this head. @vanceingalls's blocker is fixed: prepareComposite reveals the clone and its [data-start] children through revealClip (engineModePageComposite.ts:299-302). His three nits are fixed too: the doubled-attribute weight, the layout-audit mirror, and the mark-only fixture. @somanshreddy's question about older pinned builds is closed by keepInlineHideRulesForOlderShaders, which runs at bootstrap after composition scripts. The main merge kept both of main's timedClipHide changes, keyed on the mark. Every GitHub Actions workflow is green at this head.

Scope

I read all non-test source end to end: clipReveal.ts, timedClipHide.ts, entry.ts, the three shader-transitions sites, the two Studio hooks, and the layout-audit mirror. For each, I checked that the reveal and the put-back always happen together:

  • showHiddenClips restores the inline visibility and then the mark.
  • The layer reveal puts the mark back only when it restores its own visibility.
  • The gesture's ??= keeps the mark state from the start of the gesture.
  • The capture and compositor copies are thrown away, so nothing needs putting back.

I grepped the rest of packages/ for any other code that sets inline visibility to visible:

  • The generator's tl.set bookends match the clip window.
  • The engine's __render_frame__ images aren't timed clips, so they never carry the mark.
  • threeDProjection renders in its own SVG.

SKIPPED_CLIP and the closest() lookups in init.ts are JS queries, not injected rules. Sub-compositions are bundled into the page by the producer and the Studio server, so their shader scripts run before bootstrap. I didn't re-run the render fixtures and relied on the body's hash results.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants