Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0941127
fix(runtime): a scene swap refuses when an animation outside the scen…
miguel-heygen Sep 26, 2026
e140b65
test(runtime): pin the scene swap volume re-probe, host CSS variables…
miguel-heygen Sep 26, 2026
fccdac2
fix(runtime): a scene swap keeps a video its scene script animates in…
miguel-heygen Sep 26, 2026
d262c3f
fix(runtime): a scene swap stops every animation the old scene script…
miguel-heygen Sep 26, 2026
26c59c1
fix(runtime): record scene script animations only on pages a scene sw…
miguel-heygen Sep 26, 2026
b499125
docs(core): reword the scene animation recording comment
miguel-heygen Sep 26, 2026
c00f568
fix(runtime): watch media as the page parses only on pages a scene sw…
miguel-heygen Sep 26, 2026
3422031
perf(runtime): a scene swap checks for outside tweens in one pass ove…
miguel-heygen Sep 26, 2026
6ba503f
test(runtime): pin the single revert and the cleared animation record…
miguel-heygen Sep 26, 2026
bd7c0de
fix(runtime): a scene swap rebuilds media when the edit changes the s…
miguel-heygen Sep 26, 2026
40bae65
fix(runtime): a scene swap undoes the sets a scene script made
miguel-heygen Sep 26, 2026
dd08559
fix(runtime): a scene swap rechecks outside tweens and runs new scrip…
miguel-heygen Sep 26, 2026
36bdf2a
test(runtime): pin that a tween started during the caption wait leave…
miguel-heygen Sep 26, 2026
d7ffadd
fix(runtime): a scene swap refuses when its script set a value on an …
miguel-heygen Sep 26, 2026
936d9fe
docs(core): say the scene recording drops a finished tween that the t…
miguel-heygen Sep 26, 2026
f04c764
fix(runtime): a scene swap refuses when two of its animations write t…
miguel-heygen Sep 26, 2026
16bb1fc
fix(runtime): a scene swap resets kept media and refuses animations o…
miguel-heygen Sep 26, 2026
4f7c8aa
fix(runtime): a scene swap keeps a moved video in place and swaps twe…
miguel-heygen Sep 26, 2026
005f3a5
chore(core): regenerate the inlined position-edits script for the kep…
miguel-heygen Sep 26, 2026
3d5c984
fix(runtime): a scene swap checks the timeline registry as it is afte…
miguel-heygen Sep 26, 2026
c9f8ade
fix(runtime): a scene swap refuses when a scene changed during the wa…
miguel-heygen Sep 26, 2026
f48d8bd
fix(runtime): a scene swap stops every animation a revert chain regis…
miguel-heygen Sep 26, 2026
927c05d
fix(runtime): a scene swap refuses a moved host or a cancelled swap a…
miguel-heygen Sep 27, 2026
c24b06e
fix(runtime): a scene swap refuses when a scene leaves its place or t…
miguel-heygen Sep 27, 2026
7947ade
fix(runtime): a scene swap checks only its host's place, so a later b…
miguel-heygen Sep 27, 2026
3981bfe
fix(runtime): a scene swap refuses when the film's scene layout chang…
miguel-heygen Sep 27, 2026
5c665d2
fix(runtime): a scene swap refuses when the order around a scene chan…
miguel-heygen Sep 27, 2026
eea0828
test(runtime): pin that a scene swap checks the film root order when …
miguel-heygen Sep 27, 2026
7e45dfb
fix(runtime): a scene swap gives kept media the mute, rate and loop a…
miguel-heygen Sep 27, 2026
06cafc5
fix(runtime): a scene swap rebuilds media a script wrote attributes o…
miguel-heygen Sep 27, 2026
ac2492b
fix(runtime): a scene swap keeps media carrying the runtime classes o…
miguel-heygen Sep 27, 2026
b966fdc
chore(runtime): merge main
miguel-heygen Sep 28, 2026
86d0fae
chore(runtime): merge main
miguel-heygen Sep 28, 2026
b9668c8
fix(runtime): a scene swap resets a kept video's volume as a load doe…
miguel-heygen Sep 28, 2026
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
69 changes: 69 additions & 0 deletions packages/core/src/compiler/compositionScoping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,75 @@ window.__timelines.scene = tl;
expect(gsapTargets).toEqual([["Scene"], ["Scene"]]);
});

