Skip to main content

oxihuman_export/
gradient_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Gradient export: linear/radial gradient data.
6
7/// Gradient type.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum GradientType {
11    Linear,
12    Radial,
13}
14
15/// Gradient stop.
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct GradientStop {
19    pub t: f32,
20    pub color: [f32; 4],
21}
22
23/// Gradient export data.
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
26pub struct GradientExport {
27    pub gradient_type: GradientType,
28    pub stops: Vec<GradientStop>,
29}
30
31#[allow(dead_code)]
32pub fn new_gradient(gtype: GradientType) -> GradientExport {
33    GradientExport {
34        gradient_type: gtype,
35        stops: Vec::new(),
36    }
37}
38
39#[allow(dead_code)]
40pub fn grad_add_stop(g: &mut GradientExport, t: f32, color: [f32; 4]) {
41    g.stops.push(GradientStop {
42        t: t.clamp(0.0, 1.0),
43        color,
44    });
45    g.stops
46        .sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
47}
48
49#[allow(dead_code)]
50pub fn grad_stop_count(g: &GradientExport) -> usize {
51    g.stops.len()
52}
53
54#[allow(dead_code)]
55pub fn grad_sample(g: &GradientExport, t: f32) -> [f32; 4] {
56    if g.stops.is_empty() {
57        return [0.0; 4];
58    }
59    if g.stops.len() == 1 || t <= g.stops[0].t {
60        return g.stops[0].color;
61    }
62    if let Some(last) = g.stops.last() {
63        if t >= last.t {
64            return last.color;
65        }
66    }
67    for w in g.stops.windows(2) {
68        if t >= w[0].t && t <= w[1].t {
69            let f = (t - w[0].t) / (w[1].t - w[0].t);
70            let mut c = [0.0f32; 4];
71            for (i, ci) in c.iter_mut().enumerate() {
72                *ci = w[0].color[i] * (1.0 - f) + w[1].color[i] * f;
73            }
74            return c;
75        }
76    }
77    g.stops.last().map_or([0.0; 4], |s| s.color)
78}
79
80#[allow(dead_code)]
81pub fn grad_type_name(g: &GradientExport) -> &str {
82    match g.gradient_type {
83        GradientType::Linear => "linear",
84        GradientType::Radial => "radial",
85    }
86}
87
88#[allow(dead_code)]
89pub fn grad_clear(g: &mut GradientExport) {
90    g.stops.clear();
91}
92
93#[allow(dead_code)]
94pub fn gradient_to_json(g: &GradientExport) -> String {
95    format!(
96        "{{\"type\":\"{}\",\"stops\":{}}}",
97        grad_type_name(g),
98        g.stops.len()
99    )
100}
101
102#[allow(dead_code)]
103pub fn grad_validate(g: &GradientExport) -> bool {
104    g.stops.windows(2).all(|w| w[0].t <= w[1].t)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_new_linear() {
113        assert_eq!(
114            grad_type_name(&new_gradient(GradientType::Linear)),
115            "linear"
116        );
117    }
118
119    #[test]
120    fn test_add_stop() {
121        let mut g = new_gradient(GradientType::Linear);
122        grad_add_stop(&mut g, 0.5, [1.0; 4]);
123        assert_eq!(grad_stop_count(&g), 1);
124    }
125
126    #[test]
127    fn test_sample_lerp() {
128        let mut g = new_gradient(GradientType::Linear);
129        grad_add_stop(&mut g, 0.0, [0.0; 4]);
130        grad_add_stop(&mut g, 1.0, [1.0; 4]);
131        let c = grad_sample(&g, 0.5);
132        assert!((c[0] - 0.5).abs() < 1e-6);
133    }
134
135    #[test]
136    fn test_sample_empty() {
137        assert!((grad_sample(&new_gradient(GradientType::Radial), 0.5)[0]).abs() < 1e-6);
138    }
139
140    #[test]
141    fn test_clear() {
142        let mut g = new_gradient(GradientType::Linear);
143        grad_add_stop(&mut g, 0.0, [0.0; 4]);
144        grad_clear(&mut g);
145        assert_eq!(grad_stop_count(&g), 0);
146    }
147
148    #[test]
149    fn test_validate() {
150        let mut g = new_gradient(GradientType::Linear);
151        grad_add_stop(&mut g, 0.0, [0.0; 4]);
152        grad_add_stop(&mut g, 1.0, [1.0; 4]);
153        assert!(grad_validate(&g));
154    }
155
156    #[test]
157    fn test_to_json() {
158        assert!(
159            gradient_to_json(&new_gradient(GradientType::Radial)).contains("\"type\":\"radial\"")
160        );
161    }
162
163    #[test]
164    fn test_radial_type() {
165        assert_eq!(
166            grad_type_name(&new_gradient(GradientType::Radial)),
167            "radial"
168        );
169    }
170
171    #[test]
172    fn test_sample_before_first() {
173        let mut g = new_gradient(GradientType::Linear);
174        grad_add_stop(&mut g, 0.5, [1.0; 4]);
175        let c = grad_sample(&g, 0.0);
176        assert!((c[0] - 1.0).abs() < 1e-6);
177    }
178
179    #[test]
180    fn test_sorted_insertion() {
181        let mut g = new_gradient(GradientType::Linear);
182        grad_add_stop(&mut g, 0.9, [1.0; 4]);
183        grad_add_stop(&mut g, 0.1, [0.0; 4]);
184        assert!(g.stops[0].t < g.stops[1].t);
185    }
186}