Skip to content

font-weight 800/900 is painted as 700 while measured at its real weight, so text wraps over its next sibling when several weights are loaded #466

Description

@ThomasTartrau

The text painter collapses every numeric font-weight of 600 or more to Bold, i.e. it asks for weight 700, while the text measurer asks for the exact weight. When several weights of a family are loaded, the two resolve different font files:

  • font-weight: 800 / 900 is painted in the wrong weight (700, or whatever registered weight is closest to 700);
  • the box is measured with the requested face and painted with another, slightly wider one, so a text that fits on one line wraps at paint time while its box keeps a single-line height: the second line is drawn over the next sibling.

crates/rustmotion-components/src/text.rs (paint):

let font_weight = match &self.style.font_weight {
    Some(CssFontWeight::Keyword(FontWeightKw::Bold | FontWeightKw::Bolder)) => FontWeight::Bold,
    Some(CssFontWeight::Number(n)) if *n >= 600 => FontWeight::Bold,
    Some(CssFontWeight::Number(n)) => FontWeight::Weight(*n),
    _ => FontWeight::Normal,
};
// ...
FontWeight::Bold => skia_safe::font_style::Weight::BOLD,   // 700

crates/rustmotion-components/src/intrinsic.rs (measure): weight: weight_to_u16(style.font_weight.as_ref()), then Weight::from(self.weight as i32), i.e. the exact 800 or 900.

Both go through typeface_with_fallback -> closest_variant, which picks the registered weight with the smallest |v.weight - requested|, the first registered one on a tie.

Reproduction

{
  "version": "1.0",
  "video": { "width": 1920, "height": 1080, "fps": 30, "background": "#FFFFFF" },
  "fonts": [{ "family": "Inter", "source": "google", "weights": [800, 900] }],
  "scenes": [{
    "duration": 1.0,
    "children": [{
      "type": "div", "position": "absolute", "x": 0, "y": 0,
      "style": { "width": 1920, "height": 1080, "flex-direction": "column", "align-items": "center", "justify-content": "center" },
      "children": [
        { "type": "text", "content": "UN SEUL COMPTE", "style": { "font-family": "Inter", "font-weight": 900, "font-size": 150, "color": "#000000", "line-height": 1.0 } },
        { "type": "text", "content": "NEXT LINE", "style": { "font-family": "Inter", "font-weight": 900, "font-size": 96, "color": "#3366CC", "line-height": 1.0 } }
      ]
    }]
  }]
}

With "weights": [900] the title renders on one line. With [800, 900] it renders as UN SEUL / COMPTE, and COMPTE is drawn on top of NEXT LINE.

Same scenario, varying only fonts[0].weights (render --frame 0):

loaded weights title ink rows laid-out box (background) painted face (ink pixels, nowrap)
[900] 436-547 (1 line) 1331 x 149 39 966 (900)
[400, 900] 436-547 (1 line)
[500, 900] 436-696 (2 lines) 1331 x 149 23 463 (500, tie with 900 at distance 200, first registered wins)
[800, 900] 436-697 (2 lines) 1331 x 149 35 518 (800)
[400 ... 900] 436-697 (2 lines) 31 069 (700)

The box is the same in every case (measured with the 900 face). Static Inter files are near-equal in advance width across weights (UN SEUL COMPTE at 150px: 900 = 1332px, 800 = 1335px, 700 = 1335px), so the painted face is a few pixels wider than the box and the line breaks.

letter-spacing plays no part: the table above is with letter-spacing: 0. The visible symptom first showed up on titles with negative tracking, which is why it looked tracking-related.

Expected

The painter resolves the same weight as the measurer: a numeric font-weight is passed through as-is (Weight::from(n)), and only the bold/bolder keywords map to 700. The >= 600 => Bold arm looks like a leftover from a two-weight model.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions