Gradients: the Gradient panel's Method menu and paint.editGradient's method choose Classic or Perceptual

Illustrator 30.0's Method choice in the Gradient panel
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html).

- paint.editGradient takes `method: classic|perceptual` (one undo step);
  paint.setFill/setStroke take it in `gradient`, and gradient params (swatches,
  copied paints) carry it when Perceptual. Anything else is refused without
  editing. One edit can set `method` and `dither` together.
- The Gradient panel shows a Method menu (Classic, Perceptual) for linear and
  radial gradients, dimmed without a gradient; the Dither box stays under the
  stops.
- i18n: "Perceptual" where a complete catalog lacked it, and the `method`
  error in all 24 catalogs.
- Docs: G13 (estimate 14–24 h → 13–23 h, after Dither's 15–25 h → 14–24 h),
  target-app-parity (row and evidence), ui-parity, mcp.md, plugins.md.
This commit is contained in:
Taha-Mahmoodi
2026-10-23 01:53:30 +07:00
committed by bflatastic
parent 2bad646821
commit 6beb68f934
34 changed files with 194 additions and 13 deletions
+14 -2
View File
@@ -1,7 +1,7 @@
//! The Gradient panel and the Gradient tool: in-place gradient edits and the gradient vector.
use serde_json::{Value, json};
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientMethod, GradientPaint, GradientStop, Paint};
use vectorcraft_doc::appearance::stroke_paint_bounds;
use vectorcraft_doc::{Document, Node, NodeKind, StrokeGradientMode};
use vectorcraft_geom::{Affine, Point, Rect};
@@ -20,7 +20,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Gradient",
["Window", "Gradient"],
None,
"{stroke?: bool (default: the targeted item's kind, else the active proxy), kind?: linear|radial|freeform (freeform places points on each object, coloured along the stops), mode?: points|lines (freeform: how the Gradient tool adds points), stops?: [{offset 0..1, color? (needed without swatch), opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: colour swatch name (a global or spot colour or tint swatch links the stop, so swatch edits recolour it and a spot stop prints on its plate; a process colour just gives its colour), tint?: 0..100 (% of the linked swatch; default 100, or a tint swatch's own)}] (at least 2; a freeform gradient's points are recoloured along them), keepModel?: bool (as paint.setFill; a stop color the gradient already has keeps its model), angle?: deg, aspect?: %, reverse?: bool, dither?: bool (fills only: a linear or radial gradient fill is drawn with fine noise that hides banding, and as pixels in SVG and PDF exports), item?: fill/stroke item index|null (omitted: the Appearance panel's active item when it is of the edited kind), ids?|id?, strokeMode?: within|along|across (strokes only: the gradient lies on the page and shows through the stroke, runs from the start of each subpath to its end, or runs from the stroke's left edge to its right all along it; with nothing selected, for the next object drawn; type characters' own strokes always paint within), target?: \"object\"|\"contents\"} edit the gradient in place (keeps its placement); solid/none paints become the default gradient",
"{stroke?: bool (default: the targeted item's kind, else the active proxy), kind?: linear|radial|freeform (freeform places points on each object, coloured along the stops), mode?: points|lines (freeform: how the Gradient tool adds points), method?: classic|perceptual (how the colours between the stops mix: in display RGB, or perceptually (OKLab) so the steps look even, which SVG and PDF exports draw as an image; freeform gradients mix their own way), stops?: [{offset 0..1, color? (needed without swatch), opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: colour swatch name (a global or spot colour or tint swatch links the stop, so swatch edits recolour it and a spot stop prints on its plate; a process colour just gives its colour), tint?: 0..100 (% of the linked swatch; default 100, or a tint swatch's own)}] (at least 2; a freeform gradient's points are recoloured along them), keepModel?: bool (as paint.setFill; a stop color the gradient already has keeps its model), angle?: deg, aspect?: %, reverse?: bool, dither?: bool (fills only: a linear or radial gradient fill is drawn with fine noise that hides banding, and as pixels in SVG and PDF exports), item?: fill/stroke item index|null (omitted: the Appearance panel's active item when it is of the edited kind), ids?|id?, strokeMode?: within|along|across (strokes only: the gradient lies on the page and shows through the stroke, runs from the start of each subpath to its end, or runs from the stroke's left edge to its right all along it; with nothing selected, for the next object drawn; type characters' own strokes always paint within), target?: \"object\"|\"contents\"} edit the gradient in place (keeps its placement); solid/none paints become the default gradient",
has_doc,
edit_gradient
),
@@ -105,6 +105,14 @@ fn parse_kind(k: &str) -> Parsed<GradientKind> {
GradientKind::parse(k).ok_or_else(|| format!("unknown gradient kind `{k}` (linear, radial, freeform)"))
}
/// The `method` param of `p`, if given: how the colours between the stops mix.
fn method_param(p: &Value) -> Parsed<Option<GradientMethod>> {
match p.get("method") {
None | Some(Value::Null) => Ok(None),
Some(v) => v.as_str().and_then(GradientMethod::parse).map(Some).ok_or_else(|| "`method` must be classic or perceptual".into()),
}
}
/// `v` (params holding `stops`) with each stop that names a `swatch` resolved through `d`
/// ([`swatch_solid`]): a global or spot colour or a tint swatch gives the stop its colour, link and
/// tint, a process colour just its colour. A stop linked to a swatch that no longer exists keeps
@@ -229,6 +237,7 @@ pub(crate) fn parse_gradient(g: &Value) -> Parsed<GradientPaint> {
None | Some(Value::Null) => false,
Some(v) => v.as_bool().ok_or("`dither` must be true or false")?,
};
gp.gradient.method = method_param(g)?.unwrap_or_default();
gp.angle = f64_or(g, "angle", 0.0);
gp.swatch = str_param(g, "swatch").map(str::to_string);
let geom = GeomEdit::parse(g)?;
@@ -326,6 +335,9 @@ pub(crate) fn apply_gradient_edit_in(
if let Some(d) = p.get("dither").and_then(Value::as_bool) {
gp.gradient.dither = d;
}
if let Some(m) = method_param(p)? {
gp.gradient.method = m;
}
// Angle and aspect edits keep the focal point in its place in the extent ellipse.
let before = gp.geom;
if let Some(a) = p.get("angle").and_then(Value::as_f64) {
+1 -1
View File
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Fill",
[],
None,
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits), dither?: bool (linear and radial fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?|id?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear or radial `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only",
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}|{l,a,b} (CIE Lab), none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch is recorded as the gradient's swatch and fits each object, keeping its aspect; the built-in \"[Registration]\" prints on every plate), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, method?: classic|perceptual (as paint.editGradient; default classic), stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: global or spot colour (or tint) swatch the stop links to (its colour comes from the swatch), tint?: 0..100}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), focal?: [x,y] (radial, with start/end: the focal point, where the first stop sits), dither?: bool (linear and radial fills: drawn with fine noise that hides banding, and as pixels in SVG and PDF exports; strokes ignore it), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?|id?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray), target?: \"object\"|\"contents\", span?: \"object\" (default: each target fits a gradient to its own bounds)|\"selection\" (a linear or radial `gradient` without start/end is placed once on the union of the targets' bounds, so it runs across them)} sets the selection's fill and the default (new art fits a gradient to itself); a solid colour recolours only the gradient mesh point last clicked with the Mesh tool or Direct Selection; while the Type tool has characters selected (and no ids or item are given), the paint goes to those characters only",
has_doc,
|s, p| set_paint(s, p, true)
),
+57
View File
@@ -467,3 +467,60 @@ fn an_outlined_stroke_leaves_dither_behind() {
});
assert_eq!(fills, [false, false], "the fill and the outline, neither dithered");
}
/// 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.
#[test]
fn method_is_a_gradient_option_that_saves_and_loads() {
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": "#0000ff"}, {"offset": 1, "color": "#ffff00"}]);
s.execute("paint.setFill", &json!({"gradient": {"stops": stops}})).unwrap();
assert_eq!(fill_gradient(&s, id).gradient.method, GradientMethod::Classic, "classic by default");
let undo = s.doc().unwrap().history.undo.len();
s.execute("paint.editGradient", &json!({"method": "perceptual"})).unwrap();
let g = fill_gradient(&s, id);
assert_eq!(g.gradient.method, GradientMethod::Perceptual);
assert_eq!(g.gradient.stops.len(), 2, "the stops stay as they were");
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
// The params carry it, so a swatch and a copied paint keep it.
assert_eq!(vectorcraft_tools::params::gradient_params(&g)["method"], json!("perceptual"));
let mut q = gradient_param(&g);
q["name"] = json!("Even");
s.execute("swatch.new", &q).unwrap();
let Some(Paint::Gradient(sw)) = s.doc().unwrap().doc.swatch("Even").map(|w| w.paint.clone()) else { panic!("a gradient swatch") };
assert_eq!(sw.gradient.method, GradientMethod::Perceptual);
// 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, GradientMethod::Perceptual, "the native file keeps the method");
// Strokes take it too.
s.execute("paint.setStroke", &json!({"gradient": {"method": "perceptual"}})).unwrap();
let Paint::Gradient(st) = node(&s, id).appearance.stroke_paint() else { panic!("a gradient stroke") };
assert_eq!(st.gradient.method, GradientMethod::Perceptual);
s.execute("paint.editGradient", &json!({"method": "classic", "stroke": true})).unwrap();
let Paint::Gradient(st) = node(&s, id).appearance.stroke_paint() else { panic!("a gradient stroke") };
assert_eq!(st.gradient.method, GradientMethod::Classic);
// Bad values: refused, nothing changed.
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!(s.execute("paint.setFill", &json!({"gradient": {"method": bad}})).is_err());
}
assert_eq!(node(&s, id).appearance, before);
s.execute("paint.editGradient", &json!({"method": "Classic", "stroke": false})).unwrap();
assert_eq!(fill_gradient(&s, id).gradient.method, GradientMethod::Classic);
assert!(vectorcraft_tools::params::gradient_params(&fill_gradient(&s, id)).get("method").is_none());
// With dither: one edit sets both, and the params carry both back.
s.execute("paint.editGradient", &json!({"method": "perceptual", "dither": true, "stroke": false})).unwrap();
let g = fill_gradient(&s, id);
assert!(g.gradient.dither && g.gradient.method == GradientMethod::Perceptual);
let p = vectorcraft_tools::params::gradient_params(&g);
assert_eq!((&p["method"], &p["dither"]), (&json!("perceptual"), &json!(true)));
s.execute("paint.setFill", &json!({"gradient": p})).unwrap();
let back = fill_gradient(&s, id);
assert!(back.gradient.dither && back.gradient.method == GradientMethod::Perceptual);
}
+3
View File
@@ -72,6 +72,9 @@ pub fn gradient_params(g: &GradientPaint) -> Value {
if g.gradient.dither {
v["dither"] = json!(true);
}
if !g.gradient.method.is_classic() {
v["method"] = json!(g.gradient.method.label().to_lowercase());
}
v
}
+1
View File
@@ -5388,3 +5388,4 @@ axis Y Axis Y ос
@msg `dither` applies to gradient fills (stroke: false) `dither` се прилага към градиентни запълки (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` се прилага към градиентни запълки (paint.setFill)
@msg `dither` must be true or false `dither` трябва да бъде true или false
@msg `method` must be classic or perceptual `method` трябва да е classic или perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5433,3 +5433,4 @@ axis Y Axis osa Y
@msg `dither` applies to gradient fills (stroke: false) `dither` platí pro výplně přechodem (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` platí pro výplně přechodem (paint.setFill)
@msg `dither` must be true or false `dither` musí být true nebo false
@msg `method` must be classic or perceptual `method` musí být classic nebo perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5404,3 +5404,4 @@ axis Y Axis Y-akse
@msg `dither` applies to gradient fills (stroke: false) `dither` gælder for gradientfyld (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` gælder for gradientfyld (paint.setFill)
@msg `dither` must be true or false `dither` skal være true eller false
@msg `method` must be classic or perceptual `method` skal være classic eller perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5015,3 +5015,5 @@ axis Y Axis Y-Achse
@msg `dither` applies to gradient fills (stroke: false) `dither` gilt für Verlaufsflächen (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` gilt für Verlaufsflächen (paint.setFill)
@msg `dither` must be true or false `dither` muss true oder false sein
Perceptual Perzeptiv
@msg `method` must be classic or perceptual `method` muss classic oder perceptual sein
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5385,3 +5385,4 @@ axis Y Axis Άξονας Y
@msg `dither` applies to gradient fills (stroke: false) Το `dither` ισχύει για γεμίσματα ντεγκραντέ (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) Το `dither` ισχύει για γεμίσματα ντεγκραντέ (paint.setFill)
@msg `dither` must be true or false Το `dither` πρέπει να είναι true ή false
@msg `method` must be classic or perceptual Το `method` πρέπει να είναι classic ή perceptual
Can't render this file because it contains an unexpected character in line 2045 and column 19.
+1
View File
@@ -5375,3 +5375,4 @@ axis Y Axis Eje Y
@msg `dither` applies to gradient fills (stroke: false) `dither` se aplica a rellenos de degradado (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` se aplica a rellenos de degradado (paint.setFill)
@msg `dither` must be true or false `dither` debe ser true o false
@msg `method` must be classic or perceptual `method` debe ser classic o perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5464,3 +5464,4 @@ axis Y Axis Y-akseli
@msg `dither` applies to gradient fills (stroke: false) `dither` koskee liukuväritäyttöjä (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` koskee liukuväritäyttöjä (paint.setFill)
@msg `dither` must be true or false `dither` on oltava true tai false
@msg `method` must be classic or perceptual `method` on oltava classic tai perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5432,3 +5432,4 @@ axis Y Axis Axe Y
@msg `dither` applies to gradient fills (stroke: false) `dither` s’applique aux fonds en dégradé (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` s’applique aux fonds en dégradé (paint.setFill)
@msg `dither` must be true or false `dither` doit valoir true ou false
@msg `method` must be classic or perceptual `method` doit être classic ou perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5419,3 +5419,4 @@ axis Y Axis Y tengely
@msg `dither` applies to gradient fills (stroke: false) A `dither` színátmenetes kitöltésekre vonatkozik (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) A `dither` színátmenetes kitöltésekre vonatkozik (paint.setFill)
@msg `dither` must be true or false `dither`: true vagy false kell
@msg `method` must be classic or perceptual a `method` értéke classic vagy perceptual lehet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5404,3 +5404,4 @@ axis Y Axis Sumbu Y
@msg `dither` applies to gradient fills (stroke: false) `dither` berlaku untuk isian gradien (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` berlaku untuk isian gradien (paint.setFill)
@msg `dither` must be true or false `dither` harus true atau false
@msg `method` must be classic or perceptual `method` harus classic atau perceptual
Can't render this file because it contains an unexpected character in line 3816 and column 19.
+2
View File
@@ -5049,3 +5049,5 @@ axis Y Axis Asse Y
@msg `dither` applies to gradient fills (stroke: false) `dither` si applica ai riempimenti sfumati (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` si applica ai riempimenti sfumati (paint.setFill)
@msg `dither` must be true or false `dither` deve essere true o false
Perceptual Percettivo
@msg `method` must be classic or perceptual `method` deve essere classic o perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5111,3 +5111,5 @@ axis Y Axis Y 軸
@msg `dither` applies to gradient fills (stroke: false) `dither` はグラデーションの塗りに適用されます (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` はグラデーションの塗りに適用されます (paint.setFill)
@msg `dither` must be true or false `dither` には true または false を指定してください
Perceptual 知覚的
@msg `method` must be classic or perceptual `method` には classic または perceptual を指定してください
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5349,3 +5349,4 @@ axis Y Axis Y ღერძი
@msg `dither` applies to gradient fills (stroke: false) `dither` ვრცელდება გრადიენტულ შევსებებზე (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` ვრცელდება გრადიენტულ შევსებებზე (paint.setFill)
@msg `dither` must be true or false `dither` უნდა იყოს true ან false
@msg `method` must be classic or perceptual `method` უნდა იყოს classic ან perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5012,3 +5012,5 @@ axis Y Axis Y축
@msg `dither` applies to gradient fills (stroke: false) `dither`는 그레이디언트 칠에 적용됩니다 (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither`는 그레이디언트 칠에 적용됩니다 (paint.setFill)
@msg `dither` must be true or false `dither`은(는) true 또는 false여야 합니다
Perceptual 지각적
@msg `method` must be classic or perceptual `method`는 classic 또는 perceptual이어야 합니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5447,3 +5447,4 @@ axis Y Axis Y-as
@msg `dither` applies to gradient fills (stroke: false) `dither` geldt voor verloopvullingen (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` geldt voor verloopvullingen (paint.setFill)
@msg `dither` must be true or false `dither` moet true of false zijn
@msg `method` must be classic or perceptual `method` moet classic of perceptual zijn
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5285,3 +5285,4 @@ axis Y Axis Oś Y
@msg `dither` applies to gradient fills (stroke: false) `dither` dotyczy wypełnień gradientem (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` dotyczy wypełnień gradientem (paint.setFill)
@msg `dither` must be true or false `dither` musi mieć wartość true lub false
@msg `method` must be classic or perceptual `method` musi mieć wartość classic lub perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5350,3 +5350,4 @@ axis Y Axis Eixo Y
@msg `dither` applies to gradient fills (stroke: false) `dither` se aplica a preenchimentos em gradiente (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` se aplica a preenchimentos em gradiente (paint.setFill)
@msg `dither` must be true or false `dither` deve ser true ou false
@msg `method` must be classic or perceptual `method` deve ser classic ou perceptual
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -5045,3 +5045,5 @@ axis Y Axis Ось Y
@msg `dither` applies to gradient fills (stroke: false) `dither` применяется к градиентным заливкам (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` применяется к градиентным заливкам (paint.setFill)
@msg `dither` must be true or false `dither` должно быть true или false
Perceptual Перцепционный
@msg `method` must be classic or perceptual `method` должен быть classic или perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5389,3 +5389,4 @@ axis Y Axis Y-axel
@msg `dither` applies to gradient fills (stroke: false) `dither` gäller övertoningsfyllningar (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` gäller övertoningsfyllningar (paint.setFill)
@msg `dither` must be true or false `dither` måste vara true eller false
@msg `method` must be classic or perceptual `method` måste vara classic eller perceptual
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -5397,3 +5397,4 @@ axis Y Axis Y ekseni
@msg `dither` applies to gradient fills (stroke: false) `dither` degrade dolgulara uygulanır (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` degrade dolgulara uygulanır (paint.setFill)
@msg `dither` must be true or false "dither" doğru veya yanlış olmalıdır
@msg `method` must be classic or perceptual `method` classic veya perceptual olmalıdır
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -5029,3 +5029,5 @@ axis Y Axis Вісь Y
@msg `dither` applies to gradient fills (stroke: false) `dither` застосовується до градієнтних заливок (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` застосовується до градієнтних заливок (paint.setFill)
@msg `dither` must be true or false `dither` має бути true або false
Perceptual Перцептивний
@msg `method` must be classic or perceptual `method` має бути classic або perceptual
Can't render this file because it contains an unexpected character in line 733 and column 40.
+2
View File
@@ -5029,3 +5029,5 @@ axis Y Axis Trục Y
@msg `dither` applies to gradient fills (stroke: false) `dither` áp dụng cho vùng tô chuyển sắc (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` áp dụng cho vùng tô chuyển sắc (paint.setFill)
@msg `dither` must be true or false `dither` phải là true hoặc false
Perceptual Theo cảm nhận
@msg `method` must be classic or perceptual `method` phải là classic hoặc perceptual
Can't render this file because it contains an unexpected character in line 4028 and column 10.
+1
View File
@@ -5273,3 +5273,4 @@ axis Y Axis Y 轴
@msg `dither` applies to gradient fills (stroke: false) `dither` 适用于渐变填色 (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` 适用于渐变填色 (paint.setFill)
@msg `dither` must be true or false `dither` 必须是 true 或 false
@msg `method` must be classic or perceptual `method` 必须是 classic 或 perceptual
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5068,3 +5068,5 @@ axis Y Axis Y 軸
@msg `dither` applies to gradient fills (stroke: false) `dither` 適用於漸層填色 (stroke: false)
@msg `dither` applies to gradient fills (paint.setFill) `dither` 適用於漸層填色 (paint.setFill)
@msg `dither` must be true or false `dither` 必須是 true 或 false
Perceptual 感知
@msg `method` must be classic or perceptual `method` 必須是 classic 或 perceptual
Can't render this file because it contains an unexpected character in line 6 and column 47.
+39 -1
View File
@@ -23,7 +23,7 @@ use serde_json::{Value, json};
use vectorcraft_color::gradient::{
MIN_STOPS, duplicate_stop, insert_stop, midpoint_from_pos, midpoint_pos, move_stop, remove_stop, set_midpoint, swap_stop_colors,
};
use vectorcraft_color::{Color, Freeform, FreeformMode, Gradient, GradientKind, GradientPaint, GradientStop, Paint};
use vectorcraft_color::{Color, Freeform, FreeformMode, Gradient, GradientKind, GradientMethod, GradientPaint, GradientStop, Paint};
use vectorcraft_doc::{NodeId, StrokeGradientMode};
use super::{active_paint, live_run, pstate, set_pstate};
@@ -287,6 +287,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
freeform_section(app, ui, &g);
return;
}
method_row(app, ui, &g, is_grad);
// The proxy beside the angle and aspect ratio fields.
ui.horizontal(|ui| {
super::proxy(app, ui, 36.0);
@@ -334,6 +335,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// The Method row (Illustrator 30): how a linear or radial gradient mixes the colours between its
/// stops, Classic or Perceptual.
fn method_row(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad: bool) {
let methods = [(GradientMethod::Classic, tl!("Classic")), (GradientMethod::Perceptual, tl!("Perceptual"))];
let current = g.gradient.method;
let shown = methods.iter().find(|(m, _)| *m == current).map_or("", |(_, l)| *l);
ui.horizontal(|ui| {
widgets::dim_label(ui, tl!("Method:"));
let labels = methods.map(|(_, l)| l);
let chosen = ui.add_enabled_ui(is_grad, |ui| widgets::dropdown_names(ui, "grad-method", shown, &labels, 104.0)).inner;
if let Some((m, _)) = chosen.and_then(|i| methods.get(i))
&& *m != current
{
edit(app, json!({"method": m.label().to_lowercase()}), Live::Released);
}
});
}
/// The freeform points shown: the gradient's own, else (unplaced) as many as it places, coloured
/// along the stops.
pub(crate) fn shown_points(g: &GradientPaint) -> std::borrow::Cow<'_, Freeform> {
@@ -999,6 +1018,25 @@ mod tests {
assert!(!st.gradient.dither);
}
#[test]
fn the_method_menu_makes_the_gradient_perceptual() {
let mut app = app(json!({"gradient": {}}));
let mut p = Panel::new();
let texts = p.frame(&mut app, vec![], Modifiers::NONE);
assert!(texts.iter().any(|(t, _)| t == "Method:"), "{texts:?}");
let shown = texts.iter().find(|(t, _)| t == "Classic").expect("Classic shown").1;
let undo = app.session.doc().unwrap().history.undo.len();
let texts = p.click(&mut app, shown);
let option = texts.iter().find(|(t, _)| t == "Perceptual").expect("the menu lists Perceptual").1;
p.click(&mut app, option);
assert_eq!(fill(&app).gradient.method, GradientMethod::Perceptual);
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
let texts = p.frame(&mut app, vec![], Modifiers::NONE);
assert!(texts.iter().any(|(t, _)| t == "Perceptual"), "the menu shows the method: {texts:?}");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(fill(&app).gradient.method, GradientMethod::Classic);
}
#[test]
fn a_selected_diamond_shows_and_sets_its_midpoint_in_location() {
let stops = json!([{"offset": 0, "color": "#ffffff", "midpoint": 0.3}, {"offset": 1, "color": "#000000"}]);
+13 -4
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -790,6 +790,15 @@ SVG and PDF exports write such a fill as an image at the Document Raster Effects
{"name":"run_command","arguments":{"command":"paint.editGradient","params":{"dither":true}}}
```
Method (the Gradient panel's menu): `classic` (the default) mixes the colours between stops in display RGB,
`perceptual` mixes a linear or radial gradient's colours in OKLab so the steps look even. SVG and PDF exports write a
Perceptual gradient as an image at the Document Raster Effects resolution (a dithered Perceptual fill with its noise).
`paint.setFill`/`paint.setStroke` take it in `gradient` too:
```json
{"name":"run_command","arguments":{"command":"paint.editGradient","params":{"method":"perceptual"}}}
```
Freeform gradients: `paint.editGradient {kind: "freeform"}` places four or more colour points inside each selected
object (coloured along the stops; `mode: "points"|"lines"` is the Draw toggle). `paint.freeform.get` lists the points
(document coordinates), lines and the selected point; `paint.freeform.addPoint {at, color?, opacity?, spread?,
+1 -1
View File
@@ -198,7 +198,7 @@ type and `blob` but without their data, which follows the JSON in a saved file;
`paint`, `width` in points, `join` (`"miter"`, `"round"` or `"bevel"`), `cap` (`"butt"`, `"round"` or `"square"`)
and `miterLimit`. Paints use the
native file format's form: `{"type": "solid", "color", "swatch"?, "tint"?}`, `{"type": "gradient", "gradient":
{"kind": "Linear"|"Radial"|"Freeform", "stops": [{offset, color, opacity, midpoint}], "dither"?}, "geom"?, "angle",
{"kind": "Linear"|"Radial"|"Freeform", "stops": [{offset, color, opacity, midpoint}], "dither"?, "method"?: "Perceptual"}, "geom"?, "angle",
"freeform"?}`, `{"type": "pattern", "pattern", "xf"?}`. Colours are `{"model": "rgb", r, g, b}`, `{"model": "cmyk",
c, m, y, k}`, `{"model": "gray", k}` (ink: 0 white, 1 black) or `{"model": "lab", l, a, b}`; components 0–1 except
Lab's.
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long