Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,8 @@ edges re-measured to about 0.01 s, and skipped for a `--range` check.
| `line_unread` | A reading-size line (30 px or taller at 1080) is on screen for less than its floor: 0.4 s for one or two words, otherwise three quarters of 0.5 s + 0.3 s a word. The film's last line needs 2 s. |
| `dead_stretch` | Nothing on screen changes for more than 3 s, by the same classifier `keepsMoving` uses. |
| `text_below_floor` | Text under 2.6% of the frame's short side (28 px on 1080) stays on screen 0.5 s or more. One finding per file lists every such element. |
| `headline_small` | The largest text on screen stays under 5.2% of the short side (56 px on 1080, the smallest headline) for 0.5 s or more, while above the type floor. One finding per file. |
| `hero_word_narrow` | Inside a `data-kinetic-type` element, a frame's largest line spans under 60% of the frame's width at its median size. One finding per file. |
| `first_frame_empty` | Frame 0 paints nothing but the background (no text, image, video, canvas or SVG), or an opaque element covers every other clip and is named. The first frame is the thumbnail on social players. |
| `film_silent` | A film longer than 15 s has no unmuted `<audio>` and no `<video>` with sound. |

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,7 @@ describe("check pipeline", () => {
fontPx: 96,
rect: { left: 160, top: 380, width: 1600, height: 220 },
captions: false,
kinetic: false,
},
]
: [],
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/layout-audit.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2720,6 +2720,7 @@
fontPx: round(renderedFontPx(element, getComputedStyle(element))),
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
captions: isCaptionsText(element),
kinetic: element.closest("[data-kinetic-type]") !== null,
};
});
}
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/utils/checkBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1045,8 +1045,9 @@ function parseCraftTextPart(value: unknown): CraftTextPart[] {
const rect = recordField(value, "rect");
const box = rect ? requiredNumbers(rect, CRAFT_RECT_NUMBERS) : null;
const captions = booleanValue(value, "captions");
const kinetic = booleanValue(value, "kinetic") ?? false;
return nums && strings && box && captions !== null
? [{ ...nums, ...strings, rect: box, captions }]
? [{ ...nums, ...strings, rect: box, captions, kinetic }]
: [];
}

Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/utils/checkTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ export interface CraftTextPart {
fontPx: number;
rect: { left: number; top: number; width: number; height: number };
captions: boolean;
/** Inside a data-kinetic-type element, whose largest line is held to hero_word_narrow. */
kinetic: boolean;
}

export interface CraftFrame {
Expand Down Expand Up @@ -345,9 +347,18 @@ export interface CraftFinding extends CheckFinding {
seconds?: number;
floorSeconds?: number;
fontPx?: number;
/** hero_word_narrow: the hero's resting width. */
widthPx?: number;
floorPx?: number;
until?: number;
elements?: { selector: string; text: string; fontPx: number; time: number; seconds: number }[];
elements?: {
selector: string;
text: string;
fontPx: number;
widthPx?: number;
time: number;
seconds: number;
}[];
}

export interface CraftResult {
Expand Down
74 changes: 73 additions & 1 deletion packages/cli/src/utils/craftAudit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
detectDeadStretches,
detectFilmSilent,
detectFirstFrameEmpty,
detectHeadlineSmall,
detectHeroWordNarrow,
detectLineUnread,
detectTextBelowFloor,
headlinePx,
lineFloorSec,
readingWords,
typeFloorPx,
Expand All @@ -22,6 +25,9 @@ interface Shown {
to: number;
fontPx?: number;
captions?: boolean;
kinetic?: boolean;
/** Rendered width; defaults to 800 px. */
width?: number;
/** Block key; defaults to the element's own key. */
block?: number;
}
Expand All @@ -37,8 +43,9 @@ function part(shown: Shown): CraftTextPart {
text: shown.text,
opacity: 1,
fontPx,
rect: { left: 100, top: 100 + shown.key * 10, width: 800, height: fontPx * 1.2 },
rect: { left: 100, top: 100 + shown.key * 10, width: shown.width ?? 800, height: fontPx * 1.2 },
captions: shown.captions ?? false,
kinetic: shown.kinetic ?? false,
};
}

Expand Down Expand Up @@ -243,6 +250,71 @@ describe("text_below_floor", () => {
});
});

describe("headline_small", () => {
it("puts headline size at 5.2% of the short side", () => {
expect(headlinePx(CANVAS)).toBe(56);
expect(headlinePx({ width: 1080, height: 1920 })).toBe(56);
expect(headlinePx({ width: 3840, height: 2160 })).toBe(112);
});

it("warns when the largest text on screen stays under 56 px for 0.5 s or more", () => {
const shown: Shown[] = [
{ key: 1, text: "Quarterly results", from: 0, to: 4, fontPx: 40 },
{ key: 2, text: "Revenue grew in every region", from: 0, to: 4, fontPx: 30 },
];
const findings = detectHeadlineSmall(film(8, shown), 8, CANVAS);
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({
code: "headline_small",
severity: "warning",
text: "Quarterly results",
fontPx: 40,
floorPx: 56,
});
expect(findings[0]?.message).toContain("Headlines are 56–96 px");
});

it("judges a headline at the size it lands on, and leaves text under the floor to text_below_floor", () => {
const shown: Shown[] = [
{ key: 1, text: "Landed title", from: 0, to: 0.25, fontPx: 40 },
{ key: 1, text: "Landed title", from: 0.25, to: 4, fontPx: 72 },
{ key: 2, text: "A tiny 20 px title", from: 4, to: 8, fontPx: 20 },
{ key: 3, text: "Quick 40 px flash", from: 8, to: 8.3, fontPx: 40 },
];
const frames = film(10, shown);
expect(detectHeadlineSmall(frames, 10, CANVAS)).toEqual([]);
expect(detectTextBelowFloor(frames, 10, CANVAS)[0]).toMatchObject({ fontPx: 20 });
});
});

describe("hero_word_narrow", () => {
it("warns on a kinetic hero under 60% of the width, judged at its median width", () => {
const shown: Shown[] = [
// Slams in at 1.35x for a sample, then rests at 950 px of 1920 (49%).
{ key: 1, text: "ONE", from: 0, to: 0.125, fontPx: 300, width: 1300, kinetic: true },
{ key: 1, text: "ONE", from: 0.125, to: 0.5, fontPx: 220, width: 950, kinetic: true },
{ key: 2, text: "IDEA.", from: 0.5, to: 1, fontPx: 220, width: 1400, kinetic: true },
{ key: 3, text: "a kicker", from: 0, to: 1, fontPx: 40, width: 200, kinetic: true },
];
const findings = detectHeroWordNarrow(film(1, shown), CANVAS);
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({
code: "hero_word_narrow",
severity: "warning",
text: "ONE",
widthPx: 950,
floorPx: 1152,
});
expect(findings[0]?.elements).toHaveLength(1);
expect(findings[0]?.message).toContain("950 px (49%)");
});

it("does not hold text outside data-kinetic-type to it", () => {
const shown: Shown[] = [{ key: 1, text: "Title", from: 0, to: 4, fontPx: 96, width: 400 }];
expect(detectHeroWordNarrow(film(4, shown), CANVAS)).toEqual([]);
});
});

describe("first_frame_empty", () => {
it("warns when frame 0 paints nothing but the background", () => {
const findings = detectFirstFrameEmpty([
Expand Down
Loading