Skip to main content

freya_core/style/
gradient.rs

1use std::{
2    f32::consts::FRAC_PI_2,
3    fmt::{
4        self,
5        Debug,
6    },
7    hash::{
8        Hash,
9        Hasher,
10    },
11};
12
13use freya_engine::prelude::*;
14use torin::prelude::Area;
15
16use crate::style::color::Color;
17
18/// A single color stop within a gradient, placed at an `offset` percentage (`0.0..=100.0`).
19///
20/// Build it from a `(color, offset)` tuple or with [`GradientStop::new`]:
21///
22/// ```
23/// # use freya::prelude::*;
24/// let stop = GradientStop::new(Color::RED, 50.0);
25/// ```
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[derive(Clone, Debug, Default, PartialEq)]
28pub struct GradientStop {
29    color: Color,
30    offset: f32,
31}
32
33impl Hash for GradientStop {
34    fn hash<H: Hasher>(&self, state: &mut H) {
35        self.color.hash(state);
36        self.offset.to_bits().hash(state);
37    }
38}
39
40impl fmt::Display for GradientStop {
41    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42        _ = self.color.fmt(f);
43        write!(f, " {}%", self.offset * 100.0)
44    }
45}
46
47impl GradientStop {
48    /// Create a [`GradientStop`] of the given [`Color`] at the given offset percentage (`0.0..=100.0`).
49    pub fn new(color: impl Into<Color>, offset: f32) -> Self {
50        Self {
51            color: color.into(),
52            offset: offset / 100.,
53        }
54    }
55}
56
57impl<C: Into<Color>> From<(C, f32)> for GradientStop {
58    fn from((color, offset): (C, f32)) -> Self {
59        GradientStop::new(color, offset)
60    }
61}
62
63/// A gradient that transitions colors along a straight line at a given [`angle`](LinearGradient::angle).
64///
65/// Start from [`LinearGradient::new`] and add [`GradientStop`]s:
66///
67/// ```
68/// # use freya::prelude::*;
69/// let gradient = LinearGradient::new()
70///     .angle(90.0)
71///     .stop((Color::RED, 0.0))
72///     .stop((Color::BLUE, 100.0));
73/// ```
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75#[derive(Clone, Debug, Default, PartialEq)]
76pub struct LinearGradient {
77    stops: Vec<GradientStop>,
78    angle: f32,
79}
80
81impl Hash for LinearGradient {
82    fn hash<H: Hasher>(&self, state: &mut H) {
83        self.stops.hash(state);
84        self.angle.to_bits().hash(state);
85    }
86}
87
88impl LinearGradient {
89    /// Create an empty [LinearGradient] with defaults.
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Add a single stop.
95    pub fn stop(mut self, stop: impl Into<GradientStop>) -> Self {
96        self.stops.push(stop.into());
97        self
98    }
99
100    /// Add multiple stops.
101    pub fn stops<I>(mut self, stops: I) -> Self
102    where
103        I: IntoIterator<Item = GradientStop>,
104    {
105        self.stops.extend(stops);
106        self
107    }
108
109    /// Set angle (degrees).
110    pub fn angle(mut self, angle: f32) -> Self {
111        self.angle = angle;
112        self
113    }
114
115    pub fn prepare_shader(&self, bounds: Area) -> Option<Shader> {
116        let colors: Vec<SkColor4f> = self
117            .stops
118            .iter()
119            .map(|stop| SkColor4f::from(stop.color))
120            .collect();
121        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
122
123        let grad_colors = Colors::new(&colors[..], Some(&offsets[..]), TileMode::Clamp, None);
124        let grad = Gradient::new(grad_colors, Interpolation::default());
125
126        let (dy, dx) = (self.angle.to_radians() + FRAC_PI_2).sin_cos();
127        let farthest_corner = SkPoint::new(
128            if dx > 0.0 { bounds.width() } else { 0.0 },
129            if dy > 0.0 { bounds.height() } else { 0.0 },
130        );
131        let delta = farthest_corner - SkPoint::new(bounds.width(), bounds.height()) / 2.0;
132        let u = delta.x * dy - delta.y * dx;
133        let endpoint = farthest_corner + SkPoint::new(-u * dy, u * dx);
134
135        let origin = SkPoint::new(bounds.min_x(), bounds.min_y());
136        shaders::linear_gradient(
137            (
138                SkPoint::new(bounds.width(), bounds.height()) - endpoint + origin,
139                endpoint + origin,
140            ),
141            &grad,
142            None,
143        )
144    }
145}
146
147impl fmt::Display for LinearGradient {
148    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
149        write!(
150            f,
151            "linear-gradient({}deg, {})",
152            self.angle,
153            self.stops
154                .iter()
155                .map(|stop| stop.to_string())
156                .collect::<Vec<_>>()
157                .join(", ")
158        )
159    }
160}
161
162/// A gradient that transitions colors outward in a circle from the element's center.
163///
164/// Start from [`RadialGradient::new`] and add [`GradientStop`]s:
165///
166/// ```
167/// # use freya::prelude::*;
168/// let gradient = RadialGradient::new()
169///     .stop((Color::WHITE, 0.0))
170///     .stop((Color::BLACK, 100.0));
171/// ```
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173#[derive(Clone, Debug, Default, PartialEq)]
174pub struct RadialGradient {
175    stops: Vec<GradientStop>,
176}
177
178impl Hash for RadialGradient {
179    fn hash<H: Hasher>(&self, state: &mut H) {
180        self.stops.hash(state);
181    }
182}
183
184impl RadialGradient {
185    /// Create an empty [RadialGradient] with defaults.
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Add a single stop.
191    pub fn stop(mut self, stop: impl Into<GradientStop>) -> Self {
192        self.stops.push(stop.into());
193        self
194    }
195
196    /// Add multiple stops.
197    pub fn stops<I>(mut self, stops: I) -> Self
198    where
199        I: IntoIterator<Item = GradientStop>,
200    {
201        self.stops.extend(stops);
202        self
203    }
204
205    pub fn prepare_shader(&self, bounds: Area) -> Option<Shader> {
206        let colors: Vec<SkColor4f> = self
207            .stops
208            .iter()
209            .map(|stop| SkColor4f::from(stop.color))
210            .collect();
211        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
212
213        let center = bounds.center();
214
215        let grad_colors = Colors::new(&colors[..], Some(&offsets[..]), TileMode::Clamp, None);
216        let grad = Gradient::new(grad_colors, Interpolation::default());
217
218        shaders::radial_gradient(
219            (
220                SkPoint::new(center.x, center.y),
221                bounds.width().max(bounds.height()) / 2.0,
222            ),
223            &grad,
224            None,
225        )
226    }
227}
228
229impl fmt::Display for RadialGradient {
230    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
231        write!(
232            f,
233            "radial-gradient({})",
234            self.stops
235                .iter()
236                .map(|stop| stop.to_string())
237                .collect::<Vec<_>>()
238                .join(", ")
239        )
240    }
241}
242
243/// A gradient that transitions colors by sweeping around the element's center.
244///
245/// Start from [`ConicGradient::new`], add [`GradientStop`]s and optionally set the
246/// rotation [`angle`](ConicGradient::angle) and the start/end [`angles`](ConicGradient::angles):
247///
248/// ```
249/// # use freya::prelude::*;
250/// let gradient = ConicGradient::new()
251///     .angle(45.0)
252///     .stop((Color::RED, 0.0))
253///     .stop((Color::BLUE, 100.0));
254/// ```
255#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
256#[derive(Clone, Debug, Default, PartialEq)]
257pub struct ConicGradient {
258    stops: Vec<GradientStop>,
259    angles: Option<(f32, f32)>,
260    angle: Option<f32>,
261}
262
263impl Hash for ConicGradient {
264    fn hash<H: Hasher>(&self, state: &mut H) {
265        self.stops.hash(state);
266        if let Some((start, end)) = self.angles {
267            start.to_bits().hash(state);
268            end.to_bits().hash(state);
269        }
270        if let Some(angle) = self.angle {
271            angle.to_bits().hash(state);
272        }
273    }
274}
275
276impl ConicGradient {
277    /// Create an empty [ConicGradient] with defaults.
278    pub fn new() -> Self {
279        Self::default()
280    }
281
282    /// Add a single stop.
283    pub fn stop(mut self, stop: impl Into<GradientStop>) -> Self {
284        self.stops.push(stop.into());
285        self
286    }
287
288    /// Add multiple stops.
289    pub fn stops<I>(mut self, stops: I) -> Self
290    where
291        I: IntoIterator<Item = GradientStop>,
292    {
293        self.stops.extend(stops);
294        self
295    }
296
297    /// Set explicit angle (degrees) for the gradient.
298    pub fn angle(mut self, angle: f32) -> Self {
299        self.angle = Some(angle);
300        self
301    }
302
303    /// Set start/end angles (degrees).
304    pub fn angles(mut self, start: f32, end: f32) -> Self {
305        self.angles = Some((start, end));
306        self
307    }
308
309    pub fn prepare_shader(&self, bounds: Area) -> Option<Shader> {
310        let colors: Vec<SkColor4f> = self
311            .stops
312            .iter()
313            .map(|stop| SkColor4f::from(stop.color))
314            .collect();
315        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
316
317        let center = bounds.center();
318
319        let matrix =
320            Matrix::rotate_deg_pivot(-90.0 + self.angle.unwrap_or(0.0), (center.x, center.y));
321
322        let grad_colors = Colors::new(&colors[..], Some(&offsets[..]), TileMode::Clamp, None);
323        let grad = Gradient::new(grad_colors, Interpolation::default());
324
325        let (start_angle, end_angle) = self.angles.unwrap_or((0.0, 360.0));
326
327        shaders::sweep_gradient(
328            SkPoint::new(center.x, center.y),
329            (start_angle, end_angle),
330            &grad,
331            Some(&matrix),
332        )
333    }
334}
335
336impl fmt::Display for ConicGradient {
337    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
338        write!(f, "conic-gradient(")?;
339
340        if let Some(angle) = self.angle {
341            write!(f, "{angle}deg, ")?;
342        }
343
344        if let Some((start, end)) = self.angles {
345            write!(f, "from {start}deg to {end}deg, ")?;
346        }
347
348        write!(
349            f,
350            "{})",
351            self.stops
352                .iter()
353                .map(|stop| stop.to_string())
354                .collect::<Vec<_>>()
355                .join(", ")
356        )
357    }
358}