mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Document Color Mode: also converts graph designs, graphic styles, opacity masks and brush art
File > Document Color Mode and document.open {colorMode} converted the
colors of the layers, symbols, pattern tiles and swatches only. Graph
designs, graphic styles, the art of opacity masks, the tile that
pattern editing's Cancel puts back and the art of brushes kept their
old color model (only a tint of a global swatch in a graphic style or
a mask followed the swatch), so a CMYK document could still apply,
place or print RGB colors from them.
These now convert with the rest, in the same undo step: their solid
colors, gradient stops, text runs and mesh points, nested masks
included. A Hue Shift brush's key color takes the converted color of
the art color it matches, a tint of a global swatch included. The
brush library is stored again only when one of its colors changed. A
tint of a global swatch in any of these places becomes the converted
swatch's color at its tint, as in the layers. The reply's changed
count covers the colors of the layers, symbols, graph designs, pattern
tiles and opacity masks; the colors of graphic styles and brushes
convert without being counted.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -322,12 +322,16 @@ pub(crate) fn convert_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "changed": changed }))
|
||||
}
|
||||
|
||||
/// Put `d` in colour `mode` and, with `convert`, convert every colour of its art, symbols,
|
||||
/// pattern tiles and swatches through the colour settings (`intent`, default: theirs) → how many
|
||||
/// colours of the art, symbols and patterns changed. Gray colours stay Gray (they print on the
|
||||
/// black plate in either mode); RGB greys are colours like any other and separate through the
|
||||
/// CMYK profile, unless `grays` puts them on the black plate. Lab colours (spot colour
|
||||
/// definitions) are device independent and fit either mode. Document Color Mode and
|
||||
/// Put `d` in color `mode` and, with `convert`, convert every color of its art (opacity masks
|
||||
/// included), symbols, graph designs, pattern tiles (the tile that pattern editing's Cancel puts
|
||||
/// back included), graphic styles, brushes (their art and a Hue Shift key color) and swatches
|
||||
/// through the color settings (`intent`, default: theirs) → how many colors of the art (opacity
|
||||
/// masks included), symbols, graph designs and pattern tiles changed; the colors of graphic
|
||||
/// styles and brushes convert without being counted. Gray colors stay
|
||||
/// Gray (they print on the black plate in either mode); RGB grays are colors like any other and
|
||||
/// separate through the CMYK profile, unless `grays` puts them on the black plate. Lab colors
|
||||
/// (spot color definitions) are device independent and fit either mode. A tint of a global
|
||||
/// swatch becomes the converted swatch's color at its tint. Document Color Mode and
|
||||
/// `document.open {colorMode}` both convert this way.
|
||||
pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode, convert: bool, intent: Option<Intent>, grays: Grays) -> usize {
|
||||
d.color_mode = mode;
|
||||
@@ -344,18 +348,6 @@ pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode,
|
||||
Color::Rgb { .. } if k_only && Neutral::of(col).is_some() => c.convert(&c.convert(col, Model::Gray, intent), Model::Cmyk, intent),
|
||||
_ => c.convert(col, model, intent),
|
||||
};
|
||||
let mut changed = 0usize;
|
||||
let mut count = |col: &Color, _: proof::Link| {
|
||||
let n = conv(col);
|
||||
if n != *col {
|
||||
changed += 1;
|
||||
}
|
||||
n
|
||||
};
|
||||
proof::map_document_colors(d, &mut count);
|
||||
for a in d.patterns.iter_mut().flat_map(|p| &mut p.art) {
|
||||
proof::map_node_colors(Arc::make_mut(a), &mut count);
|
||||
}
|
||||
for sw in d.swatches_iter_mut() {
|
||||
match &mut sw.paint {
|
||||
Paint::Solid { color, .. } => *color = conv(color),
|
||||
@@ -369,6 +361,73 @@ pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode,
|
||||
}
|
||||
// Tints are their converted swatch's colour at their tint (exact ink percentages).
|
||||
let bases: Vec<(String, Color)> = d.swatches_iter().filter_map(|w| Some((w.name.clone(), d.global_color(&w.name)?))).collect();
|
||||
let changed = std::cell::Cell::new(0usize);
|
||||
let mut count = |col: &Color, link: proof::Link| {
|
||||
let n = match link.and_then(|(name, tint)| Some((bases.iter().find(|(n, _)| n == name)?, tint))) {
|
||||
Some(((_, base), tint)) if tint < 1.0 => base.tinted(tint),
|
||||
_ => conv(col),
|
||||
};
|
||||
if n != *col {
|
||||
changed.set(changed.get() + 1);
|
||||
}
|
||||
n
|
||||
};
|
||||
let patterns = d.patterns.iter_mut().chain(d.pattern_edit.iter_mut().filter_map(|e| e.original.as_mut()));
|
||||
for a in d
|
||||
.layers
|
||||
.iter_mut()
|
||||
.chain(d.symbols.iter_mut().map(|s| &mut s.art))
|
||||
.chain(d.graph_designs.iter_mut().map(|g| &mut g.art))
|
||||
.chain(patterns.flat_map(|p| &mut p.art))
|
||||
{
|
||||
map_art_colors(Arc::make_mut(a), &mut count);
|
||||
}
|
||||
// The reply counts the colors of the document's art only, not those of the graphic styles and brushes.
|
||||
let in_art = changed.get();
|
||||
for it in d.graphic_styles.iter_mut().flat_map(|g| &mut g.appearance.items) {
|
||||
let paint = match it {
|
||||
vectorcraft_doc::AppearanceItem::Fill(l) => &mut l.paint,
|
||||
vectorcraft_doc::AppearanceItem::Stroke(l) => &mut l.paint,
|
||||
};
|
||||
map_paint_colors(paint, &mut count);
|
||||
}
|
||||
// Convert each brush's art. A Hue Shift key color takes the converted color of the art color
|
||||
// it matches (a tint of a global swatch included), so the key still matches the converted art;
|
||||
// a key that matches no art color converts on its own.
|
||||
let in_styles = changed.get();
|
||||
let mut brushes = vectorcraft_brush::library(d);
|
||||
let mut keys = false;
|
||||
for b in &mut brushes {
|
||||
let old_key = match b.kind.colorization() {
|
||||
vectorcraft_brush::Colorization::HueShift { key } => Some(key),
|
||||
_ => None,
|
||||
};
|
||||
let mut new_key = None;
|
||||
for art in b.art_mut() {
|
||||
map_art_colors(art, &mut |col: &Color, link: proof::Link| {
|
||||
let n = count(col, link);
|
||||
if new_key.is_none() && old_key.is_some_and(|k| matches_key(col, &k)) {
|
||||
new_key = Some(n);
|
||||
}
|
||||
n
|
||||
});
|
||||
}
|
||||
let colorization = match &mut b.kind {
|
||||
vectorcraft_brush::BrushKind::Art(a) => Some(&mut a.colorization),
|
||||
vectorcraft_brush::BrushKind::Scatter(s) => Some(&mut s.colorization),
|
||||
vectorcraft_brush::BrushKind::Pattern(p) => Some(&mut p.colorization),
|
||||
vectorcraft_brush::BrushKind::Calligraphic(_) | vectorcraft_brush::BrushKind::Bristle(_) => None,
|
||||
};
|
||||
if let Some(vectorcraft_brush::Colorization::HueShift { key }) = colorization {
|
||||
let n = new_key.unwrap_or_else(|| conv(key));
|
||||
keys |= n != *key;
|
||||
*key = n;
|
||||
}
|
||||
}
|
||||
if keys || changed.get() > in_styles {
|
||||
vectorcraft_brush::store(d, &brushes);
|
||||
}
|
||||
// This pass gives tint swatches their converted swatch's color at their tint; the passes above already did the same for the art's tints.
|
||||
d.map_solid_paints(&mut |c, link, tint| match bases.iter().find(|(n, _)| Some(n) == link.as_ref()) {
|
||||
Some((_, base)) if *tint < 1.0 => {
|
||||
*c = base.tinted(*tint);
|
||||
@@ -376,7 +435,43 @@ pub(crate) fn set_color_mode(d: &mut vectorcraft_doc::Document, mode: ColorMode,
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
changed
|
||||
in_art
|
||||
}
|
||||
|
||||
/// [`proof::map_node_colors`] over `n`'s subtree and the art of every opacity mask in it.
|
||||
fn map_art_colors(n: &mut vectorcraft_doc::Node, f: &mut dyn FnMut(&Color, proof::Link) -> Color) {
|
||||
proof::map_node_colors(n, f);
|
||||
map_mask_colors(n, f);
|
||||
}
|
||||
|
||||
fn map_mask_colors(n: &mut vectorcraft_doc::Node, f: &mut dyn FnMut(&Color, proof::Link) -> Color) {
|
||||
if let Some(mask) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) {
|
||||
map_art_colors(Arc::make_mut(&mut mask.art), f);
|
||||
}
|
||||
if let Some(ch) = n.children_mut() {
|
||||
for c in ch.iter_mut() {
|
||||
map_mask_colors(Arc::make_mut(c), f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether art color `c` is the Hue Shift key color `key`, with the tolerance the brush's
|
||||
/// colorization matches it with.
|
||||
fn matches_key(c: &Color, key: &Color) -> bool {
|
||||
c.to_rgb().iter().zip(key.to_rgb()).map(|(a, b)| (a - b).abs()).sum::<f32>() < 0.02
|
||||
}
|
||||
|
||||
/// `f` applied to a solid paint's color or a gradient's stop colors, given their swatch link.
|
||||
fn map_paint_colors(p: &mut Paint, f: &mut dyn FnMut(&Color, proof::Link) -> Color) {
|
||||
match p {
|
||||
Paint::Solid { color, swatch, tint } => *color = f(color, swatch.as_deref().map(|s| (s, *tint))),
|
||||
Paint::Gradient(g) => {
|
||||
for st in &mut g.gradient.stops {
|
||||
st.color = f(&st.color, st.swatch.as_deref().map(|s| (s, st.tint)));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn values(c: &Color) -> Vec<f32> {
|
||||
|
||||
@@ -234,7 +234,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Document Color Mode",
|
||||
["File", "Document Color Mode"],
|
||||
None,
|
||||
"{mode: \"cmyk\"|\"rgb\", convert?: true (convert every colour of the art, symbols, pattern tiles and swatches through the colour settings; swatch links kept; Gray colours stay Gray, on the black plate), intent?, grays?: \"profile\" (default: RGB greys separate through the CMYK profile like any colour, into four-colour greys and a rich black) | \"black\" (to CMYK, RGB greys with R = G = B go on the black plate only, K = their grey value)} → {changed}",
|
||||
"{mode: \"cmyk\"|\"rgb\", convert?: true (convert every color of the art (opacity masks included), symbols, graph designs, pattern tiles, graphic styles, brushes and swatches through the color settings; swatch links kept; Gray colors stay Gray, on the black plate), intent?, grays?: \"profile\" (default: RGB grays separate through the CMYK profile like any color, into four-color grays and a rich black) | \"black\" (to CMYK, RGB grays with R = G = B go on the black plate only, K = their gray value)} → {changed: how many colors of the art, symbols, graph designs and pattern tiles changed; the colors of graphic styles and brushes are not counted}",
|
||||
has_doc,
|
||||
super::colormgmt::convert_mode
|
||||
),
|
||||
|
||||
@@ -47,6 +47,151 @@ fn document_color_mode_converts_grouped_swatches() {
|
||||
assert!(matches!(doc(&s).swatch("Bright Red").unwrap().paint.color(), Some(Color::Cmyk { .. })));
|
||||
}
|
||||
|
||||
/// The first solid fill color in the subtree of `n`.
|
||||
fn first_fill(n: &vectorcraft_doc::Node) -> Color {
|
||||
let mut out = None;
|
||||
n.walk(&mut |m| {
|
||||
if out.is_none() {
|
||||
out = m.appearance.fill_paint().color();
|
||||
}
|
||||
});
|
||||
out.unwrap_or_else(|| panic!("no fill in {n:?}"))
|
||||
}
|
||||
|
||||
/// The fill of graph design "Bar", the fill of brush "Holder"'s art and its Hue Shift key color,
|
||||
/// the fill of graphic style "Red" and the fill of the art of `masked`'s opacity mask.
|
||||
fn colors_outside_the_layers(s: &Session, masked: NodeId) -> Vec<Color> {
|
||||
let d = doc(s);
|
||||
let brush = vectorcraft_brush::find(d, "Holder").unwrap();
|
||||
let vectorcraft_brush::Colorization::HueShift { key } = brush.kind.colorization() else { panic!("not Hue Shift") };
|
||||
vec![
|
||||
first_fill(&d.graph_designs.iter().find(|g| g.name == "Bar").unwrap().art),
|
||||
first_fill(brush.art()[0]),
|
||||
key,
|
||||
d.graphic_style("Red").unwrap().appearance.fill_paint().color().unwrap(),
|
||||
first_fill(&d.node(masked).unwrap().extras().mask.as_deref().unwrap().art),
|
||||
]
|
||||
}
|
||||
|
||||
/// Document Color Mode converts the colors of graph designs, of the art of brushes (with a Hue
|
||||
/// Shift brush's key color), of graphic styles and of opacity-mask art as it converts the
|
||||
/// layers' art, tints of global swatches included, in one undo step.
|
||||
#[test]
|
||||
fn document_color_mode_converts_art_outside_the_layers() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
let hue_shift = json!({"colorization": {"method": "hueShift"}});
|
||||
for (cmd, p, paint) in [
|
||||
("graph.design", json!({"new": "Bar"}), json!({"swatch": "Tone", "tint": 40})),
|
||||
("brush.new", json!({"type": "art", "name": "Holder", "params": hue_shift}), json!({"color": "#ff0000"})),
|
||||
("graphicStyle.new", json!({"name": "Red"}), json!({"color": "#ff0000"})),
|
||||
] {
|
||||
let id = rect(&mut s);
|
||||
let mut paint = paint;
|
||||
paint["ids"] = json!([id.0]);
|
||||
run(&mut s, "paint.setFill", paint);
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [id.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, cmd, p);
|
||||
run(&mut s, "edit.clear", json!({"ids": [id.0]}));
|
||||
}
|
||||
let masked = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000", "ids": [masked.0]}));
|
||||
let m = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#808080", "ids": [m.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [masked.0, m.0]}));
|
||||
run(&mut s, "transparency.makeOpacityMask", json!({"clip": true, "invert": false}));
|
||||
let before = colors_outside_the_layers(&s, masked);
|
||||
assert!(before.iter().all(|c| matches!(c, Color::Rgb { .. })), "{before:?}");
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
|
||||
run(&mut s, "file.documentColorMode", json!({"mode": "cmyk"}));
|
||||
let after = colors_outside_the_layers(&s, masked);
|
||||
assert!(after.iter().all(|c| matches!(c, Color::Cmyk { .. })), "{after:?}");
|
||||
// The same red converts to the same inks wherever it is.
|
||||
let red = first_fill(doc(&s).node(masked).unwrap());
|
||||
assert_eq!(after[1..4], [red; 3]);
|
||||
// A tint of a global swatch is the converted swatch's color at that tint.
|
||||
let tone = doc(&s).swatch("Tone").unwrap().paint.color().unwrap();
|
||||
assert_eq!(after[0], tone.tinted(0.4));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(colors_outside_the_layers(&s, masked), before);
|
||||
}
|
||||
|
||||
/// A Hue Shift brush whose art is painted with a tint of a global swatch keeps its key color equal
|
||||
/// to that art color after Document Color Mode.
|
||||
#[test]
|
||||
fn document_color_mode_keeps_a_hue_shift_key_on_its_tinted_art() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.new", json!({"name": "Tone", "color": "#ff0000", "global": true}));
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"swatch": "Tone", "tint": 40, "ids": [id.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [id.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, "brush.new", json!({"type": "art", "name": "Holder", "params": {"colorization": {"method": "hueShift"}}}));
|
||||
run(&mut s, "edit.clear", json!({"ids": [id.0]}));
|
||||
let key_and_art = |s: &Session| {
|
||||
let brush = vectorcraft_brush::find(doc(s), "Holder").unwrap();
|
||||
let vectorcraft_brush::Colorization::HueShift { key } = brush.kind.colorization() else { panic!("not Hue Shift") };
|
||||
(key, first_fill(brush.art()[0]))
|
||||
};
|
||||
let (key, art) = key_and_art(&s);
|
||||
assert_eq!(key, art);
|
||||
run(&mut s, "file.documentColorMode", json!({"mode": "cmyk"}));
|
||||
let (key, art) = key_and_art(&s);
|
||||
assert!(matches!(art, Color::Cmyk { .. }), "{art:?}");
|
||||
assert_eq!(key, art);
|
||||
}
|
||||
|
||||
/// The reply's `changed` counts the colors of the document's art; the colors of brush art and
|
||||
/// graphic styles convert without being counted.
|
||||
#[test]
|
||||
fn document_color_mode_counts_the_colors_of_the_art_only() {
|
||||
let changed = |library: bool| {
|
||||
let mut s = session();
|
||||
if library {
|
||||
for (cmd, p) in [("brush.new", json!({"type": "art", "name": "Holder"})), ("graphicStyle.new", json!({"name": "Red"}))] {
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000", "ids": [id.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
run(&mut s, cmd, p);
|
||||
run(&mut s, "edit.clear", json!({"ids": [id.0]}));
|
||||
}
|
||||
}
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000", "ids": [id.0]}));
|
||||
let r = run(&mut s, "file.documentColorMode", json!({"mode": "cmyk"}));
|
||||
if library {
|
||||
let brush = vectorcraft_brush::find(doc(&s), "Holder").unwrap();
|
||||
assert!(matches!(first_fill(brush.art()[0]), Color::Cmyk { .. }));
|
||||
}
|
||||
r["changed"].as_u64().unwrap()
|
||||
};
|
||||
let in_art = changed(false);
|
||||
assert!(in_art > 0);
|
||||
assert_eq!(changed(true), in_art);
|
||||
}
|
||||
|
||||
/// Document Color Mode chosen while a pattern is being edited also converts the tile that pattern
|
||||
/// editing's Cancel puts back.
|
||||
#[test]
|
||||
fn document_color_mode_converts_the_tile_that_pattern_cancel_puts_back() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000", "ids": [id.0]}));
|
||||
run(&mut s, "paint.setStroke", json!({"none": true, "ids": [id.0]}));
|
||||
run(&mut s, "select.set", json!({"ids": [id.0]}));
|
||||
let pattern = run(&mut s, "object.pattern.make", json!({"edit": false}))["name"].as_str().unwrap().to_string();
|
||||
run(&mut s, "object.pattern.edit", json!({"name": pattern}));
|
||||
run(&mut s, "file.documentColorMode", json!({"mode": "cmyk"}));
|
||||
run(&mut s, "object.pattern.cancel", json!({}));
|
||||
let red = first_fill(doc(&s).node(id).unwrap());
|
||||
assert!(matches!(red, Color::Cmyk { .. }), "{red:?}");
|
||||
let tile: Vec<Color> = doc(&s).pattern(&pattern).unwrap().art.iter().map(|n| first_fill(n)).collect();
|
||||
assert_eq!(tile, [red]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_spot_swatches_print_on_their_own_plate() {
|
||||
let mut s = session();
|
||||
|
||||
+8
-4
@@ -589,12 +589,16 @@ come back without a path), `file.newFromTemplate {path}`, `file.revert` and `fil
|
||||
with the values a save would use). Without a path the save commands return `{dataBase64, name, folder?, warnings}`. A document saved as SVG or PDF
|
||||
remembers its options, so the next save reuses them. A PDF opened with `pages`, `cropTo` or `password` is only part
|
||||
of the file, so Save asks for a new name instead of writing it back.
|
||||
`file.documentColorMode {mode, convert?, intent?, grays?}` switches the document colour mode, converting the colours of
|
||||
the art, symbols, pattern tiles and swatches through the colour settings (`object.convertDocumentColorMode` is an
|
||||
alias kept for older scripts). Gray colours stay Gray and print on the black plate only. RGB greys are colours like any
|
||||
`file.documentColorMode {mode, convert?, intent?, grays?}` switches the document color mode, converting the colors of
|
||||
the art (opacity masks included), symbols, graph designs, pattern tiles (with the tile that pattern editing's Cancel
|
||||
puts back), graphic styles, brushes (their art and a Hue Shift key color) and swatches through the color settings, in
|
||||
one undo step (`object.convertDocumentColorMode` is an alias kept for older scripts). Gray colors stay Gray and print
|
||||
on the black plate only. RGB grays are colors like any
|
||||
other: to CMYK they separate through the profile into four-colour greys and a rich black, as Illustrator converts
|
||||
them. `grays: "black"` puts RGB greys with R = G = B on the black plate instead, K = their grey value (the inks Edit Colors ›
|
||||
Convert to Grayscale before the switch would give); other colours convert as usual.
|
||||
Convert to Grayscale before the switch would give); other colors convert as usual. The reply's `changed` counts the
|
||||
colors that changed in the art, symbols, graph designs and pattern tiles; it does not count the colors of graphic
|
||||
styles and brushes.
|
||||
|
||||
SVG import keeps what the canvas edits live. `<pattern>` becomes a pattern swatch. A `<symbol>` with `<use>` becomes a
|
||||
symbol (named after its `data-name`, else its id) with an instance per `<use>`; a `<use>` that shows it differently
|
||||
|
||||
@@ -873,6 +873,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel shows a selected colour in its own model and, with nothing selected, the new-art defaults in the mode picked last, which it keeps between sessions; Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers (the Properties panel has the Control bar's Fill and Stroke chips, Stroke link and weight spinner, with nothing selected too), panels have Illustrator's Window menu keys (Align Shift+F7, Transform Shift+F8, Pathfinder Cmd+Shift+F9, Layers F7, Character Cmd+T…; every panel can be given one in Edit › Keyboard Shortcuts), and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). An ICC CMYK profile converts straight into the RGB working space (not through sRGB, so a wider space such as Wide Gamut RGB keeps colours sRGB can't hold) with the colour settings' black-point compensation, on the canvas and in RGB exports alike (the built-in Generic CMYK compensates both ways, so RGB black separated to a rich black displays as black again). Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
|
||||
A Cmd/Ctrl-click on the Color panel's spectrum applies Complement and a Cmd/Ctrl+Shift-click applies Invert to the active Fill or Stroke (`paint.complement`, `paint.invert`); with Alt also held, the click recolors the inactive one, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) list them.
|
||||
View › Proof Setup › Color blindness – Tritanopia-type turns Proof Colors on and proofs the canvas with the tritanopia simulation of Brettel, Viénot and Mollon (1997). The Separations Preview panel's Proof Setup list offers the same target (`view.proofSetup {target: "tritanopia"}`). Grays keep their value.
|
||||
- **Document Color Mode outside the layers:** File › Document Color Mode (in one undo step) and `document.open {colorMode}` also convert the colors of graph designs, graphic styles, opacity-mask art, the tile that pattern editing's Cancel puts back and the art of brushes (with a Hue Shift brush's key color); a tint of a global swatch there becomes the converted swatch's color at its tint, as in the art.
|
||||
- **Recolor Artwork layout (#3416):** the Edit chip list uses the modal scroll helper and its actual remaining height; the tab's minimum-height space scrolls with the chips. `tests_recolor` covers 5/20/200/400 colors, window resizing, 100/125/150/200% UI scaling, scrolling to and editing the final chip, Preview, Cancel, Confirm and Undo, alongside the existing #1733 drag regressions. Scores and estimates unchanged.
|
||||
- **Edit Colors parameters:** `edit.colors.saturate` needs a number `intensity`, the value from -100% to 100% that Saturate takes ([Adjusting colors](https://web.archive.org/web/20230128232631/https://helpx.adobe.com/illustrator/using/adjusting-colors.html)), and answers "missing number `intensity`" without one; a number outside the range counts as its nearest end. `edit.colors.adjustBalance` refuses a channel and `color.harmony` refuses `steps` or `amount` that is not a number; both read `null` as a value left out. `tests::editcolors` and `tests::colorguide` cover them.
|
||||
- **Color Picker layout (#2008):** a centered draggable header, equal-width actions above aligned HSB/RGB/Hex and Lab/CMYK fields, compact field and slider, and warnings below the preview. No horizontal scrolling; narrow windows stack the sections with vertical scrolling. Tests in `dialogs/color_picker.rs` cover actual pointer/key input, typed values before OK/Enter, Cancel/Escape rollback, one Undo step, mode-size stability, warnings and layout at two UI scales. The screen sampler sits above the field and the upstream two-grid Tab order is retained. AccessKit regressions check the action names and each channel slider's displayed value, units and range. German and enlarged English UI renders were reviewed locally; no reference images are shipped. This is a layout improvement, with no change to scores or remaining-hour estimates.
|
||||
|
||||
Reference in New Issue
Block a user