it.each([
["records", `<meta name="hf-scene-parts" content="{}">`, true],
["on a page no scene swap can act on, records nothing of", "", false],
])(
"%s what a script starts on GSAP's global timeline, however it reaches GSAP",
(_, head, records) => {
const { document } = parseHTML(
`<html><head>${head}</head><body><div data-composition-id="scene"><p>x</p></div></body></html>`,
);
const children: object[] = [{ startedBefore: true }];
const start = () => {
const animation = { to: () => animation };
children.push(animation);
return animation;
};
const gsap = {
globalTimeline: { getChildren: () => [...children] },
timeline: start,
to: start,
};
const fakeWindow: Record<string, unknown> = { document, __timelines: {}, gsap };
const wrapped = wrapScopedCompositionScript(
`
gsap.timeline().to("p", { x: 1 });
const g = gsap; g.to("p", { x: 1 });
gsap["to"]("p", { x: 1 });
window.gsap.to("p", { x: 1 });
globalThis.gsap.to("p", { x: 1 });
`,
"scene",
);
vi.stubGlobal("gsap", gsap);
try {
new Function("window", "gsap", wrapped)(fakeWindow, gsap);
} finally {
vi.unstubAllGlobals();
}
expect(children).toHaveLength(6);
expect(fakeWindow.__hfSceneAnimations ?? null).toEqual(
records ? { scene: children.slice(1) } : null,
);
},
);

it("records a set, which completes as it is made, without leaving it on GSAP's global timeline", () => {
const { document } = parseHTML(
`<html><head><meta name="hf-scene-parts" content="{}"></head><body><div data-composition-id="scene"><p>x</p></div></body></html>`,
);
const children: object[] = [];
// As the library does: the global timeline drops each animation the moment it completes.
const globalTimeline = {
autoRemoveChildren: true,
getChildren: () => [...children],
remove: (child: object) => void children.splice(children.indexOf(child), 1),
};
const set = () => {
const tween = { totalProgress: () => 1 };
if (!globalTimeline.autoRemoveChildren) children.push(tween);
return tween;
};
const gsap = { globalTimeline, set };
const fakeWindow: Record<string, unknown> = { document, __timelines: {}, gsap };
const wrapped = wrapScopedCompositionScript(`gsap.set("p", { opacity: 0 });`, "scene");
new Function("window", "gsap", wrapped)(fakeWindow, gsap);
expect(fakeWindow.__hfSceneAnimations).toEqual({ scene: [expect.any(Object)] });
expect(children).toEqual([]);
expect(globalTimeline.autoRemoveChildren).toBe(true);
});

it("scopes each selector in a GSAP target array to the composition root", () => {
const { document } = parseHTML(`
<div data-composition-id="scene">
Expand Down
28 changes: 27 additions & 1 deletion packages/core/src/compiler/compositionScoping.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import postcss, { type AtRule, type Node, type Rule } from "postcss";
import { SCENE_PARTS_META } from "../sceneParts";

const AUTHORED_ROOT_ID_ATTR = "data-hf-authored-id";
const INNER_ROOT_ATTR = "data-hf-inner-root";
Expand Down Expand Up @@ -645,8 +646,33 @@ ${source.replace(/<\/(script)/gi, "<\\/$1")}
console.error(__hfErrorLabel, __hfCompId, _err);
}
};
// What the script started on the global gsap timeline, by any route, for a scene swap to revert.
// Only a page with a scene manifest can swap; elsewhere the first script stores null and none records.
var __hfRecordAnimations = function(run) {
if (window.__hfSceneAnimations === undefined) {
window.__hfSceneAnimations = window.document.querySelector(${jsonScriptLiteral(`meta[name="${SCENE_PARTS_META}"]`)})
? {}
: null;
}
var byComp = window.__hfSceneAnimations;
var globalTimeline = __hfBaseGsap && __hfBaseGsap.globalTimeline;
if (!byComp || !globalTimeline || !__hfTimelineCompId) return run();
var before = globalTimeline.getChildren(false);
// A set completes as it is made and would leave the timeline before the diff below.
var autoRemove = globalTimeline.autoRemoveChildren;
globalTimeline.autoRemoveChildren = false;
run();
globalTimeline.autoRemoveChildren = autoRemove;
var recorded = (byComp[__hfTimelineCompId] = byComp[__hfTimelineCompId] || []);
globalTimeline.getChildren(false).forEach(function(animation) {
if (before.indexOf(animation) >= 0) return;
recorded.push(animation);
// Dropped as the timeline drops a finished tween (it keeps a paused one); moved back, a tween re-adds itself.
if (autoRemove && !animation.getChildren && animation.totalProgress() === 1) globalTimeline.remove(animation);
});
};
__hfFindRoot();
__hfRun();
__hfRecordAnimations(__hfRun);
})();`;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// AUTO-GENERATED by scripts/build-position-edits-render.ts - do not edit
const POSITION_EDITS_RENDER_IIFE: string =
'"use strict";(()=>{function T(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=T(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(s){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var x=null;function R(e,t){if(x)try{x({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var W="http://www.w3.org/1999/xhtml";function N(e){return e!=null&&e.nodeType===1}function _(e){return N(e)&&(e.namespaceURI===W||e.namespaceURI==="http://www.w3.org/2000/svg")}var w="data-hf-edit-base-x",y="data-hf-edit-base-y",m="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},O=e=>{let t=[],o=0,r="";for(let s of e.trim())s==="("&&(o+=1),s===")"&&(o=Math.max(0,o-1)),/\\s/.test(s)&&o===0?(r&&t.push(r),r=""):r+=s;return r&&t.push(r),t},L=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,b=(e,t)=>L.test(e)&&L.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,V=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,s,a]=O(e);if(r===void 0)return`${t} ${o}`;if(s===void 0)return`${b(r,t)} ${o}`;let i=a===void 0?"":` ${a}`;return`${b(r,t)} ${b(s,o)}${i}`},j=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},G=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},E=new WeakMap;function B(e,t){let o=E.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(w)),s=k(e.getAttribute("data-y"))-k(e.getAttribute(y));e.hasAttribute(m)||e.setAttribute(m,G(e)),o===void 0&&j(e);let a=e.getAttribute(m)??"",i=V(a,`${r}px`,`${s}px`);e.style.setProperty("translate",i),E.set(e,e.style.getPropertyValue("translate"))}function S(e,t){let o=e.querySelectorAll(`[${m}]:not([${w}]):not([${y}])`);for(let a=0;a<o.length;a++){let i=o[a];if(i===void 0||!_(i))continue;let g=i.getAttribute(m)??"";g===""?i.style.removeProperty("translate"):i.style.setProperty("translate",g),i.removeAttribute(m),E.delete(i)}let r=e.querySelectorAll(`[${w}], [${y}]`),s=0;for(let a=0;a<r.length;a++){let i=r[a];i===void 0||!_(i)||(B(i,t),s+=1)}return s}var $="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,M=new WeakMap,H=new WeakMap;function F(e){let t=e,o=()=>{try{S(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[$]),s=n=>{D.add(n);try{Object.defineProperty(n,$,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let l=function(...u){let d=n.apply(this,u);return o(),d};return s(l),l},i=(n,l)=>{let u=M.get(n);if(u?.has(l))return!0;let d=Object.getOwnPropertyDescriptor(n,l);if(d?.configurable===!1){let c=n[l];return typeof c=="function"&&(n[l]=a(c),o()),!1}let p=n[l],f=d?.set;return Object.defineProperty(n,l,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:c=>{p=a(c),f?.call(n,c)}}),p=a(p),u??(u=new Set),u.add(l),M.set(n,u),o(),!0},g=(n,l)=>{let u=H.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let c=t[n];return c?i(c,l):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:c=>{f=c,f&&i(f,l)}}),u??(u=new Set),u.add(n),H.set(t,u)}let p=t[n];return p?i(p,l):!1},v=()=>{let n=g("__hf","seek"),l=g("__player","renderSeek");return n&&l};if(v())return;let h=120,A=t.setInterval(()=>{if(v()){t.clearInterval(A);return}h-=1,h<=0&&t.clearInterval(A)},50)}function I(){document.querySelector(`[${w}], [${y}]`)&&(S(document),F(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I,{once:!0}):I();})();\n';
'"use strict";(()=>{function T(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=T(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(s){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var x=null;function R(e,t){if(x)try{x({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var W="http://www.w3.org/1999/xhtml";function N(e){return e!=null&&e.nodeType===1}function _(e){return N(e)&&(e.namespaceURI===W||e.namespaceURI==="http://www.w3.org/2000/svg")}var w="data-hf-edit-base-x",y="data-hf-edit-base-y",m="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},O=e=>{let t=[],o=0,r="";for(let s of e.trim())s==="("&&(o+=1),s===")"&&(o=Math.max(0,o-1)),/\\s/.test(s)&&o===0?(r&&t.push(r),r=""):r+=s;return r&&t.push(r),t},L=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>L.test(e)&&L.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,V=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,s,a]=O(e);if(r===void 0)return`${t} ${o}`;if(s===void 0)return`${E(r,t)} ${o}`;let i=a===void 0?"":` ${a}`;return`${E(r,t)} ${E(s,o)}${i}`},j=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},G=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},b=new WeakMap;function B(e,t){let o=b.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(w)),s=k(e.getAttribute("data-y"))-k(e.getAttribute(y));e.hasAttribute(m)||e.setAttribute(m,G(e)),o===void 0&&j(e);let a=e.getAttribute(m)??"",i=V(a,`${r}px`,`${s}px`);e.style.setProperty("translate",i),b.set(e,e.style.getPropertyValue("translate"))}function S(e,t){let o=e.querySelectorAll(`[${m}]:not([${w}]):not([${y}])`);for(let a=0;a<o.length;a++){let i=o[a];if(i===void 0||!_(i))continue;let g=i.getAttribute(m)??"";g===""?i.style.removeProperty("translate"):i.style.setProperty("translate",g),i.removeAttribute(m),b.delete(i)}let r=e.querySelectorAll(`[${w}], [${y}]`),s=0;for(let a=0;a<r.length;a++){let i=r[a];i===void 0||!_(i)||(B(i,t),s+=1)}return s}var $="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,M=new WeakMap,H=new WeakMap;function F(e){let t=e,o=()=>{try{S(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[$]),s=n=>{D.add(n);try{Object.defineProperty(n,$,{value:!0})}catch{}},a=n=>{if(typeof n!="function"||r(n))return n;let l=function(...u){let d=n.apply(this,u);return o(),d};return s(l),l},i=(n,l)=>{let u=M.get(n);if(u?.has(l))return!0;let d=Object.getOwnPropertyDescriptor(n,l);if(d?.configurable===!1){let c=n[l];return typeof c=="function"&&(n[l]=a(c),o()),!1}let p=n[l],f=d?.set;return Object.defineProperty(n,l,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>p,set:c=>{p=a(c),f?.call(n,c)}}),p=a(p),u??(u=new Set),u.add(l),M.set(n,u),o(),!0},g=(n,l)=>{let u=H.get(t),d=Object.getOwnPropertyDescriptor(t,n);if(!u?.has(n)){if(d?.configurable===!1){let c=t[n];return c?i(c,l):!1}let f=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:d?.enumerable??!0,get:()=>f,set:c=>{f=c,f&&i(f,l)}}),u??(u=new Set),u.add(n),H.set(t,u)}let p=t[n];return p?i(p,l):!1},v=()=>{let n=g("__hf","seek"),l=g("__player","renderSeek");return n&&l};if(v())return;let h=120,A=t.setInterval(()=>{if(v()){t.clearInterval(A);return}h-=1,h<=0&&t.clearInterval(A)},50)}function I(){document.querySelector(`[${w}], [${y}]`)&&(S(document),F(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I,{once:!0}):I();})();\n';

/** Returns the pre-built position-edits render IIFE as a string constant. */
export function getPositionEditsRenderScript(): string {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/runtime/entry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { initSandboxRuntimeModular } from "./init";
import { initSandboxRuntimeModular, installAuthoredMediaCapture } from "./init";
import { installAuthoredOpacityCapture } from "./colorGrading";
import { hideTimedClipsUntilFirstPass } from "./timedClipHide";
import { fitTextFontSize } from "../text/fitTextFontSize";
Expand Down Expand Up @@ -28,6 +28,7 @@ type HyperframeWindow = Window & {
// composition's animation scripts (and the grading hide) mutate it — must run
// at script evaluation time, while the document is still parsing.
installAuthoredOpacityCapture();
installAuthoredMediaCapture();

hideTimedClipsUntilFirstPass();

Expand Down
Loading
Loading