mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:31:22 +00:00
.ai import: Stair Step spacing is fitted from its glyphs' origins
Stair Step's glyphs stay upright, so their matrices never show the path turning and the spacing fit found no turn at all. The fit now takes the path through each glyph's origin, which lies on the path, and measures how far it turns over a step from the circle through three origins in a row.
This commit is contained in:
@@ -783,6 +783,12 @@ fn path_spacing(f: &Val, effect: PathEffect) -> f64 {
|
||||
let Some(at) = g.get("21") else { continue };
|
||||
let list = |k: &str| at.get(k).and_then(Val::nums).unwrap_or_default();
|
||||
let (xs, ys, matrices, ends) = (list("1"), list("2"), list("3"), list("4"));
|
||||
if effect == PathEffect::StairStep {
|
||||
let (s, t) = stair_step_fit(&xs, &ys, &ends);
|
||||
sum += s;
|
||||
turns += t;
|
||||
continue;
|
||||
}
|
||||
let glyphs: Vec<(Point, f64, kurbo::Vec2, kurbo::Vec2)> = xs
|
||||
.iter()
|
||||
.zip(&ys)
|
||||
@@ -828,6 +834,45 @@ fn path_spacing(f: &Val, effect: PathEffect) -> f64 {
|
||||
if turns > 1e-6 && spacing.is_finite() { spacing.clamp(-1000.0, 1000.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
/// [`path_spacing`]'s sums for one line of Stair Step type, from its glyphs' origins `xs`, `ys` and
|
||||
/// where they end along the line, `ends`. Stair Step's glyphs stay upright, so their matrices don't
|
||||
/// show how the path turns; each origin lies on the path (all of them moved the same way by an
|
||||
/// alignment), and a glyph's step is the path's length from its origin to the next. The path is taken
|
||||
/// to be round through three origins in a row, which gives how far it turns over that step `t`; the
|
||||
/// text engine makes the step `s` with `s + t × spacing = a`, `a` the glyph's advance.
|
||||
fn stair_step_fit(xs: &[f64], ys: &[f64], ends: &[f64]) -> (f64, f64) {
|
||||
let origins: Vec<Point> = xs.iter().zip(ys).map(|(x, y)| Point::new(*x, *y)).collect();
|
||||
let (mut sum, mut turns) = (0.0, 0.0);
|
||||
for i in 0..origins.len().saturating_sub(1) {
|
||||
let (Some(p), Some(q)) = (origins.get(i), origins.get(i + 1)) else { continue };
|
||||
// The third origin: the one before, else the one after.
|
||||
let triple = match i.checked_sub(1).and_then(|h| origins.get(h)) {
|
||||
Some(o) => (*o, *p, *q, *o),
|
||||
None => match origins.get(i + 2) {
|
||||
Some(r) => (*p, *q, *r, *r),
|
||||
None => continue,
|
||||
},
|
||||
};
|
||||
let (a, b, c, third) = triple;
|
||||
// The inscribed angle at the third origin is half the arc's angle from `p` to `q`.
|
||||
let (to_p, to_q) = ((*p - third).hypot(), (*q - third).hypot());
|
||||
let sine = (b - a).cross(c - a) / (to_p * to_q);
|
||||
if !(sine.is_finite() && sine.abs() < 1.0) {
|
||||
continue;
|
||||
}
|
||||
let turn = 2.0 * sine.asin();
|
||||
let half = turn.abs() * 0.5;
|
||||
let chord = (*q - *p).hypot();
|
||||
let length = if half > 1e-9 { chord * half / half.sin() } else { chord };
|
||||
let advance = ends.get(i + 1).zip(ends.get(i)).map_or(f64::NAN, |(e1, e0)| e1 - e0);
|
||||
if turn.abs() <= 1.0 && length.is_finite() && advance.is_finite() {
|
||||
sum += (advance - length) * turn;
|
||||
turns += turn * turn;
|
||||
}
|
||||
}
|
||||
(sum, turns)
|
||||
}
|
||||
|
||||
impl Story {
|
||||
/// How many bytes of text the story has.
|
||||
pub(super) fn len(&self) -> usize {
|
||||
@@ -1747,6 +1792,29 @@ mod tests {
|
||||
assert!((spacing - 10.0).abs() < 1e-6, "{spacing}");
|
||||
}
|
||||
|
||||
/// Stair Step's glyphs stay upright, each origin on the path, its step the path's length to the
|
||||
/// next. On a circle of radius 100 with spacing 10, each step is 100/110 of the glyph's advance;
|
||||
/// moved 20 down by an alignment, the origins give the same spacing.
|
||||
#[test]
|
||||
fn the_spacing_of_stair_step_type_follows_the_origins_of_its_glyphs() {
|
||||
let advances = [20.0, 30.0, 10.0, 25.0, 20.0];
|
||||
for rise in [0.0, 20.0] {
|
||||
let mut s = 0.0;
|
||||
let mut glyphs = vec![];
|
||||
for a in advances {
|
||||
let angle = std::f64::consts::PI + s / 100.0;
|
||||
// `line_layout` takes centres: half an advance along the level baseline.
|
||||
glyphs.push((Point::new(100.0 * angle.cos() + a * 0.5, 100.0 * angle.sin() + rise), [1.0, 0.0, 0.0, 1.0]));
|
||||
s += a * 100.0 / 110.0;
|
||||
}
|
||||
let spacing = path_spacing(&line_layout(&advances, &glyphs, ""), PathEffect::StairStep);
|
||||
assert!((spacing - 10.0).abs() < 1e-6, "{rise}: {spacing}");
|
||||
}
|
||||
// Two glyphs say nothing of how the path turns.
|
||||
let two = line_layout(&advances[..2], &[(Point::new(0.0, 0.0), [1.0, 0.0, 0.0, 1.0]), (Point::new(30.0, 5.0), [1.0, 0.0, 0.0, 1.0])], "");
|
||||
assert_eq!(path_spacing(&two, PathEffect::StairStep), 0.0);
|
||||
}
|
||||
|
||||
/// With Ascender (baselines 20 towards the feet), glyphs facing left lie outside the path and
|
||||
/// glyphs facing right inside it. The pair either side of where the path runs straight up is left
|
||||
/// out, and the others give spacing 10.
|
||||
|
||||
Reference in New Issue
Block a user