Swatch libraries: Gradients › Organic Fades adds soft multi-stop fades that thin out to transparent

Window › Swatch Libraries › Gradients gains Organic Fades, a gradient
library of fifteen fades (ten linear, five radial). Each runs through
three to five unevenly spaced stops with off-center midpoints, mixes
with the Perceptual method, and thins out to transparent at the end of
a linear fade or at the edge of a radial one. The colors are computed
from VectorCraft's own OKLCH anchors.

The library also lists in the Swatches panel's library menu, in
swatch.library.list (category "gradients") and in the Gradient panel's
Presets. swatch.library.add keeps each fade's stops, opacities,
midpoints and method in RGB and CMYK documents.

The other built-in libraries are unchanged: their gradients still mix
with Classic and keep centered midpoints, and a test checks it.

- Gradient previews in the Swatch Libraries panel, the Gradient
  panel's Presets menu, swatch tiles and the toolbar drew every stop
  opaque. Each chip vertex now takes the sampled opacity as its alpha
  (freeform points too), and a chip with any translucent vertex draws
  a white and gray checkerboard under the gradient. Opaque gradients
  draw as before.
- When the preferences are applied, a saved gradient preset whose name
  matches a built-in preset, or an earlier saved preset, in any case,
  is renamed to the first free "<name> N" (N from 2), so a saved
  preset named like a new built-in fade stays listed and can be
  applied and deleted.
