1mod gamut;
20use gamut::PngImageData;
21
22use std::ops::RangeBounds;
23
24use crate::{
25 chart::{ScaleRangeWithStep, XYChart},
26 delegate_xy_chart_methods,
27 rendable::Rendable,
28 StyleAttr,
29};
30use colorimetry::{
31 illuminant::{CieIlluminant, CCT},
32 observer::Observer,
33 rgb::RgbSpace,
34 xyz::XYZ,
35};
36use svg::{
37 node::element::{path::Data, Group, Path, Text, SVG},
38 Node,
39};
40
41#[derive(Clone)]
42pub struct XYChromaticity {
43 pub(crate) observer: Observer,
44 pub(crate) xy_chart: XYChart,
45}
46
47delegate_xy_chart_methods!(XYChromaticity, xy_chart);
48
49impl XYChromaticity {
50 pub const ANNOTATE_SEP: u32 = 2;
51
52 pub fn new(
53 width_and_height: (u32, u32),
54 ranges: (impl RangeBounds<f64>, impl RangeBounds<f64>),
55 ) -> XYChromaticity {
56 let xy_chart = XYChart::new(width_and_height, ranges);
57 let observer = Observer::default();
58 XYChromaticity { observer, xy_chart }
59 }
60 pub fn set_observer(mut self, observer: Observer) -> Self {
61 self.observer = observer;
62 self
63 }
64
65 pub fn plot_spectral_locus(self, style_attr: Option<StyleAttr>) -> Self {
66 let obs = self.observer;
67 let locus = obs.spectral_locus();
68 self.plot_shape(locus, style_attr)
69 }
70
71 pub fn plot_spectral_locus_ticks(
86 self,
87 range: impl RangeBounds<usize>,
88 step: usize,
89 length: usize,
90 style_attr: Option<StyleAttr>,
91 ) -> Self {
92 let length = length as f64;
93 let this = self;
94 let locus = this.observer.spectral_locus();
95 let to_plot = this.xy_chart.to_plot.clone();
96 let mut data = Data::new();
97 for (xy, angle) in locus.iter_range_with_slope(range, step) {
98 let pxy1 = to_plot(xy);
99 data = data.move_to(pxy1);
100 let pxy2 = (pxy1.0 + length * angle.sin(), pxy1.1 + length * angle.cos());
101 data = data.line_to(pxy2);
102 }
103 this.draw_data("plot", data, style_attr)
104 }
105
106 pub fn plot_spectral_locus_labels(
114 self,
115 range: impl RangeBounds<usize> + Clone,
116 step: usize,
117 distance: usize,
118 style_attr: Option<StyleAttr>,
119 ) -> Self {
120 let d = distance as f64;
121 let mut self_as_mut = self;
122 let locus = self_as_mut.observer.spectral_locus();
123 let range_f64 = (
124 range.start_bound().map(|&x| x as f64),
125 range.end_bound().map(|&x| x as f64),
126 );
127 let values: ScaleRangeWithStep = (range_f64, step as f64).into();
128 let to_plot = self_as_mut.xy_chart.to_plot.clone();
129 let mut group = Group::new();
130 for ((xy, angle), v) in locus.iter_range_with_slope(range, step).zip(values.iter()) {
131 let pxy1 = to_plot(xy);
132 let pxy2 = (pxy1.0 - d * angle.sin(), pxy1.1 - d * angle.cos());
133 let label = format!("{v:.0}");
134 let rotation_angle = -angle.to_degrees();
135 let text = Text::new(label)
136 .set("x", pxy2.0)
137 .set("y", pxy2.1)
138 .set("text-anchor", "middle")
139 .set("dominant-baseline", "before-edge")
140 .set(
141 "transform",
142 format!("rotate({:.3} {:.3} {:.3})", rotation_angle, pxy2.0, pxy2.1),
143 );
144 group.append(text);
145 }
146 style_attr.unwrap_or_default().assign(&mut group);
147 self_as_mut
148 .xy_chart
149 .layers
150 .get_mut("plot")
151 .unwrap()
152 .append(group);
153 self_as_mut
154 }
155
156 pub fn plot_planckian_locus(self, style_attr: Option<StyleAttr>) -> Self {
157 let locus = self.observer.planckian_locus();
158 self.plot_poly_line(locus, style_attr)
159 }
160
161 pub fn planckian_xy_slope_angle(&self, cct: f64) -> ((f64, f64), f64) {
182 let xyz = self.observer.xyz_planckian_locus(cct);
184 let [x, y, z] = xyz.values();
185 let nom = x + y + z;
186 let [dxdt, dydt, dzdt] = self.observer.xyz_planckian_locus_slope(cct).values();
187 let dnom_dt = dxdt + dydt + dzdt;
188
189 let dx_chromaticity = dxdt * nom - x * dnom_dt; let dy_chromaticity = dydt * nom - y * dnom_dt; let angle = dy_chromaticity.atan2(dx_chromaticity);
194 (xyz.chromaticity().to_tuple(), angle)
195 }
196
197 pub fn planckian_uvp_normal_angle(&self, cct: f64) -> ((f64, f64), f64) {
203 let cct_observer = colorimetry::observer::Observer::Cie1931;
205 let xyz = cct_observer.xyz_planckian_locus(cct);
206 let [x, y, z] = xyz.values();
207 let sigma_t = x + 15.0 * y + 3.0 * z;
208 let [dxdt, dydt, dzdt] = cct_observer.xyz_planckian_locus_slope(cct).values();
209 let dsigma_dt = dxdt + 15.0 * dydt + 3.0 * dzdt;
210
211 let du_dt = 4.0 * dxdt * sigma_t - 4.0 * x * dsigma_dt; let dv_dt = 9.0 * dydt * sigma_t - 9.0 * y * dsigma_dt; let angle = dv_dt.atan2(du_dt);
216 (
217 xyz.chromaticity().to_tuple(),
218 angle + std::f64::consts::FRAC_PI_2,
219 )
220 }
221
222 pub fn cct_transform(&self, cct_duv: (f64, f64)) -> Result<(f64, f64), colorimetry::Error> {
232 let (cct, duv) = cct_duv;
233 let xyz: XYZ = CCT::new(cct, duv)?.try_into()?;
234 let xy = xyz.chromaticity().to_tuple();
235 Ok((self.xy_chart.to_plot)(xy))
236 }
237
238 pub fn plot_ansi_step7(mut self, rgb_space: RgbSpace, style_attr: Option<StyleAttr>) -> Self {
241 const DATA: &[(&str, (i32, i32), f64)] = &[
243 ("2200", (2238, 102), 0.0000),
244 ("2500", (2460, 120), 0.0000),
245 ("2700", (2725, 145), 0.0000),
246 ("3000", (3045, 175), 0.0001),
247 ("3500", (3465, 245), 0.0005),
248 ("4000", (3985, 275), 0.0010),
249 ("4500", (4503, 243), 0.0015),
250 ("5000", (5029, 283), 0.0020),
251 ("5700", (5667, 355), 0.0025),
252 ("6500", (6532, 510), 0.0031),
253 ];
254
255 let duv = |cct: i32| {
256 if cct < 2780 {
257 0.0
258 } else {
259 let r = 1.0 / cct as f64;
260 57_700.0 * r * r - 44.6 * r + 0.00854
261 }
262 };
263 const DUV_TOLERANCE: f64 = 0.006; let mut ansi = Group::new();
266 style_attr.unwrap_or_default().assign(&mut ansi);
267 for &(_, (cct, tol), duv_target) in DATA {
268 let mut data = Data::new();
269
270 let (px0, py0) = self
271 .cct_transform(((cct - tol) as f64, duv(cct - tol) - DUV_TOLERANCE))
272 .unwrap();
273 let (px1, py1) = self
274 .cct_transform(((cct - tol) as f64, duv(cct - tol) + DUV_TOLERANCE))
275 .unwrap();
276 let (px2, py2) = self
277 .cct_transform(((cct + tol) as f64, duv(cct + tol) + DUV_TOLERANCE))
278 .unwrap();
279 let (px3, py3) = self
280 .cct_transform(((cct + tol) as f64, duv(cct + tol) - DUV_TOLERANCE))
281 .unwrap();
282
283 data = data
284 .move_to((px0, py0))
285 .line_to((px1, py1))
286 .line_to((px2, py2))
287 .line_to((px3, py3))
288 .close();
289 let xyz: XYZ = CCT::new(cct as f64, duv_target)
290 .unwrap()
291 .try_into()
292 .unwrap();
293 let [r, g, b]: [u8; 3] = xyz.rgb(rgb_space).compress().into();
294 let path = Path::new()
295 .set("d", data.clone())
296 .set("style", format!("fill: rgb({r:.0}, {g:.0}, {b:.0})"));
297 ansi.append(path);
298 }
299 self.xy_chart.layers.get_mut("plot").unwrap().append(ansi);
300 self
301 }
302
303 pub fn planckian_xy_normal_angle(&self, cct: f64) -> ((f64, f64), f64) {
306 let (xy, slope_angle) = self.planckian_xy_slope_angle(cct);
307 (xy, slope_angle + std::f64::consts::FRAC_PI_2)
308 }
309
310 pub fn plot_planckian_locus_ticks(
316 self,
317 values: impl IntoIterator<Item = u32>,
318 length: usize,
319 style_attr: Option<StyleAttr>,
320 ) -> Self {
321 let mut data = Data::new();
322 let to_plot = self.xy_chart.to_plot.clone();
323 for cct in values {
324 let (xy, angle) = self.planckian_xy_normal_angle(cct as f64);
325 let (px, py) = to_plot(xy);
326 let pdx = length as f64 * angle.cos();
327 let pdy = length as f64 * angle.sin();
328 data = data
329 .move_to((px - pdx, py + pdy)) .line_to((px + pdx, py - pdy)); }
332 self.draw_data("plot", data, style_attr)
333 }
334
335 pub fn plot_planckian_locus_labels(
339 mut self,
340 values: impl IntoIterator<Item = u32>,
341 distance: usize,
342 style_attr: Option<StyleAttr>,
343 ) -> Self {
344 let mut planckian_labels = Group::new();
345 let to_plot = self.xy_chart.to_plot.clone();
346 for cct in values {
347 let (xy, angle) = self.planckian_xy_normal_angle(cct as f64);
348 let (px, py) = to_plot(xy);
349 let pdx = distance as f64 * angle.cos();
350 let pdy = distance as f64 * angle.sin();
351
352 let px2 = px - pdx;
353 let py2 = py + pdy;
354 let text = Text::new(format!("{}", cct / 100))
355 .set("x", px2)
356 .set("y", py2)
357 .set("text-anchor", "end")
358 .set("dominant-baseline", "middle")
359 .set(
360 "transform",
361 format!("rotate({:.3} {px2:.3} {py2:.3}) ", -angle.to_degrees()),
362 );
363 planckian_labels.append(text);
364 }
365 style_attr.unwrap_or_default().assign(&mut planckian_labels);
366 self.xy_chart
367 .layers
368 .get_mut("plot")
369 .unwrap()
370 .append(planckian_labels);
371 self
372 }
373
374 pub fn plot_rgb_gamut(self, rgb_space: RgbSpace, style_attr: Option<StyleAttr>) -> Self {
378 let gamut_fill = PngImageData::from_rgb_space(
384 self.observer,
385 rgb_space,
386 self.xy_chart.to_plot.clone(),
387 self.xy_chart.to_world.clone(),
388 );
389 self.plot_image(gamut_fill, style_attr)
390 }
391
392 #[allow(unused)]
394 pub fn annotate_white_points(
395 &mut self,
396 point: impl IntoIterator<Item = (CieIlluminant, (i32, i32))>,
397 ) -> &mut Self {
398 todo!()
399 }
400}
401
402impl Rendable for XYChromaticity {
404 fn render(&self) -> SVG {
405 self.xy_chart.render()
406 }
407
408 fn view_parameters(&self) -> crate::view::ViewParameters {
409 self.xy_chart.view_parameters()
410 }
411
412 fn set_view_parameters(&mut self, view_box: crate::view::ViewParameters) {
413 self.xy_chart.set_view_parameters(view_box);
414 }
415}