diff --git a/crates/engine/src/tests_tail.rs b/crates/engine/src/tests_tail.rs index 705425c0..7cd855c6 100644 --- a/crates/engine/src/tests_tail.rs +++ b/crates/engine/src/tests_tail.rs @@ -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::() }; + // 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 = 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; diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index ca37a951..3bfb5593 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -568,8 +568,17 @@ pub fn text_geom(ctx: &EvalCtx, layer: &Layer) -> Option { (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 = (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 { 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 { 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 { while j < n && lay.glyphs[j].line_index == line { j += 1; } - let xs: Vec = (i..j).map(|gi| origin(gi)[0] + lay.glyphs[gi].advance / 2.0).collect(); + let xs: Vec = (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 { } // 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), }, }; diff --git a/crates/text/src/variable.rs b/crates/text/src/variable.rs index 2072c294..6594e2dc 100644 --- a/crates/text/src/variable.rs +++ b/crates/text/src/variable.rs @@ -123,6 +123,37 @@ pub fn char_outline_varied(g: &CharGlyph, deltas: &[(String, f32)]) -> Option Option { + 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 { + 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)> {