M13.12: Variable Font Axes animators change advances (text re-spaced)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fdeb146fb2
commit
c3532df7ef
|
|
@ -314,13 +314,32 @@ fn variable_font_axes_animator() {
|
|||
let l = comp.layer(LayerId(t)).unwrap();
|
||||
effectcraft_render::text::glyph_paths(&ctx, l).iter().map(|(p, _)| effectcraft_text::kurbo::Shape::area(p).abs()).sum::<f64>()
|
||||
};
|
||||
// The distance between the two H's left edges: the H advance.
|
||||
let gap = |s: &Session| {
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let comp = s.project.comp(cid).unwrap();
|
||||
let ctx = effectcraft_render::EvalCtx::new(&s.project, cid, comp, s.time());
|
||||
let l = comp.layer(LayerId(t)).unwrap();
|
||||
let x0: Vec<f64> = effectcraft_render::text::glyph_paths(&ctx, l).iter().map(|(p, _)| effectcraft_text::kurbo::Shape::bounding_box(p).x0).collect();
|
||||
x0[1] - x0[0]
|
||||
};
|
||||
let before = area(&s);
|
||||
let gap_before = gap(&s);
|
||||
let l = s.active_comp().unwrap().layer(LayerId(t)).unwrap().clone();
|
||||
let pg = l.props.find_group(anim).unwrap().sub("properties").unwrap().props().next().unwrap().uid;
|
||||
let range = (axis.max - axis.min) as f64;
|
||||
s.execute("prop.set", json!({"layer": t, "prop": pg, "value": range})).unwrap();
|
||||
let after = area(&s);
|
||||
assert!((after - before).abs() > 1.0, "{} axis moved the outline: {before} → {after}", axis.tag);
|
||||
// Advances follow the axis too (the text is re-spaced, not only redrawn).
|
||||
let a = axes.iter().find(|a| a.tag == tag).unwrap();
|
||||
let f = effectcraft_text::fonts::face(face);
|
||||
let gid = f.glyph('H').unwrap();
|
||||
let upem = f.units_per_em() as f64;
|
||||
let units = |v: f32| effectcraft_text::variable::advance_units_at(face, gid, &[(a.tag.clone(), v)]).unwrap() as f64;
|
||||
let want = (units((a.default + range as f32).min(a.max)) - units(a.default)) * 60.0 / upem;
|
||||
let moved = gap(&s) - gap_before;
|
||||
assert!((moved - want).abs() < 0.5, "{} axis: advance change {moved}, expected {want}", a.tag);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!((area(&s) - before).abs() < 1e-6);
|
||||
let _ = info;
|
||||
|
|
|
|||
|
|
@ -568,8 +568,17 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option<TextGeom> {
|
|||
(lay, xfs)
|
||||
};
|
||||
|
||||
// Tracking (before / after each character) and Line Anchor, per line.
|
||||
// Variable Font Axes: the advance at the animated design-space position (later characters
|
||||
// on the line move with it).
|
||||
let n = lay.glyphs.len();
|
||||
let vadv: Vec<f64> = (0..n)
|
||||
.map(|gi| match (subs[gi], xfs[gi].axis_deltas()) {
|
||||
(None, d) if !d.is_empty() => effectcraft_text::variable::char_advance_delta(&lay.glyphs[gi], &d).unwrap_or(0.0),
|
||||
_ => 0.0,
|
||||
})
|
||||
.collect();
|
||||
let advance = |gi: usize| lay.glyphs[gi].advance + vadv[gi];
|
||||
// Tracking (before / after each character) and Line Anchor, per line.
|
||||
let mut shift = vec![0.0f64; n];
|
||||
let mut li_start = 0;
|
||||
while li_start < n {
|
||||
|
|
@ -585,7 +594,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option<TextGeom> {
|
|||
let em = lay.styles.get(lay.glyphs[gi].run).map_or(doc.size, |s| s.size) / 1000.0;
|
||||
pen += x.track_before * em;
|
||||
shift[gi] = pen;
|
||||
pen += x.track_after * em;
|
||||
pen += x.track_after * em + vadv[gi];
|
||||
if x.line_anchor_k > la_k {
|
||||
la = x.line_anchor;
|
||||
la_k = x.line_anchor_k;
|
||||
|
|
@ -622,7 +631,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option<TextGeom> {
|
|||
let o = origin(gi);
|
||||
let e = extents.entry(group_key(g)).or_insert([f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]);
|
||||
e[0] = e[0].min(o[0]);
|
||||
e[2] = e[2].max(o[0] + g.advance);
|
||||
e[2] = e[2].max(o[0] + advance(gi));
|
||||
e[1] = e[1].min(o[1]);
|
||||
e[3] = e[3].max(o[1]);
|
||||
}
|
||||
|
|
@ -645,7 +654,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option<TextGeom> {
|
|||
while j < n && lay.glyphs[j].line_index == line {
|
||||
j += 1;
|
||||
}
|
||||
let xs: Vec<f64> = (i..j).map(|gi| origin(gi)[0] + lay.glyphs[gi].advance / 2.0).collect();
|
||||
let xs: Vec<f64> = (i..j).map(|gi| origin(gi)[0] + advance(gi) / 2.0).collect();
|
||||
let lb = lay.line_boxes.get(line).copied().unwrap_or([0.0; 4]);
|
||||
let pos = path_text::arc_positions(&xs, [lb[0], lb[0] + lb[2]], align, pm.length(), *fm, *lm, *force);
|
||||
arc[i..j].copy_from_slice(&pos);
|
||||
|
|
@ -684,7 +693,7 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option<TextGeom> {
|
|||
}
|
||||
// Variable Font Axes: the outline redrawn at the animated design-space position.
|
||||
None => match x.axis_deltas() {
|
||||
d if !d.is_empty() => (effectcraft_text::variable::char_outline_varied(g, &d).unwrap_or_else(|| g.path.clone()), g.advance),
|
||||
d if !d.is_empty() => (effectcraft_text::variable::char_outline_varied(g, &d).unwrap_or_else(|| g.path.clone()), advance(gi)),
|
||||
_ => (g.path.clone(), g.advance),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -123,6 +123,37 @@ pub fn char_outline_varied(g: &CharGlyph, deltas: &[(String, f32)]) -> Option<Be
|
|||
Some(g.outline_xf * (*units).clone())
|
||||
}
|
||||
|
||||
/// A glyph's advance in font units at the given axis values (user units, unspecified axes at
|
||||
/// their defaults).
|
||||
pub fn advance_units_at(face: FaceId, gid: u32, coords: &[(String, f32)]) -> Option<f32> {
|
||||
let f = fonts::face(face);
|
||||
let font = f.font()?;
|
||||
let loc = font.axes().location(coords.iter().map(|(t, v)| (tag_of(t), *v)));
|
||||
font.glyph_metrics(Size::unscaled(), LocationRef::from(&loc)).advance_width(GlyphId::new(gid))
|
||||
}
|
||||
|
||||
/// The change of a laid-out character's advance (layout units) when its face's axes move by
|
||||
/// `deltas` (as [`char_outline_varied`]): Variable Font Axes animators re-space the text.
|
||||
pub fn char_advance_delta(g: &CharGlyph, deltas: &[(String, f32)]) -> Option<f64> {
|
||||
let axes = font_axes(g.face);
|
||||
let base = |t: &str, d: f32| g.variations.iter().find(|(x, _)| x.trim_end() == t.trim_end()).map(|(_, v)| *v).unwrap_or(d);
|
||||
let mut coords: Vec<(String, f32)> =
|
||||
deltas.iter().filter_map(|(t, d)| axes.iter().find(|a| a.tag == *t).map(|a| (t.clone(), (base(&a.tag, a.default) + d).clamp(a.min, a.max)))).collect();
|
||||
if coords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for (t, v) in &g.variations {
|
||||
if !coords.iter().any(|(c, _)| c == t) {
|
||||
coords.push((t.clone(), *v));
|
||||
}
|
||||
}
|
||||
let varied = advance_units_at(g.face, g.gid, &coords)?;
|
||||
let own = advance_units_at(g.face, g.gid, &g.variations)?;
|
||||
// Font units → layout units: the outline transform's horizontal scale.
|
||||
let sx = g.outline_xf.as_coeffs()[0];
|
||||
Some((varied - own) as f64 * sx)
|
||||
}
|
||||
|
||||
/// The first installed face with variation axes (system fonts are scanned), for tests and
|
||||
/// demos: (face, its axes).
|
||||
pub fn find_variable_face() -> Option<(FaceId, Vec<FontAxis>)> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue