mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix: recursive sub-composition inlining for depth-3+ nesting (#2660)
* fix(core): discover sub-composition hosts to a fixed point during inlining
inlineSubCompositions collected [data-composition-src] hosts from the
document once, before inlining began, then processed that fixed list in
a single flat loop. A host's inlined content could introduce new hosts
of its own (a sub-composition nesting another sub-composition), and
those were never discovered: two-level nesting worked because the
second level's host already existed in the root document, but any
third level's host only appeared inside content the single pass had
already finished walking, so its content silently never rendered.
Host discovery now runs as a work queue: after a host is inlined, the
newly inserted subtree is re-scanned for further hosts, which are
enqueued with their ancestry chain. A host whose source already appears
in its own ancestry is a circular reference and is reported through the
existing onMissingComposition channel instead of being inlined; a depth
ceiling backstops any gap in that check. Existing single- and two-level
fixtures are unaffected.
* fix(producer): assign runtime composition ids to hosts discovered mid-inline
The producer's per-instance runtime id assignment (assignBundledRuntimeCompositionIds)
ran once over the root document's initial hosts, before inlining. With
host discovery now iterating to a fixed point (previous commit), hosts
revealed inside an already-inlined sub-composition were never in that
pre-pass, so the identity map returned undefined for them: two sibling
instances of the same sub-composition, discovered mid-traversal, both
fell back to their shared authored id and clobbered each other's
variablesByComp entry.
hostIdentityMap is now a lazy map: a pre-pass cache hit returns
unchanged, a miss reads the host's authored data-composition-id,
allocates a collision-checked runtime id by scanning the live document,
writes it back, and caches it. No change to the shared inliner's call
signature. Existing single-instance and root-level reusable-template
cases (#2064) are unaffected.
* test(producer): add depth-3 nested sub-composition regression fixture
Adds a minimal three-level chain (root -> level-2 -> level-3) where the
deepest level renders a distinguishing full-frame marker, following the
existing sub-comp-* fixture pattern. Confirmed against the pre-fix
commit (735128a61) in a separate scratch checkout that the fixture fails
without the previous two commits (level-3's marker absent, ~0.4B fewer
matching pixels than the golden expects) and passes with them.
The golden reference video was captured on the team's render host
(devbox) rather than locally, to avoid PSNR drift from font/GPU
differences against whatever renders the regression suite in CI. Local
render against that golden also passes (PSNR ~24.5dB throughout, 0
failed frames of 100 checkpoints), confirming cross-machine consistency
as well.
This commit is contained in:
@@ -428,3 +428,128 @@ describe("inlineSubCompositions – variable defaults on a template sub-comp roo
|
||||
expect(result.variablesByComp["card"]).toMatchObject({ headline: "Overridden" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("inlineSubCompositions – recursive host discovery", () => {
|
||||
const MAX_NESTING_DEPTH = 20;
|
||||
|
||||
function compositionHtml(label: string, nestedSrcs: string[] = []) {
|
||||
const nestedHosts = nestedSrcs
|
||||
.map(
|
||||
(src, index) =>
|
||||
`<div data-composition-id="${label}-child-${index}" data-composition-src="${src}"></div>`,
|
||||
)
|
||||
.join("");
|
||||
return `<template><div data-composition-id="${label}"><span data-level="${label}">${label}</span>${nestedHosts}</div></template>`;
|
||||
}
|
||||
|
||||
function inlineFixture(rootHosts: string[], compositions: Record<string, string>) {
|
||||
const { document } = parseHTML(`<!DOCTYPE html><html><body>
|
||||
<main data-level="root">root</main>
|
||||
${rootHosts
|
||||
.map(
|
||||
(src, index) =>
|
||||
`<div data-composition-id="root-host-${index}" data-composition-src="${src}"></div>`,
|
||||
)
|
||||
.join("")}
|
||||
</body></html>`);
|
||||
const missing: Array<{ src: string; reason?: string }> = [];
|
||||
|
||||
inlineSubCompositions(
|
||||
document,
|
||||
Array.from(document.querySelectorAll("[data-composition-src]")),
|
||||
{
|
||||
resolveHtml: (src) => compositions[src] ?? null,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
onMissingComposition: (src, reason) => missing.push({ src, reason }),
|
||||
},
|
||||
);
|
||||
|
||||
return { document, missing };
|
||||
}
|
||||
|
||||
it("inlines a three-level root -> A -> B chain", () => {
|
||||
const { document, missing } = inlineFixture(["a.html"], {
|
||||
"a.html": compositionHtml("A", ["b.html"]),
|
||||
"b.html": compositionHtml("B"),
|
||||
});
|
||||
|
||||
expect(document.querySelector('[data-level="root"]')?.textContent).toBe("root");
|
||||
expect(document.querySelector('[data-level="A"]')?.textContent).toBe("A");
|
||||
expect(document.querySelector('[data-level="B"]')?.textContent).toBe("B");
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("inlines a four-level root -> A -> B -> C chain", () => {
|
||||
const { document, missing } = inlineFixture(["a.html"], {
|
||||
"a.html": compositionHtml("A", ["b.html"]),
|
||||
"b.html": compositionHtml("B", ["c.html"]),
|
||||
"c.html": compositionHtml("C"),
|
||||
});
|
||||
|
||||
expect(
|
||||
Array.from(document.querySelectorAll("[data-level]")).map((element) => element.textContent),
|
||||
).toEqual(["root", "A", "B", "C"]);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports and leaves a direct circular reference uninlined", () => {
|
||||
const { document, missing } = inlineFixture(["a.html"], {
|
||||
"a.html": compositionHtml("A", ["a.html"]),
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('[data-level="A"]')).toHaveLength(1);
|
||||
expect(document.querySelector('[data-composition-src="a.html"]')).not.toBeNull();
|
||||
expect(missing).toEqual([{ src: "a.html", reason: "circular composition reference" }]);
|
||||
});
|
||||
|
||||
it("reports and leaves an indirect circular reference uninlined", () => {
|
||||
const { document, missing } = inlineFixture(["a.html"], {
|
||||
"a.html": compositionHtml("A", ["b.html"]),
|
||||
"b.html": compositionHtml("B", ["a.html"]),
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('[data-level="A"]')).toHaveLength(1);
|
||||
expect(document.querySelectorAll('[data-level="B"]')).toHaveLength(1);
|
||||
expect(document.querySelector('[data-composition-src="a.html"]')).not.toBeNull();
|
||||
expect(missing).toEqual([{ src: "a.html", reason: "circular composition reference" }]);
|
||||
});
|
||||
|
||||
it("inlines the same sub-composition in sibling branches", () => {
|
||||
const { document, missing } = inlineFixture(["a.html"], {
|
||||
"a.html": compositionHtml("A", ["shared.html", "shared.html"]),
|
||||
"shared.html": compositionHtml("shared"),
|
||||
});
|
||||
|
||||
expect(document.querySelectorAll('[data-level="shared"]')).toHaveLength(2);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("allows the depth ceiling and stops only the branch one level beyond it", () => {
|
||||
const compositions: Record<string, string> = {
|
||||
"sibling.html": compositionHtml("sibling"),
|
||||
};
|
||||
for (const prefix of ["exact", "overflow"]) {
|
||||
const length = prefix === "exact" ? MAX_NESTING_DEPTH : MAX_NESTING_DEPTH + 1;
|
||||
for (let level = 1; level <= length; level += 1) {
|
||||
const nextSrc = level < length ? [`${prefix}-${level + 1}.html`] : [];
|
||||
compositions[`${prefix}-${level}.html`] = compositionHtml(`${prefix}-${level}`, nextSrc);
|
||||
}
|
||||
}
|
||||
|
||||
const { document, missing } = inlineFixture(
|
||||
["exact-1.html", "overflow-1.html", "sibling.html"],
|
||||
compositions,
|
||||
);
|
||||
|
||||
expect(document.querySelector(`[data-level="exact-${MAX_NESTING_DEPTH}"]`)).not.toBeNull();
|
||||
expect(document.querySelector(`[data-level="overflow-${MAX_NESTING_DEPTH}"]`)).not.toBeNull();
|
||||
expect(document.querySelector(`[data-level="overflow-${MAX_NESTING_DEPTH + 1}"]`)).toBeNull();
|
||||
expect(document.querySelector('[data-level="sibling"]')).not.toBeNull();
|
||||
expect(missing).toEqual([
|
||||
{
|
||||
src: `overflow-${MAX_NESTING_DEPTH + 1}.html`,
|
||||
reason: "nesting depth exceeded",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,6 +130,8 @@ function defaultBuildScopeSelector(compId: string): string {
|
||||
return `[data-composition-id="${escaped}"]`;
|
||||
}
|
||||
|
||||
const MAX_SUB_COMPOSITION_DEPTH = 20;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Core implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -177,7 +179,9 @@ export function inlineSubCompositions(
|
||||
const seenLinkHrefs = new Set<string>();
|
||||
const variablesByComp: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
for (const hostEl of hosts) {
|
||||
const queue = hosts.map((element) => ({ element, ancestry: [] as string[] }));
|
||||
for (let queueIndex = 0; queueIndex < queue.length; queueIndex += 1) {
|
||||
const { element: hostEl, ancestry } = queue[queueIndex]!;
|
||||
const src = hostEl.getAttribute("data-composition-src");
|
||||
if (!src) continue;
|
||||
|
||||
@@ -415,6 +419,21 @@ export function inlineSubCompositions(
|
||||
|
||||
hostEl.setAttribute("data-composition-file", src);
|
||||
hostEl.removeAttribute("data-composition-src");
|
||||
|
||||
const nestedAncestry = [...ancestry, src];
|
||||
for (const nestedHost of [...hostEl.querySelectorAll("[data-composition-src]")]) {
|
||||
const nestedSrc = nestedHost.getAttribute("data-composition-src");
|
||||
if (!nestedSrc) continue;
|
||||
if (nestedAncestry.includes(nestedSrc)) {
|
||||
onMissingComposition?.(nestedSrc, "circular composition reference");
|
||||
continue;
|
||||
}
|
||||
if (nestedAncestry.length >= MAX_SUB_COMPOSITION_DEPTH) {
|
||||
onMissingComposition?.(nestedSrc, "nesting depth exceeded");
|
||||
continue;
|
||||
}
|
||||
queue.push({ element: nestedHost, ancestry: nestedAncestry });
|
||||
}
|
||||
}
|
||||
|
||||
return { styles, scripts, externalScriptSrcs, scriptItems, externalLinks, variablesByComp };
|
||||
|
||||
@@ -1980,6 +1980,71 @@ describe("sub-composition variable injection (render path, #2064)", () => {
|
||||
expect(compiled.html).toContain('"card__hf4":{"label":"CARD_D"}');
|
||||
});
|
||||
|
||||
it("assigns unique runtime ids to repeated sub-compositions discovered during inlining", async () => {
|
||||
const projectDir = mkdtempSync(join(tmpdir(), "hf-subvar-nested-multi-"));
|
||||
mkdirSync(join(projectDir, "compositions"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectDir, "compositions", "c.html"),
|
||||
`<!DOCTYPE html>
|
||||
<html data-composition-variables='[{"id":"label","type":"string","label":"Label","default":"C_DEFAULT"}]'>
|
||||
<body>
|
||||
<div data-composition-id="c" data-width="160" data-height="120">
|
||||
<div class="c-label"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(projectDir, "compositions", "b.html"),
|
||||
`<!DOCTYPE html>
|
||||
<html data-composition-variables='[{"id":"label","type":"string","label":"Label","default":"B_DEFAULT"}]'>
|
||||
<body>
|
||||
<div data-composition-id="b" data-width="320" data-height="240">
|
||||
<div class="b-label"></div>
|
||||
<div data-composition-id="c" data-composition-src="compositions/c.html" data-variable-values='{"label":"C_CHILD"}' data-start="0" data-duration="3" data-track-index="1"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(projectDir, "compositions", "a.html"),
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div data-composition-id="a" data-width="640" data-height="240">
|
||||
<div data-composition-id="b" data-composition-src="compositions/b.html" data-variable-values='{"label":"B_LEFT"}' data-start="0" data-duration="3" data-track-index="1"></div>
|
||||
<div data-composition-id="b" data-composition-src="compositions/b.html" data-variable-values='{"label":"B_RIGHT"}' data-start="0" data-duration="3" data-track-index="2"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(projectDir, "index.html"),
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="root" class="composition" data-composition-id="host" data-start="0" data-duration="3" data-width="640" data-height="240">
|
||||
<div data-composition-id="a" data-composition-src="compositions/a.html" data-start="0" data-duration="3" data-track-index="1"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const compiled = await compileForRender(projectDir, join(projectDir, "index.html"), projectDir);
|
||||
const { document } = parseHTML(compiled.html);
|
||||
const idsByFile = (file: string) =>
|
||||
Array.from(document.querySelectorAll(`[data-composition-file="${file}"]`)).map((host) =>
|
||||
host.getAttribute("data-composition-id"),
|
||||
);
|
||||
|
||||
expect(idsByFile("compositions/b.html")).toEqual(["b__hf1", "b__hf2"]);
|
||||
expect(idsByFile("compositions/c.html")).toEqual(["c__hf1", "c__hf2"]);
|
||||
expect(compiled.html).toContain('"b__hf1":{"label":"B_LEFT"}');
|
||||
expect(compiled.html).toContain('"b__hf2":{"label":"B_RIGHT"}');
|
||||
expect(compiled.html).toContain('"c__hf1":{"label":"C_CHILD"}');
|
||||
expect(compiled.html).toContain('"c__hf2":{"label":"C_CHILD"}');
|
||||
});
|
||||
|
||||
it("leaves a single-mount sub-comp's authored id untouched while renaming a duplicated one", async () => {
|
||||
// Pins the "single instances are untouched" claim: a solo mount keeps its
|
||||
// authored data-composition-id; only the duplicated sub-comp is renamed.
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@hyperframes/core";
|
||||
import {
|
||||
assignBundledRuntimeCompositionIds,
|
||||
type BundledHostCompositionIdentity,
|
||||
buildVariablesByCompScript,
|
||||
inlineSubCompositions as inlineSubCompositionsShared,
|
||||
prepareFlattenedInnerRoot,
|
||||
@@ -753,6 +754,55 @@ function promoteCssImportsToLinkTags(html: string): string {
|
||||
return document.toString();
|
||||
}
|
||||
|
||||
class ProducerHostIdentityMap extends Map<Element, BundledHostCompositionIdentity> {
|
||||
readonly #document: Document;
|
||||
readonly #lateInstanceByCompositionId = new Map<string, number>();
|
||||
|
||||
constructor(document: Document, initialHosts: Element[]) {
|
||||
super(assignBundledRuntimeCompositionIds(initialHosts));
|
||||
this.#document = document;
|
||||
}
|
||||
|
||||
override get(host: Element): BundledHostCompositionIdentity | undefined {
|
||||
const existing = super.get(host);
|
||||
if (existing) return existing;
|
||||
|
||||
// The shared inliner discovers nested hosts after the producer's initial
|
||||
// DOM scan. Assign those late hosts on first use so their scope and
|
||||
// variables key are fixed before any content is processed.
|
||||
const authoredCompositionId =
|
||||
(
|
||||
host.getAttribute("data-hf-original-composition-id") ||
|
||||
host.getAttribute("data-composition-id") ||
|
||||
""
|
||||
).trim() || null;
|
||||
if (!authoredCompositionId) {
|
||||
const identity = { authoredCompositionId: null, runtimeCompositionId: null };
|
||||
this.set(host, identity);
|
||||
return identity;
|
||||
}
|
||||
|
||||
let instanceIndex = this.#lateInstanceByCompositionId.get(authoredCompositionId) || 0;
|
||||
let runtimeCompositionId: string;
|
||||
do {
|
||||
instanceIndex += 1;
|
||||
runtimeCompositionId = `${authoredCompositionId}__hf${instanceIndex}`;
|
||||
} while (
|
||||
Array.from(this.#document.querySelectorAll("[data-composition-id]")).some(
|
||||
(element) =>
|
||||
element !== host && element.getAttribute("data-composition-id") === runtimeCompositionId,
|
||||
)
|
||||
);
|
||||
this.#lateInstanceByCompositionId.set(authoredCompositionId, instanceIndex);
|
||||
|
||||
host.setAttribute("data-hf-original-composition-id", authoredCompositionId);
|
||||
host.setAttribute("data-composition-id", runtimeCompositionId);
|
||||
const identity = { authoredCompositionId, runtimeCompositionId };
|
||||
this.set(host, identity);
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge all `<head>` `<style>` blocks into a single tag with `@import` rules
|
||||
* at the top, and merge all inline `<body>` `<script>` blocks into one at the
|
||||
@@ -860,8 +910,10 @@ function inlineSubCompositions(
|
||||
return emitted ? document.toString() : html;
|
||||
}
|
||||
|
||||
// Assign per-instance runtime composition ids BEFORE inlining, mirroring the
|
||||
// preview bundler. When the same sub-composition (same authored
|
||||
// Assign per-instance runtime composition ids before each host is inlined,
|
||||
// mirroring the preview bundler. Initial hosts are assigned as one pre-pass;
|
||||
// hosts discovered by the shared inliner's queue are assigned lazily by the
|
||||
// map above. When the same sub-composition (same authored
|
||||
// data-composition-id) is mounted more than once — the reusable-template
|
||||
// pattern from issue #2064 — each host is rewritten to a unique runtime id
|
||||
// (`card__hf1`, `card__hf2`). Without this, every instance shares one
|
||||
@@ -869,7 +921,10 @@ function inlineSubCompositions(
|
||||
// data-variable-values clobbers the earlier ones and all-but-one instance
|
||||
// renders blank. #2066 fixed the single-instance case but left this
|
||||
// divergence (snapshot/preview correct, render wrong).
|
||||
const hostIdentityByElement = assignBundledRuntimeCompositionIds(hosts as unknown as Element[]);
|
||||
const hostIdentityByElement = new ProducerHostIdentityMap(
|
||||
document as unknown as Document,
|
||||
hosts as unknown as Element[],
|
||||
);
|
||||
|
||||
const result = inlineSubCompositionsShared(
|
||||
document as unknown as Document,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Nested sub-composition depth-3 inlining",
|
||||
"description": "Regression test for the fix in 693768d21 (core: inlineSubCompositions now discovers [data-composition-src] hosts to a fixed point instead of a single flat pass) and fa4a7f012 (producer: hostIdentityMap now lazily assigns runtime composition ids to hosts discovered mid-inline). Before these fixes, a sub-composition hosting another sub-composition (root -> level-2 -> level-3) silently dropped level-3's content: two-level nesting always worked because the second-level host already existed in the root document before inlining began, but a third-level host only appears inside content the single pass had already finished walking, so it was never discovered or inlined. This fixture's level-3 composition contributes a large, uniquely colored and positioned marker element covering more than half the frame; if the single-pass bug is reintroduced, level-3 is dropped entirely and the rendered PSNR against the golden baseline falls well below threshold.",
|
||||
"tags": ["sub-composition", "regression", "nested"],
|
||||
"minPsnr": 20,
|
||||
"maxFrameFailures": 10,
|
||||
"minAudioCorrelation": 0.0,
|
||||
"maxAudioLagWindows": 120,
|
||||
"renderConfig": {
|
||||
"fps": 24
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c8a5f2249dd4370bdd030517dd53ba977b623450a5c302d22aa121f99b1156f
|
||||
size 111160
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c2b2b6c191043eb5174c6a1e7bac6a09f13d53386144f4be363d9225d16b35aa
|
||||
size 29465
|
||||
@@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#scene-level2 {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
id="scene-level2"
|
||||
class="scene"
|
||||
data-composition-id="level2"
|
||||
data-composition-src="level-2.html"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template id="level2-template">
|
||||
<div id="level2" data-composition-id="level2" data-width="1920" data-height="1080">
|
||||
<div class="level2-bg"></div>
|
||||
<div
|
||||
id="scene-level3"
|
||||
class="level3-host"
|
||||
data-composition-id="level3"
|
||||
data-composition-src="level-3.html"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
<style>
|
||||
#level2 {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #1a2a3a;
|
||||
overflow: hidden;
|
||||
}
|
||||
#level2 .level2-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #1a2a3a;
|
||||
}
|
||||
#level2 .level3-host {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.set({}, {}, 2);
|
||||
window.__timelines["level2"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template id="level3-template">
|
||||
<div id="level3" data-composition-id="level3" data-width="1920" data-height="1080">
|
||||
<div id="level3-marker" class="level3-marker" data-nested-subcomp-marker="level3-proof">
|
||||
LEVEL3-PROOF-MARKER
|
||||
</div>
|
||||
<style>
|
||||
#level3 {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#level3 .level3-marker {
|
||||
position: absolute;
|
||||
top: 140px;
|
||||
left: 260px;
|
||||
width: 1400px;
|
||||
height: 800px;
|
||||
background: #ff00aa;
|
||||
color: #ffffff;
|
||||
font-family: Impact, sans-serif;
|
||||
font-size: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.set({}, {}, 2);
|
||||
window.__timelines["level3"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user