View: Proof Setup simulates tritanopia

View › Proof Setup had two color blindness items, protanopia and
deuteranopia. A third item, Color blindness – Tritanopia-type, proofs
the canvas with a tritanopia simulation, and the Separations Preview
panel's Proof Setup list offers it too. `view.proofSetup` takes
`target: "tritanopia"`.

The simulation follows Brettel, Viénot and Mollon (1997): a color
keeps its L and M cone responses, and its S response is replaced so
that it lies on one of two half-planes. Both contain the neutral
axis; one passes through the 485 nm color and the other through the
660 nm color. The neutral axis runs to the display's white, so grays
keep their value. The matrices are computed at
compile time from the published inputs cited in the code. The
protanopia and deuteranopia matrices are unchanged.
This commit is contained in:
dotCooCoo
2026-10-16 02:26:04 +07:00
committed by bflatastic
parent bbad808a02
commit 35f43d631f
22 changed files with 192 additions and 2 deletions
+105
View File
@@ -515,6 +515,7 @@ impl Cms {
ProofTarget::LegacyMacRgb => srgb.map(|v| lab::linear_to_srgb(v.powf(1.8))),
ProofTarget::Protanopia => simulate_cvd(srgb, &PROTANOPIA),
ProofTarget::Deuteranopia => simulate_cvd(srgb, &DEUTERANOPIA),
ProofTarget::Tritanopia => TRITANOPIA.simulate(srgb),
ProofTarget::WorkingCmyk => {
let cmyk = self.srgb_to_cmyk(srgb, proof.intent);
self.cmyk_to_srgb(cmyk, proof.simulate_paper)
@@ -582,6 +583,106 @@ fn simulate_cvd(srgb: [f32; 3], m: &[[f32; 3]; 3]) -> [f32; 3] {
lab::mat_mul(m, lab::srgb_linear(srgb)).map(|v| lab::linear_to_srgb(v.clamp(0.0, 1.0)))
}
// Tritanopia: Brettel, Viénot & Mollon (1997), "Computerized simulation of color appearance for
// dichromats", JOSA A 14(10), 2647-2655, https://doi.org/10.1364/JOSAA.14.002647. A color keeps its
// L and M cone responses, and its S response is replaced so that it lies on one of two half-planes in
// LMS space. Both half-planes contain the neutral axis (black to the display's white, so grays keep
// their value); one passes through the 485 nm spectral color and the other through the 660 nm one. A
// color uses the half-plane on its own side of the plane through the neutral axis and the S axis.
// The matrices are computed at compile time from these inputs:
// - linear sRGB → CIE XYZ: IEC 61966-2-1 (D65);
// - CIE XYZ → LMS: Smith & Pokorny (1975), as printed in https://daltonlens.org/understanding-cvd-simulation/;
// - the 485 nm and 660 nm colors: the Judd-Vos color matching functions,
// http://www.cvrl.org/database/data/cmfs/ciexyzjv.csv.
const LINEAR_SRGB_TO_XYZ: [[f64; 3]; 3] = [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]];
const XYZ_TO_LMS: [[f64; 3]; 3] = [[0.155_14, 0.543_12, -0.032_86], [-0.155_14, 0.456_84, 0.032_86], [0.0, 0.0, 0.016_08]];
const XYZ_485_NM: [f64; 3] = [0.056_985, 0.169_87, 0.586_4];
const XYZ_660_NM: [f64; 3] = [0.161_61, 0.061, 0.000_011_906];
const TRITANOPIA: Dichromat = Dichromat::tritanope();
/// A dichromat simulation with two half-planes, as matrices on linear sRGB. `near` applies to colors
/// whose dot product with `side` is 0 or more, and `far` applies to the others.
struct Dichromat {
side: [f32; 3],
near: [[f32; 3]; 3],
far: [[f32; 3]; 3],
}
impl Dichromat {
const fn tritanope() -> Self {
let to_lms = mat3_mul(&XYZ_TO_LMS, &LINEAR_SRGB_TO_XYZ);
let to_rgb = mat3_inverse(&to_lms);
let white = mat3_apply(&to_lms, [1.0; 3]);
let a485 = mat3_apply(&XYZ_TO_LMS, XYZ_485_NM);
let a660 = mat3_apply(&XYZ_TO_LMS, XYZ_660_NM);
let mut side = cross(white, [0.0, 0.0, 1.0]);
if dot(side, a485) < 0.0 {
side = [-side[0], -side[1], -side[2]];
}
// A color on the 485 nm side lands on that half-plane.
let near = mat3_mul(&to_rgb, &mat3_mul(&keep_lm_on_plane(cross(white, a485)), &to_lms));
let far = mat3_mul(&to_rgb, &mat3_mul(&keep_lm_on_plane(cross(white, a660)), &to_lms));
// `side · lms(rgb)` is `(to_lmsᵀ · side) · rgb`.
let t = &to_lms;
let side_rgb = [
t[0][0] * side[0] + t[1][0] * side[1] + t[2][0] * side[2],
t[0][1] * side[0] + t[1][1] * side[1] + t[2][1] * side[2],
t[0][2] * side[0] + t[1][2] * side[1] + t[2][2] * side[2],
];
Dichromat { side: vec3_f32(side_rgb), near: mat3_f32(&near), far: mat3_f32(&far) }
}
fn simulate(&self, srgb: [f32; 3]) -> [f32; 3] {
let rgb = lab::srgb_linear(srgb);
let s = self.side;
let m = if s[0] * rgb[0] + s[1] * rgb[1] + s[2] * rgb[2] >= 0.0 { &self.near } else { &self.far };
lab::mat_mul(m, rgb).map(|v| lab::linear_to_srgb(v.clamp(0.0, 1.0)))
}
}
/// LMS → LMS: keeps L and M and sets S so the result lies in the plane with normal `n`.
const fn keep_lm_on_plane(n: [f64; 3]) -> [[f64; 3]; 3] {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-n[0] / n[2], -n[1] / n[2], 0.0]]
}
const fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
const fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
const fn mat3_apply(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[dot(m[0], v), dot(m[1], v), dot(m[2], v)]
}
const fn mat3_mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut out = [[0.0; 3]; 3];
let mut i = 0;
while i < 3 {
out[i] = [dot(a[i], [b[0][0], b[1][0], b[2][0]]), dot(a[i], [b[0][1], b[1][1], b[2][1]]), dot(a[i], [b[0][2], b[1][2], b[2][2]])];
i += 1;
}
out
}
const fn mat3_inverse(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let c0 = cross(m[1], m[2]);
let c1 = cross(m[2], m[0]);
let c2 = cross(m[0], m[1]);
let det = dot(m[0], c0);
[[c0[0] / det, c1[0] / det, c2[0] / det], [c0[1] / det, c1[1] / det, c2[1] / det], [c0[2] / det, c1[2] / det, c2[2] / det]]
}
const fn vec3_f32(v: [f64; 3]) -> [f32; 3] {
[v[0] as f32, v[1] as f32, v[2] as f32]
}
const fn mat3_f32(m: &[[f64; 3]; 3]) -> [[f32; 3]; 3] {
[vec3_f32(m[0]), vec3_f32(m[1]), vec3_f32(m[2])]
}
/// View → Proof Setup target.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "profile", rename_all = "camelCase")]
@@ -601,6 +702,8 @@ pub enum ProofTarget {
Protanopia,
/// Colour blindness — deuteranopia type.
Deuteranopia,
/// Color blindness, tritanopia type.
Tritanopia,
}
impl ProofTarget {
@@ -615,6 +718,7 @@ impl ProofTarget {
"monitorRgb" | "monitor" => ProofTarget::MonitorRgb,
"protanopia" => ProofTarget::Protanopia,
"deuteranopia" => ProofTarget::Deuteranopia,
"tritanopia" => ProofTarget::Tritanopia,
_ => return None,
})
}
@@ -627,6 +731,7 @@ impl ProofTarget {
ProofTarget::MonitorRgb => "monitorRgb".into(),
ProofTarget::Protanopia => "protanopia".into(),
ProofTarget::Deuteranopia => "deuteranopia".into(),
ProofTarget::Tritanopia => "tritanopia".into(),
}
}
/// Whether the target simulates a CMYK press (separations make sense).
+29
View File
@@ -194,6 +194,35 @@ fn colour_blindness_proofs() {
assert_eq!(ProofTarget::parse(&ProofTarget::Cmyk("X".into()).id()), Some(ProofTarget::Cmyk("X".into())));
}
/// The expected colors were computed separately with NumPy from the same published inputs (linear
/// sRGB to XYZ from IEC 61966-2-1, Smith & Pokorny's XYZ to LMS, Judd-Vos XYZ at 485 nm and 660 nm),
/// following Brettel, Viénot & Mollon (1997) with the display's white as the neutral axis.
#[test]
fn tritanopia_proof_follows_brettel_1997() {
let c = cms();
let t = ProofSetup { target: ProofTarget::Tritanopia, ..Default::default() };
assert_eq!(ProofTarget::parse("tritanopia"), Some(ProofTarget::Tritanopia));
assert_eq!(ProofTarget::Tritanopia.id(), "tritanopia");
for g in [0.0, 0.25, 0.5, 0.75, 1.0] {
let out = c.proof_srgb([g; 3], &t);
assert!(out.iter().all(|v| (v - g).abs() < 1e-4), "gray {g} stays gray: {out:?}");
}
let cases = [
// The 660 nm half-plane.
([1.0, 0.0, 0.0], [1.0, 0.0, 0.299_231]),
([1.0, 0.0, 1.0], [0.934_206, 0.388_351, 0.467_178]),
([0.9, 0.6, 0.1], [0.926_825, 0.557_667, 0.599_649]),
// The 485 nm half-plane.
([0.0, 0.0, 1.0], [0.0, 0.377_268, 0.519_933]),
([0.0, 1.0, 0.0], [0.480_503, 0.920_568, 1.0]),
([0.2, 0.4, 0.8], [0.0, 0.464_73, 0.563_516]),
];
for (rgb, want) in cases {
let got = c.proof_srgb(rgb, &t);
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-3), "{rgb:?}: {got:?}, expected {want:?}");
}
}
#[test]
fn bad_icc_rejected() {
assert!(matches!(register_icc(b"not a profile", None), Err(CmsError::BadProfile(_))));
+1 -1
View File
@@ -84,7 +84,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Proof Setup",
["View", "Proof Setup"],
None,
"{target?: \"workingCmyk\"|\"cmyk:<profile>\"|\"legacyMacRgb\"|\"srgb\"|\"monitorRgb\"|\"protanopia\"|\"deuteranopia\", intent?, simulatePaper?, proof?: bool (also turn Proof Colors on)} → proof state",
"{target?: \"workingCmyk\"|\"cmyk:<profile>\"|\"legacyMacRgb\"|\"srgb\"|\"monitorRgb\"|\"protanopia\"|\"deuteranopia\"|\"tritanopia\", intent?, simulatePaper?, proof?: bool (also turn Proof Colors on)} → proof state",
always,
proof_setup
),
+16
View File
@@ -311,6 +311,22 @@ fn view_toggles_redraw_without_dirtying() {
s.execute("view.overprintPreview", &json!({"on": false})).unwrap();
}
#[test]
fn proof_setup_simulates_tritanopia() {
let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
let mut s = session();
rect(&mut s, 0.0, 0.0, 100.0, 100.0, json!("#0000ff"));
let r = s.execute("view.proofSetup", &json!({"target": "tritanopia", "proof": true})).unwrap();
assert_eq!(r["target"], "tritanopia");
let setup = vectorcraft_render::proof::active_proof().unwrap();
assert_eq!(setup.target, ProofTarget::Tritanopia);
let doc = s.doc().unwrap().doc.clone();
let px = render(&doc, RenderOptions { proof: Some(setup), ..Default::default() }).pixel(50, 50);
// A tritanope sees blue as a dark teal.
assert!(px[0] < 8 && px[1] > 80 && px[2] > 110 && px[2] < 150, "{px:?}");
s.execute("view.proofSetup", &json!({"target": "workingCmyk", "proof": false})).unwrap();
}
/// Whether the content has a DeviceCMYK fill/stroke operator (`c m y k k` / `K`).
fn has_cmyk_op(pdf: &str) -> bool {
let toks: Vec<&str> = pdf.split_ascii_whitespace().collect();
+1
View File
@@ -479,6 +479,7 @@
Monitor RGB RGB monitoru
Color blindness – Protanopia-type Barvoslepost – protanopie
Color blindness – Deuteranopia-type Barvoslepost – deuteranopie
Color blindness – Tritanopia-type Barvoslepost – tritanopie
Customize… Vlastní…
Proof Colors Barvy nátisku
Zoom In Zvětšit
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -469,6 +469,7 @@
Color bars Farbkontrollstreifen
Color blindness – Deuteranopia-type Farbenblindheit – Deuteranopie
Color blindness – Protanopia-type Farbenblindheit – Protanopie
Color blindness – Tritanopia-type Farbenblindheit – Tritanopie
Color Burn Farbig nachbelichten
Color conversion: Farbkonvertierung:
Color Dodge Farbig abwedeln
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -484,6 +484,7 @@
Color bars Barras de color
Color blindness – Deuteranopia-type Daltonismo – tipo deuteranopía
Color blindness – Protanopia-type Daltonismo – tipo protanopía
Color blindness – Tritanopia-type Daltonismo – tipo tritanopía
Color Burn Subexponer color
Color conversion: Conversión de color:
Color Dodge Sobreexponer color
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -485,6 +485,7 @@
Color bars Barres de couleurs
Color blindness – Deuteranopia-type Daltonisme – type deutéranopie
Color blindness – Protanopia-type Daltonisme – type protanopie
Color blindness – Tritanopia-type Daltonisme – type tritanopie
Color Burn Densité couleur +
Color conversion: Conversion des couleurs :
Color Dodge Densité couleur -
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -657,6 +657,7 @@
Color bars Színsávok
Color blindness – Deuteranopia-type Színvakság – deuteranópia
Color blindness – Protanopia-type Színvakság – protanópia
Color blindness – Tritanopia-type Színvakság – tritanópia
Color Burn Színsötétítés
Color conversion: Színátalakítás:
Color Dodge Színvilágosítás
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -484,6 +484,7 @@
Color bars Barre colore
Color blindness – Deuteranopia-type Daltonismo – tipo deuteranopia
Color blindness – Protanopia-type Daltonismo – tipo protanopia
Color blindness – Tritanopia-type Daltonismo – tipo tritanopia
Color Burn Brucia colore
Color conversion: Conversione colore:
Color Dodge Scherma colore
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -474,6 +474,7 @@
Monitor RGB モニター RGB
Color blindness – Protanopia-type P 型(1 型)色覚
Color blindness – Deuteranopia-type D 型(2 型)色覚
Color blindness – Tritanopia-type T 型(3 型)色覚
Customize… カスタマイズ…
Proof Colors 色の校正
Zoom In ズームイン
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -470,6 +470,7 @@
Monitor RGB 모니터 RGB
Color blindness – Protanopia-type 색맹 – 적색맹 유형
Color blindness – Deuteranopia-type 색맹 – 녹색맹 유형
Color blindness – Tritanopia-type 색맹 – 청색맹 유형
Customize… 사용자 지정…
Proof Colors 교정 색상
Zoom In 확대
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -436,6 +436,7 @@
Monitor RGB RGB monitora
Color blindness – Protanopia-type Daltonizm – typu protanopia
Color blindness – Deuteranopia-type Daltonizm – typu deuteranopia
Color blindness – Tritanopia-type Daltonizm – typu tritanopia
Customize… Dostosuj…
Proof Colors Próba kolorów
Zoom In Powiększ
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -475,6 +475,7 @@
Color Type: Tipo de Cor:
Color blindness – Deuteranopia-type Daltonismo – tipo deuteranopia
Color blindness – Protanopia-type Daltonismo – tipo protanopia
Color blindness – Tritanopia-type Daltonismo – tipo tritanopia
Color conversion: Conversão de cor:
Color images Imagens coloridas
Color: Cor:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -482,6 +482,7 @@
Color bars Цветные полосы
Color blindness – Deuteranopia-type Дальтонизм — дейтеранопия
Color blindness – Protanopia-type Дальтонизм — протанопия
Color blindness – Tritanopia-type Дальтонизм — тританопия
Color Burn Затемнение основы
Color conversion: Преобразование цвета:
Color Dodge Осветление основы
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -478,6 +478,7 @@
Color bars Кольорові смуги
Color blindness – Deuteranopia-type Дальтонізм - дейтеранопія
Color blindness – Protanopia-type Дальтонізм - протанопія
Color blindness – Tritanopia-type Дальтонізм - тританопія
Color Burn Затемнення основи
Color conversion: Перетворення кольору:
Color Dodge Освітлення основи
1 # Ukrainian (uk) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation.
478
479
480
481
482
483
484
+1
View File
@@ -570,6 +570,7 @@
Color bars Thanh màu
Color blindness – Deuteranopia-type Mù màu – loại Deuteranopia
Color blindness – Protanopia-type Mù màu – loại Protanopia
Color blindness – Tritanopia-type Mù màu – loại Tritanopia
Color Burn Đốt màu
Color conversion: Chuyển đổi màu:
Color Dodge Tránh màu
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
570
571
572
573
574
575
576
+1
View File
@@ -443,6 +443,7 @@
Color bars 色彩条
Color blindness – Deuteranopia-type 色盲 – 绿色盲型
Color blindness – Protanopia-type 色盲 – 红色盲型
Color blindness – Tritanopia-type 色盲 – 蓝色盲型
Color Burn 颜色加深
Color conversion: 色彩转换:
Color Dodge 颜色减淡
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -457,6 +457,7 @@
Color bars 色彩條
Color blindness – Deuteranopia-type 色盲 – 綠色盲型
Color blindness – Protanopia-type 色盲 – 紅色盲型
Color blindness – Tritanopia-type 色盲 – 藍色盲型
Color Burn 加深顏色
Color conversion: 色彩轉換:
Color Dodge 加亮顏色
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2591,6 +2591,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
cp("Monitor RGB", "view.proofSetup", json!({"target": "monitorRgb", "proof": true})),
cp("Color blindness – Protanopia-type", "view.proofSetup", json!({"target": "protanopia", "proof": true})),
cp("Color blindness – Deuteranopia-type", "view.proofSetup", json!({"target": "deuteranopia", "proof": true})),
cp("Color blindness – Tritanopia-type", "view.proofSetup", json!({"target": "tritanopia", "proof": true})),
cp("Customize…", "window.panel", json!({"panel": "separations"})),
],
),
+24 -1
View File
@@ -12,13 +12,14 @@ use crate::VectorcraftApp;
use crate::theme::Tokens;
use crate::widgets::{self, dim_label, menu_item};
const PROOF_TARGETS: [(&str, &str); 6] = [
const PROOF_TARGETS: [(&str, &str); 7] = [
("workingCmyk", "Working CMYK"),
("legacyMacRgb", "Legacy Macintosh RGB (Gamma 1.8)"),
("srgb", "Internet Standard RGB (sRGB)"),
("monitorRgb", "Monitor RGB"),
("protanopia", "Color blindness – Protanopia-type"),
("deuteranopia", "Color blindness – Deuteranopia-type"),
("tritanopia", "Color blindness – Tritanopia-type"),
];
fn run(app: &mut VectorcraftApp, id: &str, p: Value) {
@@ -185,3 +186,25 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
run(app, "view.separationsPreview", json!({"on": true}));
}
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
/// The panel's Proof Setup list offers the targets of View › Proof Setup, in the same order and
/// with the same labels, and each one is a target `view.proofSetup` accepts.
#[test]
fn proof_targets_match_the_proof_setup_menu() {
let app = VectorcraftApp::new(Session::new(), Default::default());
let menu: Vec<(String, String)> = crate::menus::menu_entries(&app)
.into_iter()
.filter(|e| e.path == ["View", "Proof Setup"] && e.command.as_deref() == Some("view.proofSetup"))
.map(|e| (e.params["target"].as_str().unwrap_or_default().to_string(), e.label))
.collect();
let panel: Vec<(String, String)> = PROOF_TARGETS.iter().map(|(id, label)| (id.to_string(), label.to_string())).collect();
assert_eq!(menu, panel);
assert!(panel.iter().any(|(id, _)| id == "tritanopia"));
assert!(panel.iter().all(|(id, _)| cms::ProofTarget::parse(id).is_some_and(|t| t.id() == *id)));
}
}
+1
View File
@@ -507,6 +507,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Paint and appearance (M3):**
- **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 and five gradient libraries 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.
- **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. 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}`).
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.
- **Eyedropper sources (#1251):** ordinary and Shift clicks, Shift+Alt appearance append, raster pixels and gradient-stop sampling can read visible locked/template art and sources outside isolation. Sampling leaves the source and selection intact; Alt reverse still uses editable hits, and hidden art stays excluded. Engine/tool/hit regressions cover the editing boundary, color models and one-step Undo/Redo. The general object/image workflow is described in [public HelpX](https://helpx.adobe.com/illustrator/using/tool-techniques/eyedropper-tool.html); the lock/isolation expectations come from the user report and these regressions, without a new full-parity measurement.
- **Sample Color from Screen:** an eyedropper button in the Color Picker and `ui.sampleScreenColor` (which can be given a key in Keyboard Shortcuts) pick a color anywhere on the screen with the system's sampler: `NSColorSampler` on macOS, the EyeDropper API in browsers that have it. The color becomes the fill or the stroke as one undo step, or the Color Picker's color, converted from display sRGB into the working RGB space. Windows, Linux and other browsers have no sampler yet; the button is gray there.
- **Stroke visibility (#1253):** adding paint to a previously unpainted zero-width stroke restores the existing 1 pt default (`PaintDefaults::default`, `Appearance::set_stroke`), for the selected appearance item, the new-art defaults and Swap Fill/Stroke. Positive widths are kept; recoloring a painted zero-width stroke keeps its explicit zero. A real Eyedropper-to-new-shape sequence reproduces the zero-width inheritance. Five engine regressions cover creation, defaults, targeted items, preserved widths and one-step Undo/Redo (`crates/engine/tests/stroke_visibility.rs`).