This commit is contained in:
dotCooCoo
2026-11-11 21:26:11 -08:00
parent aa4657e1ce
commit 1e70274189
36 changed files with 427 additions and 10 deletions
+1
View File
@@ -137,6 +137,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
- 2026-11-01: Group Overlapping Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects whose visual bounds overlap or touch, chains included, go into one group each, within their own layer or group, at the stacking place of their frontmost member; objects that overlap nothing stay as they are. One undo step, a sweep along x so thousands of loose pieces group quickly (at most 20000 objects). A VectorCraft addition: headline numbers unchanged.
- 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged.
- 2026-10-31: Arrange Tools, a built-in add-on (Object › Plug-ins › Arrange Tools): Arrange in Grid lays the selected objects out in rows and columns, a row or a column with gaps, from the selection's top-left or centred on the artboard; Swap Objects swaps two objects' places (and, with Swap Objects and Sizes, their sizes); Replace with Top Object puts a copy of the topmost selected object in place of each of the others, kept, stretched or fitted in proportion; Set Size of Each gives every selected object an exact width and/or height about a reference point; Zoom to Selection fits the view to the selection. Each is one undo step with a previewed dialog where it has options. A VectorCraft addition: headline numbers unchanged.
- 2026-11-11: Window › Swatch Libraries › Gradients › Organic Fades: fifteen soft fades (ten linear, five radial) through three to five unevenly spaced stops with off-center midpoints, mixed with the Perceptual method, that thin out to transparent at the end of a linear one and at the edge of a radial one; they also list in the Swatches panel's library menu, `swatch.library.list` and the Gradient panel's Presets.
- 2026-10-30: Gradient panel presets: a Presets menu applies one of the built-in gradient libraries' presets (grouped by theme) or a saved one to the gradient behind the active proxy, keeping its placement, as one undo step; the panel menu saves the current gradient as a preset, kept with the preferences, and deletes saved ones (`gradient.presets.list/apply/save/delete`).
- 2026-10-23: Pathfinder › Trap: the Pathfinder panel's Trap… and Effect › Pathfinder › Trap… add a thin overprinting strip of the lighter colour inside the darker one where two solid colours meet, so misregistered plates leave no gap ([trapping](https://helpx.adobe.com/illustrator/using/trapping.html)), with Thickness, Height/Width, Tint Reduction, Traps with Process Color and Reverse Traps. Illustrator effects 49/50 (SVG Filters remains); Live vector effects hours 6–10 → 4–8; headline numbers unchanged.
- 2026-10-23: OpenType SVG colour fonts draw as vector art: each glyph's SVG document is read by the SVG importer, in the text's colours where it asks for them, and draws, exports and outlines as COLR glyphs do; all colour font formats are now drawn.
+184 -1
View File
@@ -9,7 +9,7 @@ use std::sync::{Arc, OnceLock};
use serde::{Deserialize, Serialize};
use crate::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint, Swatch, SwatchGroup};
use crate::{Color, Gradient, GradientKind, GradientMethod, GradientPaint, GradientStop, Paint, Swatch, SwatchGroup};
/// A library: its name, its ungrouped swatches and its colour groups.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@@ -72,6 +72,7 @@ pub const GRADIENT_LIBRARIES: &[BuiltinLibrary] = &[
lib!("spectrum-gradients", "Spectrum Gradients", spectrum_gradients),
lib!("sky-gradients", "Sky Gradients", sky_gradients),
lib!("neutral-gradients", "Neutral Gradients", neutral_gradients),
lib!("organic-fades", "Organic Fades", organic_fades),
lib!("duotone-gradients", "Duotone Gradients", duotone_gradients),
lib!("radial-glows", "Radial Glows", radial_glows),
];
@@ -179,6 +180,14 @@ pub(crate) fn gradient(name: &str, kind: GradientKind, stops: &[(f32, Color, f32
Swatch { name: name.into(), paint: Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(kind, stops)))), global: false, spot: false }
}
/// A gradient swatch mixed by `method` through `stops` of (offset, color, opacity, midpoint).
fn gradient_with(name: &str, kind: GradientKind, method: GradientMethod, stops: &[(f32, Color, f32, f32)]) -> Swatch {
let stops =
stops.iter().map(|&(offset, color, opacity, midpoint)| GradientStop { opacity, midpoint, ..GradientStop::new(offset, color) }).collect();
let gradient = Gradient { method, ..Gradient::new(kind, stops) };
Swatch { name: name.into(), paint: Paint::Gradient(Box::new(GradientPaint::new(gradient))), global: false, spot: false }
}
/// `n` steps of OKLCH lightness from `l0` to `l1`, chroma `c(t)` and hue `h(t)` (`t` in 0..=1),
/// named "`name` 1"…
fn ramp(name: &str, n: usize, (l0, l1): (f64, f64), c: impl Fn(f64) -> f64, h: impl Fn(f64) -> f64) -> SwatchGroup {
@@ -387,6 +396,141 @@ fn radial_glows() -> (Vec<Swatch>, Vec<SwatchGroup>) {
(s, vec![])
}
/// A stop of [`organic_fades`]: offset, OKLCH `(l, c, h°)`, opacity and midpoint.
type FadeStop = (f32, (f64, f64, f64), f32, f32);
/// Soft fades through a few unevenly spaced stops with off-center midpoints, mixed perceptually,
/// that thin out to transparent: at the end of the linear ones and at the edge of the radial ones.
fn organic_fades() -> (Vec<Swatch>, Vec<SwatchGroup>) {
use GradientKind::{Linear, Radial};
let fades: [(&str, GradientKind, &[FadeStop]); 15] = [
(
"Mist",
Linear,
&[
(0.0, (0.94, 0.012, 240.0), 1.0, 0.38),
(0.35, (0.86, 0.03, 235.0), 0.85, 0.62),
(0.7, (0.8, 0.035, 230.0), 0.4, 0.45),
(1.0, (0.78, 0.035, 230.0), 0.0, 0.5),
],
),
(
"Petal",
Linear,
&[
(0.0, (0.82, 0.09, 350.0), 1.0, 0.42),
(0.28, (0.88, 0.07, 10.0), 0.9, 0.66),
(0.64, (0.93, 0.04, 30.0), 0.45, 0.35),
(1.0, (0.95, 0.03, 40.0), 0.0, 0.5),
],
),
("Fern", Linear, &[(0.0, (0.52, 0.1, 145.0), 1.0, 0.6), (0.4, (0.68, 0.11, 135.0), 0.75, 0.4), (1.0, (0.85, 0.08, 120.0), 0.0, 0.5)]),
(
"Smoke",
Linear,
&[
(0.0, (0.35, 0.01, 260.0), 1.0, 0.3),
(0.22, (0.48, 0.012, 255.0), 0.8, 0.58),
(0.58, (0.62, 0.01, 250.0), 0.35, 0.7),
(1.0, (0.72, 0.008, 250.0), 0.0, 0.5),
],
),
(
"Honey",
Linear,
&[
(0.0, (0.72, 0.15, 70.0), 1.0, 0.55),
(0.3, (0.8, 0.14, 80.0), 0.9, 0.35),
(0.75, (0.9, 0.09, 90.0), 0.3, 0.6),
(1.0, (0.95, 0.05, 95.0), 0.0, 0.5),
],
),
("Seafoam", Linear, &[(0.0, (0.7, 0.09, 175.0), 1.0, 0.45), (0.45, (0.84, 0.07, 170.0), 0.6, 0.65), (1.0, (0.95, 0.03, 165.0), 0.0, 0.5)]),
(
"Lavender",
Linear,
&[
(0.0, (0.62, 0.12, 300.0), 1.0, 0.36),
(0.3, (0.72, 0.1, 295.0), 0.85, 0.55),
(0.55, (0.82, 0.07, 290.0), 0.5, 0.68),
(0.82, (0.9, 0.04, 285.0), 0.15, 0.4),
(1.0, (0.93, 0.03, 285.0), 0.0, 0.5),
],
),
("Driftwood", Linear, &[(0.0, (0.55, 0.04, 60.0), 1.0, 0.64), (0.38, (0.68, 0.035, 70.0), 0.7, 0.42), (1.0, (0.84, 0.025, 80.0), 0.0, 0.5)]),
(
"Ink Wash",
Linear,
&[
(0.0, (0.25, 0.05, 265.0), 1.0, 0.28),
(0.18, (0.35, 0.06, 262.0), 0.9, 0.6),
(0.5, (0.5, 0.05, 258.0), 0.45, 0.72),
(1.0, (0.65, 0.04, 255.0), 0.0, 0.5),
],
),
(
"Tide",
Linear,
&[
(0.0, (0.45, 0.1, 230.0), 1.0, 0.4),
(0.26, (0.6, 0.1, 215.0), 0.8, 0.62),
(0.72, (0.76, 0.08, 200.0), 0.35, 0.44),
(1.0, (0.88, 0.05, 195.0), 0.0, 0.5),
],
),
(
"Bloom",
Radial,
&[
(0.0, (0.92, 0.05, 40.0), 1.0, 0.4),
(0.22, (0.8, 0.12, 20.0), 0.95, 0.6),
(0.6, (0.7, 0.14, 355.0), 0.4, 0.35),
(1.0, (0.66, 0.13, 345.0), 0.0, 0.5),
],
),
("Dew", Radial, &[(0.0, (0.97, 0.01, 200.0), 1.0, 0.3), (0.3, (0.88, 0.05, 195.0), 0.7, 0.58), (1.0, (0.8, 0.06, 190.0), 0.0, 0.5)]),
(
"Pollen",
Radial,
&[
(0.0, (0.96, 0.08, 100.0), 1.0, 0.45),
(0.18, (0.9, 0.15, 95.0), 1.0, 0.65),
(0.55, (0.82, 0.16, 85.0), 0.5, 0.4),
(1.0, (0.76, 0.15, 75.0), 0.0, 0.5),
],
),
// A ring: clear at the center, then a band that fades out to the edge.
(
"Halo",
Radial,
&[
(0.0, (0.98, 0.005, 90.0), 0.0, 0.6),
(0.42, (0.95, 0.02, 85.0), 0.0, 0.35),
(0.62, (0.92, 0.04, 80.0), 1.0, 0.55),
(1.0, (0.88, 0.05, 75.0), 0.0, 0.5),
],
),
(
"Lichen",
Radial,
&[
(0.0, (0.8, 0.07, 120.0), 1.0, 0.58),
(0.35, (0.7, 0.08, 130.0), 0.75, 0.4),
(0.74, (0.6, 0.07, 140.0), 0.25, 0.62),
(1.0, (0.55, 0.06, 145.0), 0.0, 0.5),
],
),
];
let s = fades
.iter()
.map(|&(name, kind, stops)| {
let stops: Vec<(f32, Color, f32, f32)> = stops.iter().map(|&(at, (l, c, h), opacity, mid)| (at, oklch(l, c, h), opacity, mid)).collect();
gradient_with(name, kind, GradientMethod::Perceptual, &stops)
})
.collect();
(s, vec![])
}
/// The sequential scales of [`perceptual`]: name and anchors of rising lightness.
const SEQUENTIAL: [(&str, Anchors); 5] = [
("Ember", &[(0.22, 0.10, 300.0), (0.55, 0.18, 30.0), (0.93, 0.15, 100.0)]),
@@ -532,6 +676,45 @@ mod tests {
let st = &g.gradient.stops;
assert!(st.len() >= 2 && st.windows(2).all(|w| w[0].offset <= w[1].offset), "{}", s.name);
assert!(st.iter().all(|s| (0.0..=1.0).contains(&s.offset) && (0.0..=1.0).contains(&s.opacity)));
assert!(st.iter().all(|s| (0.01..=0.99).contains(&s.midpoint)), "{}: a midpoint out of range", s.name);
}
}
}
}
#[test]
fn organic_fades_are_perceptual_multi_stop_fades_to_transparent() {
let lib = builtin_library("organic-fades").unwrap();
assert_eq!(GRADIENT_LIBRARIES.iter().map(|b| b.name).position(|n| n == "Organic Fades"), Some(3), "after Neutral Gradients");
assert!(lib.groups.is_empty() && (12..=16).contains(&lib.len()), "{} swatches", lib.len());
let radial = lib.iter().filter(|s| matches!(&s.paint, Paint::Gradient(g) if g.gradient.kind == GradientKind::Radial)).count();
assert!(radial >= 3 && radial * 2 <= lib.len(), "{radial} radial of {}", lib.len());
for s in lib.iter() {
let Paint::Gradient(g) = &s.paint else { panic!("{} is a gradient", s.name) };
let (g, n) = (&g.gradient, &s.name);
assert_eq!(g.method, GradientMethod::Perceptual, "{n}");
assert!((3..=5).contains(&g.stops.len()), "{n}: {} stops", g.stops.len());
assert!(g.stops.windows(2).all(|w| w[0].offset < w[1].offset), "{n}: rising offsets");
assert_eq!((g.stops[0].offset, g.stops[g.stops.len() - 1].offset), (0.0, 1.0), "{n}");
// Unevenly spaced: some gap differs from an even step.
let even = 1.0 / (g.stops.len() - 1) as f32;
assert!(g.stops.windows(2).any(|w| (w[1].offset - w[0].offset - even).abs() > 0.02), "{n}: uneven stops");
// Every segment's midpoint is off center and within the Gradient panel's 13 to 87%.
let segments = &g.stops[..g.stops.len() - 1];
assert!(segments.iter().all(|st| (st.midpoint - 0.5).abs() > 0.02 && (0.13..=0.87).contains(&st.midpoint)), "{n}: midpoints");
// Fully opaque somewhere, and clear at the end (the edge of a radial one).
assert!(g.stops.iter().any(|st| st.opacity == 1.0), "{n}: an opaque stop");
assert_eq!(g.stops[g.stops.len() - 1].opacity, 0.0, "{n}: clear at the end");
}
}
#[test]
fn the_other_libraries_keep_classic_mixing_and_centered_midpoints() {
for b in builtins().filter(|b| b.id != "organic-fades") {
for s in builtin_library(b.id).unwrap().iter() {
if let Paint::Gradient(g) = &s.paint {
assert_eq!(g.gradient.method, GradientMethod::Classic, "{}", s.name);
assert!(g.gradient.stops.iter().all(|st| st.midpoint == 0.5), "{}", s.name);
}
}
}
+34
View File
@@ -109,6 +109,40 @@ fn unlinked(g: &Gradient) -> Gradient {
g
}
/// Give each saved preset in `saved` a name no built-in preset or earlier saved one has (any case):
/// a clash becomes the first free "<name> N", N from 2. Returns the renames as (old, new).
pub(crate) fn rename_clashing_presets(saved: &mut [GradientPreset]) -> Vec<(String, String)> {
if saved.is_empty() {
return vec![];
}
let builtin: std::collections::HashSet<String> = builtin_presets().map(|e| e.name.to_ascii_lowercase()).collect();
let mut taken: std::collections::HashSet<String> =
builtin.iter().cloned().chain(saved.iter().map(|p| p.name.trim().to_ascii_lowercase())).collect();
let mut kept = std::collections::HashSet::new();
// The next suffix to try for each name, so many presets with one name take linear time.
let mut next: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
let mut renamed = vec![];
for p in saved {
let key = p.name.trim().to_ascii_lowercase();
if !builtin.contains(&key) && kept.insert(key.clone()) {
continue;
}
let base = p.name.trim();
let n = next.entry(key).or_insert(2);
let name = loop {
let candidate = format!("{base} {n}");
*n += 1;
if !taken.contains(&candidate.to_ascii_lowercase()) {
break candidate;
}
};
taken.insert(name.to_ascii_lowercase());
kept.insert(name.to_ascii_lowercase());
renamed.push((std::mem::replace(&mut p.name, name.clone()), name));
}
renamed
}
impl Session {
/// The Gradient panel's presets: the built-in ones by library, then the saved ones.
pub fn gradient_presets(&self) -> Vec<GradientPresetEntry> {
+4
View File
@@ -687,6 +687,10 @@ impl Session {
for e in self.prefs.keep_folders(&mut p) {
log::warn!("{e}");
}
// Saved gradient presets get names no built-in preset or other saved one has.
for (old, new) in super::gradpresets::rename_clashing_presets(&mut p.gradient_presets) {
log::warn!("saved gradient preset {old} is listed as {new}: another preset has its name");
}
// The edit the active tool computes on another thread is applied first (the Preferences
// dialog applies its settings here, outside a command). A failure to apply it was the
// edit's own, and the preferences apply all the same.
+52 -1
View File
@@ -38,7 +38,10 @@ fn built_in_presets_are_the_gradient_libraries_by_theme_with_unique_names() {
// Grouped by library, the gradient libraries first, then the swatch libraries holding gradients.
let mut libraries: Vec<&str> = rows.iter().map(|r| r["library"].as_str().unwrap()).collect();
libraries.dedup();
assert_eq!(libraries, ["Spectrum Gradients", "Sky Gradients", "Neutral Gradients", "Duotone Gradients", "Radial Glows", "Metallic Gradients"]);
assert_eq!(
libraries,
["Spectrum Gradients", "Sky Gradients", "Neutral Gradients", "Organic Fades", "Duotone Gradients", "Radial Glows", "Metallic Gradients"]
);
let mut seen = std::collections::HashSet::new();
assert!(names(&list).iter().all(|n| seen.insert(n.to_lowercase())), "a name names one preset");
let vignette = rows.iter().find(|r| r["name"] == "Vignette").unwrap();
@@ -65,6 +68,20 @@ fn applying_a_preset_edits_the_gradient_in_place_as_one_undo_step() {
assert_eq!(paint(&s, id, false), before);
}
#[test]
fn an_organic_fade_applies_with_its_method_midpoints_and_opacities() {
let (mut s, id) = session_with(json!({"color": "#336699"}));
let mist = s.gradient_preset("Mist").unwrap();
assert_eq!((mist.library, mist.gradient.method), (Some("Organic Fades"), GradientMethod::Perceptual));
s.execute("gradient.presets.apply", &json!({"name": "Mist"})).unwrap();
let g = paint(&s, id, false).gradient;
assert_eq!((g.kind, g.method, &g.stops), (GradientKind::Linear, GradientMethod::Perceptual, &mist.gradient.stops));
assert_eq!(s.gradient_preset_name(&g).as_deref(), Some("Mist"));
let list = s.execute("gradient.presets.list", &json!({})).unwrap();
let row = list["presets"].as_array().unwrap().iter().find(|r| r["name"] == "Mist").unwrap().clone();
assert_eq!(row["gradient"]["method"], "perceptual");
}
#[test]
fn a_preset_applies_to_the_stroke_and_to_a_solid_paint() {
let (mut s, id) = session_with(json!({"color": "#336699"}));
@@ -334,3 +351,37 @@ fn saved_presets_round_trip_with_the_preferences() {
fresh.apply_prefs(back);
assert!(fresh.gradient_preset("mine").is_some_and(|e| e.library.is_none()));
}
/// Preferences can hold saved presets named like a built-in preset (saved before it existed) or
/// like each other (edited by hand): applied, each gets a name of its own, any case.
#[test]
fn saved_presets_named_like_a_built_in_or_another_saved_one_get_a_free_name() {
let (mut s, id) = session_with(json!({"gradient": {"kind": "radial"}}));
let mist = s.gradient_preset("Mist").unwrap().gradient;
let mut own = paint(&s, id, false).gradient;
own.dither = true;
let preset = |name: &str| crate::cmd::GradientPreset { name: name.into(), gradient: own.clone() };
let mut prefs = s.prefs.clone();
prefs.gradient_presets =
vec![preset("mist"), preset("Mine"), preset("MINE"), preset("Mist 2"), preset("Plain"), preset("PLAIN"), preset("plain")];
s.apply_prefs(prefs);
let saved: Vec<&str> = s.prefs.gradient_presets.iter().map(|p| p.name.as_str()).collect();
assert_eq!(saved, ["mist 3", "Mine", "MINE 2", "Mist 2", "Plain", "PLAIN 2", "plain 3"]);
let list = s.execute("gradient.presets.list", &json!({})).unwrap();
let mut seen = std::collections::HashSet::new();
assert!(names(&list).iter().all(|n| seen.insert(n.to_lowercase())), "a name names one preset");
// The renamed one applies and deletes; the built-in Mist stays as it was.
s.execute("gradient.presets.apply", &json!({"name": "mist 3"})).unwrap();
assert_eq!(paint(&s, id, false).gradient, own);
assert_eq!(s.gradient_preset("Mist").unwrap().gradient, mist);
assert_eq!(s.execute("gradient.presets.delete", &json!({"name": "Mist 3"})).unwrap()["deleted"], "mist 3");
assert!(s.execute("gradient.presets.delete", &json!({"name": "Mist"})).is_err(), "built in");
assert_eq!(s.prefs.gradient_presets.len(), 6);
// Many presets with one name each get their own number.
let mut prefs = s.prefs.clone();
prefs.gradient_presets = (0..2000).map(|_| preset("Same")).collect();
s.apply_prefs(prefs);
let mut seen = std::collections::HashSet::new();
assert!(s.prefs.gradient_presets.iter().all(|p| seen.insert(p.name.to_lowercase())));
assert_eq!(s.prefs.gradient_presets.last().map(|p| p.name.as_str()), Some("Same 2000"));
}
+33
View File
@@ -638,6 +638,39 @@ fn gradient_libraries_are_listed_and_gradient_swatches_rename_and_take_new_gradi
assert_eq!(doc(&s).swatch("Red").unwrap().paint.color().map(|c| c.to_hex()), Some("#00ff00".into()));
}
#[test]
fn organic_fades_add_with_their_method_midpoints_and_opacities_in_rgb_and_cmyk() {
let lib = vectorcraft_color::libraries::builtin_library("organic-fades").unwrap();
for mode in ["rgb", "cmyk"] {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 100, "height": 100, "colorMode": mode}));
let list = run(&mut s, "swatch.library.list", json!({}));
let row = list["libraries"].as_array().unwrap().iter().find(|l| l["id"] == "organic-fades").unwrap().clone();
assert_eq!(
(row["name"].as_str(), row["category"].as_str(), row["count"].as_u64()),
(Some("Organic Fades"), Some("gradients"), Some(lib.len() as u64))
);
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].clone();
let r = run(&mut s, "swatch.library.add", json!({"library": "organic-fades", "names": ["Petal", "Dew"], "apply": "fill"}));
assert_eq!((r["added"].clone(), r["applied"].clone()), (json!(["Petal", "Dew"]), json!("Petal")), "{mode}");
for name in ["Petal", "Dew"] {
// The swatch comes as the library has it: stops (colors as given, opacities, midpoints) and method.
let Paint::Gradient(added) = &doc(&s).swatch(name).unwrap().paint else { panic!("{name} is a gradient") };
let Paint::Gradient(want) = &lib.swatch(name).unwrap().paint else { panic!("{name} is a gradient") };
assert_eq!(added.gradient, want.gradient, "{mode} {name}");
assert_eq!(added.gradient.method, vectorcraft_color::GradientMethod::Perceptual);
assert!(added.gradient.stops.iter().all(|st| matches!(st.color, Color::Rgb { .. })), "{mode} {name}: the stops stay RGB");
assert!(added.gradient.stops.iter().any(|st| st.opacity < 1.0 && (st.midpoint - 0.5).abs() > 0.02), "{mode} {name}");
}
// The fill takes the fade's method, opacities and midpoints.
let fill = doc(&s).node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().appearance.fill_paint();
let (Paint::Gradient(fill), Paint::Gradient(want)) = (fill, &lib.swatch("Petal").unwrap().paint) else { panic!("a gradient fill") };
assert_eq!(fill.gradient.method, want.gradient.method, "{mode}");
let shape = |g: &vectorcraft_color::Gradient| g.stops.iter().map(|st| (st.offset, st.opacity, st.midpoint)).collect::<Vec<_>>();
assert_eq!(shape(&fill.gradient), shape(&want.gradient), "{mode}");
}
}
#[test]
fn pattern_libraries_bring_their_patterns_and_add_each_once() {
let mut s = session();
+1
View File
@@ -2279,6 +2279,7 @@ font-technology Variable Променливи
Options for Selection… Опции за селектиране…
Options: Опции:
Options… Опции…
Organic Fades Органични преливания
or more spaces with a tab или повече интервала с табулация
Order: Ред:
Ordinals Поредни номера
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -814,6 +814,7 @@
Spectrum Gradients Gradijenti spektra
Sky Gradients Gradijenti neba
Neutral Gradients Neutralni gradijenti
Organic Fades Organski prelazi
Duotone Gradients Duoton gradijenti
Radial Glows Radijalni sjajevi
Save Swatch Library… Sačuvaj biblioteku uzoraka boja…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2913,6 +2913,7 @@ digits Malayalam Malaiàlam
Options for Selection… Opcions per a la selecció…
Options: Opcions:
Options… Opcions…
Organic Fades Esvaïments orgànics
or more spaces with a tab o més espais amb un tabulador
Orange Taronja
Order: Ordre:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -822,6 +822,7 @@
Spectrum Gradients Spektrální přechody
Sky Gradients Přechody oblohy
Neutral Gradients Neutrální přechody
Organic Fades Organické přechody
Duotone Gradients Dvoutónové přechody
Radial Glows Kruhové záře
Save Swatch Library… Uložit knihovnu vzorníku…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -760,6 +760,7 @@
Spectrum Gradients Spektrumgradienter
Sky Gradients Himmelgradienter
Neutral Gradients Neutrale gradienter
Organic Fades Organiske toninger
Duotone Gradients Duotonegradienter
Radial Glows Radiale gløder
Save Swatch Library… Gem bibliotek med farveprøver…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2306,6 +2306,7 @@ file-filter Templates Vorlagen
Options for Selection… Optionen für Auswahl…
Options: Optionen:
Options… Optionen…
Organic Fades Organische Verläufe
or more spaces with a tab oder mehr Leerzeichen durch einen Tabulator
Order: Reihenfolge:
Ordinals Ordinalzahlen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1840,6 +1840,7 @@ font-technology Variable Μεταβλητές
Options: Επιλογές:
Options… Επιλογές…
Ordinals Τακτικοί αριθμοί
Organic Fades Οργανικές διαβαθμίσεις
Orientation Προσανατολισμός
Orientation: Προσανατολισμός:
Original Πρωτότυπο
Can't render this file because it is too large.
+1
View File
@@ -2352,6 +2352,7 @@ font-technology Variable Variables
Options for Selection… Opciones de la selección…
Options: Opciones:
Options… Opciones…
Organic Fades Desvanecidos orgánicos
or more spaces with a tab o más espacios por una tabulación
Order: Orden:
Ordinals Ordinales
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -813,6 +813,7 @@
Spectrum Gradients Spektriliukuvärit
Sky Gradients Taivasliukuvärit
Neutral Gradients Neutraalit liukuvärit
Organic Fades Orgaaniset häivytykset
Duotone Gradients Kaksisävyliukuvärit
Radial Glows Säteittäiset hehkut
Save Swatch Library… Tallenna värimallikirjasto…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2344,6 +2344,7 @@ font-technology Variable Variables
Options for Selection… Options de la sélection…
Options: Options :
Options… Options…
Organic Fades Fondus organiques
or more spaces with a tab espaces ou plus par une tabulation
Order: Ordre :
Ordinals Ordinaux
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3170,6 +3170,7 @@ antiAlias Sharp शार्प
Options for Selection… सिलेक्शन के लिए ऑप्शन्स…
Options: ऑप्शन्स:
Options… ऑप्शन्स…
Organic Fades ऑर्गेनिक फ़ेड
or more spaces with a tab या एक टैब के साथ अधिक रिक्त स्थान
Orange नारंगी
Order: आदेश:
Can't render this file because it is too large.
+1
View File
@@ -3169,6 +3169,7 @@ mark-style Japanese Japán
Radial Blur Körkörös elmosás
Radial Blur… Körkörös elmosás…
Radial Glows Körkörös ragyogások
Organic Fades Organikus áttűnések
Radial Gradient Körkörös színátmenet
Conic Gradient Kúpos színátmenet
Radial Repeat Körkörös ismétlés
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -760,6 +760,7 @@
Spectrum Gradients Gradien spektrum
Sky Gradients Gradien langit
Neutral Gradients Gradien netral
Organic Fades Pudar organik
Duotone Gradients Gradien duotone
Radial Glows Pendar radial
Save Swatch Library… Simpan pustaka sampel warna…
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
760
761
762
763
764
765
766
+1
View File
@@ -2382,6 +2382,7 @@ file-filter Templates Modelli
Options for Selection… Opzioni per la selezione…
Options: Opzioni:
Options… Opzioni…
Organic Fades Dissolvenze organiche
or more spaces with a tab o più spazi con una tabulazione
Order: Ordine:
Ordinals Ordinali
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -846,6 +846,7 @@ digits Malayalam マラヤーラム数字
Spectrum Gradients スペクトルグラデーション
Sky Gradients 空のグラデーション
Neutral Gradients ニュートラルグラデーション
Organic Fades オーガニックフェード
Duotone Gradients ダブルトーングラデーション
Radial Glows 円形グロー
Save Swatch Library… スウォッチライブラリを保存…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -719,6 +719,7 @@
Spectrum Gradients სპექტრის გრადიენტები
Sky Gradients ცის გრადიენტები
Neutral Gradients ნეიტრალური გრადიენტები
Organic Fades ორგანული გაქრობები
Duotone Gradients ორტონიანი გრადიენტები
Radial Glows რადიალური ნათებები
Save Swatch Library… ფერის ნიმუშების ბიბლიოთეკის შენახვა…
Can't render this file because it is too large.
+1
View File
@@ -839,6 +839,7 @@ digits Malayalam 말라얄람어
Spectrum Gradients 스펙트럼 그레이디언트
Sky Gradients 하늘 그레이디언트
Neutral Gradients 중간색 그레이디언트
Organic Fades 유기적 페이드
Duotone Gradients 듀오톤 그레이디언트
Radial Glows 방사형 광선
Save Swatch Library… 견본 라이브러리 저장…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -774,6 +774,7 @@
Spectrum Gradients Spectrumverlopen
Sky Gradients Luchtverlopen
Neutral Gradients Neutrale verlopen
Organic Fades Organische vervagingen
Duotone Gradients Duotoonverlopen
Radial Glows Radiale gloed
Save Swatch Library… Staalenbibliotheek opslaan…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -769,6 +769,7 @@
Spectrum Gradients Gradienty widma
Sky Gradients Gradienty nieba
Neutral Gradients Gradienty neutralne
Organic Fades Organiczne zanikania
Duotone Gradients Gradienty dwutonowe
Radial Glows Blaski promieniste
Save Swatch Library… Zapisz bibliotekę próbek…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1949,6 +1949,7 @@ antiAlias Sharp Nítido
Options… Opções…
Order: Ordem:
Ordinals Ordinais
Organic Fades Esmaecimentos Orgânicos
Orientation Orientação
Orientation: Orientação:
Original Original
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2377,6 +2377,7 @@ file-filter Templates Шаблоны
Options for Selection… Параметры выделения…
Options: Параметры:
Options… Параметры…
Organic Fades Органические затухания
or more spaces with a tab или больше пробелов на табуляцию
Order: Порядок:
Ordinals Порядковые числительные
Can't render this file because it is too large.
+1
View File
@@ -2291,6 +2291,7 @@ font-technology Variable Variabla
Options for Selection… Alternativ för markering…
Options: Alternativ:
Options… Alternativ…
Organic Fades Organiska toningar
or more spaces with a tab eller fler blanksteg med en tabb
Order: Ordning:
Ordinals Ordningstal
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2163,6 +2163,7 @@ shape Open Açık
Options for Selection… Seçim Seçenekleri…
Options: Seçenekler:
Options… Seçenekler…
Organic Fades Organik Solmalar
or more spaces with a tab veya sekmeli daha fazla alan
Order: Sipariş:
Ordinals Sıra Sayıları
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2374,6 +2374,7 @@ file-filter Templates Шаблони
Options for Selection… Параметри виділення…
Options: Параметри:
Options… Параметри…
Organic Fades Органічні згасання
or more spaces with a tab або більше прогалин на табуляцію
Order: Порядок:
Ordinals Порядкові числівники
Can't render this file because it contains an unexpected character in line 182 and column 5.
+1
View File
@@ -2950,6 +2950,7 @@ mark-style Japanese Kiểu Nhật
Radial Blur… Mờ hướng tâm…
Radial Dividers: Đường chia hướng tâm:
Radial Glows Ánh sáng hướng tâm
Organic Fades Mờ dần tự nhiên
Radial Gradient Chuyển sắc hướng tâm
Conic Gradient Chuyển sắc hình nón
Radius Bán kính
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -2249,6 +2249,7 @@ file-filter Templates 模板
Options 选项
Options: 选项:
Options… 选项…
Organic Fades 自然渐隐
or more spaces with a tab 或多个空格加一个制表符
Order: 顺序:
Ordinals 序数字
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3340,6 +3340,7 @@ file-filter Templates 範本
Options for Selection… 選取範圍的選項…
Options: 選項:
Options… 選項…
Organic Fades 自然漸隱
or more spaces with a tab 個以上的空格取代為定位字元
Order: 順序:
Ordinals 序數字
Can't render this file because it contains an unexpected character in line 6 and column 47.
+62
View File
@@ -754,6 +754,68 @@ mod gradient_chip_tests {
assert_eq!(colors[0], Color32::BLACK);
}
/// A stop at opacity 0 is clear on the chip of every kind, and one at 50% half clear.
#[test]
fn chip_shows_the_stops_opacity() {
use vectorcraft_color::{Color, GradientStop};
for kind in [GradientKind::Linear, GradientKind::Radial, GradientKind::Conic] {
let mut gp = paint(kind, 0.0);
gp.gradient.stops = vec![
GradientStop { opacity: 0.5, ..GradientStop::new(0.0, Color::BLACK) },
GradientStop { opacity: 0.0, ..GradientStop::new(1.0, Color::BLACK) },
];
let chip = gradient_chip_colors(&gp, 20.0, 20.0, 4, 4);
let (lo, hi) = chip.iter().fold((u8::MAX, 0), |(lo, hi), c| (lo.min(c.a()), hi.max(c.a())));
// No grid point of a conic chip lands exactly at the end of its sweep.
assert!(lo <= if kind == GradientKind::Conic { 12 } else { 0 }, "{kind:?}: {chip:?}");
assert!(hi.abs_diff(128) <= 1, "{kind:?}: {chip:?}");
}
let at = |colors: &[Color32], i: usize, j: usize| colors[j * 5 + i];
let mut gp = paint(GradientKind::Linear, 0.0);
gp.gradient.stops[1].opacity = 0.0;
let chip = gradient_chip_colors(&gp, 20.0, 20.0, 4, 4);
assert_eq!(at(&chip, 0, 2), Color32::WHITE);
assert_eq!(at(&chip, 4, 2).a(), 0);
// Radial: an opaque edge around a clear center.
let mut gp = paint(GradientKind::Radial, 0.0);
gp.gradient.stops[0].opacity = 0.0;
let chip = gradient_chip_colors(&gp, 20.0, 20.0, 4, 4);
assert_eq!(at(&chip, 2, 2).a(), 0);
assert_eq!(chip[0], Color32::BLACK);
}
/// A chip with a clear stop draws a checkerboard under the gradient; an opaque one does not.
#[test]
fn a_clear_gradient_chip_has_a_checkerboard_under_it() {
let ctx = egui::Context::default();
let shapes = |gp: &GradientPaint| {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| gradient_chip(ui, Rect::from_min_size(pos2(10.0, 10.0), vec2(20.0, 20.0)), gp));
out.textures_delta.clear();
out.shapes.into_iter().map(|s| s.shape).collect::<Vec<_>>()
};
let opaque = shapes(&paint(GradientKind::Linear, 0.0));
assert!(matches!(opaque[..], [egui::Shape::Mesh(_)]), "{opaque:?}");
let mut gp = paint(GradientKind::Linear, 0.0);
gp.gradient.stops[1].opacity = 0.0;
let clear = shapes(&gp);
let n = clear.len();
assert!(n > 2 && matches!(clear[n - 1], egui::Shape::Mesh(_)), "{clear:?}");
let fills: std::collections::HashSet<_> =
clear[..n - 1].iter().filter_map(|s| if let egui::Shape::Rect(r) = s { Some(r.fill) } else { None }).collect();
assert_eq!(fills, [Color32::WHITE, Color32::from_gray(204)].into_iter().collect(), "{clear:?}");
}
#[test]
fn freeform_chip_shows_the_points_opacity() {
use vectorcraft_color::{Color, Freeform, FreeformPoint};
use vectorcraft_geom::kurbo::Point;
let mut gp = paint(GradientKind::Freeform, 0.0);
let clear = FreeformPoint { opacity: 0.0, ..FreeformPoint::new(Point::new(10.0, 10.0), Color::BLACK) };
gp.freeform = Some(Freeform { points: vec![clear], ..Default::default() });
let chip = gradient_chip_colors(&gp, 20.0, 20.0, 4, 4);
assert!(chip.iter().all(|c| c.a() == 0), "{chip:?}");
}
#[test]
fn freeform_chip_mixes_points_in_the_model_they_share() {
use vectorcraft_color::{Color, Freeform, FreeformPoint};
+29 -7
View File
@@ -39,6 +39,10 @@ pub fn gradient_chip(ui: &Ui, rect: Rect, gradient: &vectorcraft_color::Gradient
let cols = ((rect.width() / 3.0).ceil() as usize).clamp(1, max);
let rows = ((rect.height() / 3.0).ceil() as usize).clamp(1, max);
let colors = gradient_chip_colors(gradient, rect.width(), rect.height(), cols, rows);
// Stops below full opacity show a checkerboard through them.
if colors.iter().any(|c| c.a() < u8::MAX) {
checkerboard(ui, rect);
}
let mut mesh = egui::Mesh::default();
for (k, color) in colors.into_iter().enumerate() {
let (i, j) = (k % (cols + 1), k / (cols + 1));
@@ -55,14 +59,32 @@ pub fn gradient_chip(ui: &Ui, rect: Rect, gradient: &vectorcraft_color::Gradient
ui.painter().add(egui::Shape::mesh(mesh));
}
/// A white and light gray checkerboard filling `rect`, with cells a quarter of its shorter side
/// (3 to 8 points).
fn checkerboard(ui: &Ui, rect: Rect) {
let p = ui.painter();
p.rect_filled(rect, 0.0, Color32::WHITE);
let cell = (rect.width().min(rect.height()) / 4.0).clamp(3.0, 8.0);
let cols = (rect.width() / cell).ceil() as usize;
let rows = (rect.height() / cell).ceil() as usize;
for j in 0..rows {
for i in (j % 2..cols).step_by(2) {
let min = pos2(rect.left() + cell * i as f32, rect.top() + cell * j as f32);
let square = Rect::from_min_size(min, Vec2::splat(cell)).intersect(rect);
p.rect_filled(square, 0.0, Color32::from_gray(204));
}
}
}
/// The colours at the `(cols + 1) x (rows + 1)` grid points (row by row) of a `width` x `height`
/// chip showing `gp` as it would look on an object of that size.
/// chip showing `gp` as it would look on an object of that size, with the gradient's opacity as
/// their alpha.
pub(super) fn gradient_chip_colors(gp: &vectorcraft_color::GradientPaint, width: f32, height: f32, cols: usize, rows: usize) -> Vec<Color32> {
use vectorcraft_color::{GradientGeom, GradientKind};
let b = vectorcraft_geom::kurbo::Rect::new(0.0, 0.0, width as f64, height as f64);
let rgb = |c: vectorcraft_color::Color| {
let [r, g, b, _] = c.to_rgba8(1.0);
Color32::from_rgb(r, g, b)
let rgba = |(c, a): (vectorcraft_color::Color, f32)| {
let [r, g, b, a] = c.to_rgba8(if a.is_finite() { a } else { 1.0 });
Color32::from_rgba_unmultiplied(r, g, b, a)
};
let at = |k: usize| {
let (i, j) = (k % (cols + 1), k / (cols + 1));
@@ -74,9 +96,9 @@ pub(super) fn gradient_chip_colors(gp: &vectorcraft_color::GradientPaint, width:
let field = gp.freeform_on(painted).model_field(vectorcraft_color::freeform::spread_scale(painted));
return (0..n)
.map(|k| {
let ([r, g, b], _) = field.display_sample(at(k));
let ([r, g, b], a) = field.display_sample(at(k));
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
Color32::from_rgb(q(r), q(g), q(b))
Color32::from_rgba_unmultiplied(q(r), q(g), q(b), q(if a.is_finite() { a } else { 1.0 }))
})
.collect();
}
@@ -90,7 +112,7 @@ pub(super) fn gradient_chip_colors(gp: &vectorcraft_color::GradientPaint, width:
(0..n)
.map(|k| {
let t = geom.param_at(gp.gradient.kind, at(k)).clamp(0.0, 1.0) as f32;
rgb(gp.gradient.sample(t).0)
rgba(gp.gradient.sample(t))
})
.collect()
}
+1 -1
View File
@@ -1277,7 +1277,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Double-clicking the Objects on Path tool opens Objects on Path Tool Options ([Objects on Path](https://helpx.adobe.com/illustrator/using/objects-on-path.html)): the nine-point Pivot grid and the Rotate field set the attachment point and angle of the objects attached next, and groups already attached keep theirs; Reset puts the center and 0° back. Save keeps the options across tool switches and saves them with the preferences (`tool.setOption {tool: "objectsOnPath"}`).
- **Paint and appearance (M3):**
- **CSS swatch libraries:** fractional linear and radial gradient stop positions are preserved, so nearby stops such as 12.5% and 12.890625% remain distinct instead of collapsing to 13%. Reference: [CSS Images 3 color stops](https://www.w3.org/TR/css-images-3/#color-stop-syntax). The original synthetic regression is `palette_io::tests::css_preserves_fractional_gradient_stop_positions` in `crates/color/src/palette_io.rs`. Scores and hours are unchanged.
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, five gradient libraries and a pattern library (Geometric Patterns, whose swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries, six gradient libraries (Organic Fades among them: soft, perceptually mixed multi-stop fades that thin out to transparent) and a pattern library (Geometric Patterns, whose swatches bring their patterns) in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
Editing, renaming, deleting or merging a global swatch also changes the colors linked to it in graph designs, the art of brushes and the tile that pattern editing's Cancel puts back, in the same undo step as the rest of the art; Default Swatches with replace unlinks them there too.
Add Used Colors (`swatch.addUsedColors`) and Select All Unused (`swatch.unused`) look colors, swatch names, links, tints and gradients up in hash sets instead of searching every swatch and color for each color or linked paint (New Color Group from artwork too), and New Swatch with several colors (`swatch.new`), New Color Group (`swatch.newGroup`) and Edit Color Group (`swatch.editGroup`) name their new swatches from one set of the names already taken. The new swatches keep their names, order and links, and Select All Unused lists the same swatches. Evidence: `add_used_colors_adds_each_new_color_once_with_free_names`, `add_used_colors_global_links_every_paint_of_a_new_color`, `unused_matches_links_tints_colors_gradients_and_patterns_exactly`, `new_swatches_from_colors_are_numbered_after_names_already_taken`, `new_color_group_numbers_its_colors_after_the_group_and_its_moved_swatches`, `new_color_group_from_artwork_lists_each_color_once_with_free_names` and `edit_color_group_numbers_extra_colors_after_the_names_it_keeps` in `crates/engine/src/tests/swatchcmds.rs`; `free_swatch_names_match_naming_each_swatch_as_it_is_added` and `free_swatch_names_match_new_swatch_names_as_swatches_are_added` in `crates/doc/src/swatches.rs`.
- **Colorized images (#1466):** a fill color applied to a selected grayscale or 1-bit image (`paint.setFill`) colorizes it with black, white, a process color or a spot color, and refuses a process color when the image has an alpha channel ([Convert color modes](https://web.archive.org/web/20251220223850/https://helpx.adobe.com/illustrator/desktop/manage-colors/select-and-adjust-colors/convert-color-modes.html), "Colorize Grayscale or 1-bit images"); black and white are accepted there too. The canvas draws each pixel in the color as far as the pixel is dark, with white pixels transparent; PDF export writes the image in the color (a spot color as its ink, Registration in every ink) with the image as its soft mask; Separations Preview and printed separations show it on its color's plates; native files keep the color. SVG writes the fill color through a coverage mask (or a colorized PNG in Tiny 1.2); EPS writes binary coverage as an image mask in the color's ink, and a grayscale image as ink samples scaled by coverage, with white pixels left out at Level 3 and gray midtones written over white.