mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 18:16:01 +00:00
Merge pull request #7191 from dotCooCoo/feat/gradient-hue-blend
Gradients: Hue Clockwise and Hue Counterclockwise mix stops around the hue wheel
This commit is contained in:
@@ -57,6 +57,28 @@ fn type_gives_font_properties_and_gradients_css_gradients() {
|
||||
assert!(text.contains("background-image: linear-gradient(90deg, "), "{text}");
|
||||
}
|
||||
|
||||
/// CSS gradients mix linearly: a gradient mixed around the hue wheel is written through the stops
|
||||
/// on its way around the wheel, so red to blue lists green one way and magenta the other.
|
||||
#[test]
|
||||
fn gradients_mixed_around_the_hue_wheel_give_css_the_stops_on_their_way() {
|
||||
let mut s = session();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 0, "y": 100, "width": 100, "height": 50}));
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]);
|
||||
let gradient = |s: &mut Session, method: &str| {
|
||||
run(s, "paint.setFill", json!({"gradient": {"kind": "linear", "method": method, "stops": stops}}));
|
||||
let text = css(&run(s, "css.selection", json!({}))).to_string();
|
||||
text.lines().find(|l| l.contains("linear-gradient(")).unwrap_or_else(|| panic!("a gradient in {text}")).to_string()
|
||||
};
|
||||
let classic = gradient(&mut s, "classic");
|
||||
assert_eq!(classic.matches('%').count(), 2, "{classic}");
|
||||
for (method, middle, other) in [("hue-counterclockwise", "#00ff00 50%", "#ff00ff"), ("hue-clockwise", "#ff00ff 50%", "#00ff00")] {
|
||||
let line = gradient(&mut s, method);
|
||||
assert!(line.matches('%').count() > 2, "{method}: {line}");
|
||||
assert!(line.contains("#ff0000 0%") && line.contains("#0000ff 100%"), "{method}: {line}");
|
||||
assert!(line.contains(middle) && !line.contains(other), "{method}: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_covers_the_document_and_an_empty_selection_has_none() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -511,7 +511,7 @@ fn an_outlined_stroke_leaves_dither_behind() {
|
||||
|
||||
/// The Gradient panel's Method (Illustrator 30): `method` set and cleared through the gradient
|
||||
/// commands, on fills and strokes, kept by the params, swatches and a native save and load;
|
||||
/// anything but classic or perceptual is refused without editing.
|
||||
/// anything but the four method names is refused without editing.
|
||||
#[test]
|
||||
fn method_is_a_gradient_option_that_saves_and_loads() {
|
||||
use vectorcraft_color::GradientMethod;
|
||||
@@ -548,7 +548,7 @@ fn method_is_a_gradient_option_that_saves_and_loads() {
|
||||
let before = node(&s, id).appearance.clone();
|
||||
for bad in [json!("smooth"), json!(1), json!(true)] {
|
||||
let e = s.execute("paint.editGradient", &json!({"method": bad})).unwrap_err().to_string();
|
||||
assert!(e.contains("`method` must be classic or perceptual"), "{e}");
|
||||
assert!(e.contains("`method` must be classic, perceptual, hue-clockwise or hue-counterclockwise"), "{e}");
|
||||
assert!(s.execute("paint.setFill", &json!({"gradient": {"method": bad}})).is_err());
|
||||
}
|
||||
assert_eq!(node(&s, id).appearance, before);
|
||||
@@ -566,6 +566,76 @@ fn method_is_a_gradient_option_that_saves_and_loads() {
|
||||
assert!(back.gradient.dither && back.gradient.method == GradientMethod::Perceptual);
|
||||
}
|
||||
|
||||
/// The hue methods: `method` hue-clockwise and hue-counterclockwise set through the gradient
|
||||
/// commands on fills and strokes, as one undo step, kept by the params, a swatch, a saved preset
|
||||
/// and a native save and load, and written with or without the hyphen.
|
||||
#[test]
|
||||
fn the_hue_methods_are_set_saved_and_loaded() {
|
||||
use vectorcraft_color::GradientMethod;
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let stops = json!([{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]);
|
||||
s.execute("paint.setFill", &json!({"gradient": {"stops": stops}})).unwrap();
|
||||
for (name, method, middle) in
|
||||
[("hue-clockwise", GradientMethod::HueClockwise, "#ff00ff"), ("hue-counterclockwise", GradientMethod::HueCounterclockwise, "#00ff00")]
|
||||
{
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("paint.editGradient", &json!({"method": name})).unwrap();
|
||||
let g = fill_gradient(&s, id);
|
||||
assert_eq!(g.gradient.method, method);
|
||||
assert_eq!(g.gradient.stops.len(), 2, "{name}: the stops stay as they were");
|
||||
assert_eq!(g.gradient.sample(0.5).0.to_hex(), middle, "{name}: halfway around the wheel");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "{name}: one undo step");
|
||||
// The params carry it, so a swatch, a preset and a copied paint keep it.
|
||||
let p = vectorcraft_tools::params::gradient_params(&g);
|
||||
assert_eq!(p["method"], json!(name));
|
||||
let mut q = gradient_param(&g);
|
||||
q["name"] = json!(format!("Wheel {name}"));
|
||||
s.execute("swatch.new", &q).unwrap();
|
||||
let Some(Paint::Gradient(sw)) = s.doc().unwrap().doc.swatch(&format!("Wheel {name}")).map(|w| w.paint.clone()) else {
|
||||
panic!("a gradient swatch")
|
||||
};
|
||||
assert_eq!(sw.gradient.method, method, "{name}: the swatch");
|
||||
s.execute("gradient.presets.save", &json!({"name": format!("Preset {name}")})).unwrap();
|
||||
s.execute("paint.editGradient", &json!({"method": "classic"})).unwrap();
|
||||
s.execute("gradient.presets.apply", &json!({"name": format!("Preset {name}")})).unwrap();
|
||||
assert_eq!(fill_gradient(&s, id).gradient.method, method, "{name}: the preset");
|
||||
// Saved and opened again.
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let Paint::Gradient(saved) = back.node(id).unwrap().appearance.fill_paint() else { panic!("a gradient fill") };
|
||||
assert_eq!(saved.gradient.method, method, "{name}: the native file keeps the method");
|
||||
}
|
||||
// Strokes take it too, and the name reads without the hyphen and in any case.
|
||||
for (name, method) in [("hue-clockwise", GradientMethod::HueClockwise), ("hue-counterclockwise", GradientMethod::HueCounterclockwise)] {
|
||||
for written in [name.replace('-', " "), name.replace('-', "").to_uppercase()] {
|
||||
s.execute("paint.setStroke", &json!({"gradient": {"method": "classic"}})).unwrap();
|
||||
s.execute("paint.editGradient", &json!({"method": written, "stroke": true})).unwrap();
|
||||
let Paint::Gradient(st) = node(&s, id).appearance.stroke_paint() else { panic!("a gradient stroke") };
|
||||
assert_eq!(st.gradient.method, method, "{written}");
|
||||
}
|
||||
}
|
||||
// Reverse gives the same ramp the other way round: the hue method turns with the stops, so
|
||||
// the middle keeps its color, unless the edit names a method too.
|
||||
s.execute("paint.editGradient", &json!({"method": "hue-clockwise", "stroke": false})).unwrap();
|
||||
let before = fill_gradient(&s, id).gradient;
|
||||
s.execute("paint.editGradient", &json!({"reverse": true, "stroke": false})).unwrap();
|
||||
let after = fill_gradient(&s, id).gradient;
|
||||
assert_eq!(after.method, GradientMethod::HueCounterclockwise);
|
||||
assert_eq!((after.stops[0].color.to_hex(), after.stops[1].color.to_hex()), ("#0000ff".to_string(), "#ff0000".to_string()));
|
||||
for t in [0.25, 0.5, 0.75] {
|
||||
assert_eq!(after.sample(t).0.to_hex(), before.sample(1.0 - t).0.to_hex(), "reversed at {t}");
|
||||
}
|
||||
s.execute("paint.editGradient", &json!({"reverse": true, "method": "hue-counterclockwise", "stroke": false})).unwrap();
|
||||
assert_eq!(fill_gradient(&s, id).gradient.method, GradientMethod::HueCounterclockwise, "a method in the edit wins");
|
||||
// Half a name is refused, nothing changed.
|
||||
let before = node(&s, id).appearance.clone();
|
||||
for bad in ["hue", "clockwise", "hue-clock"] {
|
||||
let e = s.execute("paint.editGradient", &json!({"method": bad})).unwrap_err().to_string();
|
||||
assert!(e.contains("`method` must be classic, perceptual, hue-clockwise or hue-counterclockwise"), "{bad}: {e}");
|
||||
}
|
||||
assert_eq!(node(&s, id).appearance, before);
|
||||
}
|
||||
|
||||
/// A conic gradient through `paint.setFill`, the Gradient panel's edits, transforms, the params
|
||||
/// agents and swatches read, and the native format.
|
||||
#[test]
|
||||
|
||||
@@ -416,6 +416,34 @@ fn spot_gradients_drawn_as_images_warn() {
|
||||
assert!(!spot_warning(&warnings), "{warnings:?}");
|
||||
}
|
||||
|
||||
/// A spot gradient mixed around the hue wheel is an image in process colors with the warning,
|
||||
/// where the same stops mixed by Perceptual are a Separation or DeviceN shading: the colors
|
||||
/// between its stops are not tints or mixes of the stops' inks.
|
||||
#[test]
|
||||
fn hue_wheel_spot_gradients_export_as_images_and_warn() {
|
||||
let (mut s, _) = setup();
|
||||
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": {"c": 1, "m": 0, "y": 1, "k": 0}, "spot": true}));
|
||||
let spot_warning = |w: &[String]| w.iter().any(|w| w.contains("spot colors in gradients exported as images"));
|
||||
let ink_to_leaf = json!({"stops": [{"offset": 0, "swatch": "Ink"}, {"offset": 1, "swatch": "Leaf"}]});
|
||||
for method in ["hue-clockwise", "hue-counterclockwise"] {
|
||||
for (what, stops) in [("Ink → paper", ink_to_white(80.0)), ("Ink → Leaf", ink_to_leaf.clone())] {
|
||||
for kind in ["linear", "radial", "conic", "diamond"] {
|
||||
run(&mut s, "paint.editGradient", with(stops.clone(), json!({"method": method, "kind": kind})));
|
||||
let (pdf, warnings) = pdf_of(&s);
|
||||
assert!(pdf.contains("/Subtype/Image"), "{method} {kind} {what}: an image");
|
||||
assert!(!pdf.contains("/Separation") && !pdf.contains("/DeviceN"), "{method} {kind} {what}: no spot ink shading");
|
||||
assert!(spot_warning(&warnings), "{method} {kind} {what}: {warnings:?}");
|
||||
}
|
||||
}
|
||||
// Without spot inks: an image, without the warning.
|
||||
let red_blue = json!({"stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}], "kind": "linear"});
|
||||
run(&mut s, "paint.editGradient", with(red_blue, json!({"method": method})));
|
||||
let (pdf, warnings) = pdf_of(&s);
|
||||
assert!(pdf.contains("/Subtype/Image"), "{method}: a process gradient is an image");
|
||||
assert!(!spot_warning(&warnings), "{method}: {warnings:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeform_gradients_without_points_warn_about_spot_stops() {
|
||||
let (mut s, id) = setup();
|
||||
|
||||
Reference in New Issue
Block a user