diff --git a/ROADMAP.md b/ROADMAP.md index 327036c37..3e8cfb9d3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/crates/color/src/libraries.rs b/crates/color/src/libraries.rs index e96760d5c..5b5fd3653 100644 --- a/crates/color/src/libraries.rs +++ b/crates/color/src/libraries.rs @@ -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, Vec) { (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, Vec) { + 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); } } } diff --git a/crates/engine/src/cmd/gradpresets.rs b/crates/engine/src/cmd/gradpresets.rs index 2f69a9df3..d27809fa7 100644 --- a/crates/engine/src/cmd/gradpresets.rs +++ b/crates/engine/src/cmd/gradpresets.rs @@ -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 " 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 = builtin_presets().map(|e| e.name.to_ascii_lowercase()).collect(); + let mut taken: std::collections::HashSet = + 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 = 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 { diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index a92b4095a..b053796c9 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -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. diff --git a/crates/engine/src/tests/gradpresets.rs b/crates/engine/src/tests/gradpresets.rs index 437f96d26..807094a1b 100644 --- a/crates/engine/src/tests/gradpresets.rs +++ b/crates/engine/src/tests/gradpresets.rs @@ -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")); +} diff --git a/crates/engine/src/tests/swatchlib.rs b/crates/engine/src/tests/swatchlib.rs index 3ecb5e68e..55ab34564 100644 --- a/crates/engine/src/tests/swatchlib.rs +++ b/crates/engine/src/tests/swatchlib.rs @@ -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::>(); + assert_eq!(shape(&fill.gradient), shape(&want.gradient), "{mode}"); + } +} + #[test] fn pattern_libraries_bring_their_patterns_and_add_each_once() { let mut s = session(); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index bc67d8fd8..f4d90e842 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2279,6 +2279,7 @@ font-technology Variable Променливи Options for Selection… Опции за селектиране… Options: Опции: Options… Опции… + Organic Fades Органични преливания or more spaces with a tab или повече интервала с табулация Order: Ред: Ordinals Поредни номера diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 6454cffcc..940c0126e 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 1567e0e8b..1ca9327d8 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -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: diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e9df0812f..8608f0929 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index e259ecba8..44037015c 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index abfaaa13a..e7664ff26 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 4611904ff..05b9f3144 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1840,6 +1840,7 @@ font-technology Variable Μεταβλητές Options: Επιλογές: Options… Επιλογές… Ordinals Τακτικοί αριθμοί + Organic Fades Οργανικές διαβαθμίσεις Orientation Προσανατολισμός Orientation: Προσανατολισμός: Original Πρωτότυπο diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index b91d89b8c..cb934ae2e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index e29645989..1b79b87b3 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index ff206b931..2639bae89 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 6698f10a4..8d072d0dc 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3170,6 +3170,7 @@ antiAlias Sharp शार्प Options for Selection… सिलेक्शन के लिए ऑप्शन्स… Options: ऑप्शन्स: Options… ऑप्शन्स… + Organic Fades ऑर्गेनिक फ़ेड or more spaces with a tab या एक टैब के साथ अधिक रिक्त स्थान Orange नारंगी Order: आदेश: diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index a8d02ca4c..9e71e19f9 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 04581cd72..405f9a583 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 82827964f..e8999f6fa 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 42a4aa3cf..5428c3587 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -846,6 +846,7 @@ digits Malayalam マラヤーラム数字 Spectrum Gradients スペクトルグラデーション Sky Gradients 空のグラデーション Neutral Gradients ニュートラルグラデーション + Organic Fades オーガニックフェード Duotone Gradients ダブルトーングラデーション Radial Glows 円形グロー Save Swatch Library… スウォッチライブラリを保存… diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 30b0abd6b..4dd9b37a9 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -719,6 +719,7 @@ Spectrum Gradients სპექტრის გრადიენტები Sky Gradients ცის გრადიენტები Neutral Gradients ნეიტრალური გრადიენტები + Organic Fades ორგანული გაქრობები Duotone Gradients ორტონიანი გრადიენტები Radial Glows რადიალური ნათებები Save Swatch Library… ფერის ნიმუშების ბიბლიოთეკის შენახვა… diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 3bc8360b4..cbdbc4a70 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -839,6 +839,7 @@ digits Malayalam 말라얄람어 Spectrum Gradients 스펙트럼 그레이디언트 Sky Gradients 하늘 그레이디언트 Neutral Gradients 중간색 그레이디언트 + Organic Fades 유기적 페이드 Duotone Gradients 듀오톤 그레이디언트 Radial Glows 방사형 광선 Save Swatch Library… 견본 라이브러리 저장… diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index c6f37b432..ad919321d 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 3647b56f5..5cd25f472 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 530a62719..17507906d 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 341dc196f..fee37c423 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2377,6 +2377,7 @@ file-filter Templates Шаблоны Options for Selection… Параметры выделения… Options: Параметры: Options… Параметры… + Organic Fades Органические затухания or more spaces with a tab или больше пробелов на табуляцию Order: Порядок: Ordinals Порядковые числительные diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index ed59d9811..8a6aedc46 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index dc89f3380..d62342f91 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -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ı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 8d67846ec..ccd1d0745 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2374,6 +2374,7 @@ file-filter Templates Шаблони Options for Selection… Параметри виділення… Options: Параметри: Options… Параметри… + Organic Fades Органічні згасання or more spaces with a tab або більше прогалин на табуляцію Order: Порядок: Ordinals Порядкові числівники diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 8f1ea434f..7003b979c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 05c6539bf..7f6e23476 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2249,6 +2249,7 @@ file-filter Templates 模板 Options 选项 Options: 选项: Options… 选项… + Organic Fades 自然渐隐 or more spaces with a tab 或多个空格加一个制表符 Order: 顺序: Ordinals 序数字 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index cb8a61362..279dedb1c 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3340,6 +3340,7 @@ file-filter Templates 範本 Options for Selection… 選取範圍的選項… Options: 選項: Options… 選項… + Organic Fades 自然漸隱 or more spaces with a tab 個以上的空格取代為定位字元 Order: 順序: Ordinals 序數字 diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index d8914ce39..adf536f2a 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -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::>() + }; + 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}; diff --git a/crates/ui-egui/src/widgets/chips.rs b/crates/ui-egui/src/widgets/chips.rs index dca209ce0..017ba588c 100644 --- a/crates/ui-egui/src/widgets/chips.rs +++ b/crates/ui-egui/src/widgets/chips.rs @@ -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 { 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() } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 908895ad6..60804ae07 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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.