Skip to main content

oxframe_core/
paint.rs

1use kurbo::Point;
2
3use crate::{Color, Lerp};
4
5/// Shape of a [`Gradient`].
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum GradientKind {
8    /// Along the line `from` → `to`.
9    Linear {
10        /// Where the first stop is.
11        from: Point,
12        /// Where the last stop is.
13        to: Point,
14    },
15    /// Circles around `center`, color at `radius` is the last stop.
16    Radial {
17        /// Where the first stop is.
18        center: Point,
19        /// Distance of the last stop from the center.
20        radius: f64,
21    },
22}
23
24/// A gradient in the node's local coordinates (origin at the node center).
25#[derive(Debug, Clone, PartialEq)]
26pub struct Gradient {
27    /// Linear or radial, with its geometry.
28    pub kind: GradientKind,
29    /// `(offset in 0..=1, color)`, sorted by offset.
30    pub stops: Vec<(f64, Color)>,
31}
32
33impl Gradient {
34    /// Linear gradient from `from` to `to`; add colors with `stops`.
35    pub fn linear(from: impl Into<Point>, to: impl Into<Point>) -> Self {
36        Self {
37            kind: GradientKind::Linear {
38                from: from.into(),
39                to: to.into(),
40            },
41            stops: Vec::new(),
42        }
43    }
44
45    /// Radial gradient around `center`; add colors with `stops`.
46    pub fn radial(center: impl Into<Point>, radius: f64) -> Self {
47        Self {
48            kind: GradientKind::Radial {
49                center: center.into(),
50                radius,
51            },
52            stops: Vec::new(),
53        }
54    }
55
56    /// Adds a color stop at `offset` (0..=1).
57    pub fn stop(mut self, offset: f64, color: Color) -> Self {
58        self.stops.push((offset.clamp(0.0, 1.0), color));
59        self.stops.sort_by(|a, b| a.0.total_cmp(&b.0));
60        self
61    }
62
63    /// Evenly spaced stops.
64    pub fn stops(mut self, colors: impl IntoIterator<Item = Color>) -> Self {
65        let colors: Vec<Color> = colors.into_iter().collect();
66        let n = colors.len().max(2) - 1;
67        self.stops = colors
68            .into_iter()
69            .enumerate()
70            .map(|(i, c)| (i as f64 / n as f64, c))
71            .collect();
72        self
73    }
74
75    /// Multiplies every stop's alpha (node opacity).
76    pub fn with_opacity(&self, opacity: f64) -> Self {
77        Self {
78            kind: self.kind,
79            stops: self
80                .stops
81                .iter()
82                .map(|(o, c)| (*o, c.with_alpha((f64::from(c.a) * opacity) as f32)))
83                .collect(),
84        }
85    }
86
87    /// Whether any stop is not fully transparent.
88    pub fn is_visible(&self) -> bool {
89        self.stops.iter().any(|(_, c)| c.a > 0.0)
90    }
91}
92
93/// Same kind and stop count: points, offsets and colors interpolate.
94/// Otherwise the gradient switches at the midpoint.
95impl Lerp for Gradient {
96    fn lerp(&self, to: &Self, t: f64) -> Self {
97        let kind = match (self.kind, to.kind) {
98            (GradientKind::Linear { from: a, to: b }, GradientKind::Linear { from: c, to: d }) => {
99                GradientKind::Linear {
100                    from: Lerp::lerp(&a, &c, t),
101                    to: Lerp::lerp(&b, &d, t),
102                }
103            }
104            (
105                GradientKind::Radial {
106                    center: a,
107                    radius: r1,
108                },
109                GradientKind::Radial {
110                    center: b,
111                    radius: r2,
112                },
113            ) => GradientKind::Radial {
114                center: Lerp::lerp(&a, &b, t),
115                radius: r1.lerp(&r2, t),
116            },
117            _ => return if t < 0.5 { self.clone() } else { to.clone() },
118        };
119        if self.stops.len() != to.stops.len() {
120            return if t < 0.5 { self.clone() } else { to.clone() };
121        }
122        Self {
123            kind,
124            stops: self
125                .stops
126                .iter()
127                .zip(&to.stops)
128                .map(|((o1, c1), (o2, c2))| (o1.lerp(o2, t), c1.lerp(c2, t)))
129                .collect(),
130        }
131    }
132}
133
134/// How a node's layer is composited onto what is below it.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
136pub enum BlendMode {
137    #[default]
138    /// Paints over (source-over).
139    Normal,
140    /// Darkens: multiplies colors.
141    Multiply,
142    /// Lightens: inverse of multiply.
143    Screen,
144    /// Multiply or screen, depending on the backdrop.
145    Overlay,
146    /// Keeps the darker color.
147    Darken,
148    /// Keeps the lighter color.
149    Lighten,
150    /// Brightens the backdrop.
151    ColorDodge,
152    /// Darkens the backdrop.
153    ColorBurn,
154    /// Multiply or screen, depending on the source.
155    HardLight,
156    /// A softer hard light.
157    SoftLight,
158    /// Absolute difference of the colors.
159    Difference,
160    /// Like difference, lower contrast.
161    Exclusion,
162    /// Source hue, backdrop saturation and luminosity.
163    Hue,
164    /// Source saturation.
165    Saturation,
166    /// Source hue and saturation.
167    Color,
168    /// Source luminosity.
169    Luminosity,
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn gradient_lerp() {
178        let a = Gradient::linear((0.0, 0.0), (10.0, 0.0)).stops([Color::BLACK, Color::WHITE]);
179        let b = Gradient::linear((0.0, 0.0), (20.0, 0.0)).stops([Color::WHITE, Color::BLACK]);
180        let m = a.lerp(&b, 0.5);
181        assert_eq!(
182            m.kind,
183            GradientKind::Linear {
184                from: Point::ZERO,
185                to: Point::new(15.0, 0.0)
186            }
187        );
188        assert_eq!(m.stops.len(), 2);
189        let c = Gradient::radial((0.0, 0.0), 5.0).stop(0.0, Color::BLACK);
190        assert_eq!(a.lerp(&c, 0.4), a);
191        assert_eq!(a.lerp(&c, 0.6), c);
192    }
193
194    #[test]
195    fn evenly_spaced_stops() {
196        let g = Gradient::linear((0.0, 0.0), (1.0, 0.0)).stops([
197            Color::BLACK,
198            Color::WHITE,
199            Color::BLACK,
200        ]);
201        assert_eq!(
202            g.stops.iter().map(|s| s.0).collect::<Vec<_>>(),
203            vec![0.0, 0.5, 1.0]
204        );
205    }
206}