mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 01:28:22 +00:00
Smooth Color now counts the steps between each pair of keys from the keys' colors and visible bounds, which changes the step count of most Smooth Color blends. The counts are VectorCraft's own values: - When the keys' solid fills or strokes differ (in groups, member by member; in text, run by run), the count is 255 times the largest change of one channel as a fraction of its range, a half step rounding down, from 1 to 254. An RGB channel's range is 256 values, CMYK and gray count in percent, and two tints of one swatch by their tints. It is at most eight per point of the largest of the keys' widths and heights and the distance between their centers. Black to white was 128 steps and is now 254. - When the colors match, a solid paint meets no paint, or a key holds a gradient or a pattern, the count is the largest distance between corresponding visible bounding-box edges divided by the keys' mean width or height, rounded, less one, from 1 to 1000. Two 20 pt squares 200 pt apart were 100 steps and are now 9. Keys without bounds make the color count when their colors differ, and 1 step otherwise. A blend nested in another blend's keys counts toward the step limit by its color steps when its colors differ, and as up to 1000 steps when they match (256 before). An outer blend of 200 steps between two such same-color Smooth Color blends now draws 99 steps. A Smooth Color blend in an .ai file whose drawn steps follow this rule now opens as a live blend. Blend Options: https://web.archive.org/web/20241217212108/https://helpx.adobe.com/illustrator/using/blending-objects.html Docs: docs/mcp.md, docs/target-app-parity.md.
1914 lines
91 KiB
Rust
1914 lines
91 KiB
Rust
//! Live blends: interpolating key objects and laying the steps out along the spine.
|
||
//!
|
||
//! - **Paths** are matched anchor by anchor ([`PathPair`]): subpaths paired in order (a missing
|
||
//! one grows out of a point), resampled to equal anchor counts, closed ones turned to the same
|
||
//! winding and to the start points that keep them from twisting, or to the anchors the Blend
|
||
//! tool clicked.
|
||
//! - **Paint**: colours, gradients (stops resampled to a common set when their counts differ),
|
||
//! opacity, stroke weight, dashes, miter limit and width profiles interpolate; what can't
|
||
//! (caps, joins, brushes, fonts, symbols, text content) switches halfway.
|
||
//! - **Groups** pair their children in stacking order; the children one group has more of grow
|
||
//! out of the other group's centre. Compound paths stay compound paths.
|
||
//! - **Text, symbols and images** interpolate their transforms (rotation, scale, shear and
|
||
//! position separately); text interpolates its colours and sizes run by run.
|
||
//! - **Spine** ([`blend_spine`]): the straight lines between the key centres until it is edited
|
||
//! or replaced; then a path whose anchors the keys sit on (`key_anchors`). Steps follow it by
|
||
//! arc length; Align to Path turns them to its direction.
|
||
|
||
use std::f64::consts::PI;
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::{Color, Gradient, GradientPaint, GradientStop, Paint};
|
||
use vectorcraft_geom::kurbo::ParamCurveArclen;
|
||
use vectorcraft_geom::{Affine, Anchor, BezPath, FillRule, PathData, Point, Rect, SubPath, Vec2};
|
||
|
||
use crate::appearance::{Appearance, AppearanceItem, Dash, StrokeLayer, WidthProfile};
|
||
use crate::live::{BlendOrientation, BlendSpacing, BlendSpec};
|
||
use crate::node::{Node, NodeId, NodeKind};
|
||
use crate::text::TextObject;
|
||
|
||
// =====================================================================================
|
||
// Colour / paint interpolation
|
||
// =====================================================================================
|
||
|
||
/// Interpolate two colours in their shared model (display RGB when the models differ).
|
||
pub fn lerp_color(a: &Color, b: &Color, t: f32) -> Color {
|
||
a.lerp_keep_model(b, t)
|
||
}
|
||
|
||
fn lerp_f32(x: f32, y: f32, t: f32) -> f32 {
|
||
x + (y - x) * t
|
||
}
|
||
|
||
fn lerp_f64(x: f64, y: f64, t: f64) -> f64 {
|
||
x + (y - x) * t
|
||
}
|
||
|
||
/// `like` with every stop painting `c`, linked as the solid is (`swatch` at `tint`).
|
||
fn solid_gradient(c: Color, swatch: Option<&str>, tint: f32, like: &Gradient) -> Gradient {
|
||
let mut g = like.clone();
|
||
for s in &mut g.stops {
|
||
s.color = c;
|
||
s.opacity = 1.0;
|
||
s.swatch = swatch.map(str::to_string);
|
||
s.tint = tint;
|
||
}
|
||
g
|
||
}
|
||
|
||
/// The swatch and tint `g` paints at `t`: a stop's own link, or the shared swatch of the two stops
|
||
/// around `t` at the tint between theirs (unlinked when they differ), as [`Gradient::sample_with`] walks.
|
||
fn link_at(g: &Gradient, t: f32) -> (Option<String>, f32) {
|
||
let stops = &g.stops;
|
||
let link = |s: &GradientStop| (s.swatch.clone(), s.tint);
|
||
let Some(first) = stops.first() else { return (None, 1.0) };
|
||
if t <= first.offset {
|
||
return link(first);
|
||
}
|
||
for w in stops.windows(2) {
|
||
let (a, b) = (&w[0], &w[1]);
|
||
if t <= b.offset {
|
||
let span = (b.offset - a.offset).max(1e-6);
|
||
let u = (t - a.offset) / span;
|
||
let m = a.midpoint.clamp(0.01, 0.99);
|
||
let v = if u < m { 0.5 * u / m } else { 0.5 + 0.5 * (u - m) / (1.0 - m) };
|
||
return match (&a.swatch, &b.swatch) {
|
||
(Some(x), Some(y)) if x == y => (Some(x.clone()), lerp_f32(a.tint, b.tint, v)),
|
||
_ if v <= 0.0 => link(a),
|
||
_ if v >= 1.0 => link(b),
|
||
_ => (None, 1.0),
|
||
};
|
||
}
|
||
}
|
||
stops.last().map_or((None, 1.0), link)
|
||
}
|
||
|
||
/// `g` with its stops at `offsets`, each painting what `g` paints there (midpoints centred).
|
||
fn resample_stops(g: &Gradient, offsets: &[f32]) -> Gradient {
|
||
let mut out = g.clone();
|
||
out.stops = offsets
|
||
.iter()
|
||
.map(|&o| {
|
||
let (color, opacity) = g.sample_keep_model(o);
|
||
let (swatch, tint) = link_at(g, o);
|
||
GradientStop { opacity, swatch, tint, ..GradientStop::new(o, color) }
|
||
})
|
||
.collect();
|
||
out
|
||
}
|
||
|
||
/// Where a gradient changes: its stop offsets and its off-centre midpoints.
|
||
fn gradient_breaks(g: &Gradient, out: &mut Vec<f32>) {
|
||
for (i, s) in g.stops.iter().enumerate() {
|
||
out.push(s.offset);
|
||
if let Some(n) = g.stops.get(i + 1)
|
||
&& (s.midpoint - 0.5).abs() > 1e-3
|
||
{
|
||
out.push(s.offset + (n.offset - s.offset) * s.midpoint);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Two gradients of one kind with the same number of stops: as they are when the counts match,
|
||
/// else both resampled at every place either changes.
|
||
fn common_stops(a: &Gradient, b: &Gradient) -> Option<(Gradient, Gradient)> {
|
||
if a.kind != b.kind {
|
||
return None;
|
||
}
|
||
if a.stops.len() == b.stops.len() {
|
||
return Some((a.clone(), b.clone()));
|
||
}
|
||
if a.stops.is_empty() || b.stops.is_empty() {
|
||
return None;
|
||
}
|
||
let mut offs = Vec::with_capacity(2 * (a.stops.len() + b.stops.len()));
|
||
gradient_breaks(a, &mut offs);
|
||
gradient_breaks(b, &mut offs);
|
||
offs.retain(|o| o.is_finite());
|
||
offs.sort_by(f32::total_cmp);
|
||
offs.dedup_by(|x, y| (*x - *y).abs() < 1e-4);
|
||
Some((resample_stops(a, &offs), resample_stops(b, &offs)))
|
||
}
|
||
|
||
fn lerp_gradient(a: &GradientPaint, b: &GradientPaint, t: f32) -> Option<GradientPaint> {
|
||
let (ga, gb) = common_stops(&a.gradient, &b.gradient)?;
|
||
let mut out = if t < 0.5 { a.clone() } else { b.clone() };
|
||
out.gradient.stops = ga
|
||
.stops
|
||
.iter()
|
||
.zip(&gb.stops)
|
||
.map(|(sa, sb)| {
|
||
// Stops on two tints of one swatch stay linked to it, as solids do (#3234).
|
||
let (swatch, tint) = match (&sa.swatch, &sb.swatch) {
|
||
(Some(x), Some(y)) if x == y => (Some(x.clone()), lerp_f32(sa.tint, sb.tint, t)),
|
||
_ => (None, 1.0),
|
||
};
|
||
GradientStop {
|
||
opacity: lerp_f32(sa.opacity, sb.opacity, t),
|
||
midpoint: lerp_f32(sa.midpoint, sb.midpoint, t),
|
||
swatch,
|
||
tint,
|
||
..GradientStop::new(lerp_f32(sa.offset, sb.offset, t), lerp_color(&sa.color, &sb.color, t))
|
||
}
|
||
})
|
||
.collect();
|
||
out.angle = a.angle + (b.angle - a.angle) * t as f64;
|
||
out.swatch = None;
|
||
if let (Some(ga), Some(gb)) = (a.geom, b.geom) {
|
||
let mut g = ga;
|
||
g.start = ga.start.lerp(gb.start, t as f64);
|
||
g.end = ga.end.lerp(gb.end, t as f64);
|
||
g.aspect = ga.aspect + (gb.aspect - ga.aspect) * t as f64;
|
||
g.focal = (ga.focal.is_some() || gb.focal.is_some()).then(|| ga.focal_point().lerp(gb.focal_point(), t as f64));
|
||
out.geom = Some(g);
|
||
} else {
|
||
out.geom = None;
|
||
}
|
||
Some(out)
|
||
}
|
||
|
||
/// Interpolate paints: solid↔solid, gradients of one kind (stops resampled to a common set),
|
||
/// solid↔gradient; otherwise switch halfway.
|
||
pub fn lerp_paint(a: &Paint, b: &Paint, t: f32) -> Paint {
|
||
match (a, b) {
|
||
// Two tints of one swatch blend into tints of that swatch (a spot stays a spot on export).
|
||
(Paint::Solid { color: x, swatch: Some(sa), tint: ta }, Paint::Solid { color: y, swatch: Some(sb), tint: tb }) if sa == sb => {
|
||
Paint::Solid { color: lerp_color(x, y, t), swatch: Some(sa.clone()), tint: lerp_f32(*ta, *tb, t) }
|
||
}
|
||
(Paint::Solid { color: x, .. }, Paint::Solid { color: y, .. }) => Paint::solid(lerp_color(x, y, t)),
|
||
(Paint::Gradient(ga), Paint::Gradient(gb)) => match lerp_gradient(ga, gb, t) {
|
||
Some(g) => Paint::Gradient(Box::new(g)),
|
||
None => (if t < 0.5 { a } else { b }).clone(),
|
||
},
|
||
(Paint::Solid { color, swatch, tint }, Paint::Gradient(g)) => {
|
||
let ga = GradientPaint { gradient: solid_gradient(*color, swatch.as_deref(), *tint, &g.gradient), ..(**g).clone() };
|
||
Paint::Gradient(Box::new(lerp_gradient(&ga, g, t).unwrap_or_else(|| (**g).clone())))
|
||
}
|
||
(Paint::Gradient(g), Paint::Solid { color, swatch, tint }) => {
|
||
let gb = GradientPaint { gradient: solid_gradient(*color, swatch.as_deref(), *tint, &g.gradient), ..(**g).clone() };
|
||
Paint::Gradient(Box::new(lerp_gradient(g, &gb, t).unwrap_or_else(|| (**g).clone())))
|
||
}
|
||
_ => (if t < 0.5 { a } else { b }).clone(),
|
||
}
|
||
}
|
||
|
||
/// Interpolate dash patterns. A solid stroke counts as the other's pattern with its gaps closed,
|
||
/// so dashes open out of a solid line; patterns of different lengths repeat to a common one.
|
||
fn lerp_dash(x: Option<&Dash>, y: Option<&Dash>, t: f64) -> Option<Dash> {
|
||
let solid_like = |d: &Dash| Dash {
|
||
// An odd pattern repeats twice per period, so pair up the doubled one to keep every gap closed.
|
||
pattern: if d.pattern.len() % 2 == 1 { d.pattern.repeat(2) } else { d.pattern.clone() }
|
||
.chunks(2)
|
||
.flat_map(|c| [c.iter().sum::<f64>(), 0.0])
|
||
.collect(),
|
||
offset: d.offset,
|
||
align_corners: d.align_corners,
|
||
};
|
||
let (x, y) = match (x.filter(|d| d.is_dashed()), y.filter(|d| d.is_dashed())) {
|
||
(None, None) => return None,
|
||
(Some(d), None) => (d.clone(), solid_like(d)),
|
||
(None, Some(d)) => (solid_like(d), d.clone()),
|
||
(Some(a), Some(b)) => (a.clone(), b.clone()),
|
||
};
|
||
// An odd pattern repeats twice per period (dash, gap, dash / gap, dash, gap).
|
||
let even = |p: &[f64]| if p.len() % 2 == 1 { p.repeat(2) } else { p.to_vec() };
|
||
let (pa, pb) = (even(&x.pattern), even(&y.pattern));
|
||
let n = if pa.is_empty() || pb.is_empty() { 0 } else { lcm(pa.len(), pb.len()).min(12) };
|
||
let at = |p: &[f64], i: usize| p.get(i % p.len().max(1)).copied().unwrap_or(0.0);
|
||
Some(Dash {
|
||
pattern: (0..n).map(|i| lerp_f64(at(&pa, i), at(&pb, i), t)).collect(),
|
||
offset: lerp_f64(x.offset, y.offset, t),
|
||
align_corners: if t < 0.5 { x.align_corners } else { y.align_corners },
|
||
})
|
||
}
|
||
|
||
fn lcm(a: usize, b: usize) -> usize {
|
||
let (mut x, mut y) = (a, b);
|
||
while y != 0 {
|
||
(x, y) = (y, x % y);
|
||
}
|
||
a.checked_div(x).map_or(0, |q| q * b)
|
||
}
|
||
|
||
/// Interpolate width profiles (none is uniform) at every position either has a width point.
|
||
fn lerp_profile(x: Option<&WidthProfile>, y: Option<&WidthProfile>, t: f64) -> Option<WidthProfile> {
|
||
if x.is_none() && y.is_none() {
|
||
return None;
|
||
}
|
||
let uniform = WidthProfile { points: vec![(0.0, 1.0, 1.0), (1.0, 1.0, 1.0)] };
|
||
let (x, y) = (x.unwrap_or(&uniform), y.unwrap_or(&uniform));
|
||
let mut at: Vec<f64> = x.points.iter().chain(&y.points).map(|p| p.0).filter(|p| p.is_finite()).collect();
|
||
at.sort_by(f64::total_cmp);
|
||
at.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||
let points = at
|
||
.into_iter()
|
||
.map(|p| {
|
||
let ((l1, r1), (l2, r2)) = (x.at(p), y.at(p));
|
||
(p, lerp_f64(l1, l2, t), lerp_f64(r1, r2, t))
|
||
})
|
||
.collect();
|
||
Some(WidthProfile { points })
|
||
}
|
||
|
||
/// Interpolate a stroke's paint, weight, opacity, miter limit, dashes, arrowhead scale and width
|
||
/// profile into `o` (a copy of the nearer stroke, so caps, joins, alignment, arrowheads and
|
||
/// brushes switch halfway).
|
||
fn lerp_stroke(o: &mut StrokeLayer, x: &StrokeLayer, y: &StrokeLayer, t: f64) {
|
||
let tf = t as f32;
|
||
o.paint = lerp_paint(&x.paint, &y.paint, tf);
|
||
o.width = lerp_f64(x.width, y.width, t);
|
||
o.opacity = lerp_f32(x.opacity, y.opacity, tf);
|
||
o.miter_limit = lerp_f64(x.miter_limit, y.miter_limit, t);
|
||
o.dash = lerp_dash(x.dash.as_ref(), y.dash.as_ref(), t);
|
||
o.arrow_scale = (lerp_f64(x.arrow_scale.0, y.arrow_scale.0, t), lerp_f64(x.arrow_scale.1, y.arrow_scale.1, t));
|
||
o.profile = lerp_profile(x.profile.as_ref(), y.profile.as_ref(), t);
|
||
}
|
||
|
||
/// Interpolate appearance stacks: item by item when their structure matches, else the top fill
|
||
/// and stroke only (on the structure of the nearer key).
|
||
pub fn lerp_appearance(a: &Appearance, b: &Appearance, t: f64) -> Appearance {
|
||
let tf = t as f32;
|
||
let mut out = if t < 0.5 { a.clone() } else { b.clone() };
|
||
let same = a.items.len() == b.items.len()
|
||
&& a.items.iter().zip(&b.items).all(|(x, y)| {
|
||
matches!((x, y), (AppearanceItem::Fill(_), AppearanceItem::Fill(_)) | (AppearanceItem::Stroke(_), AppearanceItem::Stroke(_)))
|
||
});
|
||
if same {
|
||
for ((it, x), y) in out.items.iter_mut().zip(&a.items).zip(&b.items) {
|
||
match (it, x, y) {
|
||
(AppearanceItem::Fill(o), AppearanceItem::Fill(x), AppearanceItem::Fill(y)) => {
|
||
o.paint = lerp_paint(&x.paint, &y.paint, tf);
|
||
o.opacity = lerp_f32(x.opacity, y.opacity, tf);
|
||
}
|
||
(AppearanceItem::Stroke(o), AppearanceItem::Stroke(x), AppearanceItem::Stroke(y)) => lerp_stroke(o, x, y, t),
|
||
_ => {}
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
let (fa, fb) = (a.fill_paint(), b.fill_paint());
|
||
if !(fa.is_none() && fb.is_none()) {
|
||
out.set_fill(lerp_paint(&fa, &fb, tf));
|
||
}
|
||
let (sa, sb) = (a.stroke_paint(), b.stroke_paint());
|
||
if !(sa.is_none() && sb.is_none()) {
|
||
out.set_stroke(lerp_paint(&sa, &sb, tf));
|
||
let w = a.stroke_width() + (b.stroke_width() - a.stroke_width()) * t;
|
||
let (xs, ys) = (a.stroke().cloned(), b.stroke().cloned());
|
||
if let Some(s) = out.stroke_mut() {
|
||
if let (Some(x), Some(y)) = (&xs, &ys) {
|
||
lerp_stroke(s, x, y, t);
|
||
}
|
||
s.width = w;
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Transforms
|
||
// =====================================================================================
|
||
|
||
/// An affine split into translation, rotation, x scale, shear and y scale (`R · [sx sh; 0 sy]`).
|
||
fn decompose(m: Affine) -> [f64; 6] {
|
||
let [a, b, c, d, e, f] = m.as_coeffs();
|
||
let sx = a.hypot(b);
|
||
let ang = if sx > 1e-12 { b.atan2(a) } else { 0.0 };
|
||
let (s, co) = ang.sin_cos();
|
||
[e, f, ang, sx, co * c + s * d, -s * c + co * d]
|
||
}
|
||
|
||
fn compose([e, f, ang, sx, sh, sy]: [f64; 6]) -> Affine {
|
||
let (s, co) = ang.sin_cos();
|
||
Affine::new([co * sx, s * sx, co * sh - s * sy, s * sh + co * sy, e, f])
|
||
}
|
||
|
||
/// Interpolate two transforms by their parts: the rotation takes the shorter way round.
|
||
pub fn lerp_affine(x: Affine, y: Affine, t: f64) -> Affine {
|
||
let (p, mut q) = (decompose(x), decompose(y));
|
||
let turn = q[2] - p[2];
|
||
q[2] = p[2] + (turn + PI).rem_euclid(2.0 * PI) - PI;
|
||
let mut out = [0.0; 6];
|
||
for (o, (a, b)) in out.iter_mut().zip(p.iter().zip(&q)) {
|
||
*o = lerp_f64(*a, *b, t);
|
||
}
|
||
compose(out)
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Path interpolation
|
||
// =====================================================================================
|
||
|
||
/// Grow a subpath to `n` anchors by splitting its longest segments.
|
||
fn grow(sp: &mut SubPath, n: usize) {
|
||
let mut guard = 0;
|
||
while sp.anchors.len() < n && guard < 20_000 {
|
||
guard += 1;
|
||
if sp.segment_count() == 0 {
|
||
match sp.anchors.last().copied() {
|
||
Some(a) => sp.anchors.push(Anchor::corner(a.p)),
|
||
None => return,
|
||
}
|
||
continue;
|
||
}
|
||
let seg = (0..sp.segment_count())
|
||
.max_by(|i, j| {
|
||
let (ci, cj) = (sp.segment(*i), sp.segment(*j));
|
||
(ci.p3 - ci.p0).hypot().total_cmp(&(cj.p3 - cj.p0).hypot())
|
||
})
|
||
.unwrap_or(0);
|
||
sp.insert_anchor(seg, 0.5);
|
||
}
|
||
}
|
||
|
||
fn lerp_anchor(x: &Anchor, y: &Anchor, t: f64) -> Anchor {
|
||
Anchor { p: x.p.lerp(y.p, t), h_in: x.h_in.lerp(y.h_in, t), h_out: x.h_out.lerp(y.h_out, t), kind: if t < 0.5 { x.kind } else { y.kind } }
|
||
}
|
||
|
||
/// Start `sp` at anchor `i`: a closed subpath turns round to it, an open one clicked at its last
|
||
/// anchor runs the other way (a click on any other anchor of an open path is ignored).
|
||
fn start_at(sp: &mut SubPath, i: usize) {
|
||
let n = sp.anchors.len();
|
||
if i >= n {
|
||
return;
|
||
}
|
||
if sp.closed {
|
||
sp.anchors.rotate_left(i);
|
||
} else if i + 1 == n && n > 1 {
|
||
sp.reverse();
|
||
}
|
||
}
|
||
|
||
/// Reverse a closed subpath keeping its start anchor first.
|
||
fn reverse_closed(sp: &mut SubPath) {
|
||
sp.reverse();
|
||
sp.anchors.rotate_right(1);
|
||
}
|
||
|
||
/// Anchor positions of a subpath relative to its box, scaled to unit size.
|
||
fn normalised(sp: &SubPath) -> Vec<Vec2> {
|
||
let pts = sp.anchors.iter().map(|a| a.p);
|
||
let (mut lo, mut hi) = (Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN));
|
||
for p in pts.clone() {
|
||
lo = Point::new(lo.x.min(p.x), lo.y.min(p.y));
|
||
hi = Point::new(hi.x.max(p.x), hi.y.max(p.y));
|
||
}
|
||
let c = lo.midpoint(hi);
|
||
let s = (hi.x - lo.x).max(hi.y - lo.y).max(1e-9);
|
||
pts.map(|p| (p - c) / s).collect()
|
||
}
|
||
|
||
/// The rotation of `b`'s anchors that best matches `a`'s (least squared distance between
|
||
/// corresponding anchors, each shape centred and scaled to unit size): the start point that keeps
|
||
/// a closed blend from twisting. Long subpaths try every few rotations first, then refine.
|
||
fn best_rotation(a: &SubPath, b: &SubPath) -> usize {
|
||
let (qa, qb) = (normalised(a), normalised(b));
|
||
let m = qa.len().min(qb.len());
|
||
if m < 2 {
|
||
return 0;
|
||
}
|
||
let cost = |s: usize| -> f64 { (0..m).map(|j| (qa[j] - qb[(j + s) % m]).hypot2()).sum() };
|
||
let stride = m.div_ceil(512);
|
||
let mut best = (0..m).step_by(stride).min_by(|x, y| cost(*x).total_cmp(&cost(*y))).unwrap_or(0);
|
||
if stride > 1 {
|
||
best = (best + m - stride..=best + m + stride).map(|s| s % m).min_by(|x, y| cost(*x).total_cmp(&cost(*y))).unwrap_or(best);
|
||
}
|
||
best
|
||
}
|
||
|
||
/// Two paths prepared for interpolation: subpaths paired in order, with equal anchor counts, the
|
||
/// same winding and matching start points.
|
||
#[derive(Clone, Debug)]
|
||
pub struct PathPair {
|
||
a: Vec<SubPath>,
|
||
b: Vec<SubPath>,
|
||
}
|
||
|
||
impl PathPair {
|
||
/// `starts`: the anchor of each path's first subpath its blend starts from (the Blend tool's
|
||
/// clicks); with neither, closed subpaths start where they twist least.
|
||
pub fn new(a: &PathData, b: &PathData, starts: (Option<usize>, Option<usize>)) -> Self {
|
||
let n = a.subpaths.len().max(b.subpaths.len());
|
||
let ca = a.bounds().map(|r| r.center()).unwrap_or_default();
|
||
let cb = b.bounds().map(|r| r.center()).unwrap_or_default();
|
||
let degenerate = |like: &SubPath, c: Point| SubPath::new(vec![Anchor::corner(c); like.anchors.len().max(1)], like.closed);
|
||
let (mut va, mut vb) = (Vec::with_capacity(n), Vec::with_capacity(n));
|
||
for i in 0..n {
|
||
let (mut sa, mut sb) = match (a.subpaths.get(i), b.subpaths.get(i)) {
|
||
(Some(x), Some(y)) => (x.clone(), y.clone()),
|
||
(Some(x), None) => (x.clone(), degenerate(x, cb)),
|
||
(None, Some(y)) => (degenerate(y, ca), y.clone()),
|
||
(None, None) => continue,
|
||
};
|
||
let explicit = i == 0 && (starts.0.is_some() || starts.1.is_some());
|
||
if explicit {
|
||
if let Some(s) = starts.0 {
|
||
start_at(&mut sa, s);
|
||
}
|
||
if let Some(s) = starts.1 {
|
||
start_at(&mut sb, s);
|
||
}
|
||
}
|
||
let closed = sa.closed && sb.closed && sa.anchors.len() > 2 && sb.anchors.len() > 2;
|
||
if closed && (sa.area() * sb.area()) < 0.0 {
|
||
reverse_closed(&mut sb);
|
||
}
|
||
let m = sa.anchors.len().max(sb.anchors.len());
|
||
grow(&mut sa, m);
|
||
grow(&mut sb, m);
|
||
if closed && !explicit {
|
||
let r = best_rotation(&sa, &sb);
|
||
sb.anchors.rotate_left(r);
|
||
}
|
||
va.push(sa);
|
||
vb.push(sb);
|
||
}
|
||
Self { a: va, b: vb }
|
||
}
|
||
|
||
/// The path at `t` (0 = `a`, 1 = `b`).
|
||
pub fn at(&self, t: f64) -> PathData {
|
||
let subs = self
|
||
.a
|
||
.iter()
|
||
.zip(&self.b)
|
||
.map(|(sa, sb)| {
|
||
let anchors = sa.anchors.iter().zip(&sb.anchors).map(|(x, y)| lerp_anchor(x, y, t)).collect();
|
||
SubPath::new(anchors, if t < 0.5 { sa.closed } else { sb.closed })
|
||
})
|
||
.collect();
|
||
PathData::new(subs)
|
||
}
|
||
|
||
/// Subpaths at `t`, in order (see [`Self::at`]).
|
||
fn len(&self) -> usize {
|
||
self.a.len()
|
||
}
|
||
}
|
||
|
||
/// Interpolate two paths anchor-by-anchor (see [`PathPair`]).
|
||
pub fn lerp_path(a: &PathData, b: &PathData, t: f64) -> PathData {
|
||
PathPair::new(a, b, (None, None)).at(t)
|
||
}
|
||
|
||
/// Path data of a path or compound path node (compound children concatenated), its fill rule and
|
||
/// the number of subpaths each compound member holds.
|
||
pub(crate) fn node_path(n: &Node) -> Option<(PathData, FillRule, Option<Vec<usize>>)> {
|
||
match &n.kind {
|
||
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule, None)),
|
||
NodeKind::Compound { children, rule } => {
|
||
let members: Vec<&PathData> = children.iter().filter_map(|c| c.path_data()).collect();
|
||
let counts = members.iter().map(|p| p.subpaths.len()).collect();
|
||
let subs = members.iter().flat_map(|p| p.subpaths.iter().cloned()).collect();
|
||
Some((PathData::new(subs), *rule, Some(counts)))
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Object interpolation
|
||
// =====================================================================================
|
||
|
||
/// How two key objects interpolate.
|
||
#[derive(Clone, Debug)]
|
||
enum Shape {
|
||
/// Groups of one kind: children paired in stacking order.
|
||
Group { children: Vec<Lerp>, clip: bool },
|
||
/// Gradient meshes with the same grid.
|
||
Mesh,
|
||
/// Paths and compound paths; `members`: the subpath count of each member when both keys are
|
||
/// compound paths.
|
||
Path { pair: PathPair, rules: (FillRule, FillRule), members: Option<(Vec<usize>, Vec<usize>)> },
|
||
/// Text, symbols and images: their transforms interpolate.
|
||
Placed,
|
||
/// Anything else: a copy of the nearer key moved and scaled into the interpolated box.
|
||
Other,
|
||
}
|
||
|
||
/// Two key objects prepared for interpolation, so each step of a blend only interpolates.
|
||
#[derive(Clone, Debug)]
|
||
pub struct Lerp {
|
||
a: Node,
|
||
b: Node,
|
||
shape: Shape,
|
||
}
|
||
|
||
/// `n` shrunk to a point at `c`: the counterpart of an object only one of two groups has.
|
||
fn collapsed(n: &Node, c: Point) -> Node {
|
||
let mut x = n.clone();
|
||
if let Some(b) = n.geometric_bounds() {
|
||
x.transform(Affine::translate(c.to_vec2()) * Affine::scale(1e-6) * Affine::translate(-b.center().to_vec2()), false);
|
||
}
|
||
x
|
||
}
|
||
|
||
fn center_of(n: &Node) -> Point {
|
||
n.geometric_bounds().map(|b| b.center()).unwrap_or_default()
|
||
}
|
||
|
||
impl Lerp {
|
||
/// Prepare `a` → `b`; `starts` as in [`PathPair::new`].
|
||
pub fn new(a: &Node, b: &Node, starts: (Option<usize>, Option<usize>)) -> Self {
|
||
let shape = match (&a.kind, &b.kind) {
|
||
(NodeKind::Group { children: ca, clip: k1 }, NodeKind::Group { children: cb, clip: k2 })
|
||
if k1 == k2 && !(*k1 && ca.len() != cb.len()) =>
|
||
{
|
||
let (oa, ob) = (center_of(a), center_of(b));
|
||
let n = ca.len().max(cb.len());
|
||
let children = (0..n)
|
||
.filter_map(|i| match (ca.get(i), cb.get(i)) {
|
||
(Some(x), Some(y)) => Some(Lerp::new(x, y, (None, None))),
|
||
(Some(x), None) => Some(Lerp::new(x, &collapsed(x, ob), (None, None))),
|
||
(None, Some(y)) => Some(Lerp::new(&collapsed(y, oa), y, (None, None))),
|
||
(None, None) => None,
|
||
})
|
||
.collect();
|
||
Shape::Group { children, clip: *k1 }
|
||
}
|
||
(NodeKind::Mesh(ma), NodeKind::Mesh(mb)) if ma.rows == mb.rows && ma.cols == mb.cols && ma.points.len() == mb.points.len() => Shape::Mesh,
|
||
(NodeKind::Text(_), NodeKind::Text(_))
|
||
| (NodeKind::SymbolInstance { .. }, NodeKind::SymbolInstance { .. })
|
||
| (NodeKind::Image(_), NodeKind::Image(_))
|
||
| (NodeKind::PlacedDocument(_), NodeKind::PlacedDocument(_)) => Shape::Placed,
|
||
_ => match (node_path(a), node_path(b)) {
|
||
(Some((pa, ra, ma)), Some((pb, rb, mb))) => {
|
||
Shape::Path { pair: PathPair::new(&pa, &pb, starts), rules: (ra, rb), members: ma.zip(mb) }
|
||
}
|
||
_ => Shape::Other,
|
||
},
|
||
};
|
||
Self { a: a.clone(), b: b.clone(), shape }
|
||
}
|
||
|
||
/// The object at `t` (0 = `a`, 1 = `b`), with id 0.
|
||
pub fn at(&self, t: f64) -> Node {
|
||
self.at_eased(t, t)
|
||
}
|
||
|
||
/// The object whose shape is at `t` and whose colours (fills, strokes, opacity, mesh and text
|
||
/// colours) are at `tc`: a blend with its colours eased apart from its spacing.
|
||
pub fn at_eased(&self, t: f64, tc: f64) -> Node {
|
||
let (a, b) = (&self.a, &self.b);
|
||
let base = if t < 0.5 { a } else { b };
|
||
let mut n = match &self.shape {
|
||
Shape::Group { children, clip } => {
|
||
let mut g =
|
||
Node::new(NodeId(0), NodeKind::Group { children: children.iter().map(|c| Arc::new(c.at_eased(t, tc))).collect(), clip: *clip });
|
||
g.isolate = base.isolate;
|
||
g.knockout = base.knockout;
|
||
g
|
||
}
|
||
Shape::Mesh => {
|
||
let mut n = base.clone();
|
||
if let (NodeKind::Mesh(m), NodeKind::Mesh(ma), NodeKind::Mesh(mb)) = (&mut n.kind, &a.kind, &b.kind) {
|
||
for ((p, x), y) in m.points.iter_mut().zip(&ma.points).zip(&mb.points) {
|
||
p.p = x.p.lerp(y.p, t);
|
||
p.color = lerp_color(&x.color, &y.color, tc as f32);
|
||
p.opacity = lerp_f32(x.opacity, y.opacity, tc as f32);
|
||
for h in 0..4 {
|
||
p.handles[h] = x.handles[h].lerp(y.handles[h], t);
|
||
}
|
||
}
|
||
}
|
||
n.extras_mut().mask = None;
|
||
n
|
||
}
|
||
Shape::Path { pair, rules, members } => {
|
||
let path = pair.at(t);
|
||
let rule = if t < 0.5 { rules.0 } else { rules.1 };
|
||
match members {
|
||
Some((ma, mb)) => compound(path, rule, if t < 0.5 { ma } else { mb }, pair.len()),
|
||
None => {
|
||
let mut n = Node::path(NodeId(0), path, Appearance::default());
|
||
if let NodeKind::Path { rule: r, .. } = &mut n.kind {
|
||
*r = rule;
|
||
}
|
||
n
|
||
}
|
||
}
|
||
}
|
||
Shape::Placed => {
|
||
let mut n = base.clone();
|
||
match (&mut n.kind, &a.kind, &b.kind) {
|
||
(NodeKind::Text(o), NodeKind::Text(x), NodeKind::Text(y)) => lerp_text(o, x, y, t, tc),
|
||
(NodeKind::SymbolInstance { xf: o, .. }, NodeKind::SymbolInstance { xf: x, .. }, NodeKind::SymbolInstance { xf: y, .. }) => {
|
||
*o = lerp_affine(*x, *y, t)
|
||
}
|
||
(NodeKind::Image(o), NodeKind::Image(x), NodeKind::Image(y)) => o.xf = lerp_affine(x.xf, y.xf, t),
|
||
(NodeKind::PlacedDocument(o), NodeKind::PlacedDocument(x), NodeKind::PlacedDocument(y)) => o.xf = lerp_affine(x.xf, y.xf, t),
|
||
_ => {}
|
||
}
|
||
n.extras_mut().mask = None;
|
||
n
|
||
}
|
||
Shape::Other => {
|
||
// Different structure: move/scale a copy of the nearer key into the interpolated box.
|
||
let mut n = base.clone();
|
||
if let (Some(ba), Some(bb), Some(bn)) = (a.geometric_bounds(), b.geometric_bounds(), base.geometric_bounds()) {
|
||
let w = ba.width() + (bb.width() - ba.width()) * t;
|
||
let h = ba.height() + (bb.height() - ba.height()) * t;
|
||
let c = ba.center().lerp(bb.center(), t);
|
||
let sx = if bn.width() > 1e-9 { w / bn.width() } else { 1.0 };
|
||
let sy = if bn.height() > 1e-9 { h / bn.height() } else { 1.0 };
|
||
n.transform(
|
||
Affine::translate(c.to_vec2()) * Affine::scale_non_uniform(sx, sy) * Affine::translate(-bn.center().to_vec2()),
|
||
false,
|
||
);
|
||
}
|
||
if let NodeKind::Path { live, .. } = &mut n.kind {
|
||
*live = None;
|
||
}
|
||
// A copy's members are new objects too.
|
||
for c in n.children_mut().into_iter().flatten() {
|
||
zero_ids(Arc::make_mut(c));
|
||
}
|
||
n.extras_mut().mask = None;
|
||
n
|
||
}
|
||
};
|
||
n.id = NodeId(0);
|
||
n.appearance = lerp_appearance(&a.appearance, &b.appearance, tc).into();
|
||
n.opacity = lerp_f32(a.opacity, b.opacity, tc as f32);
|
||
n.blend = base.blend;
|
||
n.visible = true;
|
||
n.locked = false;
|
||
n.name = None;
|
||
n
|
||
}
|
||
}
|
||
|
||
/// `n` and its descendants with id 0 (generated objects).
|
||
fn zero_ids(n: &mut Node) {
|
||
n.id = NodeId(0);
|
||
for c in n.children_mut().into_iter().flatten() {
|
||
zero_ids(Arc::make_mut(c));
|
||
}
|
||
}
|
||
|
||
/// A compound path of `path`'s subpaths, split into members as `counts` says (one member per
|
||
/// subpath when the counts don't add up to `total`).
|
||
fn compound(path: PathData, rule: FillRule, counts: &[usize], total: usize) -> Node {
|
||
let mut subs = path.subpaths.into_iter();
|
||
let counts: Vec<usize> = if counts.iter().sum::<usize>() == total && !counts.contains(&0) { counts.to_vec() } else { vec![1; total] };
|
||
let children =
|
||
counts.iter().map(|&c| Arc::new(Node::path(NodeId(0), PathData::new(subs.by_ref().take(c).collect()), Appearance::default()))).collect();
|
||
Node::new(NodeId(0), NodeKind::Compound { children, rule })
|
||
}
|
||
|
||
/// Interpolate text `x` → `y` into `o` (a copy of the nearer): the transform by its parts, an
|
||
/// area frame anchor by anchor, and each run's colours, stroke weight and size from the runs at
|
||
/// the same place (by character) of both.
|
||
/// The run of `of` at fraction `u` of its characters.
|
||
fn run_at(of: &TextObject, u: f64) -> Option<&crate::text::TextRun> {
|
||
let total: usize = of.runs.iter().map(|r| r.text.chars().count()).sum();
|
||
let target = (u * total as f64) as usize;
|
||
let mut seen = 0;
|
||
for r in &of.runs {
|
||
seen += r.text.chars().count();
|
||
if seen > target {
|
||
return Some(r);
|
||
}
|
||
}
|
||
of.runs.last()
|
||
}
|
||
|
||
/// The fraction of the characters of `runs` at the middle of each run.
|
||
fn run_middles(runs: &[crate::text::TextRun]) -> Vec<f64> {
|
||
let total: usize = runs.iter().map(|r| r.text.chars().count()).sum::<usize>().max(1);
|
||
let mut start = 0usize;
|
||
runs.iter()
|
||
.map(|r| {
|
||
let len = r.text.chars().count();
|
||
let u = (start as f64 + len as f64 / 2.0) / total as f64;
|
||
start += len;
|
||
u
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn lerp_text(o: &mut TextObject, x: &TextObject, y: &TextObject, t: f64, tc: f64) {
|
||
o.xf = lerp_affine(x.xf, y.xf, t);
|
||
if let (crate::text::TextKind::Area { frame: f }, crate::text::TextKind::Area { frame: fx }, crate::text::TextKind::Area { frame: fy }) =
|
||
(&mut o.kind, &x.kind, &y.kind)
|
||
{
|
||
*f = lerp_path(fx, fy, t);
|
||
}
|
||
let tf = tc as f32;
|
||
let mut resized = false;
|
||
let middles = run_middles(&o.runs);
|
||
for (r, u) in o.runs.iter_mut().zip(middles) {
|
||
let (Some(rx), Some(ry)) = (run_at(x, u), run_at(y, u)) else { continue };
|
||
let (sx, sy) = (&rx.style, &ry.style);
|
||
r.style.fill = lerp_paint(&sx.fill, &sy.fill, tf);
|
||
r.style.stroke = lerp_paint(&sx.stroke, &sy.stroke, tf);
|
||
r.style.stroke_width = lerp_f64(sx.stroke_width, sy.stroke_width, t);
|
||
let size = lerp_f64(sx.size, sy.size, t);
|
||
resized |= size != r.style.size;
|
||
r.style.size = size;
|
||
}
|
||
if resized {
|
||
o.cached_bounds = None;
|
||
o.cached_baselines.clear();
|
||
}
|
||
}
|
||
|
||
/// Interpolate two objects at `t` (0 = `a`, 1 = `b`). A blend prepares each pair once with
|
||
/// [`Lerp`] instead.
|
||
pub fn lerp_node(a: &Node, b: &Node, t: f64) -> Node {
|
||
Lerp::new(a, b, (None, None)).at(t)
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Step counts
|
||
// =====================================================================================
|
||
|
||
/// Largest channel difference between two solid colors, as a fraction of the channel's range: two
|
||
/// tints of the same swatch by their tints, RGB by its 0 to 255 values over 256, CMYK and gray by
|
||
/// their percentages, and colors in different spaces (or Lab) by the difference of their RGB
|
||
/// values over 255. A solid paint against no paint, or anything that isn't solid, differs by 0:
|
||
/// [`lerp_paint`] switches such paints halfway rather than mixing them.
|
||
fn paint_difference(x: &Paint, y: &Paint) -> f64 {
|
||
let (Paint::Solid { color: p, swatch: sp, tint: tp }, Paint::Solid { color: q, swatch: sq, tint: tq }) = (x, y) else {
|
||
return 0.0;
|
||
};
|
||
if sp.is_some() && sp == sq && tp != tq {
|
||
return f64::from((tp - tq).abs());
|
||
}
|
||
let most = |a: &[f32], b: &[f32]| a.iter().zip(b).fold(0.0f64, |m, (u, v)| m.max(f64::from((u - v).abs())));
|
||
match (p, q) {
|
||
(Color::Rgb { r, g, b }, Color::Rgb { r: r2, g: g2, b: b2 }) => most(&[*r, *g, *b], &[*r2, *g2, *b2]) * 255.0 / 256.0,
|
||
(Color::Cmyk { c, m, y, k }, Color::Cmyk { c: c2, m: m2, y: y2, k: k2 }) => most(&[*c, *m, *y, *k], &[*c2, *m2, *y2, *k2]),
|
||
(Color::Gray { k }, Color::Gray { k: k2 }) => most(&[*k], &[*k2]),
|
||
_ => most(&p.to_rgb(), &q.to_rgb()),
|
||
}
|
||
}
|
||
|
||
/// Largest difference (see [`paint_difference`]) between pairs of fills and strokes, or `None`
|
||
/// when any of them is a gradient or a pattern.
|
||
fn paints_difference(pairs: &[(&Paint, &Paint)]) -> Option<f64> {
|
||
let mut d = 0.0f64;
|
||
for (x, y) in pairs {
|
||
if [x, y].iter().any(|p| matches!(p, Paint::Gradient(_) | Paint::Pattern { .. })) {
|
||
return None;
|
||
}
|
||
d = d.max(paint_difference(x, y));
|
||
}
|
||
Some(d)
|
||
}
|
||
|
||
/// Largest channel difference (0..1, see [`paint_difference`]) between the solid colors of two
|
||
/// keys: their top fill and stroke, the runs of two text objects paired as [`lerp_text`] pairs
|
||
/// them, and the members of two groups paired as [`Lerp::new`] pairs them. `None` when any of
|
||
/// these holds a gradient or a pattern, whose steps Smooth Color counts by distance.
|
||
fn color_distance(a: &Node, b: &Node) -> Option<f64> {
|
||
let mut d =
|
||
paints_difference(&[(&a.appearance.fill_paint(), &b.appearance.fill_paint()), (&a.appearance.stroke_paint(), &b.appearance.stroke_paint())])?;
|
||
match (&a.kind, &b.kind) {
|
||
(NodeKind::Group { children: ca, clip: k1 }, NodeKind::Group { children: cb, clip: k2 }) if k1 == k2 && !(*k1 && ca.len() != cb.len()) => {
|
||
for (x, y) in ca.iter().zip(cb) {
|
||
d = d.max(color_distance(x, y)?);
|
||
}
|
||
}
|
||
(NodeKind::Text(x), NodeKind::Text(y)) => {
|
||
// The runs `lerp_text` pairs: those of `x` and of `y` at the middle of each run of either.
|
||
for u in run_middles(&x.runs).into_iter().chain(run_middles(&y.runs)) {
|
||
let (Some(rx), Some(ry)) = (run_at(x, u), run_at(y, u)) else { continue };
|
||
d = d.max(paints_difference(&[(&rx.style.fill, &ry.style.fill), (&rx.style.stroke, &ry.style.stroke)])?);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
d.is_finite().then_some(d)
|
||
}
|
||
|
||
/// Smooth Color's steps between two keys by distance: the largest distance between their
|
||
/// corresponding visible bounding-box edges, in mean key widths (left and right edges) or heights
|
||
/// (top and bottom), rounded, less one, from 1 to 1000. Keys with no width are measured in their
|
||
/// mean height across as well, keys with no height in their mean width, and keys with neither in
|
||
/// points.
|
||
fn spatial_steps(ba: Rect, bb: Rect) -> usize {
|
||
let (w, h) = ((ba.width() + bb.width()) / 2.0, (ba.height() + bb.height()) / 2.0);
|
||
let size = |s: f64, other: f64| {
|
||
if s > 1e-9 {
|
||
s
|
||
} else if other > 1e-9 {
|
||
other
|
||
} else {
|
||
1.0
|
||
}
|
||
};
|
||
let (w, h) = (size(w, h), size(h, w));
|
||
let q = ((ba.x0 - bb.x0).abs().max((ba.x1 - bb.x1).abs()) / w).max((ba.y0 - bb.y0).abs().max((ba.y1 - bb.y1).abs()) / h);
|
||
if !q.is_finite() {
|
||
return 1;
|
||
}
|
||
((q + 0.5).floor() - 1.0).clamp(1.0, 1000.0) as usize
|
||
}
|
||
|
||
/// `255 × d` steps for a color difference `d` (see [`color_distance`]), a half step rounding down,
|
||
/// from 1 to 254.
|
||
fn level_steps(d: f64) -> f64 {
|
||
// The tolerance keeps a difference that lands on a half step (128 of 256 RGB values gives
|
||
// 127.5), stored as an `f32`, from rounding up.
|
||
(255.0 * d - 0.5 - 1e-4).ceil().clamp(1.0, 254.0)
|
||
}
|
||
|
||
/// Smooth Color's steps between two keys whose colors differ by `d` (see [`color_distance`]):
|
||
/// [`level_steps`], and at most eight per point of the largest of the keys' widths and heights
|
||
/// and the distance between their centers along either axis.
|
||
fn color_steps(d: f64, ba: Rect, bb: Rect) -> usize {
|
||
let by_color = level_steps(d);
|
||
let (ca, cb) = (ba.center(), bb.center());
|
||
let reach = [(ca.x - cb.x).abs(), (ca.y - cb.y).abs(), ba.width(), ba.height(), bb.width(), bb.height()].into_iter().fold(0.0f64, f64::max);
|
||
let by_density = (8.0 * reach + 0.5).floor().clamp(1.0, 254.0);
|
||
if !by_density.is_finite() {
|
||
return by_color as usize;
|
||
}
|
||
by_color.min(by_density) as usize
|
||
}
|
||
|
||
/// Number of intermediate steps between two keys. Specified Distance counts along the spine, `len`
|
||
/// long between them. Smooth Color counts from the keys' colors and visible bounds, whatever the
|
||
/// spine: keys whose colors differ make [`color_steps`] ([`level_steps`] when either key has no
|
||
/// bounds), and others make [`spatial_steps`] (1 when either key has no bounds).
|
||
pub fn blend_step_count(a: &Node, b: &Node, spacing: BlendSpacing, len: f64) -> usize {
|
||
match spacing {
|
||
BlendSpacing::Steps(n) => n.clamp(1, 1000) as usize,
|
||
BlendSpacing::Distance(d) => {
|
||
let d = d.max(0.01);
|
||
((len / d).round() as i64 - 1).clamp(0, 1000) as usize
|
||
}
|
||
BlendSpacing::SmoothColor => {
|
||
let bounds = a.visual_bounds().zip(b.visual_bounds());
|
||
match (color_distance(a, b), bounds) {
|
||
// Colors that differ by less than a thousandth of a step count as the same.
|
||
(Some(d), Some((ba, bb))) if d * 255.0 > 1e-3 => color_steps(d, ba, bb),
|
||
(Some(d), None) if d * 255.0 > 1e-3 => level_steps(d) as usize,
|
||
// Same colors, gradients and patterns: the distance between the objects.
|
||
(_, Some((ba, bb))) => spatial_steps(ba, bb),
|
||
(_, None) => 1,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Spine
|
||
// =====================================================================================
|
||
|
||
/// A spine (the first subpath of a path) flattened to a polyline, with cumulative lengths and the
|
||
/// arc length at each of its anchors.
|
||
pub struct Spine {
|
||
pts: Vec<Point>,
|
||
cum: Vec<f64>,
|
||
anchors: Vec<f64>,
|
||
closed: bool,
|
||
}
|
||
|
||
impl Spine {
|
||
pub fn new(path: &PathData) -> Option<Self> {
|
||
let sp = path.subpaths.first()?;
|
||
let mut pts: Vec<Point> = Vec::new();
|
||
let mut cum: Vec<f64> = Vec::new();
|
||
let mut anchors = Vec::with_capacity(sp.anchors.len());
|
||
for i in 0..sp.segment_count() {
|
||
anchors.push(cum.last().copied().unwrap_or(0.0));
|
||
let c = sp.segment(i);
|
||
let mut bp = BezPath::new();
|
||
bp.move_to(c.p0);
|
||
if sp.segment_is_line(i) {
|
||
bp.line_to(c.p3);
|
||
} else {
|
||
bp.curve_to(c.p1, c.p2, c.p3);
|
||
}
|
||
vectorcraft_geom::kurbo::flatten(bp.iter(), 0.1, |el| {
|
||
if let vectorcraft_geom::PathEl::MoveTo(p) | vectorcraft_geom::PathEl::LineTo(p) = el
|
||
&& p.x.is_finite()
|
||
&& p.y.is_finite()
|
||
&& pts.last().is_none_or(|q| q.distance(p) > 1e-9)
|
||
{
|
||
let s = match (pts.last(), cum.last()) {
|
||
(Some(q), Some(l)) => l + q.distance(p),
|
||
_ => 0.0,
|
||
};
|
||
pts.push(p);
|
||
cum.push(s);
|
||
}
|
||
});
|
||
}
|
||
if !sp.closed {
|
||
anchors.push(cum.last().copied().unwrap_or(0.0));
|
||
}
|
||
if pts.len() < 2 {
|
||
return None;
|
||
}
|
||
Some(Self { pts, cum, anchors, closed: sp.closed })
|
||
}
|
||
pub fn length(&self) -> f64 {
|
||
*self.cum.last().unwrap_or(&0.0)
|
||
}
|
||
/// Arc length at anchor `i`.
|
||
pub fn anchor_length(&self, i: usize) -> Option<f64> {
|
||
self.anchors.get(i).copied()
|
||
}
|
||
/// Arc length of the point of the spine nearest `p`. An open spine's end segments run on past
|
||
/// its ends as lines, so the result is below 0 before the start and above [`Spine::length`]
|
||
/// beyond the end. With `from`, the arc length of an earlier point a drag followed, each point
|
||
/// counts as farther by half its distance along the spine from there, so a pointer passing
|
||
/// near another part of the spine stays on the part it follows. Where two points count as
|
||
/// near as each other, the one earlier along the spine counts.
|
||
pub fn length_near(&self, p: Point, from: Option<f64>) -> f64 {
|
||
self.nearest(p, from).map_or(0.0, |(_, s)| s)
|
||
}
|
||
/// [`Spine::length_near`] with how near the point counts: its distance from `p`, plus half its
|
||
/// distance along the spine from `from`. `None` for a point that is not finite.
|
||
pub fn nearest(&self, p: Point, from: Option<f64>) -> Option<(f64, f64)> {
|
||
if !p.x.is_finite() || !p.y.is_finite() {
|
||
return None;
|
||
}
|
||
let last = self.pts.len().saturating_sub(2);
|
||
let mut best: Option<(f64, f64)> = None;
|
||
for (i, (w, c)) in self.pts.windows(2).zip(&self.cum).enumerate() {
|
||
let [a, b] = [w[0], w[1]];
|
||
let d = b - a;
|
||
let l2 = d.dot(d);
|
||
if !l2.is_finite() || l2 <= 0.0 {
|
||
continue;
|
||
}
|
||
let t = (p - a).dot(d) / l2;
|
||
let t = match (self.closed, i == 0, i == last) {
|
||
(false, true, true) => t,
|
||
(false, true, false) => t.min(1.0),
|
||
(false, false, true) => t.max(0.0),
|
||
_ => t.clamp(0.0, 1.0),
|
||
};
|
||
let s = c + t * l2.sqrt();
|
||
let dist = p.distance(a + d * t) + from.filter(|f| f.is_finite()).map_or(0.0, |f| self.apart(s, f) / 2.0);
|
||
if dist.is_finite() && best.is_none_or(|(b, _)| dist < b) {
|
||
best = Some((dist, s));
|
||
}
|
||
}
|
||
best
|
||
}
|
||
/// How far apart arc lengths `s` and `f` are along the spine: the shorter way round a closed
|
||
/// spine, across its first anchor.
|
||
pub fn apart(&self, s: f64, f: f64) -> f64 {
|
||
let len = self.length();
|
||
let d = (s - f).abs();
|
||
if self.closed && len > 0.0 { d.rem_euclid(len).min(len - d.rem_euclid(len)) } else { d }
|
||
}
|
||
/// Point and tangent angle (radians) at arc-length fraction `f` (0..1).
|
||
pub fn at(&self, f: f64) -> (Point, f64) {
|
||
self.at_length(f.clamp(0.0, 1.0) * self.length())
|
||
}
|
||
/// Point and tangent angle (radians) at arc length `s` (clamped to the spine).
|
||
pub fn at_length(&self, s: f64) -> (Point, f64) {
|
||
let last = self.pts.len().saturating_sub(2);
|
||
let s = if s.is_finite() { s.clamp(0.0, self.length()) } else { 0.0 };
|
||
let i = match self.cum.binary_search_by(|c| c.total_cmp(&s)) {
|
||
Ok(i) => i.min(last),
|
||
Err(i) => i.saturating_sub(1).min(last),
|
||
};
|
||
let (Some(p0), Some(p1), Some(c0), Some(c1)) = (self.pts.get(i), self.pts.get(i + 1), self.cum.get(i), self.cum.get(i + 1)) else {
|
||
return (self.pts.first().copied().unwrap_or_default(), 0.0);
|
||
};
|
||
let u = ((s - c0) / (c1 - c0).max(1e-12)).clamp(0.0, 1.0);
|
||
let d = *p1 - *p0;
|
||
(p0.lerp(*p1, u), d.y.atan2(d.x))
|
||
}
|
||
}
|
||
|
||
/// The anchor of the first subpath each key sits on, when they are all valid for `n` anchors.
|
||
fn pinned(spec: &BlendSpec, keys: usize, n: usize) -> Option<Vec<usize>> {
|
||
(spec.key_anchors.len() == keys && keys >= 2 && spec.key_anchors.iter().all(|a| (*a as usize) < n))
|
||
.then(|| spec.key_anchors.iter().map(|a| *a as usize).collect())
|
||
}
|
||
|
||
/// The spine of a blend of `keys` and the anchor of it each key sits on:
|
||
/// - without a stored spine: the straight lines between the key centres;
|
||
/// - a pinned spine (`key_anchors`): the stored spine's first subpath with each key's anchor (and
|
||
/// its handles) moved onto the key's centre, so moving a key moves its end of the spine;
|
||
/// - a spine from Replace Spine: its first subpath, `None` for the anchors (the keys spread
|
||
/// evenly along it by arc length).
|
||
pub fn blend_spine(keys: &[Arc<Node>], spec: &BlendSpec) -> Option<(PathData, Option<Vec<usize>>)> {
|
||
if keys.len() < 2 {
|
||
return None;
|
||
}
|
||
let Some(stored) = &spec.spine else {
|
||
let centers: Vec<Point> = keys.iter().map(|k| center_of(k)).collect();
|
||
return Some((PathData::single(SubPath::polyline(¢ers, false)), Some((0..keys.len()).collect())));
|
||
};
|
||
let mut first = stored.subpaths.first()?.clone();
|
||
let Some(ka) = pinned(spec, keys.len(), first.anchors.len()) else { return Some((PathData::single(first), None)) };
|
||
for (k, &ai) in keys.iter().zip(&ka) {
|
||
let c = center_of(k);
|
||
if let Some(a) = first.anchors.get_mut(ai) {
|
||
let d = c - a.p;
|
||
a.translate(d);
|
||
}
|
||
}
|
||
Some((PathData::single(first), Some(ka)))
|
||
}
|
||
|
||
/// [`blend_spine`] with every key on an anchor: a spine from Replace Spine gets anchors where its
|
||
/// keys sit (by arc length), so editing it keeps them in place.
|
||
pub fn pin_spine(keys: &[Arc<Node>], spec: &BlendSpec) -> Option<(PathData, Vec<usize>)> {
|
||
let (mut path, anchors) = blend_spine(keys, spec)?;
|
||
if let Some(a) = anchors {
|
||
return Some((path, a));
|
||
}
|
||
let k = keys.len();
|
||
let total = Spine::new(&path)?.length();
|
||
let closed = path.subpaths.first().is_some_and(|s| s.closed);
|
||
let mut out = vec![0usize];
|
||
for i in 1..k.saturating_sub(1) {
|
||
let s = total * i as f64 / (k - 1) as f64;
|
||
out.push(anchor_at_length(&mut path, s)?);
|
||
}
|
||
let last = path.subpaths.first().map_or(0, |sp| if closed { 0 } else { sp.anchors.len().saturating_sub(1) });
|
||
out.push(last);
|
||
Some((path, out))
|
||
}
|
||
|
||
/// The anchor of the first subpath at arc length `s`: an existing one within a hundredth of a
|
||
/// point, else a new one splitting the segment there.
|
||
fn anchor_at_length(path: &mut PathData, s: f64) -> Option<usize> {
|
||
let spine = Spine::new(path)?;
|
||
let sp = path.subpaths.first_mut()?;
|
||
let n = sp.segment_count();
|
||
let seg = (0..n).rev().find(|i| spine.anchor_length(*i).is_some_and(|l| l <= s)).unwrap_or(0);
|
||
let from = spine.anchor_length(seg).unwrap_or(0.0);
|
||
let to = spine.anchor_length(seg + 1).unwrap_or(spine.length());
|
||
if s - from < 0.01 {
|
||
return Some(seg);
|
||
}
|
||
if to - s < 0.01 {
|
||
return Some((seg + 1) % sp.anchors.len().max(1));
|
||
}
|
||
let c = sp.segment(seg);
|
||
// The flattened length and the curve's own differ slightly: scale into the curve's.
|
||
let arc = c.arclen(1e-4).max(1e-12);
|
||
let t = c.inv_arclen((s - from) / (to - from).max(1e-12) * arc, 1e-4).clamp(1e-4, 1.0 - 1e-4);
|
||
Some(sp.insert_anchor(seg, t))
|
||
}
|
||
|
||
/// Where a blend's keys and steps go along its spine (see [`blend_spine`]).
|
||
struct Rail {
|
||
spine: Spine,
|
||
/// Arc length of each key.
|
||
stops: Vec<f64>,
|
||
centers: Vec<Point>,
|
||
rotate: bool,
|
||
}
|
||
|
||
impl Rail {
|
||
/// `None` when nothing moves: no stored spine and the steps keep the page's orientation.
|
||
fn new(keys: &[Arc<Node>], spec: &BlendSpec) -> Option<Self> {
|
||
let rotate = spec.orientation == BlendOrientation::AlignToPath;
|
||
if spec.spine.is_none() && !rotate {
|
||
return None;
|
||
}
|
||
let (path, anchors) = blend_spine(keys, spec)?;
|
||
let spine = Spine::new(&path)?;
|
||
let total = spine.length();
|
||
let k = keys.len();
|
||
let stops = match anchors {
|
||
Some(ka) => {
|
||
let mut v: Vec<f64> = Vec::with_capacity(k);
|
||
for a in ka {
|
||
let mut s = spine.anchor_length(a).unwrap_or(0.0);
|
||
// On a closed spine the key back at the start sits at its far end.
|
||
if spine.closed
|
||
&& let Some(prev) = v.last()
|
||
&& s <= *prev + 1e-9
|
||
{
|
||
s += total;
|
||
}
|
||
v.push(s);
|
||
}
|
||
v
|
||
}
|
||
None => (0..k).map(|i| total * i as f64 / (k - 1).max(1) as f64).collect(),
|
||
};
|
||
Some(Self { spine, stops, centers: keys.iter().map(|k| center_of(k)).collect(), rotate })
|
||
}
|
||
|
||
/// Arc length between keys `i` and `i + 1`.
|
||
fn len(&self, i: usize) -> f64 {
|
||
match (self.stops.get(i), self.stops.get(i + 1)) {
|
||
(Some(a), Some(b)) => (b - a).abs(),
|
||
_ => 0.0,
|
||
}
|
||
}
|
||
|
||
/// Move `n`, the object at `t` between keys `i` and `i + 1` (a key at `t` = 0), onto the
|
||
/// spine: by how far the spine there is from the straight line between the key centres (so
|
||
/// steps keep their own interpolated position on a straight spine), turned to the spine's
|
||
/// direction with Align to Path.
|
||
fn place(&self, n: &mut Node, i: usize, t: f64) {
|
||
let (Some(&s0), Some(&c0)) = (self.stops.get(i), self.centers.get(i)) else { return };
|
||
let (s, base) = match (self.stops.get(i + 1), self.centers.get(i + 1)) {
|
||
(Some(&s1), Some(&c1)) if t > 0.0 => (lerp_f64(s0, s1, t), c0.lerp(c1, t)),
|
||
_ => (s0, c0),
|
||
};
|
||
let (p, ang) = self.spine.at_length(if self.spine.closed { s.rem_euclid(self.spine.length().max(1e-12)) } else { s });
|
||
let off = p - base;
|
||
let mut m = if off.hypot() > 1e-9 { Affine::translate(off) } else { Affine::IDENTITY };
|
||
if self.rotate && ang.abs() > 1e-12 {
|
||
let c = (center_of(n) + off).to_vec2();
|
||
m = Affine::translate(c) * Affine::rotate(ang) * Affine::translate(-c) * m;
|
||
}
|
||
if m != Affine::IDENTITY {
|
||
n.transform(m, false);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A box round everything a blend of `keys` can paint once [`Rail::place`] puts its keys and steps
|
||
/// on its spine, turned to it with Align to Path: the spine's box grown by three times the farthest
|
||
/// any key paints from its centre. A step's shape lies within that distance of the point on the
|
||
/// spine it is moved to, and so does its own centre, which it turns about. `None` when nothing
|
||
/// moves.
|
||
pub(crate) fn rail_reach(keys: &[Arc<Node>], spec: &BlendSpec, symbols: Option<&crate::SymbolBoxes>) -> Option<Rect> {
|
||
if spec.spine.is_none() && spec.orientation != BlendOrientation::AlignToPath {
|
||
return None;
|
||
}
|
||
let spine = blend_spine(keys, spec)?.0.bounds()?;
|
||
let radius = keys
|
||
.iter()
|
||
.filter_map(|k| {
|
||
let (c, b) = (center_of(k), k.visual(symbols)?);
|
||
let corners = [Point::new(b.x0, b.y0), Point::new(b.x1, b.y0), Point::new(b.x0, b.y1), Point::new(b.x1, b.y1)];
|
||
Some(corners.into_iter().map(|q| (q - c).hypot()).fold(0.0, f64::max))
|
||
})
|
||
.fold(0.0, f64::max);
|
||
Some(spine.inflate(3.0 * radius, 3.0 * radius))
|
||
}
|
||
|
||
// =====================================================================================
|
||
// Blend evaluation
|
||
// =====================================================================================
|
||
|
||
/// Whether `n`, every part of it, paints opaquely with Normal blending and no opacity mask or
|
||
/// effect: knocking such objects out of each other changes nothing.
|
||
fn paints_opaquely(n: &Node) -> bool {
|
||
let paint = |p: &Paint| match p {
|
||
Paint::Gradient(g) => g.gradient.stops.iter().all(|s| s.opacity >= 1.0),
|
||
Paint::Pattern { .. } => false,
|
||
_ => true,
|
||
};
|
||
n.opacity >= 1.0
|
||
&& n.blend == vectorcraft_color::BlendMode::Normal
|
||
&& n.extras().mask.is_none()
|
||
&& n.appearance.effects.is_empty()
|
||
&& n.appearance
|
||
.items
|
||
.iter()
|
||
.all(|i| i.opacity() >= 1.0 && i.blend() == vectorcraft_color::BlendMode::Normal && i.effects().is_empty() && paint(i.paint()))
|
||
&& n.children().is_none_or(|c| c.iter().all(|c| paints_opaquely(c)))
|
||
}
|
||
|
||
/// Whether knocking out the children of `n` (a blend's evaluated steps) shows: some step is
|
||
/// translucent or blends.
|
||
pub fn knockout_shows(n: &Node) -> bool {
|
||
n.children().is_some_and(|c| !c.iter().all(|c| paints_opaquely(c)))
|
||
}
|
||
|
||
/// [`knockout_shows`] for evaluated steps.
|
||
pub fn steps_knockout_shows(steps: &[Arc<Node>]) -> bool {
|
||
!steps.iter().all(|c| paints_opaquely(c))
|
||
}
|
||
|
||
/// Most steps a blend makes in all, the steps of the blends inside its keys and steps included.
|
||
/// Each step of a blend whose keys are blends is a copy of one of them, which makes its own steps
|
||
/// when drawn, so nesting multiplies the step counts capped pair by pair: five levels of 15 steps
|
||
/// are 1.3 million steps from a 23 KB file, 3.4 GB to draw (#3579). Past it, a blend makes
|
||
/// proportionally fewer steps between each pair of keys ([`fit_steps`]).
|
||
pub const MAX_BLEND_STEPS: usize = 100_000;
|
||
|
||
/// The steps between each pair of keys, as the blend's spacing asks for them.
|
||
fn requested_steps(keys: &[Arc<Node>], spec: &BlendSpec, rail: Option<&Rail>) -> Vec<usize> {
|
||
keys.windows(2)
|
||
.enumerate()
|
||
.map(|(i, w)| match w {
|
||
[a, b] => blend_step_count(a, b, spec.spacing, rail.map_or_else(|| center_of(a).distance(center_of(b)), |r| r.len(i))),
|
||
_ => 0,
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Most steps `spacing` makes between keys `a` and `b`, whatever their size and distance
|
||
/// ([`blend_step_count`]'s clamps). A copy of a blend is moved and scaled, never recolored
|
||
/// ([`Shape::Other`]), so Smooth Color between colors that differ makes no more than their color
|
||
/// change asks for, whatever the copy's scale or rotation. By distance, a copy turned by Align to
|
||
/// Path can measure its edges differently, so it counts as up to 1000.
|
||
fn most_steps(a: &Node, b: &Node, spacing: BlendSpacing) -> usize {
|
||
match spacing {
|
||
BlendSpacing::Steps(n) => n.clamp(1, 1000) as usize,
|
||
BlendSpacing::Distance(_) => 1000,
|
||
BlendSpacing::SmoothColor => match color_distance(a, b) {
|
||
Some(d) if d * 255.0 > 1e-3 => level_steps(d) as usize,
|
||
_ => 1000,
|
||
},
|
||
}
|
||
}
|
||
|
||
/// `want` steps between each pair of keys, fewer where they would make more than
|
||
/// [`MAX_BLEND_STEPS`] in all with `keys` steps already made by the keys and `each[i]` made by
|
||
/// one step of pair `i` (itself and what the blends in it make): every pair keeps the same share
|
||
/// of its steps, rounded down, and none when the keys alone fill the budget.
|
||
fn fit_steps(want: &[usize], each: &[usize], keys: usize) -> Vec<usize> {
|
||
let room = MAX_BLEND_STEPS.saturating_sub(keys) as u128;
|
||
let demand = want.iter().zip(each).map(|(&n, &c)| n as u128 * c as u128).fold(0u128, u128::saturating_add);
|
||
if demand <= room {
|
||
return want.to_vec();
|
||
}
|
||
// `demand` is more than `room`, so it isn't 0, and each share is below `n`.
|
||
want.iter().map(|&n| usize::try_from(n as u128 * room / demand).unwrap_or(0)).collect()
|
||
}
|
||
|
||
/// Counts the steps blends make in all ([`MAX_BLEND_STEPS`]), each object counted once by its
|
||
/// address while the count runs.
|
||
#[derive(Default)]
|
||
struct StepCount {
|
||
known: std::collections::HashMap<(usize, bool), usize>,
|
||
}
|
||
|
||
impl StepCount {
|
||
/// Steps the blends in `n` make in all when drawn, each keeping to [`MAX_BLEND_STEPS`].
|
||
/// `copy`: `n` is part of a blend's step, a copy moved and scaled into place. A blend spaced
|
||
/// by distance, or by Smooth Color, can make more steps scaled up, so a copy counts the most
|
||
/// its spacing can make between each pair ([`most_steps`]), an upper bound for any scale.
|
||
fn node(&mut self, n: &Node, copy: bool) -> usize {
|
||
let at = (std::ptr::from_ref(n) as usize, copy);
|
||
if let Some(&known) = self.known.get(&at) {
|
||
return known;
|
||
}
|
||
let count = match &n.kind {
|
||
NodeKind::Blend { children, spec } if copy => self.most(children, spec),
|
||
NodeKind::Blend { children, spec } => self.blend(children, spec, Rail::new(children, spec).as_ref()).1,
|
||
// Each instance draws the source again.
|
||
NodeKind::Repeat(r) => r.instances(None).saturating_mul(self.nodes(&r.source, copy)),
|
||
_ => self.nodes(n.children().map_or(&[], Vec::as_slice), copy),
|
||
};
|
||
self.known.insert(at, count);
|
||
count
|
||
}
|
||
|
||
fn nodes(&mut self, nodes: &[Arc<Node>], copy: bool) -> usize {
|
||
nodes.iter().map(|c| self.node(c, copy)).fold(0, usize::saturating_add)
|
||
}
|
||
|
||
/// What one step between keys `a` and `b` makes: itself and the steps of the blends in it.
|
||
/// Groups pair their children as [`Lerp::new`] does; anything else is a copy of the nearer key.
|
||
fn step(&mut self, a: &Node, b: &Node) -> usize {
|
||
let inner = match (&a.kind, &b.kind) {
|
||
(NodeKind::Group { children: ca, clip: k1 }, NodeKind::Group { children: cb, clip: k2 })
|
||
if k1 == k2 && !(*k1 && ca.len() != cb.len()) =>
|
||
{
|
||
(0..ca.len().max(cb.len()))
|
||
.map(|i| match (ca.get(i), cb.get(i)) {
|
||
(Some(x), Some(y)) => self.step(x, y).saturating_sub(1),
|
||
(Some(x), None) | (None, Some(x)) => self.node(x, true),
|
||
(None, None) => 0,
|
||
})
|
||
.fold(0, usize::saturating_add)
|
||
}
|
||
_ => self.node(a, true).max(self.node(b, true)),
|
||
};
|
||
inner.saturating_add(1)
|
||
}
|
||
|
||
/// What each step between each pair of `keys` makes ([`Self::step`]).
|
||
fn steps(&mut self, keys: &[Arc<Node>]) -> Vec<usize> {
|
||
keys.windows(2)
|
||
.map(|w| match w {
|
||
[a, b] => self.step(a, b),
|
||
_ => 1,
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// The steps between each pair of keys a blend makes ([`fit_steps`]), and the steps it makes
|
||
/// in all, its keys' included.
|
||
fn blend(&mut self, keys: &[Arc<Node>], spec: &BlendSpec, rail: Option<&Rail>) -> (Vec<usize>, usize) {
|
||
let inside = self.nodes(keys, false);
|
||
let each = self.steps(keys);
|
||
let kept = fit_steps(&requested_steps(keys, spec, rail), &each, inside);
|
||
let total = kept.iter().zip(&each).map(|(&n, &c)| n.saturating_mul(c)).fold(inside, usize::saturating_add);
|
||
(kept, total)
|
||
}
|
||
|
||
/// The most steps a copy of a blend makes in all, at any scale: what [`Self::blend`] counts
|
||
/// with [`most_steps`] between each pair, before rounding.
|
||
fn most(&mut self, keys: &[Arc<Node>], spec: &BlendSpec) -> usize {
|
||
let inside = self.nodes(keys, true);
|
||
let each = self.steps(keys);
|
||
let most = keys.windows(2).map(|w| match w {
|
||
[a, b] => most_steps(a, b, spec.spacing),
|
||
_ => 0,
|
||
});
|
||
let demand = each.iter().zip(most).map(|(&c, m)| c.saturating_mul(m)).fold(0, usize::saturating_add);
|
||
inside.saturating_add(demand.min(MAX_BLEND_STEPS.saturating_sub(inside)))
|
||
}
|
||
}
|
||
|
||
/// How many steps `keys` blended with `spec` make between each pair of keys: as many as the
|
||
/// spacing asks for, fewer when they and the steps of the blends nested in them would pass
|
||
/// [`MAX_BLEND_STEPS`] ([`blend_expand`] makes these).
|
||
pub fn blend_steps(keys: &[Arc<Node>], spec: &BlendSpec) -> Vec<usize> {
|
||
StepCount::default().blend(keys, spec, Rail::new(keys, spec).as_ref()).0
|
||
}
|
||
|
||
/// Evaluate a blend: the keys and generated steps in paint order. The steps keep to
|
||
/// [`MAX_BLEND_STEPS`] in all ([`blend_steps`]).
|
||
pub fn blend_expand(keys: &[Arc<Node>], spec: &BlendSpec) -> Vec<Node> {
|
||
let k = keys.len();
|
||
if k < 2 {
|
||
return keys.iter().map(|n| (**n).clone()).collect();
|
||
}
|
||
let rail = Rail::new(keys, spec);
|
||
let (counts, _) = StepCount::default().blend(keys, spec, rail.as_ref());
|
||
let mut out = Vec::new();
|
||
for (i, a) in keys.iter().enumerate() {
|
||
let mut key = (**a).clone();
|
||
if let Some(r) = &rail {
|
||
r.place(&mut key, i, 0.0);
|
||
}
|
||
out.push(key);
|
||
let Some(b) = keys.get(i + 1) else { break };
|
||
let n = counts.get(i).copied().unwrap_or(0);
|
||
let lerp = Lerp::new(a, b, (spec.start(i), spec.start(i + 1)));
|
||
for j in 1..=n {
|
||
// Even position of the step, then where the easing puts its shape and its colours.
|
||
let u = j as f64 / (n + 1) as f64;
|
||
let (t, tc) = (if rail.is_some() { spec.ease.apply(u) } else { u }, spec.color_position(u));
|
||
let mut s = lerp.at_eased(t, tc);
|
||
if let Some(r) = &rail {
|
||
r.place(&mut s, i, t);
|
||
}
|
||
out.push(s);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use vectorcraft_geom::{Rect, shapes};
|
||
|
||
#[test]
|
||
fn clicked_start_points_pair_up() {
|
||
let a = shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
|
||
let mut b = shapes::rectangle(Rect::new(100.0, 0.0, 110.0, 10.0));
|
||
b.subpaths[0].anchors.rotate_left(2);
|
||
// Starting both at their anchor 0 pairs each corner of `a` with the opposite one of `b`:
|
||
// the middle step shrinks to a point.
|
||
assert!(PathPair::new(&a, &b, (Some(0), Some(0))).at(0.5).bounds().unwrap().height() < 1e-6);
|
||
// Starting `b` at its anchor 2 (its first corner again) pairs like corners.
|
||
let r = PathPair::new(&a, &b, (Some(0), Some(2))).at(0.5).bounds().unwrap();
|
||
assert!((r.width() - 10.0).abs() < 1e-6 && (r.height() - 10.0).abs() < 1e-6, "{r:?}");
|
||
// An open path clicked at its end runs the other way.
|
||
let line = |x: f64| PathData::single(SubPath::polyline(&[Point::new(x, 0.0), Point::new(x + 10.0, 0.0)], false));
|
||
let m = PathPair::new(&line(0.0), &line(100.0), (None, Some(1))).at(0.5);
|
||
assert_eq!(m.subpaths[0].anchors[0].p, Point::new(55.0, 0.0));
|
||
}
|
||
|
||
fn key(x: f64) -> Arc<Node> {
|
||
Arc::new(Node::path(NodeId(1), vectorcraft_geom::shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), Appearance::default()))
|
||
}
|
||
|
||
#[test]
|
||
fn a_replaced_spine_gets_anchors_where_its_keys_sit() {
|
||
let keys = vec![key(0.0), key(100.0), key(200.0)];
|
||
let line = PathData::single(SubPath::polyline(&[Point::new(0.0, 50.0), Point::new(300.0, 50.0)], false));
|
||
let spec = BlendSpec { spine: Some(line), ..Default::default() };
|
||
let (path, anchors) = pin_spine(&keys, &spec).unwrap();
|
||
assert_eq!(anchors, vec![0, 1, 2]);
|
||
assert!((path.subpaths[0].anchors[1].p.x - 150.0).abs() < 1e-6, "the middle key sits halfway");
|
||
// Pinned, each key's anchor follows it.
|
||
let pinned = BlendSpec { spine: Some(path), key_anchors: vec![0, 1, 2], ..Default::default() };
|
||
let (moved, _) = blend_spine(&[key(0.0), key(100.0), key(300.0)], &pinned).unwrap();
|
||
assert_eq!(moved.subpaths[0].anchors[2].p, Point::new(305.0, 5.0));
|
||
}
|
||
|
||
#[test]
|
||
fn steps_follow_a_bent_spine() {
|
||
let keys = vec![key(0.0), key(100.0)];
|
||
// A spine through the key centres bent up through (55, -95).
|
||
let spine = PathData::single(SubPath::polyline(&[Point::new(5.0, 5.0), Point::new(55.0, -95.0), Point::new(105.0, 5.0)], false));
|
||
let spec = BlendSpec { spacing: BlendSpacing::Steps(1), spine: Some(spine), key_anchors: vec![0, 2], ..Default::default() };
|
||
let out = blend_expand(&keys, &spec);
|
||
let mid = out[1].geometric_bounds().unwrap().center();
|
||
assert!(mid.distance(Point::new(55.0, -95.0)) < 1e-6, "{mid:?}");
|
||
assert_eq!(out[0].geometric_bounds().unwrap().center(), Point::new(5.0, 5.0), "keys stay put");
|
||
}
|
||
|
||
#[test]
|
||
fn easing_keeps_the_ends_and_stays_monotone() {
|
||
use crate::live::{BlendEase, BlendEaseType as K};
|
||
for kind in [K::In, K::Out, K::InOut, K::OutIn] {
|
||
for ramp in [0.0, 0.5, 1.0, f64::NAN, f64::INFINITY] {
|
||
let e = BlendEase { kind, ramp };
|
||
assert_eq!((e.apply(0.0), e.apply(1.0)), (0.0, 1.0));
|
||
let ys: Vec<f64> = (0..=100).map(|i| e.apply(i as f64 / 100.0)).collect();
|
||
assert!(ys.windows(2).all(|w| w[0] <= w[1]));
|
||
assert_eq!(e.apply(f64::NAN), 0.0);
|
||
if ramp == 0.0 {
|
||
assert_eq!(e.apply(0.25), 0.25);
|
||
}
|
||
}
|
||
}
|
||
let e = |kind| BlendEase { kind, ramp: 1.0 };
|
||
assert!(e(K::In).apply(0.25) < 0.25);
|
||
assert!(e(K::Out).apply(0.25) > 0.25);
|
||
assert!(e(K::InOut).apply(0.25) < 0.25 && e(K::InOut).apply(0.75) > 0.75);
|
||
assert!(e(K::OutIn).apply(0.25) > 0.25 && e(K::OutIn).apply(0.75) < 0.75);
|
||
}
|
||
|
||
#[test]
|
||
fn colour_easing_moves_colours_apart_from_positions() {
|
||
let rect = |x: f64, c: Color| {
|
||
let mut n = Node::path(NodeId(1), shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), Appearance::default());
|
||
n.appearance = Appearance::basic(Paint::solid(c), Paint::None, 0.0).into();
|
||
Arc::new(n)
|
||
};
|
||
let keys = vec![rect(0.0, Color::BLACK), rect(100.0, Color::WHITE)];
|
||
let mid = |spec: &BlendSpec| {
|
||
let out = blend_expand(&keys, spec);
|
||
let n = &out[1];
|
||
(n.geometric_bounds().unwrap().x0, n.appearance.fill_paint())
|
||
};
|
||
// Position easing runs along a spine: Align to Path gives the blend one.
|
||
let base = BlendSpec { spacing: BlendSpacing::Steps(1), orientation: BlendOrientation::AlignToPath, ..Default::default() };
|
||
let (x0, p0) = mid(&base);
|
||
// Colour easing alone leaves the position and moves the colour.
|
||
let colour = BlendSpec { color_shift: 50.0, ..base.clone() };
|
||
let (x1, p1) = mid(&colour);
|
||
assert!((x0 - x1).abs() < 1e-9 && p0 != p1);
|
||
// Position easing alone leaves the colour and moves the position.
|
||
let pos = BlendSpec { ease: crate::live::BlendEase { kind: crate::live::BlendEaseType::In, ramp: 1.0 }, ..base.clone() };
|
||
let (x2, p2) = mid(&pos);
|
||
assert!((x0 - x2).abs() > 1e-3 && p0 == p2);
|
||
assert_eq!(mid(&pos).0, x2, "deterministic");
|
||
}
|
||
|
||
#[test]
|
||
fn affine_lerp_turns_the_short_way() {
|
||
let x = Affine::rotate(170f64.to_radians());
|
||
let y = Affine::rotate(-170f64.to_radians());
|
||
let m = lerp_affine(x, y, 0.5);
|
||
let ang = decompose(m)[2].to_degrees();
|
||
assert!((ang.abs() - 180.0).abs() < 1e-6, "{ang}");
|
||
let s = lerp_affine(Affine::scale(1.0), Affine::translate((10.0, 0.0)) * Affine::scale(3.0), 0.5);
|
||
assert!((s.as_coeffs()[0] - 2.0).abs() < 1e-9 && (s.as_coeffs()[4] - 5.0).abs() < 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn closed_paths_start_where_they_twist_least() {
|
||
let a = shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
|
||
let mut b = shapes::rectangle(Rect::new(100.0, 0.0, 110.0, 10.0));
|
||
b.subpaths[0].anchors.rotate_left(2);
|
||
let mid = lerp_path(&a, &b, 0.5);
|
||
let r = mid.bounds().unwrap();
|
||
assert!((r.width() - 10.0).abs() < 1e-6 && (r.height() - 10.0).abs() < 1e-6, "no twist: {r:?}");
|
||
// Clicked start points win over the automatic match.
|
||
let pair = PathPair::new(&a, &b, (Some(0), Some(0)));
|
||
assert!(pair.at(0.5).bounds().unwrap().height() < 1e-6, "the clicked anchors pair up");
|
||
}
|
||
|
||
#[test]
|
||
fn blend_between_tints_of_one_swatch_keeps_the_link() {
|
||
let tinted = |tint: f32| Paint::Solid { color: Color::cmyk(1.0, 0.7, 0.0, 0.4).tinted(tint), swatch: Some("Spot".into()), tint };
|
||
let Paint::Solid { swatch, tint, .. } = lerp_paint(&tinted(0.2), &tinted(1.0), 0.5) else { panic!() };
|
||
assert_eq!(swatch.as_deref(), Some("Spot"));
|
||
assert!((tint - 0.6).abs() < 1e-6, "{tint}");
|
||
let other = Paint::Solid { color: Color::BLACK, swatch: Some("Other".into()), tint: 1.0 };
|
||
assert!(matches!(lerp_paint(&tinted(0.2), &other, 0.5), Paint::Solid { swatch: None, .. }));
|
||
|
||
// Gradient stops on tints of one swatch stay linked too; a process solid unlinks them.
|
||
let spot_gradient = |tint: f32| {
|
||
let mut stop = GradientStop::new(0.0, Color::cmyk(1.0, 0.7, 0.0, 0.4).tinted(tint));
|
||
stop.swatch = Some("Spot".into());
|
||
stop.tint = tint;
|
||
let stops = vec![stop.clone(), GradientStop { offset: 1.0, ..stop }];
|
||
Paint::Gradient(Box::new(GradientPaint::new(Gradient { stops, ..Gradient::default() })))
|
||
};
|
||
let Paint::Gradient(m) = lerp_paint(&spot_gradient(0.2), &spot_gradient(1.0), 0.5) else { panic!() };
|
||
assert!(m.gradient.stops.iter().all(|s| s.swatch.as_deref() == Some("Spot") && (s.tint - 0.6).abs() < 1e-6), "{:?}", m.gradient.stops);
|
||
let Paint::Gradient(m) = lerp_paint(&tinted(0.2), &spot_gradient(1.0), 0.5) else { panic!() };
|
||
assert!(m.gradient.stops.iter().all(|s| s.swatch.as_deref() == Some("Spot") && (s.tint - 0.6).abs() < 1e-6), "{:?}", m.gradient.stops);
|
||
let Paint::Gradient(m) = lerp_paint(&Paint::solid(Color::BLACK), &spot_gradient(1.0), 0.5) else { panic!() };
|
||
assert!(m.gradient.stops.iter().all(|s| s.swatch.is_none()), "{:?}", m.gradient.stops);
|
||
}
|
||
|
||
#[test]
|
||
fn resampled_gradient_stops_keep_the_swatch_link() {
|
||
let spot = |offset: f32, tint: f32| {
|
||
let mut stop = GradientStop::new(offset, Color::cmyk(1.0, 0.7, 0.0, 0.4).tinted(tint));
|
||
stop.swatch = Some("Spot".into());
|
||
stop.tint = tint;
|
||
stop
|
||
};
|
||
let paint = |stops: Vec<GradientStop>| Paint::Gradient(Box::new(GradientPaint::new(Gradient { stops, ..Gradient::default() })));
|
||
let two = paint(vec![spot(0.0, 0.2), spot(1.0, 1.0)]);
|
||
let three = paint(vec![spot(0.0, 0.2), spot(0.5, 0.6), spot(1.0, 1.0)]);
|
||
let Paint::Gradient(m) = lerp_paint(&two, &three, 0.5) else { panic!() };
|
||
assert_eq!(m.gradient.stops.len(), 3);
|
||
assert!(m.gradient.stops.iter().all(|s| s.swatch.as_deref() == Some("Spot")), "{:?}", m.gradient.stops);
|
||
let tints: Vec<f32> = m.gradient.stops.iter().map(|s| s.tint).collect();
|
||
assert!((tints[1] - 0.6).abs() < 1e-6 && (tints[2] - 1.0).abs() < 1e-6, "{tints:?}");
|
||
|
||
// Spot on one side, process on the other: the stops are unlinked.
|
||
let mut plain = spot(0.5, 1.0);
|
||
plain.swatch = None;
|
||
let mixed = paint(vec![spot(0.0, 1.0), plain, spot(1.0, 1.0)]);
|
||
let Paint::Gradient(m) = lerp_paint(&two, &mixed, 0.5) else { panic!() };
|
||
assert!(m.gradient.stops.iter().any(|s| s.swatch.is_none()), "{:?}", m.gradient.stops);
|
||
}
|
||
|
||
#[test]
|
||
fn gradients_with_different_stop_counts_interpolate() {
|
||
let g = |stops: &[(f32, Color)]| {
|
||
let gradient = Gradient { stops: stops.iter().map(|(o, c)| GradientStop::new(*o, *c)).collect(), ..Gradient::default() };
|
||
Paint::Gradient(Box::new(GradientPaint::new(gradient)))
|
||
};
|
||
let a = g(&[(0.0, Color::BLACK), (1.0, Color::BLACK)]);
|
||
let b = g(&[(0.0, Color::WHITE), (0.5, Color::WHITE), (1.0, Color::WHITE)]);
|
||
let Paint::Gradient(m) = lerp_paint(&a, &b, 0.25) else { panic!() };
|
||
assert_eq!(m.gradient.stops.len(), 3);
|
||
assert!(m.gradient.stops.iter().all(|s| (s.color.to_rgb()[0] - 0.25).abs() < 1e-4), "{:?}", m.gradient.stops);
|
||
}
|
||
|
||
#[test]
|
||
fn dashes_and_profiles_interpolate() {
|
||
let d = lerp_dash(Some(&Dash { pattern: vec![10.0, 10.0], ..Default::default() }), None, 0.5).unwrap();
|
||
assert_eq!(d.pattern, vec![15.0, 5.0]);
|
||
let d =
|
||
lerp_dash(Some(&Dash { pattern: vec![4.0], ..Default::default() }), Some(&Dash { pattern: vec![2.0, 6.0], ..Default::default() }), 0.5);
|
||
assert_eq!(d.unwrap().pattern, vec![3.0, 5.0]);
|
||
let p = lerp_profile(Some(&WidthProfile::lens()), None, 0.5).unwrap();
|
||
assert_eq!(p.at(0.0), (0.5, 0.5));
|
||
assert_eq!(p.at(0.5), (1.0, 1.0));
|
||
}
|
||
|
||
#[test]
|
||
fn odd_dash_blends_to_solid_with_closed_gaps() {
|
||
let d = lerp_dash(Some(&Dash { pattern: vec![2.0, 3.0, 4.0], ..Default::default() }), None, 1.0).unwrap();
|
||
assert!(d.pattern.iter().skip(1).step_by(2).all(|g| *g == 0.0), "{:?}", d.pattern);
|
||
}
|
||
|
||
#[test]
|
||
fn copied_steps_get_new_members() {
|
||
let rect = |x: f64| Arc::new(Node::path(NodeId(7), shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), Appearance::default()));
|
||
let a = Node::new(NodeId(1), NodeKind::Group { children: vec![rect(0.0), rect(20.0)], clip: true });
|
||
let b = Node::new(NodeId(2), NodeKind::Group { children: vec![rect(100.0)], clip: true });
|
||
let mid = lerp_node(&a, &b, 0.4);
|
||
let mut ids = vec![];
|
||
mid.walk(&mut |n| ids.push(n.id));
|
||
assert!(ids.iter().all(|i| *i == NodeId(0)), "{ids:?}");
|
||
}
|
||
|
||
/// A blend of `a` and `b` spaced by `spacing`.
|
||
fn blend_of(a: Node, b: Node, spacing: BlendSpacing) -> Node {
|
||
let spec = BlendSpec { spacing, ..Default::default() };
|
||
Node::new(NodeId(1), NodeKind::Blend { children: vec![Arc::new(a), Arc::new(b)], spec: Box::new(spec) })
|
||
}
|
||
|
||
/// A blend's keys and options.
|
||
fn blend_parts(n: &Node) -> Option<(&Vec<Arc<Node>>, &BlendSpec)> {
|
||
match &n.kind {
|
||
NodeKind::Blend { children, spec } => Some((children, spec)),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// A square of side `side` at (`x`, `y`).
|
||
fn square(x: f64, y: f64, side: f64) -> Node {
|
||
Node::path(NodeId(1), shapes::rectangle(Rect::new(x, y, x + side, y + side)), Appearance::default())
|
||
}
|
||
|
||
/// `levels` of blends of 15 steps whose keys are the blends a level down (#3579's input).
|
||
fn nested(levels: u32, x: f64) -> Node {
|
||
if levels == 0 {
|
||
return square(x, 0.0, 4.0);
|
||
}
|
||
let w = 40.0 * f64::from(levels);
|
||
blend_of(nested(levels - 1, x), nested(levels - 1, x + w), BlendSpacing::Steps(15))
|
||
}
|
||
|
||
/// The steps drawing `n` makes in all, each blend evaluated as the renderer does: one level at
|
||
/// a time, the blends in its keys and steps evaluated again.
|
||
fn drawn_steps(n: &Node) -> usize {
|
||
match &n.kind {
|
||
NodeKind::Blend { children, spec } => {
|
||
let out = blend_expand(children, spec);
|
||
out.len() - children.len() + out.iter().map(drawn_steps).sum::<usize>()
|
||
}
|
||
_ => n.children().map_or(0, |c| c.iter().map(|c| drawn_steps(c)).sum()),
|
||
}
|
||
}
|
||
|
||
/// Steps `levels` of [`nested`] blends of 15 steps make in all: each level its 15 steps,
|
||
/// and again the steps of the level below in each key and each step.
|
||
fn nested_steps(levels: u32) -> usize {
|
||
(17usize.pow(levels) - 1) * 15 / 16
|
||
}
|
||
|
||
/// #3579: blends whose keys are blends draw their steps again in every step above them. Five
|
||
/// levels of 15 steps made 1.3 million steps from a 23 KB file. Now a blend makes fewer steps
|
||
/// where they would pass `MAX_BLEND_STEPS`, and none where its keys already fill it, so each
|
||
/// level past that adds only what its keys draw.
|
||
#[test]
|
||
fn nested_blends_keep_to_the_step_budget() {
|
||
assert_eq!(nested_steps(5), 1_331_115);
|
||
let four = nested_steps(4);
|
||
assert!(four < MAX_BLEND_STEPS && 2 * four > MAX_BLEND_STEPS);
|
||
let five = nested(5, 0.0);
|
||
let (children, spec) = blend_parts(&five).unwrap();
|
||
assert_eq!(blend_steps(children, spec), vec![0]);
|
||
assert_eq!(drawn_steps(&five), 2 * four);
|
||
assert_eq!(drawn_steps(&nested(6, 0.0)), 4 * four);
|
||
// A blend whose keys leave room keeps the same share of its steps between each pair.
|
||
let mut three = nested(3, 0.0);
|
||
if let NodeKind::Blend { spec, .. } = &mut three.kind {
|
||
spec.spacing = BlendSpacing::Steps(1000);
|
||
}
|
||
let (children, spec) = blend_parts(&three).unwrap();
|
||
let inside = 2 * nested_steps(2);
|
||
let each = 1 + nested_steps(2);
|
||
let kept = (MAX_BLEND_STEPS - inside) * 1000 / (1000 * each);
|
||
assert_eq!(blend_steps(children, spec), vec![kept]);
|
||
let drawn = drawn_steps(&three);
|
||
assert_eq!(drawn, inside + kept * each);
|
||
assert!(drawn <= MAX_BLEND_STEPS && drawn > MAX_BLEND_STEPS - each, "{drawn}");
|
||
}
|
||
|
||
/// Nested blends within the budget make every step their spacing asks for, as before.
|
||
#[test]
|
||
fn nested_blends_within_the_budget_draw_every_step() {
|
||
for levels in 1..=4 {
|
||
let n = nested(levels, 0.0);
|
||
let (children, spec) = blend_parts(&n).unwrap();
|
||
assert_eq!(blend_steps(children, spec), vec![15]);
|
||
assert_eq!(drawn_steps(&n), nested_steps(levels), "{levels}");
|
||
}
|
||
// The keys, then 15 steps, then the last key, each a blend whose keys moved with it.
|
||
let two = nested(2, 0.0);
|
||
let (children, spec) = blend_parts(&two).unwrap();
|
||
let out = blend_expand(children, spec);
|
||
assert_eq!(out.len(), 17);
|
||
assert_eq!(out[0], *children[0]);
|
||
assert!(out.iter().all(|s| matches!(s.kind, NodeKind::Blend { .. })));
|
||
assert_eq!(out[8].geometric_bounds(), children[0].geometric_bounds().map(|b| b + Vec2::new(40.0, 0.0)));
|
||
}
|
||
|
||
/// A blend spaced by distance makes more steps scaled up, so a step's copy of one counts the
|
||
/// most its spacing can make. Here a tiny blend of one step is blended with a large blend of
|
||
/// one step: copies of the tiny one, scaled up toward the large one's size, make up to 1000
|
||
/// steps each, which neither key's own count shows.
|
||
#[test]
|
||
fn scaled_copies_of_blends_spaced_by_distance_keep_to_the_budget() {
|
||
let tiny = blend_of(square(0.0, 0.0, 1.0), square(2.0, 0.0, 1.0), BlendSpacing::Distance(1.0));
|
||
let large = blend_of(square(0.0, 100.0, 1000.0), square(2000.0, 100.0, 1000.0), BlendSpacing::Steps(1));
|
||
assert_eq!((drawn_steps(&tiny), drawn_steps(&large)), (1, 1));
|
||
let n = blend_of(tiny, large, BlendSpacing::Steps(1000));
|
||
let (children, spec) = blend_parts(&n).unwrap();
|
||
let steps = blend_steps(children, spec);
|
||
assert!(steps[0] < 1000, "{steps:?}");
|
||
let drawn = drawn_steps(&n);
|
||
assert!(drawn <= MAX_BLEND_STEPS && drawn > 1000, "{drawn}");
|
||
}
|
||
|
||
/// A flat blend of many keys shares the budget evenly between its pairs.
|
||
#[test]
|
||
fn many_keys_share_the_step_budget() {
|
||
let keys: Vec<Arc<Node>> = (0..300).map(|i| key(f64::from(i) * 20.0)).collect();
|
||
let spec = BlendSpec { spacing: BlendSpacing::Steps(1000), ..Default::default() };
|
||
let steps = blend_steps(&keys, &spec);
|
||
assert!(steps.iter().all(|&n| n == MAX_BLEND_STEPS * 1000 / (299 * 1000)), "{:?}", &steps[..3]);
|
||
assert_eq!(blend_expand(&keys, &spec).len(), 300 + steps.iter().sum::<usize>());
|
||
// Within the budget, every step stays.
|
||
assert_eq!(blend_steps(&keys[..50], &spec), vec![1000; 49]);
|
||
assert_eq!(fit_steps(&[10, 20], &[1, 1], MAX_BLEND_STEPS), vec![0, 0]);
|
||
}
|
||
|
||
/// A key for the Smooth Color tests: a rectangle of `size` (width, height) centered on `c`,
|
||
/// with `fill`, and `stroke` at `width`.
|
||
fn smooth_key(c: (f64, f64), size: (f64, f64), fill: Paint, stroke: Paint, width: f64) -> Node {
|
||
let ((cx, cy), (w, h)) = (c, size);
|
||
let rect = Rect::new(cx - w / 2.0, cy - h / 2.0, cx + w / 2.0, cy + h / 2.0);
|
||
Node::path(NodeId(1), shapes::rectangle(rect), Appearance::basic(fill, stroke, width))
|
||
}
|
||
|
||
/// Smooth Color's steps between two filled `size` rectangles centered on `a` and `b`.
|
||
fn smooth(a: (f64, f64), b: (f64, f64), size: (f64, f64), fill_a: Color, fill_b: Color) -> usize {
|
||
let key = |c, fill| smooth_key(c, size, Paint::solid(fill), Paint::None, 0.0);
|
||
blend_step_count(&key(a, fill_a), &key(b, fill_b), BlendSpacing::SmoothColor, a.0.max(b.0) - a.0.min(b.0))
|
||
}
|
||
|
||
/// Smooth Color counts 255 steps for a full change of the channel that changes most, a half
|
||
/// step rounding down: RGB values over 256, CMYK and gray in percent, a spot color by its
|
||
/// tint, colors in different spaces by their RGB values over 255, a fill or a stroke,
|
||
/// whichever changes more.
|
||
#[test]
|
||
fn smooth_color_counts_a_step_per_level_of_color_change() {
|
||
let (a, b, size) = ((50.0, 150.0), (250.0, 150.0), (20.0, 20.0));
|
||
let black = Color::rgb8(0, 0, 0);
|
||
for (level, steps) in [(1, 1), (2, 2), (5, 5), (64, 64), (127, 127), (128, 127), (129, 128), (200, 199), (254, 253), (255, 254)] {
|
||
assert_eq!(smooth(a, b, size, black, Color::rgb8(level, 0, 0)), steps, "black to red {level}");
|
||
assert_eq!(smooth(a, b, size, black, Color::rgb8(level, level, level)), steps, "black to gray {level}");
|
||
}
|
||
assert_eq!(smooth(a, b, size, Color::rgb8(255, 0, 0), Color::rgb8(0, 0, 255)), 254);
|
||
assert_eq!(smooth(a, b, size, black, Color::rgb8(64, 128, 0)), 127, "the channel that changes most");
|
||
assert_eq!(smooth(a, b, size, Color::rgb8(17, 42, 93), Color::rgb8(91, 147, 220)), 127);
|
||
for (percent, steps) in [(0.2, 1), (1.0, 3), (5.0, 13), (10.0, 25), (25.0, 64), (50.0, 127), (100.0, 254)] {
|
||
let p = percent / 100.0;
|
||
assert_eq!(smooth(a, b, size, Color::cmyk(0.0, 0.0, 0.0, 0.0), Color::cmyk(p, 0.0, 0.0, 0.0)), steps, "cyan {percent}%");
|
||
assert_eq!(smooth(a, b, size, Color::gray(0.0), Color::gray(p)), steps, "gray {percent}%");
|
||
}
|
||
let spot = |tint: f32| Paint::Solid { color: Color::rgb8(255, 0, 0).tinted(tint), swatch: Some("Spot".into()), tint };
|
||
let key = |c, fill| smooth_key(c, size, fill, Paint::None, 0.0);
|
||
assert_eq!(blend_step_count(&key(a, spot(1.0)), &key(b, spot(0.5)), BlendSpacing::SmoothColor, 200.0), 127, "a tint of 50%");
|
||
let stroked = |c, fill: Color, stroke: Color| smooth_key(c, size, Paint::solid(fill), Paint::solid(stroke), 1.0);
|
||
let (red, blue) = (Color::rgb8(255, 0, 0), Color::rgb8(0, 0, 255));
|
||
assert_eq!(blend_step_count(&stroked(a, red, black), &stroked(b, red, blue), BlendSpacing::SmoothColor, 200.0), 254, "the stroke changes");
|
||
let outline = |c, stroke: Color| smooth_key(c, size, Paint::None, Paint::solid(stroke), 1.0);
|
||
assert_eq!(blend_step_count(&outline(a, red), &outline(b, blue), BlendSpacing::SmoothColor, 200.0), 254, "strokes alone");
|
||
assert_eq!(smooth(a, b, size, Color::rgb8(255, 255, 127), Color::cmyk(0.0, 0.0, 0.0, 0.0)), 128, "RGB against CMYK white");
|
||
assert_eq!(smooth(a, b, size, Color::rgb8(255, 255, 127), Color::rgb8(255, 255, 255)), 127, "RGB against RGB white");
|
||
}
|
||
|
||
/// Smooth Color reads the colors of group members paired in order and of text runs, and
|
||
/// counts a solid paint against no paint by distance, since such a paint switches halfway
|
||
/// instead of mixing.
|
||
#[test]
|
||
fn smooth_color_reads_the_colors_inside_groups_and_text() {
|
||
let (red, blue) = (Color::rgb8(255, 0, 0), Color::rgb8(0, 0, 255));
|
||
let square = |x: f64, c: Color| Arc::new(smooth_key((x, 150.0), (20.0, 20.0), Paint::solid(c), Paint::None, 0.0));
|
||
let group = |x: f64, top: Color| Node::new(NodeId(1), NodeKind::Group { children: vec![square(x, red), square(x, top)], clip: false });
|
||
let steps = |a: &Node, b: &Node| blend_step_count(a, b, BlendSpacing::SmoothColor, 200.0);
|
||
assert_eq!(steps(&group(50.0, red), &group(250.0, blue)), 254, "groups whose members change color");
|
||
assert_eq!(steps(&group(50.0, red), &group(250.0, red)), 9, "groups of the same colors");
|
||
let text = |x: f64, c: Color| {
|
||
let style = crate::CharStyle { fill: Paint::solid(c), ..Default::default() };
|
||
Node::new(NodeId(1), NodeKind::Text(Box::new(crate::TextObject::point(Point::new(x, 150.0), "Hi", style))))
|
||
};
|
||
assert_eq!(steps(&text(50.0, red), &text(250.0, blue)), 254, "text whose runs change color");
|
||
let mut two_runs = text(250.0, red);
|
||
if let NodeKind::Text(t) = &mut two_runs.kind {
|
||
let style = crate::CharStyle { fill: Paint::solid(blue), ..t.runs[0].style.clone() };
|
||
t.runs.push(crate::text::TextRun::new("Hi", style));
|
||
}
|
||
assert_eq!(steps(&text(50.0, red), &two_runs), 254, "a run that turns blue against one red run");
|
||
let empty = |x: f64| smooth_key((x, 150.0), (20.0, 20.0), Paint::None, Paint::None, 0.0);
|
||
assert_eq!(steps(&smooth_key((50.0, 150.0), (20.0, 20.0), Paint::solid(red), Paint::None, 0.0), &empty(250.0)), 9, "a fill against none");
|
||
let nothing = |c: Color| Node::path(NodeId(1), PathData::new(vec![]), Appearance::basic(Paint::solid(c), Paint::None, 0.0));
|
||
assert_eq!(steps(¬hing(red), ¬hing(blue)), 254, "keys without bounds whose colors differ");
|
||
assert_eq!(steps(¬hing(red), ¬hing(red)), 1, "keys without bounds of the same color");
|
||
}
|
||
|
||
/// Smooth Color makes at most eight steps per point of the largest of the keys' widths and
|
||
/// heights and the distance between their centers along either axis.
|
||
#[test]
|
||
fn smooth_color_makes_at_most_eight_steps_a_point() {
|
||
let (red, blue) = (Color::rgb8(255, 0, 0), Color::rgb8(0, 0, 255));
|
||
for (dx, steps) in [(0.0, 160), (20.0, 160), (20.1, 161), (25.0, 200), (31.5, 252), (40.0, 254)] {
|
||
assert_eq!(smooth((50.0, 150.0), (50.0 + dx, 150.0), (20.0, 20.0), red, blue), steps, "{dx} pt apart");
|
||
}
|
||
assert_eq!(smooth((100.0, 150.0), (100.0, 175.0), (20.0, 20.0), red, blue), 200, "25 pt apart vertically");
|
||
assert_eq!(smooth((100.0, 150.0), (100.0, 150.0), (5.0, 5.0), red, blue), 40, "5 pt squares on each other");
|
||
assert_eq!(smooth((100.0, 150.0), (100.0, 150.0), (25.0, 5.0), red, blue), 200, "the longer side counts");
|
||
assert_eq!(smooth((100.0, 150.0), (100.0, 150.0), (1.0, 1.0), red, blue), 8);
|
||
}
|
||
|
||
/// Smooth Color between keys of the same colors counts the largest distance between their
|
||
/// corresponding visible edges in mean key widths or heights, rounded, less one; so do keys
|
||
/// with gradients, whatever their colors.
|
||
#[test]
|
||
fn smooth_color_between_the_same_colors_counts_by_distance() {
|
||
let red = Color::rgb8(255, 0, 0);
|
||
let same = |a, b, size| smooth(a, b, size, red, red);
|
||
for (dx, steps) in [(25.0, 1), (50.0, 2), (100.0, 4), (200.0, 9), (400.0, 19), (800.0, 39)] {
|
||
assert_eq!(same((50.0, 150.0), (50.0 + dx, 150.0), (20.0, 20.0)), steps, "20 pt squares {dx} pt apart");
|
||
}
|
||
for (side, steps) in [(5.0, 39), (10.0, 19), (40.0, 4), (80.0, 2)] {
|
||
assert_eq!(same((50.0, 150.0), (250.0, 150.0), (side, side)), steps, "{side} pt squares");
|
||
}
|
||
assert_eq!(same((50.0, 150.0), (250.0, 150.0), (80.0, 10.0)), 2, "wide");
|
||
assert_eq!(same((50.0, 150.0), (250.0, 150.0), (10.0, 80.0)), 19, "tall");
|
||
assert_eq!(same((50.0, 250.0), (250.0, 50.0), (20.0, 20.0)), 9, "diagonal");
|
||
assert_eq!(same((50.0, 250.0), (250.0, 50.0), (80.0, 10.0)), 19, "wide and diagonal");
|
||
assert_eq!(same((50.0, 150.0), (1050.0, 150.0), (1.0, 1.0)), 999);
|
||
assert_eq!(same((50.0, 150.0), (1052.0, 150.0), (1.0, 1.0)), 1000, "at most 1000");
|
||
assert_eq!(same((50.0, 150.0), (50.0, 150.0), (20.0, 20.0)), 1, "at least one");
|
||
assert_eq!(same((50.0, 150.0), (250.0, 150.0), (0.0, 20.0)), 9, "vertical lines, measured in their height");
|
||
assert_eq!(same((50.0, 150.0), (50.0, 350.0), (20.0, 0.0)), 9, "horizontal lines, measured in their width");
|
||
assert_eq!(same((50.0, 150.0), (60.0, 150.0), (0.0, 0.0)), 9, "points, measured in points");
|
||
let key = |c, size, stroke: f64| smooth_key(c, size, Paint::solid(red), Paint::solid(Color::BLACK), stroke);
|
||
let wide =
|
||
blend_step_count(&key((50.0, 150.0), (10.0, 20.0), 0.0), &key((250.0, 150.0), (80.0, 20.0), 0.0), BlendSpacing::SmoothColor, 200.0);
|
||
assert_eq!(wide, 4, "keys of different widths");
|
||
let strokes =
|
||
blend_step_count(&key((50.0, 150.0), (20.0, 20.0), 1.0), &key((250.0, 150.0), (20.0, 20.0), 10.0), BlendSpacing::SmoothColor, 200.0);
|
||
assert_eq!(strokes, 7, "strokes widen the visible edges");
|
||
let gradient = |a: Color, b: Color| {
|
||
Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(
|
||
Default::default(),
|
||
vec![GradientStop::new(0.0, a), GradientStop::new(1.0, b)],
|
||
))))
|
||
};
|
||
let shaded = |c, fill| smooth_key(c, (20.0, 20.0), fill, Paint::None, 0.0);
|
||
let blue = Color::rgb8(0, 0, 255);
|
||
for (from, to) in [(gradient(red, blue), gradient(red, blue)), (gradient(red, blue), gradient(blue, red))] {
|
||
assert_eq!(blend_step_count(&shaded((50.0, 150.0), from), &shaded((250.0, 150.0), to), BlendSpacing::SmoothColor, 200.0), 9);
|
||
}
|
||
}
|
||
|
||
/// A Smooth Color blend counts each pair of keys on its own.
|
||
#[test]
|
||
fn smooth_color_counts_each_pair_of_keys() {
|
||
let key = |x: f64, c: Color| Arc::new(smooth_key((x, 150.0), (20.0, 20.0), Paint::solid(c), Paint::None, 0.0));
|
||
let (red, blue) = (Color::rgb8(255, 0, 0), Color::rgb8(0, 0, 255));
|
||
let spec = BlendSpec::default();
|
||
assert_eq!(blend_steps(&[key(50.0, red), key(150.0, red), key(250.0, blue)], &spec), vec![4, 254]);
|
||
assert_eq!(blend_steps(&[key(50.0, red), key(150.0, blue), key(250.0, red)], &spec), vec![254, 254]);
|
||
}
|
||
|
||
/// A Smooth Color blend between different colors inside another blend's keys counts as the
|
||
/// steps its colors ask for, so the outer blend keeps the steps it asks for within
|
||
/// [`MAX_BLEND_STEPS`]: 200 steps, each a copy making 254 more. The copies keep their keys'
|
||
/// colors, also halfway between a red-to-blue and a blue-to-red blend.
|
||
#[test]
|
||
fn a_smooth_color_blend_in_a_key_counts_its_color_steps() {
|
||
let (red, blue) = (Color::rgb8(255, 0, 0), Color::rgb8(0, 0, 255));
|
||
let key = |x: f64, c: Color| smooth_key((x, 150.0), (20.0, 20.0), Paint::solid(c), Paint::None, 0.0);
|
||
let inner = |x: f64, from: Color, to: Color| blend_of(key(x, from), key(x + 100.0, to), BlendSpacing::SmoothColor);
|
||
let outer = blend_of(inner(0.0, red, blue), inner(500.0, red, blue), BlendSpacing::Steps(200));
|
||
let (keys, spec) = blend_parts(&outer).unwrap();
|
||
assert_eq!(blend_steps(keys, spec), vec![200]);
|
||
let crossed = blend_of(inner(0.0, red, blue), inner(500.0, blue, red), BlendSpacing::Steps(199));
|
||
let (keys, spec) = blend_parts(&crossed).unwrap();
|
||
assert_eq!(blend_steps(keys, spec), vec![199]);
|
||
let drawn = blend_expand(keys, spec);
|
||
let Some(NodeKind::Blend { children, .. }) = drawn.get(100).map(|n| &n.kind) else { panic!("a copy of a blend halfway") };
|
||
let fills: Vec<_> = children.iter().map(|k| k.appearance.fill_paint().color()).collect();
|
||
assert!(fills == [Some(red), Some(blue)] || fills == [Some(blue), Some(red)], "{fills:?}");
|
||
}
|
||
|
||
/// A Smooth Color blend between the same colors inside another blend's keys counts as up to
|
||
/// 1000 steps, which a copy turned to a path can ask for: 200 steps of copies of it come
|
||
/// down to 99.
|
||
#[test]
|
||
fn a_smooth_color_blend_of_the_same_colors_in_a_key_counts_up_to_1000_steps() {
|
||
let red = Color::rgb8(255, 0, 0);
|
||
let key = |x: f64| smooth_key((x, 150.0), (20.0, 20.0), Paint::solid(red), Paint::None, 0.0);
|
||
let inner = |x: f64| blend_of(key(x), key(x + 100.0), BlendSpacing::SmoothColor);
|
||
let outer = blend_of(inner(0.0), inner(500.0), BlendSpacing::Steps(200));
|
||
let (keys, spec) = blend_parts(&outer).unwrap();
|
||
assert_eq!(blend_steps(keys, spec), vec![99]);
|
||
}
|
||
}
|