1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// The following code was copied and modified from
// https://github.com/iced-rs/iced/blob/31d1d5fecbef50fa319cabd5d4194f1e4aaefa21/graphics/src/gradient.rs
// Iced license (MIT): https://github.com/iced-rs/iced/blob/31d1d5fecbef50fa319cabd5d4194f1e4aaefa21/LICENSE

use half::f16;
use std::cmp::Ordering;
use std::f32::consts::FRAC_PI_2;

use super::color::PackedSrgb;
use crate::math::{Angle, Point, Rect};

pub const MAX_STOPS: usize = 8;

/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD),
/// or conically (TBD).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Gradient {
    /// A linear gradient interpolates colors along a direction at a specific angle.
    Linear(LinearGradient),
}

impl Gradient {
    /// Adjust the opacity of the gradient by a multiplier applied to each color stop.
    pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self {
        match &mut self {
            Gradient::Linear(linear) => {
                for stop in linear.stops.iter_mut().flatten() {
                    *stop.color.a_mut() *= alpha_multiplier;
                }
            }
        }

        self
    }

    pub fn packed(&self, bounds: Rect) -> PackedGradient {
        PackedGradient::new(self, bounds)
    }
}

impl From<LinearGradient> for Gradient {
    fn from(gradient: LinearGradient) -> Self {
        Self::Linear(gradient)
    }
}

impl Default for Gradient {
    fn default() -> Self {
        Gradient::Linear(LinearGradient::new(Angle::default()))
    }
}

/// A point along the gradient vector where the specified [`color`] is unmixed.
///
/// [`color`]: Self::color
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct ColorStop {
    /// Offset along the gradient vector in the range `[0.0, 1.0]`.
    pub offset: f32,

    /// The color of the gradient at the specified [`offset`].
    ///
    /// [`offset`]: Self::offset
    pub color: PackedSrgb,
}

/// A linear gradient.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LinearGradient {
    /// How the [`Gradient`] is angled within its bounds.
    pub angle: Angle,
    /// [`ColorStop`]s along the linear gradient path.
    pub stops: [Option<ColorStop>; MAX_STOPS],
}

impl LinearGradient {
    /// Creates a new [`Linear`] gradient with the given angle in [`Angle`].
    pub const fn new(angle: Angle) -> Self {
        Self {
            angle: angle,
            stops: [None; 8],
        }
    }

    /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient.
    ///
    /// Any `offset` that is not within `0.0..=1.0` will be silently ignored.
    ///
    /// Any stop added after the 8th will be silently ignored.
    pub fn add_stop(mut self, offset: f32, color: impl Into<PackedSrgb>) -> Self {
        if offset.is_finite() && (0.0..=1.0).contains(&offset) {
            let (Ok(index) | Err(index)) = self.stops.binary_search_by(|stop| match stop {
                None => Ordering::Greater,
                Some(stop) => stop.offset.partial_cmp(&offset).unwrap(),
            });

            if index < 8 {
                self.stops[index] = Some(ColorStop {
                    offset,
                    color: color.into(),
                });
            }
        } else {
            log::warn!("Gradient color stop must be within 0.0..=1.0 range.");
        };

        self
    }

    /// Adds multiple [`ColorStop`]s to the gradient.
    ///
    /// Any stop added after the 8th will be silently ignored.
    pub fn add_stops(mut self, stops: impl IntoIterator<Item = ColorStop>) -> Self {
        for stop in stops {
            self = self.add_stop(stop.offset, stop.color);
        }

        self
    }
}

/// Packed [`Gradient`] data for use in shader code.
#[repr(C)]
#[derive(Default, Debug, Copy, Clone, PartialEq, bytemuck::Zeroable, bytemuck::Pod)]
pub struct PackedGradient {
    /// 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32
    pub colors: [[u32; 2]; 8],
    /// 8 offsets, 8x 16 bit floats packed into 4 u32s
    pub offsets: [u32; 4],
    /// `[start.x, start.y, end.x, end.y]` in logical points
    pub direction: [f32; 4],
}

impl PackedGradient {
    pub fn new(gradient: &Gradient, bounds: Rect) -> Self {
        match gradient {
            Gradient::Linear(linear) => {
                let mut colors = [[0u32; 2]; 8];
                let mut offsets = [f16::from(0u8); 8];

                for (index, stop) in linear.stops.iter().enumerate() {
                    let packed_color = stop.map(|s| s.color).unwrap_or(PackedSrgb::default());

                    colors[index] = [
                        pack_f16s([
                            f16::from_f32(packed_color.r()),
                            f16::from_f32(packed_color.g()),
                        ]),
                        pack_f16s([
                            f16::from_f32(packed_color.b()),
                            f16::from_f32(packed_color.a()),
                        ]),
                    ];

                    offsets[index] = f16::from_f32(stop.map(|s| s.offset).unwrap_or(2.0));
                }

                let offsets = [
                    pack_f16s([offsets[0], offsets[1]]),
                    pack_f16s([offsets[2], offsets[3]]),
                    pack_f16s([offsets[4], offsets[5]]),
                    pack_f16s([offsets[6], offsets[7]]),
                ];

                let (start, end) = to_distance(linear.angle, &bounds);

                let direction = [start.x, start.y, end.x, end.y];

                PackedGradient {
                    colors,
                    offsets,
                    direction,
                }
            }
        }
    }
}

/// Calculates the line in which the angle intercepts the `bounds`.
fn to_distance(angle: Angle, bounds: &Rect) -> (Point, Point) {
    let angle = angle - Angle { radians: FRAC_PI_2 };

    let r = Point::new(f32::cos(angle.radians), f32::sin(angle.radians));
    let bounds_center = bounds.center();

    let distance_to_rect = f32::max(
        f32::abs(r.x * bounds.size.width / 2.0),
        f32::abs(r.y * bounds.size.height / 2.0),
    );

    let start = Point::new(
        bounds_center.x - (r.x * distance_to_rect),
        bounds_center.y - (r.y * distance_to_rect),
    );
    let end = Point::new(
        bounds_center.x + (r.x * distance_to_rect),
        bounds_center.y + (r.y * distance_to_rect),
    );

    (start, end)
}

/// Packs two f16s into one u32.
fn pack_f16s(f: [f16; 2]) -> u32 {
    let one = (f[0].to_bits() as u32) << 16;
    let two = f[1].to_bits() as u32;

    one | two
}