mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 11:52:10 +00:00
Blend: Smooth Color counts a step per level of color change
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.
This commit is contained in:
+337
-61
@@ -699,6 +699,34 @@ fn compound(path: PathData, rule: FillRule, counts: &[usize], total: usize) -> N
|
||||
/// 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 }) =
|
||||
@@ -706,27 +734,10 @@ fn lerp_text(o: &mut TextObject, x: &TextObject, y: &TextObject, t: f64, tc: f64
|
||||
{
|
||||
*f = lerp_path(fx, fy, t);
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
let total: usize = o.runs.iter().map(|r| r.text.chars().count()).sum::<usize>().max(1);
|
||||
let mut start = 0usize;
|
||||
let tf = tc as f32;
|
||||
let mut resized = false;
|
||||
for r in &mut o.runs {
|
||||
let len = r.text.chars().count();
|
||||
let u = (start as f64 + len as f64 / 2.0) / total as f64;
|
||||
start += len;
|
||||
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);
|
||||
@@ -752,33 +763,115 @@ pub fn lerp_node(a: &Node, b: &Node, t: f64) -> Node {
|
||||
// Step counts
|
||||
// =====================================================================================
|
||||
|
||||
/// Largest channel difference (0..1) between the solid colours of two objects' top fill and stroke.
|
||||
fn color_distance(a: &Node, b: &Node) -> f32 {
|
||||
let mut d = 0.0f32;
|
||||
let mut cmp = |x: &Paint, y: &Paint| {
|
||||
let cols = |p: &Paint| -> Vec<Color> {
|
||||
match p {
|
||||
Paint::Solid { color, .. } => vec![*color],
|
||||
Paint::Gradient(g) => g.gradient.stops.iter().map(|s| s.color).collect(),
|
||||
_ => vec![],
|
||||
}
|
||||
};
|
||||
let (cx, cy) = (cols(x), cols(y));
|
||||
for (i, c) in cx.iter().enumerate() {
|
||||
if let Some(o) = cy.get(i).or(cy.first()) {
|
||||
let (p, q) = (c.to_rgb(), o.to_rgb());
|
||||
for k in 0..3 {
|
||||
d = d.max((p[k] - q[k]).abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 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;
|
||||
};
|
||||
cmp(&a.appearance.fill_paint(), &b.appearance.fill_paint());
|
||||
cmp(&a.appearance.stroke_paint(), &b.appearance.stroke_paint());
|
||||
d
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of intermediate steps between two keys `len` apart (along the spine).
|
||||
/// 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,
|
||||
@@ -787,17 +880,14 @@ pub fn blend_step_count(a: &Node, b: &Node, spacing: BlendSpacing, len: f64) ->
|
||||
((len / d).round() as i64 - 1).clamp(0, 1000) as usize
|
||||
}
|
||||
BlendSpacing::SmoothColor => {
|
||||
let cd = color_distance(a, b);
|
||||
if cd > 1.0 / 255.0 {
|
||||
((cd * 255.0 / 2.0).ceil() as usize).clamp(1, 256)
|
||||
} else {
|
||||
// Same colours: base the count on the distance between the objects.
|
||||
let (ba, bb) = (a.geometric_bounds(), b.geometric_bounds());
|
||||
let dist = match (ba, bb) {
|
||||
(Some(x), Some(y)) => (x.x0 - y.x0).abs().max((x.x1 - y.x1).abs()).max((x.y0 - y.y0).abs()).max((x.y1 - y.y1).abs()),
|
||||
_ => len,
|
||||
};
|
||||
((dist / 2.0).ceil() as usize).clamp(1, 256)
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1148,13 +1238,19 @@ fn requested_steps(keys: &[Arc<Node>], spec: &BlendSpec, rail: Option<&Rail>) ->
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Most steps `spacing` makes between two keys, whatever their size and distance
|
||||
/// ([`blend_step_count`]'s clamps).
|
||||
fn most_steps(spacing: BlendSpacing) -> usize {
|
||||
/// 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 => 256,
|
||||
BlendSpacing::SmoothColor => match color_distance(a, b) {
|
||||
Some(d) if d * 255.0 > 1e-3 => level_steps(d) as usize,
|
||||
_ => 1000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1182,8 +1278,8 @@ struct StepCount {
|
||||
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 distance where the colours match) makes more steps scaled up, so a
|
||||
/// copy counts the most its spacing can make between each pair, an upper bound for any scale.
|
||||
/// 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) {
|
||||
@@ -1249,7 +1345,11 @@ impl StepCount {
|
||||
fn most(&mut self, keys: &[Arc<Node>], spec: &BlendSpec) -> usize {
|
||||
let inside = self.nodes(keys, true);
|
||||
let each = self.steps(keys);
|
||||
let demand = each.iter().map(|&c| c.saturating_mul(most_steps(spec.spacing))).fold(0, usize::saturating_add);
|
||||
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)))
|
||||
}
|
||||
}
|
||||
@@ -1634,4 +1734,180 @@ mod tests {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,13 +237,13 @@ fn blend_smooth_colour_steps_from_colour_distance() {
|
||||
let mut s = session();
|
||||
let (_, _, g) = blend(&mut s, json!({}));
|
||||
let n = steps_of(&s, g).len() - 2;
|
||||
assert_eq!(n, 128, "black→white: one step per two levels");
|
||||
// Consecutive steps differ by at most ~2 levels.
|
||||
assert_eq!(n, 254, "black→white: one step per level");
|
||||
// Consecutive steps differ by about a level.
|
||||
let st = steps_of(&s, g);
|
||||
for w in st.windows(2) {
|
||||
let x = w[0].appearance.fill_paint().color().unwrap().to_rgb()[0];
|
||||
let y = w[1].appearance.fill_paint().color().unwrap().to_rgb()[0];
|
||||
assert!((x - y).abs() <= 2.5 / 255.0);
|
||||
assert!((x - y).abs() <= 1.5 / 255.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,9 @@ fn blend_smooth_colour_same_colours_uses_distance() {
|
||||
let b = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.blend.make", &json!({"smooth": true})).unwrap());
|
||||
assert_eq!(steps_of(&s, g).len() - 2, 20);
|
||||
// The edges are 40 pt apart, 3.6 times the keys' 11 pt visible width (their 1 pt strokes
|
||||
// included): rounded, less one.
|
||||
assert_eq!(steps_of(&s, g).len() - 2, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7240,19 +7240,25 @@ pub(super) fn blend_file(spine: &str, ids: [Option<&str>; 2], record: &str) -> S
|
||||
/// [`blend_file`] with the left key in RGB `from`, the right one in `to`, and the steps drawn
|
||||
/// between their colors.
|
||||
fn blend_file_colored(spine: &str, ids: [Option<&str>; 2], record: &str, from: [f64; 3], to: [f64; 3]) -> String {
|
||||
blend_file_apart(spine, ids, record, from, to, 100.0, 3)
|
||||
}
|
||||
|
||||
/// [`blend_file_colored`] with the right key `apart` points right of the left one and `between`
|
||||
/// steps drawn between them.
|
||||
fn blend_file_apart(spine: &str, ids: [Option<&str>; 2], record: &str, from: [f64; 3], to: [f64; 3], apart: f64, between: usize) -> String {
|
||||
let mut art = String::from("%_0 Ae\n%_u\n");
|
||||
art += &square(0.0, 40.0, 20.0, from, true);
|
||||
art += &blend_part("K", ids[0]);
|
||||
art += &square(100.0, 40.0, 20.0, to, true);
|
||||
art += &square(apart, 40.0, 20.0, to, true);
|
||||
art += &blend_part("K", ids[1]);
|
||||
art += spine;
|
||||
art += "%_N\n";
|
||||
art += &blend_part("S", None);
|
||||
art += "%_U\n%_9 () XW\nu\n";
|
||||
for i in 0..5 {
|
||||
let t = i as f64 / 4.0;
|
||||
for i in 0..between + 2 {
|
||||
let t = i as f64 / (between + 1) as f64;
|
||||
let rgb = [0, 1, 2].map(|c| from[c] + (to[c] - from[c]) * t);
|
||||
art += &square(t * 100.0, 40.0, 20.0, rgb, false);
|
||||
art += &square(t * apart, 40.0, 20.0, rgb, false);
|
||||
}
|
||||
art += "U\n9 () XW\n0 Ae\n";
|
||||
art += record;
|
||||
@@ -7328,7 +7334,7 @@ fn a_blends_spine_written_backwards_keeps_its_keys_order() {
|
||||
|
||||
/// A blend's spacing comes from its record: Specified Distance with its distance (25 pt from one
|
||||
/// step's edge to the corresponding edge of the next lays three out between keys 100 pt apart),
|
||||
/// Smooth Color (keys 5/255 apart in blue take three steps), and Specified Steps with the steps
|
||||
/// Smooth Color (keys 3/255 apart in blue take three steps), and Specified Steps with the steps
|
||||
/// counted from the drawn ones, whatever its record's word 4 holds. The distance counts along the
|
||||
/// spine between the keys, also where the spine runs on past the last key.
|
||||
#[test]
|
||||
@@ -7336,7 +7342,7 @@ fn a_blend_keeps_its_spacing() {
|
||||
use vectorcraft_doc::live::BlendSpacing;
|
||||
let straight = "%_10 50 m\n%_110 50 L\n";
|
||||
let longer = "%_10 50 m\n%_110 50 L\n%_1010 50 L\n";
|
||||
let (red, nearly_red, blue) = ([1.0, 0.0, 0.0], [1.0, 0.0, 5.0 / 255.0], [0.0, 0.0, 1.0]);
|
||||
let (red, nearly_red, blue) = ([1.0, 0.0, 0.0], [1.0, 0.0, 3.0 / 255.0], [0.0, 0.0, 1.0]);
|
||||
for (spine, record, (from, to), spacing) in [
|
||||
(straight, blend_record_spaced(&['A', 'B'], 2, 25.0, 3), (red, blue), BlendSpacing::Distance(25.0)),
|
||||
(longer, blend_record_spaced(&['A', 'B'], 2, 25.0, 3), (red, blue), BlendSpacing::Distance(25.0)),
|
||||
@@ -7352,6 +7358,23 @@ fn a_blend_keeps_its_spacing() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooth Color blends of two 20 pt squares 200 pt apart come in live with the steps drawn
|
||||
/// between them: 254 from red to blue, and 9 between two red squares.
|
||||
#[test]
|
||||
fn a_smooth_color_blend_comes_in_with_its_drawn_steps() {
|
||||
use vectorcraft_doc::live::BlendSpacing;
|
||||
let (red, blue) = ([1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
|
||||
for (to, between) in [(blue, 254), (red, 9)] {
|
||||
let record = blend_record_spaced(&['A', 'B'], 0, 4.0, between as u32);
|
||||
let body = blend_file_apart("%_10 50 m\n%_210 50 L\n", [Some("A"), Some("B")], &record, red, to, 200.0, between);
|
||||
let (blend, warnings) = read_blend(&body);
|
||||
let NodeKind::Blend { children, spec } = &blend.kind else { panic!("a live blend for {between} steps: {:?} {warnings:?}", blend.kind) };
|
||||
assert_eq!(spec.spacing, BlendSpacing::SmoothColor);
|
||||
assert_eq!(vectorcraft_doc::blend::blend_expand(children, spec).len(), between + 2);
|
||||
assert!(!warnings.iter().any(|w| w == super::blend::DRAWN), "{between}: {warnings:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A blend whose spacing doesn't lay out its drawn steps keeps them: a distance that lays out
|
||||
/// nine, and Smooth Color between red and blue (more steps than the three drawn); and so does a
|
||||
/// spacing the reader doesn't know or a distance that isn't a positive number.
|
||||
|
||||
+13
-1
@@ -6464,7 +6464,19 @@ shows, and so do the click-to-size dialogs (`perspective.draw` with `at`: the cl
|
||||
|
||||
`object.blend.make` blends the selected objects (or `ids`) into a live blend. Spacing and orientation default to
|
||||
what `object.blend.options` set with no blend selected (`object.blend.info` reads them: `target: "defaults"`),
|
||||
else Smooth Color and Align to Page. `starts` gives, per object, the anchor of its first subpath the blend starts
|
||||
else Smooth Color and Align to Page. Smooth Color counts the steps between each pair of keys on its own, from the
|
||||
keys' colors and visible bounds rather than along the spine. The counts below are VectorCraft's own values. The
|
||||
colors compared are each key's solid fill and stroke, the fills and strokes of two text objects' runs at the same fraction of their characters, and
|
||||
those of two groups' members paired in order. When they differ, 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,
|
||||
so pure red to pure blue makes 254 steps. CMYK and gray count by their percentages, two tints of the same swatch by
|
||||
their tints, and colors in different spaces (or Lab) by their RGB values over 255. The count is then at most eight
|
||||
per point of the largest of the keys' widths and heights and the distance between their centers along either axis,
|
||||
and when a key has no bounds it is the color count alone. 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 the keys' corresponding visible
|
||||
bounding-box edges divided by their mean width (left and right edges) or height (top and bottom), rounded, less one,
|
||||
from 1 to 1000. Keys with no width or no height are measured by their other side, keys with neither in points, and
|
||||
keys without bounds make 1 step. `starts` gives, per object, the anchor of its first subpath the blend starts
|
||||
from (as the Blend tool's clicks on anchor points do; an open path's last anchor runs it the other way). With the
|
||||
Blend tool, `press_key` Escape drops the object clicked first and stops the next click adding to the blend
|
||||
just made; the document and the selection stay as they are. A blend
|
||||
|
||||
@@ -1088,6 +1088,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own, and regions they bound merge without seams where the lines cross at rounded coordinates (#893); filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points is refused with advice rather than running out of memory, an image over 64 megapixels is traced scaled down to that size (aspect kept, paths at the image's own size) and one over 192 megapixels is refused, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image (a crop box on the image, starting on its part over the artboard, dragged by its handles or inside it, the outside dimmed, with X/Y/W/H, Apply and Cancel in the Control bar, Enter and Escape, #734; `object.cropImage {rect}` for agents, `{trim: true}` cuts an image to its pixels that aren't fully transparent, pixel for pixel) and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), View, the Image Trace panel and Expand, plus Release in Properties; the five View modes (Tracing Result, with Outlines, Outlines, Outlines with Source Image, Source Image) redraw a trace on screen without tracing again, are saved with it and `imageTrace.setView` sets them, while exports and Expand keep the result; the panel's Advanced section is a header that stays open or closed as it was left (part of #641); Color mode's Palette: Limited, Full Tone (as many colours as the tones need, Less … More), Automatic (flat art with its own colours, else Full Tone; a heuristic) and Document Library (the most used colours of the document's swatches or a swatch library, filled with the swatch colours as they are), part of #641; Advanced › Create: Fills and/or Strokes, where lines up to the Stroke width trace as stroked centre lines (distance transform, thinning, a skeleton graph with spurs pruned, curve fitting; one width per line; without Fills, wider areas become stroked outlines), part of #641), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). A single selected mirror repeat shows a prominent axis with a center move handle and two rotation handles. Drag the line or center to change spacing, or an end to change angle; Shift snaps rotation to 45°. The original artwork stays in place, the reflected copy previews live, Escape cancels and each drag takes one undo step through `object.repeat.options`. Controls follow zoom/view rotation and hide with edges or the bounding box. Engine and headless canvas regressions passed for gestures, history and visibility; native move, rotation and Escape screenshots were reviewed. [Public mirror-repeat reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-mirror-repeat-options.html). A single selected grid repeat shows a handle below the grid for rows, one right of it for columns and one past its bottom-right corner for both, and sliders between its first two columns and rows for the horizontal and vertical spacing; the source keeps its size, each drag is one undo step and Escape cancels it ([public grid-repeat reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html), #2691). Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756).
|
||||
Choosing the Blend tool again while it is active (its toolbar button, W or `tool.select`) finishes the blend: the object clicked first is dropped and the next click no longer adds to the blend just made, so the click after it starts a new blend ([Blending objects](https://web.archive.org/web/20241217212108/https://helpx.adobe.com/illustrator/using/blending-objects.html)).
|
||||
Window › Blend exposes easing Type, Ramp and independent color Shift through `object.blend.options`.
|
||||
Smooth Color counts the steps of each pair of keys from their color change, or from the distance between their visible bounding-box edges when their colors match or they hold gradients or patterns ([Blend Options](https://web.archive.org/web/20241217212108/https://helpx.adobe.com/illustrator/using/blending-objects.html)); the step counts, their caps and the colors compared are VectorCraft's own.
|
||||
Envelope Options › Anti-Alias off draws an envelope's distorted images without smoothing, and Preserve Shape Using › Transparency gives each piece of a distorted image an opacity mask in its shape instead of a clipping mask ([Envelope panel options](http://web.archive.org/web/20260513013456/https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/envelope-panel-options.html)).
|
||||
The four polynomial distributions are original approximations of the public description, not measured
|
||||
numeric parity. See [Blend panel overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/apply-transparency-and-blending/blend-panel-overview.html).
|
||||
|
||||
Reference in New Issue
Block a user