fix(core): grade groups resolve per composition; freeze frames keep their lane (#6685)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-10-28 10:51:17 -07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4719543e97
commit c569760a7a
5 changed files with 56 additions and 5 deletions
+25
View File
@@ -113,3 +113,28 @@ describe("ensureColorGroupInertStyle", () => {
expect(document.head.querySelectorAll("style")).toHaveLength(1);
});
});
describe("group ids are scoped to their composition", () => {
it("pairs a member with the group in its own composition before one elsewhere with the same id", () => {
document.body.innerHTML = `
<div data-composition-id="scene-1">
<hf-color-group id="cam-a" data-color-grading='${LANE_GRADE}'></hf-color-group>
<video id="first" data-color-group="cam-a"></video>
</div>
<div data-composition-id="scene-2">
<hf-color-group id="cam-a" data-color-grading='${OWN_GRADE}'></hf-color-group>
<video id="second" data-color-group="cam-a"></video>
</div>
`;
expect(effectiveColorGrading(document.getElementById("first")!, document)).toBe(LANE_GRADE);
expect(effectiveColorGrading(document.getElementById("second")!, document)).toBe(OWN_GRADE);
});
it("falls back to the document when the member's composition declares no such group", () => {
document.body.innerHTML = `
<hf-color-group id="cam-a" data-color-grading='${LANE_GRADE}'></hf-color-group>
<div data-composition-id="scene-1"><video id="v" data-color-group="cam-a"></video></div>
`;
expect(effectiveColorGrading(document.getElementById("v")!, document)).toBe(LANE_GRADE);
});
});
+15 -3
View File
@@ -27,10 +27,22 @@ export function colorGroupOf(el: Element): string | null {
}
/**
* The `<hf-color-group>` for an id, or null. Tag-checked so an unrelated
* The `<hf-color-group>` for an id, or null. Ids are unique only per
* composition file, so a member's own composition is searched first, as audio
* groups pair per composition; then the document. Tag-checked so an unrelated
* element sharing the id is never read as a lane's grade.
*/
export function resolveColorGroupElement(root: GroupLookupRoot, groupId: string): Element | null {
export function resolveColorGroupElement(
root: GroupLookupRoot,
groupId: string,
member?: Element,
): Element | null {
const scope =
typeof member?.closest === "function" ? member.closest("[data-composition-id]") : null;
const scoped = scope
? Array.from(scope.getElementsByTagName(HF_COLOR_GROUP_TAG)).find((g) => g.id === groupId)
: undefined;
if (scoped) return scoped;
const el = root?.getElementById(groupId) ?? null;
return el?.tagName.toLowerCase() === HF_COLOR_GROUP_TAG ? el : null;
}
@@ -44,7 +56,7 @@ export function effectiveColorGrading(el: Element, root: GroupLookupRoot): strin
if (own !== null) return own;
const groupId = colorGroupOf(el);
if (!groupId) return null;
return resolveColorGroupElement(root, groupId)?.getAttribute(HF_COLOR_GRADING_ATTR) ?? null;
return resolveColorGroupElement(root, groupId, el)?.getAttribute(HF_COLOR_GRADING_ATTR) ?? null;
}
/**
@@ -384,7 +384,13 @@ export async function measureVideoShownBoxes(
const fullSize = new Set<string>();
const gradedThroughGroup = (video: Element): boolean => {
const groupId = video.getAttribute(groupAttr);
const group = groupId ? document.getElementById(groupId) : null;
if (!groupId) return false;
// the member's own composition first: ids are unique only per composition file
const scope = video.closest("[data-composition-id]");
const group =
(scope &&
Array.from(scope.getElementsByTagName(groupTag)).find((g) => g.id === groupId)) ||
document.getElementById(groupId);
return group?.localName === groupTag && group.hasAttribute(gradingAttr);
};
const keepsFullSize = (video: Element): boolean => {
@@ -163,6 +163,14 @@ describe("applyFreezeFrameToHtml", () => {
).toBe("11");
});
it("keeps a lane clip's grade group on the still, so the still is graded with its lane", () => {
const graded = applyFreezeFrameToHtml(
project.replace('id="talk"', 'id="talk" data-color-group="cam-a"'),
{ target: { id: "talk" }, playhead: 3.2, imageSrc: "assets/freeze/talk-3200.png" },
);
expect(at(graded?.html ?? "", "talk-freeze")?.getAttribute("data-color-group")).toBe("cam-a");
});
it("returns null when the playhead is not inside the video", () => {
expect(
applyFreezeFrameToHtml(project, { target: { id: "talk" }, playhead: 9, imageSrc: "x.png" }),
@@ -138,7 +138,7 @@ function growRootToFit(document: Document): void {
if (end > rootDuration + EPSILON) root.setAttribute("data-duration", String(round3(end)));
}
const COPIED_PICTURE_ATTRS = ["class", "style", "data-color-grading"];
const COPIED_PICTURE_ATTRS = ["class", "style", "data-color-grading", "data-color-group"];
function buildStill(
document: Document,