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    #[cfg_attr(feature = "serde", serde(default))]
78    pub(crate) dither: bool,
79    stops: Vec<GradientStop>,
80    angle: f32,
81}
82
83impl Hash for LinearGradient {
84    fn hash<H: Hasher>(&self, state: &mut H) {
85        self.dither.hash(state);
86        self.stops.hash(state);
87        self.angle.to_bits().hash(state);
88    }
89}
90
91impl LinearGradient {
92    /// Create an empty [LinearGradient] with defaults.
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Enable dithering to reduce color banding, disabled by default.
98    pub fn dither(mut self, dither: bool) -> Self {
99        self.dither = dither;
100        self
101    }
102
103    /// Add a single stop.
104    pub fn stop(mut self, stop: impl Into<GradientStop>) -> Self {
105        self.stops.push(stop.into());
106        self
107    }
108
109    /// Add multiple stops.
110    pub fn stops<I>(mut self, stops: I) -> Self
111    where
112        I: IntoIterator<Item = GradientStop>,
113    {
114        self.stops.extend(stops);
115        self
116    }
117
118    /// Set angle (degrees).
119    pub fn angle(mut self, angle: f32) -> Self {
120        self.angle = angle;
121        self
122    }
123
124    pub fn prepare_shader(&self, bounds: Area) -> Option<Shader> {
125        let colors: Vec<SkColor4f> = self
126            .stops
127            .iter()
128            .map(|stop| SkColor4f::from(stop.color))
129            .collect();
130        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
131
132        let grad_colors = Colors::new(&colors[..], Some(&offsets[..]), TileMode::Clamp, None);
133        let grad = Gradient::new(grad_colors, Interpolation::default());
134
135        let (dy, dx) = (self.angle.to_radians() + FRAC_PI_2).sin_cos();
136        let farthest_corner = SkPoint::new(
137            if dx > 0.0 { bounds.width() } else { 0.0 },
138            if dy > 0.0 { bounds.height() } else { 0.0 },
139        );
140        let delta = farthest_corner - SkPoint::new(bounds.width(), bounds.height()) / 2.0;
141        let u = delta.x * dy - delta.y * dx;
142        let endpoint = farthest_corner + SkPoint::new(-u * dy, u * dx);
143
144        let origin = SkPoint::new(bounds.min_x(), bounds.min_y());
145        shaders::linear_gradient(
146            (
147                SkPoint::new(bounds.width(), bounds.height()) - endpoint + origin,
148                endpoint + origin,
149            ),
150            &grad,
151            None,
152        )
153    }
154}
155
156impl fmt::Display for LinearGradient {
157    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158        write!(
159            f,
160            "linear-gradient({}deg, {})",
161            self.angle,
162            self.stops
163                .iter()
164                .map(|stop| stop.to_string())
165                .collect::<Vec<_>>()
166                .join(", ")
167        )
168    }
169}
170
171/// A gradient that transitions colors outward in a circle from the element's center.
172///
173/// Start from [`RadialGradient::new`] and add [`GradientStop`]s:
174///
175/// ```
176/// # use freya::prelude::*;
177/// let gradient = RadialGradient::new()
178///     .stop((Color::WHITE, 0.0))
179///     .stop((Color::BLACK, 100.0));
180/// ```
181#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
182#[derive(Clone, Debug, Default, PartialEq)]
183pub struct RadialGradient {
184    #[cfg_attr(feature = "serde", serde(default))]
185    pub(crate) dither: bool,
186    stops: Vec<GradientStop>,
187}
188
189impl Hash for RadialGradient {
190    fn hash<H: Hasher>(&self, state: &mut H) {
191        self.dither.hash(state);
192        self.stops.hash(state);
193    }
194}
195
196impl RadialGradient {
197    /// Create an empty [RadialGradient] with defaults.
198    pub fn new() -> Self {
199        Self::default()
200    }
201
202    /// Enable dithering to reduce color banding, disabled by default.
203    pub fn dither(mut self, dither: bool) -> Self {
204        self.dither = dither;
205        self
206    }
207
208    /// Add a single stop.
209    pub fn stop(mut self, stop: impl Into<GradientStop>) -> Self {
210        self.stops.push(stop.into());
211        self
212    }
213
214    /// Add multiple stops.
215    pub fn stops<I>(mut self, stops: I) -> Self
216    where
217        I: IntoIterator<Item = GradientStop>,
218    {
219        self.stops.extend(stops);
220        self
221    }
222
223    pub fn prepare_shader(&self, bounds: Area) -> Option<Shader> {
224        let colors: Vec<SkColor4f> = self
225            .stops
226            .iter()
227            .map(|stop| SkColor4f::from(stop.color))
228            .collect();
229        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
230
231        let center = bounds.center();
232
233        let grad_colors = Colors::new(&colors[..], Some(&offsets[..]), TileMode::Clamp, None);
234        let grad = Gradient::new(grad_colors, Interpolation::default());
235
236        shaders::radial_gradient(
237            (
238                SkPoint::new(center.x, center.y),
239                bounds.width().max(bounds.height()) / 2.0,
240            ),
241            &grad,
242            None,
243        )
244    }
245}
246
247impl fmt::Display for RadialGradient {
248    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
249        write!(
250            f,
251            "radial-gradient({})",
252            self.stops
253                .iter()
254                .map(|stop| stop.to_string())
255                .collect::<Vec<_>>()
256                .join(", ")
257        )
258    }
259}
260
261/// A gradient that transitions colors by sweeping around the element's center.
262///
263/// Start from [`ConicGradient::new`], add [`GradientStop`]s and optionally set the
264/// rotation [`angle`](ConicGradient::angle) and the start/end [`angles`](ConicGradient::angles):
265///
266/// ```
267/// # use freya::prelude::*;
268/// let gradient = ConicGradient::new()
269///     .angle(45.0)
270///     .stop((Color::RED, 0.0))
271///     .stop((Color::BLUE, 100.0));
272/// ```
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274#[derive(Clone, Debug, Default, PartialEq)]
275pub struct ConicGradient {
276    #[cfg_attr(feature = "serde", serde(default))]
277    pub(crate) dither: bool,
278    stops: Vec<GradientStop>,
279    angles: Option<(f32, f32)>,
280    angle: Option<f32>,
281}
282
283impl Hash for ConicGradient {
284    fn hash<H: Hasher>(&self, state: &mut H) {
285        self.dither.hash(state);
286        self.stops.hash(state);
287        if let Some((start, end)) = self.angles {
288            start.to_bits().hash(state);
289            end.to_bits().hash(state);
290        }
291        if let Some(angle) = self.angle {
292            angle.to_bits().hash(state);
293        }
294    }
295}
296
297impl ConicGradient {
298    /// Create an empty [ConicGradient] with defaults.
299    pub fn new() -> Self {
300        Self::default()
301    }
302
303    /// Enable dithering to reduce color banding, disabled by default.
304    pub fn dither(mut self, dither: bool) -> Self {
305        self.dither = dither;
306        self
307    }
308
309    /// Add a single stop.
310    pub fn stop(mut self, stop: impl Into<GradientStop>) -> Self {
311        self.stops.push(stop.into());
312        self
313    }
314
315    /// Add multiple stops.
316    pub fn stops<I>(mut self, stops: I) -> Self
317    where
318        I: IntoIterator<Item = GradientStop>,
319    {
320        self.stops.extend(stops);
321        self
322    }
323
324    /// Set explicit angle (degrees) for the gradient.
325    pub fn angle(mut self, angle: f32) -> Self {
326        self.angle = Some(angle);
327        self
328    }
329
330    /// Set start/end angles (degrees).
331    pub fn angles(mut self, start: f32, end: f32) -> Self {
332        self.angles = Some((start, end));
333        self
334    }
335
336    pub fn prepare_shader(&self, bounds: Area) -> Option<Shader> {
337        let colors: Vec<SkColor4f> = self
338            .stops
339            .iter()
340            .map(|stop| SkColor4f::from(stop.color))
341            .collect();
342        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
343
344        let center = bounds.center();
345
346        let matrix =
347            Matrix::rotate_deg_pivot(-90.0 + self.angle.unwrap_or(0.0), (center.x, center.y));
348
349        let grad_colors = Colors::new(&colors[..], Some(&offsets[..]), TileMode::Clamp, None);
350        let grad = Gradient::new(grad_colors, Interpolation::default());
351
352        let (start_angle, end_angle) = self.angles.unwrap_or((0.0, 360.0));
353
354        shaders::sweep_gradient(
355            SkPoint::new(center.x, center.y),
356            (start_angle, end_angle),
357            &grad,
358            Some(&matrix),
359        )
360    }
361}
362
363impl fmt::Display for ConicGradient {
364    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
365        write!(f, "conic-gradient(")?;
366
367        if let Some(angle) = self.angle {
368            write!(f, "{angle}deg, ")?;
369        }
370
371        if let Some((start, end)) = self.angles {
372            write!(f, "from {start}deg to {end}deg, ")?;
373        }
374
375        write!(
376            f,
377            "{})",
378            self.stops
379                .iter()
380                .map(|stop| stop.to_string())
381                .collect::<Vec<_>>()
382                .join(", ")
383        )
384    }
385}