mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(cli): flag caption-zone by DOM box overlap (#3580)
A card centered at y=.860 can still cover the painted V2A pill. Intersect the element's getBoundingClientRect with the keepout instead of testing whether its center sits inside the band.
This commit is contained in:
@@ -624,7 +624,7 @@ drift), `*.motion.json` assertions, and WCAG AA contrast.
|
||||
| `--timeout` | Render-ready budget in ms; also raises page navigation above its 10s floor (default 3000) |
|
||||
| `--no-contrast` | Skip the WCAG pass while iterating |
|
||||
| `--strict` | Exit non-zero on warnings too (default: errors only) |
|
||||
| `--caption-zone "<x0=..;y0=..;x1=..;y1=..>"` | Opt-in band gate. Flags content whose centre sits inside the fractional band. Optional `severity` and `seek`. |
|
||||
| `--caption-zone "<x0=..;y0=..;x1=..;y1=..>"` | Opt-in band gate. Flags a text element's DOM box that overlaps the fractional band. Optional `severity` and `seek`. |
|
||||
| `--frame-check` | Opt-in out-of-frame detection for `img`, `svg`, `video`, and `canvas` |
|
||||
| `--layout` | Layout knobs, currently `proseCoverageFloor=0.05` (0–1, default 0.15) |
|
||||
| `--browser-gpu` / `--no-browser-gpu` | Hardware GPU capture, or deterministic SwiftShader (default: auto-detect) |
|
||||
|
||||
@@ -461,7 +461,7 @@ it("rejects malformed caption-zone specs instead of silently disabling the gate"
|
||||
});
|
||||
});
|
||||
|
||||
it("flags only text whose center is inside the caption band at the default end seek", async () => {
|
||||
it("flags text whose DOM box overlaps the caption band at the default end seek", async () => {
|
||||
const collectGeometryCandidates = vi.fn(async (time: number) => [
|
||||
geometryCandidate({
|
||||
kind: "text",
|
||||
@@ -507,10 +507,58 @@ it("flags only text whose center is inside the caption band at the default end s
|
||||
text: "Centered title",
|
||||
time: 10,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: "caption_zone_collision",
|
||||
severity: "warning",
|
||||
selector: "#overlap-only",
|
||||
text: "Overlap only",
|
||||
time: 10,
|
||||
}),
|
||||
]);
|
||||
expect(report.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a 1920×1080 card centered at y=.860 that overlaps the 5% keepout", async () => {
|
||||
const collectGeometryCandidates = vi.fn(async (time: number) => [
|
||||
geometryCandidate({
|
||||
kind: "text",
|
||||
tag: "div",
|
||||
text: "Demand card",
|
||||
selector: "#at-860",
|
||||
rect: fixtureRect(200, 889, 400, 80),
|
||||
time,
|
||||
}),
|
||||
geometryCandidate({
|
||||
kind: "text",
|
||||
tag: "div",
|
||||
text: "Clear above keepout",
|
||||
selector: "#tiny-860",
|
||||
rect: fixtureRect(200, 919, 400, 20),
|
||||
time,
|
||||
}),
|
||||
]);
|
||||
const { report } = await runScenario(
|
||||
fakeDriver({
|
||||
getDuration: vi.fn(async () => 10),
|
||||
collectGeometryCandidates,
|
||||
}),
|
||||
{
|
||||
samples: 1,
|
||||
contrast: false,
|
||||
captionZone: { x0: 0.118, y0: 0.875, x1: 0.882, y1: 0.925, severity: "error" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(report.layout.findings).toEqual([
|
||||
expect.objectContaining({
|
||||
code: "caption_zone_collision",
|
||||
severity: "error",
|
||||
selector: "#at-860",
|
||||
}),
|
||||
]);
|
||||
expect(report.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("skips caption_zone_collision when data-layout-allow-caption-zone is set", async () => {
|
||||
const collectGeometryCandidates = vi.fn(async (time: number) => [
|
||||
geometryCandidate({
|
||||
@@ -574,7 +622,7 @@ it("keeps overlap waivers from suppressing changelog caption-rail collisions", a
|
||||
expect(report.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("filters caption candidates by the element box while centering the text rect", async () => {
|
||||
it("skips full-frame and tiny wrappers when measuring the caption box", async () => {
|
||||
const collectGeometryCandidates = vi.fn(async (time: number) => [
|
||||
geometryCandidate({
|
||||
kind: "text",
|
||||
|
||||
@@ -240,19 +240,19 @@ function geometryIssueAnchor(candidate: CheckGeometryCandidate, time: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function captionCenterInZone(
|
||||
rect: CheckGeometryCandidate["rect"],
|
||||
function captionBoxOverlapsZone(
|
||||
box: CheckGeometryCandidate["elementRect"],
|
||||
zone: NonNullable<CheckOptions["captionZone"]>,
|
||||
canvas: Canvas,
|
||||
): { inside: boolean; cy: number } {
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
const inside =
|
||||
cx >= zone.x0 * canvas.width &&
|
||||
cx <= zone.x1 * canvas.width &&
|
||||
cy >= zone.y0 * canvas.height &&
|
||||
cy <= zone.y1 * canvas.height;
|
||||
return { inside, cy };
|
||||
): { overlaps: boolean; cy: number } {
|
||||
const zx0 = zone.x0 * canvas.width;
|
||||
const zy0 = zone.y0 * canvas.height;
|
||||
const zx1 = zone.x1 * canvas.width;
|
||||
const zy1 = zone.y1 * canvas.height;
|
||||
const bx1 = box.left + box.width;
|
||||
const by1 = box.top + box.height;
|
||||
const overlaps = box.left < zx1 && bx1 > zx0 && box.top < zy1 && by1 > zy0;
|
||||
return { overlaps, cy: box.top + box.height / 2 };
|
||||
}
|
||||
|
||||
function captionFinding(
|
||||
@@ -265,8 +265,8 @@ function captionFinding(
|
||||
if (!zone || candidate.kind !== "text" || !candidateIsSized(candidate, canvas)) return null;
|
||||
// Backstop for mocks/non-browser sources; browser already strips via closest() (own attrs only here).
|
||||
if ("data-layout-allow-caption-zone" in candidate.dataAttributes) return null;
|
||||
const { inside, cy } = captionCenterInZone(candidate.rect, zone, canvas);
|
||||
if (!inside) return null;
|
||||
const { overlaps, cy } = captionBoxOverlapsZone(candidate.elementRect, zone, canvas);
|
||||
if (!overlaps) return null;
|
||||
const text = candidate.text.slice(0, 48);
|
||||
const pctFromBottom = Math.round(((canvas.height - cy) / canvas.height) * 100);
|
||||
return {
|
||||
@@ -276,7 +276,7 @@ function captionFinding(
|
||||
code: "caption_zone_collision",
|
||||
severity: zone.severity === "error" ? "error" : "warning",
|
||||
text,
|
||||
message: `<${candidate.tag}> "${text}" is centred in the reserved caption band (~${pctFromBottom}% up from the bottom).`,
|
||||
message: `<${candidate.tag}> "${text}" overlaps the reserved caption band (~${pctFromBottom}% up from the bottom).`,
|
||||
fixHint:
|
||||
"Keep main content outside the configured caption band, or mark intentional lower-third copy with data-layout-allow-caption-zone.",
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"files": 7
|
||||
},
|
||||
"hyperframes-cli": {
|
||||
"hash": "5d02a1713635e7e7",
|
||||
"hash": "70bf0363795a160b",
|
||||
"files": 11
|
||||
},
|
||||
"hyperframes-core": {
|
||||
|
||||
@@ -67,7 +67,7 @@ npx hyperframes check --caption-zone "x0=0;y0=.82;x1=1;y1=1;severity=error;seek=
|
||||
npx hyperframes check --frame-check # media (img/svg/video/canvas) out-of-frame detection
|
||||
```
|
||||
|
||||
`--caption-zone` takes fractional band geometry (`x0/y0/x1/y1` required, 0-1 fractions of the composition's own canvas, portrait included) with optional `severity` and comma-separated `seek` fractions; it flags content whose center sits inside the band. Waive intentional lower-third copy with `data-layout-allow-caption-zone` on the element or its nearest wrapper (see Escape hatches). `--frame-check` reports media elements breaching the canvas beyond `max(120px, 6% of the min canvas dimension)`.
|
||||
`--caption-zone` takes fractional band geometry (`x0/y0/x1/y1` required, 0-1 fractions of the composition's own canvas, portrait included) with optional `severity` and comma-separated `seek` fractions; it flags a text element's DOM box (`getBoundingClientRect`) that overlaps the band. Waive intentional lower-third copy with `data-layout-allow-caption-zone` on the element or its nearest wrapper (see Escape hatches). `--frame-check` reports media elements breaching the canvas beyond `max(120px, 6% of the min canvas dimension)`.
|
||||
|
||||
**Fixing contrast errors** — thresholds are 4.5:1 for normal text, 3:1 for large text (24px+, or 19px+ bold). The finding's `suggestedColor` already picks the nearest compliant color in the right direction (brighten on dark backgrounds, darken on light); apply it or adjust within the palette family, then re-run `check`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user