Skip to main content

r/interpreter/builtins/graphics/
color.rs

1//! Color palette generation functions — `rainbow()`, `heat.colors()`,
2//! `terrain.colors()`, `topo.colors()`, `cm.colors()`, `gray.colors()`,
3//! `hsv()`, `hcl()`, and `colorRampPalette()`.
4
5use crate::interpreter::builtins::CallArgs;
6use crate::interpreter::environment::Environment;
7use crate::interpreter::value::*;
8use crate::interpreter::BuiltinContext;
9use crate::parser::ast::{Arg, Expr, Param};
10use minir_macros::{builtin, interpreter_builtin};
11
12// region: Color space conversions
13
14/// Convert HSV (h in [0,1], s in [0,1], v in [0,1]) to RGB (each in [0,1]).
15fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (f64, f64, f64) {
16    if s <= 0.0 {
17        return (v, v, v);
18    }
19    // Wrap h to [0,1)
20    let h = h - h.floor();
21    let h6 = h * 6.0;
22    let sector = h6.floor();
23    let f = h6 - sector;
24    let p = v * (1.0 - s);
25    let q = v * (1.0 - s * f);
26    let t = v * (1.0 - s * (1.0 - f));
27
28    let sector_i = sector as i32;
29    match sector_i {
30        0 => (v, t, p),
31        1 => (q, v, p),
32        2 => (p, v, t),
33        3 => (p, q, v),
34        4 => (t, p, v),
35        _ => (v, p, q), // sector 5
36    }
37}
38
39/// Convert HCL (Hue in degrees, Chroma, Luminance) to RGB (each in [0,1]).
40/// Uses the CIE LCH -> Lab -> XYZ -> sRGB pipeline.
41fn hcl_to_rgb(h_deg: f64, c: f64, l: f64) -> (f64, f64, f64) {
42    // LCH -> Lab
43    let h_rad = h_deg.to_radians();
44    let lab_a = c * h_rad.cos();
45    let lab_b = c * h_rad.sin();
46
47    // Lab -> XYZ (D65 illuminant)
48    const XN: f64 = 0.950_470;
49    const YN: f64 = 1.0;
50    const ZN: f64 = 1.088_830;
51    const KAPPA: f64 = 903.296_3; // (29/3)^3
52    const EPSILON: f64 = 0.008_856; // (6/29)^3
53
54    let fy = (l + 16.0) / 116.0;
55    let fx = fy + lab_a / 500.0;
56    let fz = fy - lab_b / 200.0;
57
58    let x = if fx.powi(3) > EPSILON {
59        XN * fx.powi(3)
60    } else {
61        XN * (116.0 * fx - 16.0) / KAPPA
62    };
63    let y = if l > KAPPA * EPSILON {
64        YN * fy.powi(3)
65    } else {
66        YN * l / KAPPA
67    };
68    let z = if fz.powi(3) > EPSILON {
69        ZN * fz.powi(3)
70    } else {
71        ZN * (116.0 * fz - 16.0) / KAPPA
72    };
73
74    // XYZ -> linear sRGB (D65)
75    let rl = 3.240_479_f64 * x - 1.537_150 * y - 0.498_535 * z;
76    let gl = -0.969_256_f64 * x + 1.875_992 * y + 0.041_556 * z;
77    let bl = 0.055_648_f64 * x - 0.204_043 * y + 1.057_311 * z;
78
79    // Linear -> sRGB gamma
80    fn gamma(u: f64) -> f64 {
81        if u <= 0.003_130_8 {
82            12.92 * u
83        } else {
84            1.055 * u.powf(1.0 / 2.4) - 0.055
85        }
86    }
87
88    (
89        gamma(rl).clamp(0.0, 1.0),
90        gamma(gl).clamp(0.0, 1.0),
91        gamma(bl).clamp(0.0, 1.0),
92    )
93}
94
95/// Parse a hex color string like "#RRGGBB" or "#RRGGBBAA" into (r, g, b, a) with values in [0,1].
96fn parse_hex_color(s: &str) -> Result<(f64, f64, f64, f64), RError> {
97    let s = s.trim_start_matches('#');
98    let (r, g, b, a) = match s.len() {
99        6 => {
100            let r = u8::from_str_radix(&s[0..2], 16);
101            let g = u8::from_str_radix(&s[2..4], 16);
102            let b = u8::from_str_radix(&s[4..6], 16);
103            match (r, g, b) {
104                (Ok(r), Ok(g), Ok(b)) => (r, g, b, 255u8),
105                _ => {
106                    return Err(RError::new(
107                        RErrorKind::Argument,
108                        format!("invalid hex color: #{s}"),
109                    ))
110                }
111            }
112        }
113        8 => {
114            let r = u8::from_str_radix(&s[0..2], 16);
115            let g = u8::from_str_radix(&s[2..4], 16);
116            let b = u8::from_str_radix(&s[4..6], 16);
117            let a = u8::from_str_radix(&s[6..8], 16);
118            match (r, g, b, a) {
119                (Ok(r), Ok(g), Ok(b), Ok(a)) => (r, g, b, a),
120                _ => {
121                    return Err(RError::new(
122                        RErrorKind::Argument,
123                        format!("invalid hex color: #{s}"),
124                    ))
125                }
126            }
127        }
128        _ => {
129            return Err(RError::new(
130                RErrorKind::Argument,
131                format!("invalid hex color: #{s} (expected 6 or 8 hex digits)"),
132            ))
133        }
134    };
135    Ok((
136        f64::from(r) / 255.0,
137        f64::from(g) / 255.0,
138        f64::from(b) / 255.0,
139        f64::from(a) / 255.0,
140    ))
141}
142
143/// Format (r, g, b, a) each in [0,1] as a hex color string.
144/// If alpha is 1.0, returns "#RRGGBB"; otherwise "#RRGGBBAA".
145fn rgb_to_hex(r: f64, g: f64, b: f64, a: f64) -> String {
146    let ri = (r.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
147    let gi = (g.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
148    let bi = (b.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
149    if (a - 1.0).abs() < 1e-10 {
150        format!("#{:02X}{:02X}{:02X}", ri, gi, bi)
151    } else {
152        let ai = (a.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
153        format!("#{:02X}{:02X}{:02X}{:02X}", ri, gi, bi, ai)
154    }
155}
156
157// endregion
158
159// region: Helper to extract double from value
160
161/// Extract a scalar f64 from an RValue, returning a default if NULL or missing.
162fn double_scalar(val: Option<&RValue>, default: f64) -> f64 {
163    match val {
164        Some(RValue::Null) | None => default,
165        Some(v) => v
166            .as_vector()
167            .and_then(|v| v.as_double_scalar())
168            .unwrap_or(default),
169    }
170}
171
172/// Extract n (first positional arg, required) as a non-negative integer.
173fn extract_n(args: &CallArgs) -> Result<usize, RError> {
174    let n_val = args.value("n", 0).ok_or_else(|| {
175        RError::new(
176            RErrorKind::Argument,
177            "argument 'n' is missing, with no default".to_string(),
178        )
179    })?;
180    let n = n_val
181        .as_vector()
182        .and_then(|v| v.as_integer_scalar())
183        .ok_or_else(|| {
184            RError::new(
185                RErrorKind::Argument,
186                "'n' must be a positive integer".to_string(),
187            )
188        })?;
189    if n < 0 {
190        return Err(RError::new(
191            RErrorKind::Argument,
192            "'n' must be a non-negative integer".to_string(),
193        ));
194    }
195    Ok(n as usize)
196}
197
198// endregion
199
200// region: hsv() builtin
201
202/// Convert HSV color values to hex color strings.
203///
204/// Vectorized over h, s, v, alpha — all inputs are recycled to the length
205/// of the longest input.
206///
207/// @param h hue, values in [0,1] (default 0)
208/// @param s saturation, values in [0,1] (default 1)
209/// @param v value (brightness), values in [0,1] (default 1)
210/// @param alpha transparency, values in [0,1] (default 1)
211/// @return character vector of hex color strings
212#[builtin(namespace = "grDevices")]
213fn builtin_hsv(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
214    let ca = CallArgs::new(args, named);
215
216    // Get vectorized inputs
217    let h_vec = extract_doubles(&ca, "h", 0, 0.0);
218    let s_vec = extract_doubles(&ca, "s", 1, 1.0);
219    let v_vec = extract_doubles(&ca, "v", 2, 1.0);
220    let alpha_vec = extract_doubles(&ca, "alpha", 3, 1.0);
221
222    let n = h_vec
223        .len()
224        .max(s_vec.len())
225        .max(v_vec.len())
226        .max(alpha_vec.len());
227
228    let mut result = Vec::with_capacity(n);
229    for i in 0..n {
230        let h = h_vec[i % h_vec.len()];
231        let s = s_vec[i % s_vec.len()];
232        let v = v_vec[i % v_vec.len()];
233        let a = alpha_vec[i % alpha_vec.len()];
234        let (r, g, b) = hsv_to_rgb(h, s, v);
235        result.push(Some(rgb_to_hex(r, g, b, a)));
236    }
237
238    Ok(RValue::vec(Vector::Character(result.into())))
239}
240
241/// Extract a vector of doubles from named/positional arg, defaulting to a single-element vector.
242fn extract_doubles(ca: &CallArgs, name: &str, pos: usize, default: f64) -> Vec<f64> {
243    match ca.value(name, pos) {
244        Some(RValue::Vector(rv)) => {
245            let doubles = rv.to_doubles();
246            if doubles.is_empty() {
247                vec![default]
248            } else {
249                doubles.into_iter().map(|d| d.unwrap_or(default)).collect()
250            }
251        }
252        Some(RValue::Null) | None => vec![default],
253        _ => vec![default],
254    }
255}
256
257// endregion
258
259// region: hcl() builtin
260
261/// Convert HCL (Hue-Chroma-Luminance) color values to hex color strings.
262///
263/// Vectorized over h, c, l, alpha — all inputs are recycled to the length
264/// of the longest input.
265///
266/// @param h hue in degrees [0,360] (default 0)
267/// @param c chroma (default 35)
268/// @param l luminance in [0,100] (default 85)
269/// @param alpha transparency in [0,1] (default 1)
270/// @return character vector of hex color strings
271#[builtin(namespace = "grDevices")]
272fn builtin_hcl(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
273    let ca = CallArgs::new(args, named);
274
275    let h_vec = extract_doubles(&ca, "h", 0, 0.0);
276    let c_vec = extract_doubles(&ca, "c", 1, 35.0);
277    let l_vec = extract_doubles(&ca, "l", 2, 85.0);
278    let alpha_vec = extract_doubles(&ca, "alpha", 3, 1.0);
279
280    let n = h_vec
281        .len()
282        .max(c_vec.len())
283        .max(l_vec.len())
284        .max(alpha_vec.len());
285
286    let mut result = Vec::with_capacity(n);
287    for i in 0..n {
288        let h = h_vec[i % h_vec.len()];
289        let c = c_vec[i % c_vec.len()];
290        let l = l_vec[i % l_vec.len()];
291        let a = alpha_vec[i % alpha_vec.len()];
292        let (r, g, b) = hcl_to_rgb(h, c, l);
293        result.push(Some(rgb_to_hex(r, g, b, a)));
294    }
295
296    Ok(RValue::vec(Vector::Character(result.into())))
297}
298
299// endregion
300
301// region: rainbow()
302
303/// Generate a rainbow color palette using HSV color space.
304///
305/// @param n number of colors to generate
306/// @param s saturation (default 1)
307/// @param v value/brightness (default 1)
308/// @param start starting hue in [0,1] (default 0)
309/// @param end ending hue (default max(1, n-1)/n)
310/// @param alpha transparency in [0,1] (default 1)
311/// @return character vector of hex color strings
312#[builtin(namespace = "grDevices", min_args = 1)]
313fn builtin_rainbow(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
314    let ca = CallArgs::new(args, named);
315    let n = extract_n(&ca)?;
316
317    if n == 0 {
318        return Ok(RValue::vec(Vector::Character(
319            Vec::<Option<String>>::new().into(),
320        )));
321    }
322
323    let s = double_scalar(ca.value("s", 1), 1.0);
324    let v = double_scalar(ca.value("v", 2), 1.0);
325    let start = double_scalar(ca.value("start", 3), 0.0);
326    let default_end = if n > 1 {
327        (n as f64 - 1.0) / n as f64
328    } else {
329        1.0
330    };
331    let end = double_scalar(ca.value("end", 4), default_end);
332    let alpha = double_scalar(ca.value("alpha", 5), 1.0);
333
334    let mut result = Vec::with_capacity(n);
335    for i in 0..n {
336        let h = if n == 1 {
337            start
338        } else {
339            start + (end - start) * (i as f64) / (n as f64 - 1.0)
340        };
341        let (r, g, b) = hsv_to_rgb(h, s, v);
342        result.push(Some(rgb_to_hex(r, g, b, alpha)));
343    }
344
345    Ok(RValue::vec(Vector::Character(result.into())))
346}
347
348// endregion
349
350// region: heat.colors()
351
352/// Generate a heat map color palette (red through yellow to white).
353///
354/// @param n number of colors to generate
355/// @param alpha transparency in [0,1] (default 1)
356/// @return character vector of hex color strings
357#[builtin(name = "heat.colors", namespace = "grDevices", min_args = 1)]
358fn builtin_heat_colors(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
359    let ca = CallArgs::new(args, named);
360    let n = extract_n(&ca)?;
361
362    if n == 0 {
363        return Ok(RValue::vec(Vector::Character(
364            Vec::<Option<String>>::new().into(),
365        )));
366    }
367
368    let alpha = double_scalar(ca.value("alpha", 1), 1.0);
369
370    // R's heat.colors: first 1/4 are pure reds with increasing intensity,
371    // next 1/4 go from red to yellow, then yellow to white.
372    // Simplified: n colors from red (h=0) through yellow (h=1/6) with
373    // increasing value and saturation changes.
374    // Actual R implementation uses hsv:
375    //   j <- n %/% 4; i <- 1:n
376    //   hsv(h = 1/6 * (i-1)/(j-1) clamped, s = 1 - (i-1)/n clamped, v = 1)
377    // More precisely from R source:
378    //   heat.colors(n) uses:
379    //     h = (1:n - 1) / (3 * n)   # hue from 0 to 1/3
380    //     s = 1 - (1:n - 1) / (n - 1) when n > 1  # saturation from 1 to 0
381    //     v = 1
382    // Actually in R source, heat.colors is defined as:
383    //   j <- n %/% 4
384    //   i <- 1:n
385    //   c(rainbow(n - j, start = 0, end = 1/6),
386    //     if (j > 0) hsv(h = 1/6, s = seq.int(1 - 1/(2*j), 1/(2*j), length.out = j), v = 1))
387    let j = n / 4;
388    let nrainbow = n - j;
389
390    let mut result = Vec::with_capacity(n);
391
392    // First part: rainbow from red to yellow
393    for i in 0..nrainbow {
394        let h = if nrainbow == 1 {
395            0.0
396        } else {
397            (1.0 / 6.0) * (i as f64) / (nrainbow as f64 - 1.0)
398        };
399        let (r, g, b) = hsv_to_rgb(h, 1.0, 1.0);
400        result.push(Some(rgb_to_hex(r, g, b, alpha)));
401    }
402
403    // Second part: yellow to white (decreasing saturation at h=1/6)
404    if j > 0 {
405        for i in 0..j {
406            let s = if j == 1 {
407                0.5
408            } else {
409                let start_s = 1.0 - 1.0 / (2.0 * j as f64);
410                let end_s = 1.0 / (2.0 * j as f64);
411                start_s + (end_s - start_s) * (i as f64) / (j as f64 - 1.0)
412            };
413            let (r, g, b) = hsv_to_rgb(1.0 / 6.0, s, 1.0);
414            result.push(Some(rgb_to_hex(r, g, b, alpha)));
415        }
416    }
417
418    Ok(RValue::vec(Vector::Character(result.into())))
419}
420
421// endregion
422
423// region: terrain.colors()
424
425/// Generate a terrain color palette (green through yellow/brown to white).
426///
427/// @param n number of colors to generate
428/// @param alpha transparency in [0,1] (default 1)
429/// @return character vector of hex color strings
430#[builtin(name = "terrain.colors", namespace = "grDevices", min_args = 1)]
431fn builtin_terrain_colors(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
432    let ca = CallArgs::new(args, named);
433    let n = extract_n(&ca)?;
434
435    if n == 0 {
436        return Ok(RValue::vec(Vector::Character(
437            Vec::<Option<String>>::new().into(),
438        )));
439    }
440
441    let alpha = double_scalar(ca.value("alpha", 1), 1.0);
442
443    // R's terrain.colors uses:
444    //   k <- n %/% 2
445    //   h <- c(4/12, 2/12, 0/12)   # green -> yellow -> red-ish
446    //   terrain(n):
447    //     j <- n %/% 3
448    //     c(hsv(h=2/6 to 1/6, s=1, v=0.65 to 0.9), hsv(h=1/6, s=1 to 0, v=0.9 to 0.95), grey(0.95, 1.0))
449    // Simplified implementation matching R's output:
450    let j = n / 3;
451    let k = n - 2 * j;
452
453    let mut result = Vec::with_capacity(n);
454
455    // First third: green to yellow (h: 2/6 -> 1/6, s=1, v=0.65 -> 0.9)
456    for i in 0..j {
457        let t = if j <= 1 {
458            0.0
459        } else {
460            i as f64 / (j as f64 - 1.0)
461        };
462        let h = 2.0 / 6.0 + (1.0 / 6.0 - 2.0 / 6.0) * t;
463        let v = 0.65 + (0.9 - 0.65) * t;
464        let (r, g, b) = hsv_to_rgb(h, 1.0, v);
465        result.push(Some(rgb_to_hex(r, g, b, alpha)));
466    }
467
468    // Second third: yellow to near-white (h=1/6, s: 1->0, v: 0.9->0.95)
469    for i in 0..j {
470        let t = if j <= 1 {
471            0.0
472        } else {
473            i as f64 / (j as f64 - 1.0)
474        };
475        let s = 1.0 - t;
476        let v = 0.9 + (0.95 - 0.9) * t;
477        let (r, g, b) = hsv_to_rgb(1.0 / 6.0, s, v);
478        result.push(Some(rgb_to_hex(r, g, b, alpha)));
479    }
480
481    // Final third: grays from 0.95 to 1.0
482    for i in 0..k {
483        let t = if k <= 1 {
484            0.0
485        } else {
486            i as f64 / (k as f64 - 1.0)
487        };
488        let grey = 0.95 + 0.05 * t;
489        result.push(Some(rgb_to_hex(grey, grey, grey, alpha)));
490    }
491
492    Ok(RValue::vec(Vector::Character(result.into())))
493}
494
495// endregion
496
497// region: topo.colors()
498
499/// Generate a topographic color palette (blue through green to yellow).
500///
501/// @param n number of colors to generate
502/// @param alpha transparency in [0,1] (default 1)
503/// @return character vector of hex color strings
504#[builtin(name = "topo.colors", namespace = "grDevices", min_args = 1)]
505fn builtin_topo_colors(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
506    let ca = CallArgs::new(args, named);
507    let n = extract_n(&ca)?;
508
509    if n == 0 {
510        return Ok(RValue::vec(Vector::Character(
511            Vec::<Option<String>>::new().into(),
512        )));
513    }
514
515    let alpha = double_scalar(ca.value("alpha", 1), 1.0);
516
517    // R's topo.colors uses a fixed set of anchor colors interpolated:
518    // From R source: c(hsv(h=43/60, s=1, v=seq(0.4,1,...)),
519    //                  hsv(h=seq(43,31)/60, s=1, v=1),
520    //                  hsv(h=seq(31,23)/60, s=1, v=1),
521    //                  hsv(h=seq(23,11)/60, s=seq(1,0,...), v=1))
522    // Simplified: interpolate between blue -> cyan -> green -> yellow
523    let anchors: [(f64, f64, f64); 5] = [
524        (0.55, 0.0, 1.0), // deep blue (dark)
525        (0.55, 1.0, 1.0), // blue
526        (0.43, 1.0, 1.0), // cyan
527        (0.25, 1.0, 1.0), // green
528        (0.17, 0.0, 1.0), // light yellow
529    ];
530
531    let mut result = Vec::with_capacity(n);
532    for i in 0..n {
533        let t = if n == 1 {
534            0.0
535        } else {
536            i as f64 / (n as f64 - 1.0)
537        };
538        let pos = t * (anchors.len() - 1) as f64;
539        let idx = (pos.floor() as usize).min(anchors.len() - 2);
540        let frac = pos - idx as f64;
541
542        let (h1, s1, v1) = anchors[idx];
543        let (h2, s2, v2) = anchors[idx + 1];
544        let h = h1 + (h2 - h1) * frac;
545        let s = s1 + (s2 - s1) * frac;
546        let v = v1 + (v2 - v1) * frac;
547
548        let (r, g, b) = hsv_to_rgb(h, s, v);
549        result.push(Some(rgb_to_hex(r, g, b, alpha)));
550    }
551
552    Ok(RValue::vec(Vector::Character(result.into())))
553}
554
555// endregion
556
557// region: cm.colors()
558
559/// Generate a cyan-magenta diverging color palette.
560///
561/// @param n number of colors to generate
562/// @param alpha transparency in [0,1] (default 1)
563/// @return character vector of hex color strings
564#[builtin(name = "cm.colors", namespace = "grDevices", min_args = 1)]
565fn builtin_cm_colors(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
566    let ca = CallArgs::new(args, named);
567    let n = extract_n(&ca)?;
568
569    if n == 0 {
570        return Ok(RValue::vec(Vector::Character(
571            Vec::<Option<String>>::new().into(),
572        )));
573    }
574
575    let alpha = double_scalar(ca.value("alpha", 1), 1.0);
576
577    // R's cm.colors: cyan (#00FFFF) -> white (#FFFFFF) -> magenta (#FF00FF)
578    // i <- 1:n; even <- n %% 2 == 0
579    // For odd n, the middle color is white.
580    // Lower half: cyan to white; upper half: white to magenta.
581    let mut result = Vec::with_capacity(n);
582    for i in 0..n {
583        let t = if n == 1 {
584            0.5
585        } else {
586            i as f64 / (n as f64 - 1.0)
587        };
588        let (r, g, b) = if t < 0.5 {
589            // Cyan to white
590            let s = t * 2.0; // 0 at i=0, 1 at midpoint
591            (s, 1.0, 1.0)
592        } else {
593            // White to magenta
594            let s = (t - 0.5) * 2.0; // 0 at midpoint, 1 at i=n-1
595            (1.0, 1.0 - s, 1.0)
596        };
597        result.push(Some(rgb_to_hex(r, g, b, alpha)));
598    }
599
600    Ok(RValue::vec(Vector::Character(result.into())))
601}
602
603// endregion
604
605// region: gray.colors()
606
607/// Generate a gray-scale color palette.
608///
609/// @param n number of colors to generate
610/// @param start starting gray level in [0,1] (default 0.3)
611/// @param end ending gray level in [0,1] (default 0.9)
612/// @param gamma gamma correction (default 2.2)
613/// @param alpha transparency in [0,1] (default 1)
614/// @return character vector of hex color strings
615#[builtin(name = "gray.colors", namespace = "grDevices", min_args = 1, names = ["grey.colors"])]
616fn builtin_gray_colors(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
617    let ca = CallArgs::new(args, named);
618    let n = extract_n(&ca)?;
619
620    if n == 0 {
621        return Ok(RValue::vec(Vector::Character(
622            Vec::<Option<String>>::new().into(),
623        )));
624    }
625
626    let start = double_scalar(ca.value("start", 1), 0.3);
627    let end = double_scalar(ca.value("end", 2), 0.9);
628    let gamma = double_scalar(ca.value("gamma", 3), 2.2);
629    let alpha = double_scalar(ca.value("alpha", 4), 1.0);
630
631    let mut result = Vec::with_capacity(n);
632    for i in 0..n {
633        let t = if n == 1 {
634            0.0
635        } else {
636            i as f64 / (n as f64 - 1.0)
637        };
638        // Apply gamma correction like R does
639        let grey = (start + (end - start) * t).powf(1.0 / gamma);
640        let grey = grey.clamp(0.0, 1.0);
641        result.push(Some(rgb_to_hex(grey, grey, grey, alpha)));
642    }
643
644    Ok(RValue::vec(Vector::Character(result.into())))
645}
646
647// endregion
648
649// region: grey() / gray()
650
651/// Convert grey levels to hex color strings.
652///
653/// Vectorized: accepts a numeric vector of levels in [0,1] where 0=black, 1=white.
654///
655/// @param level numeric vector of grey levels in [0,1]
656/// @param alpha optional transparency in [0,1] (default 1, fully opaque)
657/// @return character vector of hex color strings
658#[builtin(name = "grey", namespace = "grDevices", min_args = 1, names = ["gray"])]
659fn builtin_grey(args: &[RValue], named: &[(String, RValue)]) -> Result<RValue, RError> {
660    let ca = CallArgs::new(args, named);
661    let levels = extract_doubles(&ca, "level", 0, 0.0);
662    let alpha_val = ca.value("alpha", 1);
663    let alpha = match alpha_val {
664        Some(RValue::Null) | None => None,
665        Some(v) => Some(
666            v.as_vector()
667                .and_then(|rv| rv.as_double_scalar())
668                .unwrap_or(1.0),
669        ),
670    };
671
672    let mut result = Vec::with_capacity(levels.len());
673    for level in &levels {
674        let g = level.clamp(0.0, 1.0);
675        let a = alpha.unwrap_or(1.0);
676        result.push(Some(rgb_to_hex(g, g, g, a)));
677    }
678
679    Ok(RValue::vec(Vector::Character(result.into())))
680}
681
682// endregion
683
684// region: colorRampPalette()
685
686/// Create a color interpolation function from a vector of colors.
687///
688/// Returns a function that takes an integer n and produces n interpolated
689/// colors between the input colors.
690///
691/// @param colors character vector of hex color strings
692/// @return a function(n) that generates n interpolated colors
693#[interpreter_builtin(name = "colorRampPalette", namespace = "grDevices", min_args = 1)]
694fn interp_color_ramp_palette(
695    args: &[RValue],
696    named: &[(String, RValue)],
697    context: &BuiltinContext,
698) -> Result<RValue, RError> {
699    let ca = CallArgs::new(args, named);
700
701    // Extract the colors argument — must be a character vector of hex colors
702    let colors_val = ca.value("colors", 0).ok_or_else(|| {
703        RError::new(
704            RErrorKind::Argument,
705            "argument 'colors' is missing".to_string(),
706        )
707    })?;
708
709    let colors_vec = match colors_val {
710        RValue::Vector(rv) => match &rv.inner {
711            Vector::Character(cv) => cv.iter().filter_map(|s| s.clone()).collect::<Vec<String>>(),
712            _ => {
713                return Err(RError::new(
714                    RErrorKind::Argument,
715                    "'colors' must be a character vector of color values".to_string(),
716                ))
717            }
718        },
719        _ => {
720            return Err(RError::new(
721                RErrorKind::Argument,
722                "'colors' must be a character vector of color values".to_string(),
723            ))
724        }
725    };
726
727    if colors_vec.len() < 2 {
728        return Err(RError::new(
729            RErrorKind::Argument,
730            "colorRampPalette requires at least 2 colors".to_string(),
731        ));
732    }
733
734    // Store the colors in a closure environment so the returned function can use them
735    let env = context.env();
736    let closure_env = Environment::new_child(env);
737    closure_env.set(
738        ".CRP_COLORS".to_string(),
739        RValue::vec(Vector::Character(
740            colors_vec
741                .into_iter()
742                .map(Some)
743                .collect::<Vec<Option<String>>>()
744                .into(),
745        )),
746    );
747
748    // Build: function(n) .colorRampInterp(.CRP_COLORS, n)
749    let body = Expr::Call {
750        func: Box::new(Expr::Symbol(".colorRampInterp".to_string())),
751        span: None,
752        args: vec![
753            Arg {
754                name: None,
755                value: Some(Expr::Symbol(".CRP_COLORS".to_string())),
756            },
757            Arg {
758                name: None,
759                value: Some(Expr::Symbol("n".to_string())),
760            },
761        ],
762    };
763
764    let params = vec![Param {
765        name: "n".to_string(),
766        default: None,
767        is_dots: false,
768    }];
769
770    Ok(RValue::Function(RFunction::Closure {
771        params,
772        body,
773        env: closure_env,
774    }))
775}
776
777/// Internal helper: interpolate between a vector of hex colors to produce n colors.
778///
779/// This is the hidden builtin that colorRampPalette closures call.
780///
781/// @param colors character vector of hex colors
782/// @param n number of output colors
783/// @return character vector of n interpolated hex colors
784#[builtin(name = ".colorRampInterp", namespace = "grDevices", min_args = 2)]
785fn builtin_color_ramp_interp(
786    args: &[RValue],
787    _named: &[(String, RValue)],
788) -> Result<RValue, RError> {
789    let colors_val = args.first().ok_or_else(|| {
790        RError::new(
791            RErrorKind::Argument,
792            "missing 'colors' argument".to_string(),
793        )
794    })?;
795    let n_val = args
796        .get(1)
797        .ok_or_else(|| RError::new(RErrorKind::Argument, "missing 'n' argument".to_string()))?;
798
799    let colors: Vec<String> = match colors_val {
800        RValue::Vector(rv) => match &rv.inner {
801            Vector::Character(cv) => cv.iter().filter_map(|s| s.clone()).collect(),
802            _ => {
803                return Err(RError::new(
804                    RErrorKind::Argument,
805                    "colors must be a character vector".to_string(),
806                ))
807            }
808        },
809        _ => {
810            return Err(RError::new(
811                RErrorKind::Argument,
812                "colors must be a character vector".to_string(),
813            ))
814        }
815    };
816
817    let n = n_val
818        .as_vector()
819        .and_then(|v| v.as_integer_scalar())
820        .ok_or_else(|| {
821            RError::new(
822                RErrorKind::Argument,
823                "'n' must be a positive integer".to_string(),
824            )
825        })?;
826
827    if n < 0 {
828        return Err(RError::new(
829            RErrorKind::Argument,
830            "'n' must be a non-negative integer".to_string(),
831        ));
832    }
833    let n = n as usize;
834
835    if n == 0 {
836        return Ok(RValue::vec(Vector::Character(
837            Vec::<Option<String>>::new().into(),
838        )));
839    }
840
841    // Parse all anchor colors into RGBA
842    let mut anchors = Vec::with_capacity(colors.len());
843    for c in &colors {
844        anchors.push(parse_hex_color(c)?);
845    }
846
847    let mut result = Vec::with_capacity(n);
848    for i in 0..n {
849        let t = if n == 1 {
850            0.0
851        } else {
852            i as f64 / (n as f64 - 1.0)
853        };
854        let pos = t * (anchors.len() - 1) as f64;
855        let idx = (pos.floor() as usize).min(anchors.len() - 2);
856        let frac = pos - idx as f64;
857
858        let (r1, g1, b1, a1) = anchors[idx];
859        let (r2, g2, b2, a2) = anchors[idx + 1];
860        let r = r1 + (r2 - r1) * frac;
861        let g = g1 + (g2 - g1) * frac;
862        let b = b1 + (b2 - b1) * frac;
863        let a = a1 + (a2 - a1) * frac;
864
865        result.push(Some(rgb_to_hex(r, g, b, a)));
866    }
867
868    Ok(RValue::vec(Vector::Character(result.into())))
869}
870
871// endregion