rootvg_core/
gradient.rs

1// The following code was copied and modified from
2// https://github.com/iced-rs/iced/blob/31d1d5fecbef50fa319cabd5d4194f1e4aaefa21/graphics/src/gradient.rs
3// Iced license (MIT): https://github.com/iced-rs/iced/blob/31d1d5fecbef50fa319cabd5d4194f1e4aaefa21/LICENSE
4
5use half::f16;
6use std::cmp::Ordering;
7use std::f32::consts::FRAC_PI_2;
8
9use super::color::PackedSrgb;
10use crate::math::{Angle, Point, Rect};
11
12pub const MAX_STOPS: usize = 8;
13
14/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD),
15/// or conically (TBD).
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum Gradient {
18    /// A linear gradient interpolates colors along a direction at a specific angle.
19    Linear(LinearGradient),
20}
21
22impl Gradient {
23    /// Adjust the opacity of the gradient by a multiplier applied to each color stop.
24    pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self {
25        match &mut self {
26            Gradient::Linear(linear) => {
27                for stop in linear.stops.iter_mut().flatten() {
28                    *stop.color.a_mut() *= alpha_multiplier;
29                }
30            }
31        }
32
33        self
34    }
35
36    pub fn packed(&self, bounds: Rect) -> PackedGradient {
37        PackedGradient::new(self, bounds)
38    }
39}
40
41impl From<LinearGradient> for Gradient {
42    fn from(gradient: LinearGradient) -> Self {
43        Self::Linear(gradient)
44    }
45}
46
47impl Default for Gradient {
48    fn default() -> Self {
49        Gradient::Linear(LinearGradient::new(Angle::default()))
50    }
51}
52
53/// A point along the gradient vector where the specified [`color`] is unmixed.
54///
55/// [`color`]: Self::color
56#[derive(Debug, Default, Clone, Copy, PartialEq)]
57pub struct ColorStop {
58    /// Offset along the gradient vector in the range `[0.0, 1.0]`.
59    pub offset: f32,
60
61    /// The color of the gradient at the specified [`offset`].
62    ///
63    /// [`offset`]: Self::offset
64    pub color: PackedSrgb,
65}
66
67/// A linear gradient.
68#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct LinearGradient {
70    /// How the [`Gradient`] is angled within its bounds.
71    pub angle: Angle,
72    /// [`ColorStop`]s along the linear gradient path.
73    pub stops: [Option<ColorStop>; MAX_STOPS],
74}
75
76impl LinearGradient {
77    /// Creates a new [`Linear`] gradient with the given angle in [`Angle`].
78    pub const fn new(angle: Angle) -> Self {
79        Self {
80            angle: angle,
81            stops: [None; 8],
82        }
83    }
84
85    /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient.
86    ///
87    /// Any `offset` that is not within `0.0..=1.0` will be silently ignored.
88    ///
89    /// Any stop added after the 8th will be silently ignored.
90    pub fn add_stop(mut self, offset: f32, color: impl Into<PackedSrgb>) -> Self {
91        if offset.is_finite() && (0.0..=1.0).contains(&offset) {
92            let (Ok(index) | Err(index)) = self.stops.binary_search_by(|stop| match stop {
93                None => Ordering::Greater,
94                Some(stop) => stop.offset.partial_cmp(&offset).unwrap(),
95            });
96
97            if index < 8 {
98                self.stops[index] = Some(ColorStop {
99                    offset,
100                    color: color.into(),
101                });
102            }
103        } else {
104            log::warn!("Gradient color stop must be within 0.0..=1.0 range.");
105        };
106
107        self
108    }
109
110    /// Adds multiple [`ColorStop`]s to the gradient.
111    ///
112    /// Any stop added after the 8th will be silently ignored.
113    pub fn add_stops(mut self, stops: impl IntoIterator<Item = ColorStop>) -> Self {
114        for stop in stops {
115            self = self.add_stop(stop.offset, stop.color);
116        }
117
118        self
119    }
120}
121
122/// Packed [`Gradient`] data for use in shader code.
123#[repr(C)]
124#[derive(Default, Debug, Copy, Clone, PartialEq, bytemuck::Zeroable, bytemuck::Pod)]
125pub struct PackedGradient {
126    /// 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32
127    pub colors: [[u32; 2]; 8],
128    /// 8 offsets, 8x 16 bit floats packed into 4 u32s
129    pub offsets: [u32; 4],
130    /// `[start.x, start.y, end.x, end.y]` in logical points
131    pub direction: [f32; 4],
132}
133
134impl PackedGradient {
135    pub fn new(gradient: &Gradient, bounds: Rect) -> Self {
136        match gradient {
137            Gradient::Linear(linear) => {
138                let mut colors = [[0u32; 2]; 8];
139                let mut offsets = [f16::from(0u8); 8];
140
141                for (index, stop) in linear.stops.iter().enumerate() {
142                    let packed_color = stop.map(|s| s.color).unwrap_or(PackedSrgb::default());
143
144                    colors[index] = [
145                        pack_f16s([
146                            f16::from_f32(packed_color.r()),
147                            f16::from_f32(packed_color.g()),
148                        ]),
149                        pack_f16s([
150                            f16::from_f32(packed_color.b()),
151                            f16::from_f32(packed_color.a()),
152                        ]),
153                    ];
154
155                    offsets[index] = f16::from_f32(stop.map(|s| s.offset).unwrap_or(2.0));
156                }
157
158                let offsets = [
159                    pack_f16s([offsets[0], offsets[1]]),
160                    pack_f16s([offsets[2], offsets[3]]),
161                    pack_f16s([offsets[4], offsets[5]]),
162                    pack_f16s([offsets[6], offsets[7]]),
163                ];
164
165                let (start, end) = to_distance(linear.angle, &bounds);
166
167                let direction = [start.x, start.y, end.x, end.y];
168
169                PackedGradient {
170                    colors,
171                    offsets,
172                    direction,
173                }
174            }
175        }
176    }
177}
178
179/// Calculates the line in which the angle intercepts the `bounds`.
180fn to_distance(angle: Angle, bounds: &Rect) -> (Point, Point) {
181    let angle = angle - Angle { radians: FRAC_PI_2 };
182
183    let r = Point::new(f32::cos(angle.radians), f32::sin(angle.radians));
184    let bounds_center = bounds.center();
185
186    let distance_to_rect = f32::max(
187        f32::abs(r.x * bounds.size.width / 2.0),
188        f32::abs(r.y * bounds.size.height / 2.0),
189    );
190
191    let start = Point::new(
192        bounds_center.x - (r.x * distance_to_rect),
193        bounds_center.y - (r.y * distance_to_rect),
194    );
195    let end = Point::new(
196        bounds_center.x + (r.x * distance_to_rect),
197        bounds_center.y + (r.y * distance_to_rect),
198    );
199
200    (start, end)
201}
202
203/// Packs two f16s into one u32.
204fn pack_f16s(f: [f16; 2]) -> u32 {
205    let one = (f[0].to_bits() as u32) << 16;
206    let two = f[1].to_bits() as u32;
207
208    one | two
209}