feat(studio): add color grading inspector controls

This commit is contained in:
ukimsanov
2026-06-16 13:41:41 -07:00
parent d1162cd1b1
commit 8b92f37635
28 changed files with 1433 additions and 179 deletions
@@ -36,6 +36,25 @@ describe("parseTimelineFromDOM — hfId from data-hf-id", () => {
expect(plain).toBeDefined();
expect(plain?.hfId).toBeUndefined();
});
it("ignores runtime-owned color grading canvases with timing attributes", () => {
const doc = makeDoc(`
<div data-composition-id="root">
<img id="photo" class="clip" data-start="0" data-duration="5" />
<canvas
class="__hf_color_grading_canvas__"
data-hf-color-grading-canvas="true"
data-hyperframes-ignore
data-start="0"
data-duration="5"
></canvas>
</div>
`);
const elements = parseTimelineFromDOM(doc, 10);
expect(elements.map((el) => el.tag)).toEqual(["img"]);
});
});
describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
@@ -52,4 +71,21 @@ describe("createImplicitTimelineLayersFromDOM — hfId from data-hf-id", () => {
expect(layer).toBeDefined();
expect(layer?.hfId).toBe("hf-xyz789");
});
it("ignores runtime-owned color grading canvases as implicit layers", () => {
const doc = makeDoc(`
<div data-composition-id="root" data-duration="5">
<img id="photo" class="clip" data-start="0" data-duration="5" />
<canvas
class="__hf_color_grading_canvas__"
data-hf-color-grading-canvas="true"
data-hyperframes-ignore
></canvas>
</div>
`);
const layers = createImplicitTimelineLayersFromDOM(doc, 5);
expect(layers).toEqual([]);
});
});
@@ -22,6 +22,7 @@ import {
buildTimelineElementKey,
buildTimelineElementIdentity,
getTimelineElementIdentity,
isTimelineIgnoredElement,
} from "./timelineElementHelpers";
// Re-export helpers that were previously public from this module so that
@@ -230,6 +231,7 @@ export function parseTimelineFromDOM(doc: Document, rootDuration: number): Timel
nodes.forEach((node) => {
if (node === rootComp) return;
if (isTimelineIgnoredElement(node)) return;
const el = node as HTMLElement;
const startStr = el.getAttribute("data-start");
if (startStr == null) return;
@@ -23,6 +23,19 @@ function readDurationAttribute(el: Element | null | undefined): number {
return isFinitePositive(duration) ? duration : 0;
}
export function isTimelineIgnoredElement(el: Element): boolean {
return Boolean(
el.closest(
[
"[data-hyperframes-ignore]",
"[data-hyperframes-picker-ignore]",
"[data-hf-ignore]",
"[data-hf-color-grading-canvas]",
].join(","),
),
);
}
export function readTimelineDurationFromDocument(doc: Document | null | undefined): number {
if (!doc) return 0;
const rootDuration = readDurationAttribute(doc.querySelector("[data-composition-id]"));
@@ -30,6 +43,7 @@ export function readTimelineDurationFromDocument(doc: Document | null | undefine
let maxEnd = 0;
for (const node of Array.from(doc.querySelectorAll("[data-start]"))) {
if (isTimelineIgnoredElement(node)) continue;
const start = Number.parseFloat(node.getAttribute("data-start") ?? "");
const duration = readDurationAttribute(node);
if (!Number.isFinite(start) || start < 0 || duration <= 0) continue;
@@ -241,7 +255,9 @@ export function getTimelineElementIdentity(element: TimelineElement): string {
function getTimelineDomNodes(doc: Document): Element[] {
const rootComp = doc.querySelector("[data-composition-id]");
return Array.from(doc.querySelectorAll("[data-start]")).filter((node) => node !== rootComp);
return Array.from(doc.querySelectorAll("[data-start]")).filter(
(node) => node !== rootComp && !isTimelineIgnoredElement(node),
);
}
function numbersNearlyEqual(a: number, b: number): boolean {
@@ -295,6 +311,7 @@ export function findTimelineDomNodeForClip(
export function isImplicitTimelineLayerCandidate(root: Element, el: Element): el is HTMLElement {
if (!isHtmlElement(el)) return false;
if (isTimelineIgnoredElement(el)) return false;
if (el.parentElement !== root) return false;
const tagName = el.tagName.toLowerCase();
if (IMPLICIT_TIMELINE_LAYER_SKIP_TAGS.has(tagName)) return false;