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.
The
textpainter collapses every numericfont-weightof 600 or more toBold, 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/900is painted in the wrong weight (700, or whatever registered weight is closest to 700);crates/rustmotion-components/src/text.rs(paint):crates/rustmotion-components/src/intrinsic.rs(measure):weight: weight_to_u16(style.font_weight.as_ref()), thenWeight::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 asUN SEUL/COMPTE, andCOMPTEis drawn on top ofNEXT LINE.Same scenario, varying only
fonts[0].weights(render --frame 0):nowrap)[900][400, 900][500, 900][800, 900][400 ... 900]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 COMPTEat 150px: 900 = 1332px, 800 = 1335px, 700 = 1335px), so the painted face is a few pixels wider than the box and the line breaks.letter-spacingplays no part: the table above is withletter-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-weightis passed through as-is (Weight::from(n)), and only thebold/bolderkeywords map to 700. The>= 600 => Boldarm looks like a leftover from a two-weight model.