mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix(core): reproduce figma's vertical text trim via text-box-trim
A figma text node whose box is shorter than its line-height carries vertically-trimmed (cap-to-baseline) bounds. The mapper positioned the box at those bounds but let the browser lay glyphs with half-leading, pushing them ~6px low on a 70px font (glyph-centroid measurement against figma's own render: +9.1px vs figma's +3.4px inside the same pill). Emitting text-box-trim: trim-both / text-box-edge: cap alphabetic reproduces the trim in the render engine; post-fix centroid agrees within 0.4px and the motion verifier's min window score improved 20.3 -> 25.3dB. Trim applies only to single-line trimmed text; boxes matching their line-height are untouched. Skill: component imports now include a static fidelity self-check step against figma's PNG export of the same node. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ccab6207c4
commit
d6d0fccbf2
@@ -102,6 +102,45 @@ describe("nodeToHtml", () => {
|
||||
expect(out.html).not.toContain('id="3d-object-headphones"');
|
||||
});
|
||||
|
||||
it("emits text-box-trim for vertically trimmed text (box height < line-height)", () => {
|
||||
const out = nodeToHtml(
|
||||
frame([
|
||||
{
|
||||
id: "1:2",
|
||||
name: "Headline",
|
||||
type: "TEXT",
|
||||
absoluteBoundingBox: BOX(140, 260, 304, 51),
|
||||
fills: [SOLID_BLUE],
|
||||
characters: "Unlocked",
|
||||
style: { fontFamily: "Inter", fontWeight: 700, fontSize: 70, lineHeightPx: 66.5 },
|
||||
},
|
||||
]),
|
||||
{ resolved: [], unresolved: [] },
|
||||
);
|
||||
// figma's trimmed bounds (51px box for a 66.5px line) place cap height at
|
||||
// the box top; browsers overflow the glyphs below without text-box-trim
|
||||
expect(out.html).toContain("text-box-trim: trim-both");
|
||||
expect(out.html).toContain("text-box-edge: cap alphabetic");
|
||||
});
|
||||
|
||||
it("does not trim text whose box matches its line-height", () => {
|
||||
const out = nodeToHtml(
|
||||
frame([
|
||||
{
|
||||
id: "1:2",
|
||||
name: "Body",
|
||||
type: "TEXT",
|
||||
absoluteBoundingBox: BOX(140, 260, 304, 39),
|
||||
fills: [SOLID_BLUE],
|
||||
characters: "Subtitle",
|
||||
style: { fontFamily: "Inter", fontWeight: 400, fontSize: 32, lineHeightPx: 38.4 },
|
||||
},
|
||||
]),
|
||||
{ resolved: [], unresolved: [] },
|
||||
);
|
||||
expect(out.html).not.toContain("text-box-trim");
|
||||
});
|
||||
|
||||
it("emits var() with literal fallback for resolved bindings", () => {
|
||||
const out = nodeToHtml(
|
||||
frame([
|
||||
|
||||
@@ -152,6 +152,23 @@ function textCss(node: FigmaNodeDocument, styles: string[]): void {
|
||||
if (typeof s.lineHeightPx === "number") styles.push(`line-height: ${round(s.lineHeightPx)}px`);
|
||||
if (typeof s.letterSpacing === "number" && s.letterSpacing !== 0)
|
||||
styles.push(`letter-spacing: ${round(s.letterSpacing)}px`);
|
||||
if (isVerticallyTrimmed(node, s.lineHeightPx)) {
|
||||
styles.push("text-box-trim: trim-both", "text-box-edge: cap alphabetic");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertical trim: a figma text box SHORTER than its line-height is
|
||||
* cap-height-trimmed bounds. Browsers place glyphs with half-leading and
|
||||
* overflow the short box downward (~6px low on a 70px font, measured
|
||||
* against figma's own render). text-box-trim reproduces figma's trim in
|
||||
* the render engine (Chrome). Single-line text only.
|
||||
*/
|
||||
function isVerticallyTrimmed(node: FigmaNodeDocument, lineHeightPx: unknown): boolean {
|
||||
if (typeof lineHeightPx !== "number") return false;
|
||||
const box = boxOf(node);
|
||||
if (box === null || box.height >= lineHeightPx - 1) return false;
|
||||
return typeof node.characters === "string" && !node.characters.includes("\n");
|
||||
}
|
||||
|
||||
interface RenderContext {
|
||||
|
||||
Reference in New Issue
Block a user