fix(core): graded media honors object-position vertically instead of mirroring it (#6691)

* fix(core): graded media honors object-position vertically instead of mirroring it

* fix(core): name the bottom-up position instead of commenting it
This commit is contained in:
Miguel Ángel
2026-10-28 18:48:57 +00:00
committed by GitHub
parent 9aa6346a77
commit aa4a71fe41
2 changed files with 50 additions and 2 deletions
@@ -8,6 +8,7 @@ import {
} from "../colorGrading";
import { HF_COLOR_GROUP_ATTR, HF_COLOR_GROUP_TAG } from "../colorGroups";
import {
calculateObjectFitUv,
createColorGradingRuntime,
installAuthoredOpacityCapture,
type RuntimeColorGradingApi,
@@ -2619,3 +2620,49 @@ describe("installAuthoredOpacityCapture", () => {
el.remove();
});
});
describe("calculateObjectFitUv", () => {
// The shader reads uv = (v_uv - offset) / scale; v_uv.y and the flipped source
// texture both run bottom-up, so this reports the CSS-facing fraction from the top.
function sampledFromSourceTop(
uv: ReturnType<typeof calculateObjectFitUv>,
boxEdgeY: 0 | 1,
): number {
return 1 - (boxEdgeY - uv.offsetY) / uv.scaleY;
}
function sampledFromSourceLeft(
uv: ReturnType<typeof calculateObjectFitUv>,
boxEdgeX: 0 | 1,
): number {
return (boxEdgeX - uv.offsetX) / uv.scaleX;
}
it("shows the top of a covered portrait source at object-position 50% 6%", () => {
const uv = calculateObjectFitUv(1920, 1080, 1080, 1920, "cover", "50% 6%");
expect(sampledFromSourceTop(uv, 1)).toBeCloseTo(0.041, 3);
expect(sampledFromSourceTop(uv, 0)).toBeCloseTo(0.3574, 3);
});
it("shows the bottom of a covered portrait source at object-position 50% 94%", () => {
const uv = calculateObjectFitUv(1920, 1080, 1080, 1920, "cover", "50% 94%");
expect(sampledFromSourceTop(uv, 1)).toBeCloseTo(0.6426, 3);
expect(sampledFromSourceTop(uv, 0)).toBeCloseTo(0.959, 3);
});
it("shows the left of a covered landscape source at object-position 6% 50%", () => {
const uv = calculateObjectFitUv(1080, 1920, 1920, 1080, "cover", "6% 50%");
expect(sampledFromSourceLeft(uv, 0)).toBeCloseTo(0.041, 3);
expect(sampledFromSourceLeft(uv, 1)).toBeCloseTo(0.3574, 3);
});
it("centers a covered source when no object-position is set", () => {
const uv = calculateObjectFitUv(1920, 1080, 1080, 1920, "cover", "");
expect(sampledFromSourceTop(uv, 1)).toBeCloseTo(0.3415, 3);
expect(sampledFromSourceTop(uv, 0)).toBeCloseTo(0.6585, 3);
});
});
+3 -2
View File
@@ -2851,7 +2851,7 @@ function parseObjectPosition(value: string): { x: number; y: number } {
}
// fallow-ignore-next-line complexity
function calculateObjectFitUv(
export function calculateObjectFitUv(
boxWidth: number,
boxHeight: number,
sourceWidth: number,
@@ -2882,7 +2882,8 @@ function calculateObjectFitUv(
}
const position = parseObjectPosition(objectPosition || "center");
const offsetX = ((boxWidth - drawWidth) * position.x) / boxWidth;
const offsetY = ((boxHeight - drawHeight) * position.y) / boxHeight;
const positionFromBottom = 1 - position.y;
const offsetY = ((boxHeight - drawHeight) * positionFromBottom) / boxHeight;
return {
scaleX: drawWidth / boxWidth,
scaleY: drawHeight / boxHeight,