@hyperframes/studio 0.8.149 → 0.8.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-C-2YwYre.js → hyperframes-player-DJt8E7k4.js} +1 -1
- package/dist/assets/{index-ByJU5J6n.js → index-BGks9KLI.js} +131 -131
- package/dist/assets/{index-BybGk67U.js → index-DWIHrAY2.js} +1 -1
- package/dist/assets/{index-Cfs2r3fz.js → index-DgbLblGR.js} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.html +1 -1
- package/dist/index.js +16 -19
- package/package.json +8 -8
- package/src/hooks/gsapShared.ts +1 -11
- package/src/hooks/usePersistentEditHistory.test.ts +43 -1
- package/src/hooks/usePersistentEditHistory.ts +3 -1
- package/src/player/lib/timelineDOM.test.ts +55 -0
- package/src/player/lib/timelineDOM.ts +5 -0
- package/src/player/store/timelineElement.ts +2 -1
- package/src/utils/audioMeterMath.test.ts +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{h as P}from"./index-
|
|
1
|
+
import{h as P}from"./index-BGks9KLI.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var h=e[o+x]-t,_=r.some(function(g){if(g.interval===h)return g.count+=1});_||r.push({interval:h,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
|
package/dist/index.d.ts
CHANGED
|
@@ -354,9 +354,10 @@ interface TimelineElement {
|
|
|
354
354
|
/** Clip-edge fades from `data-fade-in` / `data-fade-out`, seconds; absent means none. */
|
|
355
355
|
fadeIn?: number;
|
|
356
356
|
fadeOut?: number;
|
|
357
|
-
/** Verbatim `data-fx-chain` / `data-automation
|
|
357
|
+
/** Verbatim `data-fx-chain` / `data-automation` (`masterChain`: the root's); see automationLaneData. */
|
|
358
358
|
fxChain?: string;
|
|
359
359
|
automation?: string;
|
|
360
|
+
masterChain?: string;
|
|
360
361
|
/** Path from data-composition-src — identifies sub-composition elements */
|
|
361
362
|
compositionSrc?: string;
|
|
362
363
|
/** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
|
package/dist/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = "paper";
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-BGks9KLI.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="/assets/index-DkQKsFxx.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -5026,17 +5026,7 @@ function matchesSelector(tweenSelector, querySelector) {
|
|
|
5026
5026
|
}
|
|
5027
5027
|
|
|
5028
5028
|
// src/hooks/gsapShared.ts
|
|
5029
|
-
|
|
5030
|
-
opacity: 1,
|
|
5031
|
-
x: 0,
|
|
5032
|
-
y: 0,
|
|
5033
|
-
scale: 1,
|
|
5034
|
-
scaleX: 1,
|
|
5035
|
-
scaleY: 1,
|
|
5036
|
-
rotation: 0,
|
|
5037
|
-
width: 100,
|
|
5038
|
-
height: 100
|
|
5039
|
-
};
|
|
5029
|
+
import { KEYFRAME_PROPERTY_DEFAULTS } from "@hyperframes/core/keyframe-defaults";
|
|
5040
5030
|
function keyframeEases(anim) {
|
|
5041
5031
|
if (!anim.keyframes) return { easeEach: anim.ease };
|
|
5042
5032
|
return { ease: runEaseOf(anim), easeEach: anim.keyframes.easeEach };
|
|
@@ -5327,14 +5317,14 @@ function synthesizeFlatTweenKeyframes(anim) {
|
|
|
5327
5317
|
if (anim.method === "from") {
|
|
5328
5318
|
for (const [k, v] of Object.entries(toProps)) {
|
|
5329
5319
|
rawStart[k] = v;
|
|
5330
|
-
rawEnd[k] =
|
|
5320
|
+
rawEnd[k] = KEYFRAME_PROPERTY_DEFAULTS[k] ?? 0;
|
|
5331
5321
|
}
|
|
5332
5322
|
} else if (anim.method === "fromTo" && fromProps) {
|
|
5333
5323
|
Object.assign(rawStart, fromProps);
|
|
5334
5324
|
Object.assign(rawEnd, toProps);
|
|
5335
5325
|
} else {
|
|
5336
5326
|
for (const [k, v] of Object.entries(toProps)) {
|
|
5337
|
-
rawStart[k] =
|
|
5327
|
+
rawStart[k] = KEYFRAME_PROPERTY_DEFAULTS[k] ?? 0;
|
|
5338
5328
|
rawEnd[k] = v;
|
|
5339
5329
|
}
|
|
5340
5330
|
}
|
|
@@ -23030,7 +23020,7 @@ function propertyStops(animation2, property, start) {
|
|
|
23030
23020
|
}));
|
|
23031
23021
|
const first = stops[0];
|
|
23032
23022
|
if (!first || first.percentage <= 0 || typeof first.value !== "number") return stops;
|
|
23033
|
-
return [{ percentage: 0, value: start ??
|
|
23023
|
+
return [{ percentage: 0, value: start ?? KEYFRAME_PROPERTY_DEFAULTS[property] ?? 0 }, ...stops];
|
|
23034
23024
|
}
|
|
23035
23025
|
function interpolableEnds(before, after) {
|
|
23036
23026
|
if (typeof before.value !== "number" || typeof after.value !== "number") return null;
|
|
@@ -29115,6 +29105,7 @@ import { useCallback as useCallback38, useRef as useRef46 } from "react";
|
|
|
29115
29105
|
// src/player/lib/timelineDOM.ts
|
|
29116
29106
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
29117
29107
|
import { readClipTiming as readClipTiming2 } from "@hyperframes/core/composition-contract";
|
|
29108
|
+
import { readMasterFxChain } from "@hyperframes/core/audio-groups";
|
|
29118
29109
|
import { linkScopeOf } from "@hyperframes/core/media-link";
|
|
29119
29110
|
import { createRuntimeStartTimeResolver as createRuntimeStartTimeResolver2 } from "@hyperframes/core/runtime/start-resolver";
|
|
29120
29111
|
|
|
@@ -29693,6 +29684,8 @@ function createTimelineElementFromManifestClip(params) {
|
|
|
29693
29684
|
if (info.fxChain) entry2.audioGroupFxChain = info.fxChain;
|
|
29694
29685
|
if (info.automation) entry2.audioGroupAutomation = info.automation;
|
|
29695
29686
|
}
|
|
29687
|
+
const masterChain = readMasterFxChain(doc ?? hostEl.ownerDocument);
|
|
29688
|
+
if (masterChain) entry2.masterChain = masterChain;
|
|
29696
29689
|
const fxChain = hostEl.getAttribute("data-fx-chain");
|
|
29697
29690
|
if (fxChain) entry2.fxChain = fxChain;
|
|
29698
29691
|
const automation = hostEl.getAttribute("data-automation");
|
|
@@ -29815,6 +29808,8 @@ function parseTimelineFromDOM(doc, rootDuration, timelines) {
|
|
|
29815
29808
|
const resolvedSrc = mediaEl.src || void 0;
|
|
29816
29809
|
if (resolvedSrc) entry2.src = resolvedSrc;
|
|
29817
29810
|
}
|
|
29811
|
+
const domMasterChain = readMasterFxChain(doc);
|
|
29812
|
+
if (domMasterChain) entry2.masterChain = domMasterChain;
|
|
29818
29813
|
const domFxChain = el.getAttribute("data-fx-chain");
|
|
29819
29814
|
if (domFxChain) entry2.fxChain = domFxChain;
|
|
29820
29815
|
const domAutomation = el.getAttribute("data-automation");
|
|
@@ -47639,7 +47634,7 @@ async function commitWholePropertyOffset(selection, anim, newValues, currentPct,
|
|
|
47639
47634
|
const ts = resolveTweenStart(effectiveAnim);
|
|
47640
47635
|
const td = resolveTweenDuration(effectiveAnim);
|
|
47641
47636
|
const keys = Object.keys(newValues);
|
|
47642
|
-
const at = (props, key2) => typeof props[key2] === "number" ? props[key2] :
|
|
47637
|
+
const at = (props, key2) => typeof props[key2] === "number" ? props[key2] : KEYFRAME_PROPERTY_DEFAULTS[key2] ?? 0;
|
|
47643
47638
|
const kfs = effectiveAnim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(effectiveAnim)?.keyframes;
|
|
47644
47639
|
if (!kfs || kfs.length === 0) {
|
|
47645
47640
|
await callbacks.commitMutation(
|
|
@@ -74831,7 +74826,9 @@ function claimHeld(reply, label) {
|
|
|
74831
74826
|
if (reply.ok)
|
|
74832
74827
|
return reply.body?.claimed?.id ?? null;
|
|
74833
74828
|
if (reply.status !== 404)
|
|
74834
|
-
console.error(
|
|
74829
|
+
console.error(
|
|
74830
|
+
`[history] ${reply.status}: "${label}" was not recorded as your edit: ${reply.error}`
|
|
74831
|
+
);
|
|
74835
74832
|
return null;
|
|
74836
74833
|
}
|
|
74837
74834
|
async function overwroteVersions(files) {
|
|
@@ -82590,7 +82587,7 @@ async function commitKeyframeAtTimeImpl(selection, absoluteTime, animations, pro
|
|
|
82590
82587
|
}
|
|
82591
82588
|
const backfillDefaults = {};
|
|
82592
82589
|
for (const key2 of Object.keys(properties)) {
|
|
82593
|
-
backfillDefaults[key2] =
|
|
82590
|
+
backfillDefaults[key2] = KEYFRAME_PROPERTY_DEFAULTS[key2] ?? 0;
|
|
82594
82591
|
}
|
|
82595
82592
|
await commitMutation(
|
|
82596
82593
|
selection,
|
|
@@ -83011,7 +83008,7 @@ function useGsapPropertyDebounce(commitMutationSafely, sdk) {
|
|
|
83011
83008
|
const addGsapProperty = useCallback137(
|
|
83012
83009
|
// fallow-ignore-next-line complexity
|
|
83013
83010
|
async (selection, animationId, property) => {
|
|
83014
|
-
let defaultValue =
|
|
83011
|
+
let defaultValue = KEYFRAME_PROPERTY_DEFAULTS[property] ?? 0;
|
|
83015
83012
|
const el = selection.element;
|
|
83016
83013
|
if (property === "width" || property === "height") {
|
|
83017
83014
|
const rect = el.getBoundingClientRect();
|
|
@@ -83133,7 +83130,7 @@ function useGsapPropertyDebounce(commitMutationSafely, sdk) {
|
|
|
83133
83130
|
);
|
|
83134
83131
|
const addGsapFromProperty = useCallback137(
|
|
83135
83132
|
async (selection, animationId, property) => {
|
|
83136
|
-
const defaultValue =
|
|
83133
|
+
const defaultValue = KEYFRAME_PROPERTY_DEFAULTS[property] ?? 0;
|
|
83137
83134
|
const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
|
|
83138
83135
|
if (sdkSession && sdkDeps) {
|
|
83139
83136
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.151",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/player": "0.8.151",
|
|
64
|
+
"@hyperframes/sdk": "0.8.151",
|
|
65
|
+
"@hyperframes/core": "0.8.151",
|
|
66
|
+
"@hyperframes/shader-transitions": "0.8.151",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.151",
|
|
68
|
+
"@hyperframes/parsers": "0.8.151"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"vite": "^6.4.2",
|
|
83
83
|
"vitest": "^4.1.11",
|
|
84
84
|
"zustand": "^5.0.0",
|
|
85
|
-
"@hyperframes/producer": "0.8.
|
|
85
|
+
"@hyperframes/producer": "0.8.151"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"react": "19",
|
package/src/hooks/gsapShared.ts
CHANGED
|
@@ -25,17 +25,7 @@ export interface IframeGsap {
|
|
|
25
25
|
|
|
26
26
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
export
|
|
29
|
-
opacity: 1,
|
|
30
|
-
x: 0,
|
|
31
|
-
y: 0,
|
|
32
|
-
scale: 1,
|
|
33
|
-
scaleX: 1,
|
|
34
|
-
scaleY: 1,
|
|
35
|
-
rotation: 0,
|
|
36
|
-
width: 100,
|
|
37
|
-
height: 100,
|
|
38
|
-
};
|
|
28
|
+
export { KEYFRAME_PROPERTY_DEFAULTS as PROPERTY_DEFAULTS } from "@hyperframes/core/keyframe-defaults";
|
|
39
29
|
|
|
40
30
|
/** A tween's eases for re-writing it as keyframes: a flat tween's ease must become easeEach,
|
|
41
31
|
* since GSAP eases each percentage segment power1.inOut whatever the tween-level ease says. */
|
|
@@ -561,7 +561,9 @@ it("a claim the server refused is logged, and the write stays undoable as a chan
|
|
|
561
561
|
files: { "index.html": { before: "A", after: "B" } },
|
|
562
562
|
}),
|
|
563
563
|
);
|
|
564
|
-
expect(logged).toHaveBeenCalledWith(
|
|
564
|
+
expect(logged).toHaveBeenCalledWith(
|
|
565
|
+
'[history] 500: "Moved Title" was not recorded as your edit: disk full',
|
|
566
|
+
);
|
|
565
567
|
restoreFetch();
|
|
566
568
|
expect(await act(() => hook().undo({ readFile }))).toMatchObject({
|
|
567
569
|
ok: true,
|
|
@@ -570,6 +572,46 @@ it("a claim the server refused is logged, and the write stays undoable as a chan
|
|
|
570
572
|
expect(file()).toBe("A");
|
|
571
573
|
});
|
|
572
574
|
|
|
575
|
+
it.each([
|
|
576
|
+
[
|
|
577
|
+
"the server refused",
|
|
578
|
+
409,
|
|
579
|
+
() => answer("/history/claim", 409, { error: "newer edit" }),
|
|
580
|
+
"newer edit",
|
|
581
|
+
],
|
|
582
|
+
[
|
|
583
|
+
"could not reach the server",
|
|
584
|
+
0,
|
|
585
|
+
() => {
|
|
586
|
+
const real = globalThis.fetch;
|
|
587
|
+
vi.stubGlobal("fetch", (url: string, init?: RequestInit) =>
|
|
588
|
+
url.endsWith("/history/claim")
|
|
589
|
+
? Promise.reject(new TypeError("fetch failed"))
|
|
590
|
+
: real(url, init),
|
|
591
|
+
);
|
|
592
|
+
},
|
|
593
|
+
"Studio could not reach its server.",
|
|
594
|
+
],
|
|
595
|
+
] as const)(
|
|
596
|
+
"a claim that %s logs under [history] with its status",
|
|
597
|
+
async (_, status, stub, reason) => {
|
|
598
|
+
const { hook, save } = await studio();
|
|
599
|
+
const logged = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
600
|
+
cleanup.push(() => logged.mockRestore());
|
|
601
|
+
stub();
|
|
602
|
+
save("B");
|
|
603
|
+
await act(() =>
|
|
604
|
+
hook().recordEdit({
|
|
605
|
+
label: "Moved Title",
|
|
606
|
+
files: { "index.html": { before: "A", after: "B" } },
|
|
607
|
+
}),
|
|
608
|
+
);
|
|
609
|
+
expect(logged).toHaveBeenCalledWith(
|
|
610
|
+
`[history] ${status}: "Moved Title" was not recorded as your edit: ${reason}`,
|
|
611
|
+
);
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
573
615
|
it("a step whose reply cannot be read says so, instead of throwing", async () => {
|
|
574
616
|
const { hook, readFile } = await studio();
|
|
575
617
|
answer("/history/step", 200, "<html>");
|
|
@@ -154,7 +154,9 @@ function claimHeld(reply: Awaited<ReturnType<typeof post>>, label: string): stri
|
|
|
154
154
|
if (reply.ok)
|
|
155
155
|
return (reply.body as { claimed: { id: string } | null } | null)?.claimed?.id ?? null;
|
|
156
156
|
if (reply.status !== 404)
|
|
157
|
-
console.error(
|
|
157
|
+
console.error(
|
|
158
|
+
`[history] ${reply.status}: "${label}" was not recorded as your edit: ${reply.error}`,
|
|
159
|
+
);
|
|
158
160
|
return null;
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -632,6 +632,61 @@ describe("audio FX attributes on parsed elements", () => {
|
|
|
632
632
|
});
|
|
633
633
|
});
|
|
634
634
|
|
|
635
|
+
describe("the master chain on parsed elements", () => {
|
|
636
|
+
const CHAIN = '{"version":1,"nodes":[{"type":"truepeak","id":"m1","params":{}}]}';
|
|
637
|
+
const parseVideo = (rootAttrs: string, inner = ""): TimelineElement | undefined => {
|
|
638
|
+
const doc = new DOMParser().parseFromString(
|
|
639
|
+
`<div data-composition-id="main" data-start="0" data-duration="10" ${rootAttrs}>
|
|
640
|
+
<video id="talk" data-start="0" data-duration="10" data-has-audio="true"></video>${inner}
|
|
641
|
+
</div>`,
|
|
642
|
+
"text/html",
|
|
643
|
+
);
|
|
644
|
+
return parseTimelineFromDOM(doc, 10).find((e) => e.domId === "talk");
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
it("copies the root's data-fx-chain onto an element that has none of its own", () => {
|
|
648
|
+
const talk = parseVideo(`data-fx-chain='${CHAIN}'`);
|
|
649
|
+
expect(talk?.masterChain).toBe(CHAIN);
|
|
650
|
+
expect(talk?.fxChain).toBeUndefined();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("leaves it unset when the root carries none", () => {
|
|
654
|
+
expect(parseVideo("")?.masterChain).toBeUndefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("ignores a sub-composition root's chain", () => {
|
|
658
|
+
const inner = `<div data-composition-id="inner" data-fx-chain='${CHAIN}'></div>`;
|
|
659
|
+
expect(parseVideo("", inner)?.masterChain).toBeUndefined();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("carries it on the manifest path too", () => {
|
|
663
|
+
const doc = makeDoc(`
|
|
664
|
+
<div data-composition-id="main" data-fx-chain='${CHAIN}'>
|
|
665
|
+
<video id="talk" data-has-audio="true"></video>
|
|
666
|
+
</div>
|
|
667
|
+
`);
|
|
668
|
+
const element = createTimelineElementFromManifestClip({
|
|
669
|
+
clip: {
|
|
670
|
+
id: "talk",
|
|
671
|
+
label: "Talk",
|
|
672
|
+
kind: "video",
|
|
673
|
+
tagName: "video",
|
|
674
|
+
start: 0,
|
|
675
|
+
duration: 10,
|
|
676
|
+
track: 0,
|
|
677
|
+
compositionId: null,
|
|
678
|
+
parentCompositionId: "main",
|
|
679
|
+
compositionSrc: null,
|
|
680
|
+
assetUrl: null,
|
|
681
|
+
},
|
|
682
|
+
fallbackIndex: 0,
|
|
683
|
+
doc,
|
|
684
|
+
hostEl: doc.getElementById("talk"),
|
|
685
|
+
});
|
|
686
|
+
expect(element.masterChain).toBe(CHAIN);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
635
690
|
// A composition clip trimmed by 30 px collapsed to its first nested video's 1.77 s: that length capped the trim.
|
|
636
691
|
describe("a composition clip's source length", () => {
|
|
637
692
|
const scene = `
|
|
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
12
12
|
import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
|
+
import { readMasterFxChain } from "@hyperframes/core/audio-groups";
|
|
15
16
|
import { linkScopeOf } from "@hyperframes/core/media-link";
|
|
16
17
|
import { createRuntimeStartTimeResolver } from "@hyperframes/core/runtime/start-resolver";
|
|
17
18
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
@@ -167,6 +168,8 @@ export function createTimelineElementFromManifestClip(params: {
|
|
|
167
168
|
if (info.fxChain) entry.audioGroupFxChain = info.fxChain;
|
|
168
169
|
if (info.automation) entry.audioGroupAutomation = info.automation;
|
|
169
170
|
}
|
|
171
|
+
const masterChain = readMasterFxChain(doc ?? hostEl.ownerDocument);
|
|
172
|
+
if (masterChain) entry.masterChain = masterChain;
|
|
170
173
|
const fxChain = hostEl.getAttribute("data-fx-chain");
|
|
171
174
|
if (fxChain) entry.fxChain = fxChain;
|
|
172
175
|
const automation = hostEl.getAttribute("data-automation");
|
|
@@ -330,6 +333,8 @@ export function parseTimelineFromDOM(
|
|
|
330
333
|
// Read from the element, like the manifest path does: without these an audio
|
|
331
334
|
// clip parsed straight from the DOM reserved no automation height and drew no
|
|
332
335
|
// lanes, while the property panel still showed its chain.
|
|
336
|
+
const domMasterChain = readMasterFxChain(doc);
|
|
337
|
+
if (domMasterChain) entry.masterChain = domMasterChain;
|
|
333
338
|
const domFxChain = el.getAttribute("data-fx-chain");
|
|
334
339
|
if (domFxChain) entry.fxChain = domFxChain;
|
|
335
340
|
const domAutomation = el.getAttribute("data-automation");
|
|
@@ -62,9 +62,10 @@ export interface TimelineElement {
|
|
|
62
62
|
/** Clip-edge fades from `data-fade-in` / `data-fade-out`, seconds; absent means none. */
|
|
63
63
|
fadeIn?: number;
|
|
64
64
|
fadeOut?: number;
|
|
65
|
-
/** Verbatim `data-fx-chain` / `data-automation
|
|
65
|
+
/** Verbatim `data-fx-chain` / `data-automation` (`masterChain`: the root's); see automationLaneData. */
|
|
66
66
|
fxChain?: string;
|
|
67
67
|
automation?: string;
|
|
68
|
+
masterChain?: string;
|
|
68
69
|
/** Path from data-composition-src — identifies sub-composition elements */
|
|
69
70
|
compositionSrc?: string;
|
|
70
71
|
/** Set by data-timeline-locked on the host element — disables move and trim in Studio. */
|
|
@@ -69,6 +69,15 @@ describe("hasProjectAudio", () => {
|
|
|
69
69
|
expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, ...fields })])).toBe(true);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it("counts an audible video when the composition root carries a master chain", () => {
|
|
73
|
+
const masterChain = '{"version":1,"nodes":[]}';
|
|
74
|
+
expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, masterChain })])).toBe(true);
|
|
75
|
+
expect(
|
|
76
|
+
hasProjectAudio([clip({ tag: "video", muted: true, hasAudio: true, masterChain })]),
|
|
77
|
+
).toBe(false);
|
|
78
|
+
expect(hasProjectAudio([clip({ tag: "video", masterChain })])).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
72
81
|
it("ignores a muted video even when it carries processing", () => {
|
|
73
82
|
expect(hasProjectAudio([clip({ tag: "video", hasAudio: true, muted: true, volume: 2 })])).toBe(
|
|
74
83
|
false,
|