Skip to main content

agg_gui/
paints.rs

1//! Paint definitions consumed by [`DrawCtx`](crate::draw_ctx::DrawCtx).
2//!
3//! Solid color fills are expressed directly via [`Color`]; this module hosts
4//! the richer paint kinds that need their own data: [`LinearGradientPaint`],
5//! [`RadialGradientPaint`], and [`PatternPaint`], plus the supporting
6//! [`GradientStop`] / [`GradientSpread`] / [`FillRule`] types and the CPU-side
7//! `sample` implementations the software backend uses.
8//!
9//! `draw_ctx` re-exports every public name here, so existing call sites that
10//! reach for `agg_gui::draw_ctx::RadialGradientPaint` (etc.) continue to
11//! resolve unchanged.
12
13use std::sync::Arc;
14
15use crate::color::Color;
16use agg_rust::trans_affine::TransAffine;
17
18/// Fill rule used when rasterizing closed paths.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum FillRule {
21    /// Non-zero winding rule.
22    NonZero,
23    /// Even-odd parity rule.
24    EvenOdd,
25}
26
27impl Default for FillRule {
28    fn default() -> Self {
29        Self::NonZero
30    }
31}
32
33/// How a gradient behaves outside the normalized `0..=1` range.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum GradientSpread {
36    /// Clamp to the nearest edge stop.
37    Pad,
38    /// Mirror each repeated interval.
39    Reflect,
40    /// Repeat the gradient ramp.
41    Repeat,
42}
43
44impl Default for GradientSpread {
45    fn default() -> Self {
46        Self::Pad
47    }
48}
49
50/// One color stop in a bridge-level gradient paint.
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct GradientStop {
53    pub offset: f64,
54    pub color: Color,
55}
56
57/// Linear gradient fill paint expressed in local drawing coordinates.
58#[derive(Clone, Debug, PartialEq)]
59pub struct LinearGradientPaint {
60    pub x1: f64,
61    pub y1: f64,
62    pub x2: f64,
63    pub y2: f64,
64    pub transform: TransAffine,
65    pub spread: GradientSpread,
66    pub stops: Vec<GradientStop>,
67}
68
69impl LinearGradientPaint {
70    pub fn sample(&self, mut x: f64, mut y: f64) -> Color {
71        if self.stops.is_empty() {
72            return Color::transparent();
73        }
74
75        self.transform.inverse_transform(&mut x, &mut y);
76
77        let dx = self.x2 - self.x1;
78        let dy = self.y2 - self.y1;
79        let len2 = dx * dx + dy * dy;
80        let t = if len2 > f64::EPSILON {
81            ((x - self.x1) * dx + (y - self.y1) * dy) / len2
82        } else {
83            0.0
84        };
85        let t = apply_spread(t, self.spread);
86
87        sample_stops(&self.stops, t)
88    }
89}
90
91/// Radial/focal gradient fill paint expressed in local drawing coordinates.
92#[derive(Clone, Debug, PartialEq)]
93pub struct RadialGradientPaint {
94    pub cx: f64,
95    pub cy: f64,
96    pub r: f64,
97    pub fx: f64,
98    pub fy: f64,
99    pub transform: TransAffine,
100    pub spread: GradientSpread,
101    pub stops: Vec<GradientStop>,
102}
103
104impl RadialGradientPaint {
105    /// Convenience constructor for the common case: focal point at the centre,
106    /// identity transform, `Pad` spread. Stops are `(offset, color)` pairs.
107    pub fn centered(cx: f64, cy: f64, r: f64, stops: &[(f64, Color)]) -> Self {
108        Self {
109            cx,
110            cy,
111            r,
112            fx: cx,
113            fy: cy,
114            transform: TransAffine::default(),
115            spread: GradientSpread::Pad,
116            stops: stops
117                .iter()
118                .map(|(offset, color)| GradientStop {
119                    offset: *offset,
120                    color: *color,
121                })
122                .collect(),
123        }
124    }
125
126    pub fn sample(&self, mut x: f64, mut y: f64) -> Color {
127        if self.stops.is_empty() {
128            return Color::transparent();
129        }
130
131        self.transform.inverse_transform(&mut x, &mut y);
132
133        let dx = x - self.fx;
134        let dy = y - self.fy;
135        let fx = self.fx - self.cx;
136        let fy = self.fy - self.cy;
137        let a = dx * dx + dy * dy;
138        let t = if a <= f64::EPSILON || self.r <= f64::EPSILON {
139            0.0
140        } else {
141            let b = 2.0 * (fx * dx + fy * dy);
142            let c = fx * fx + fy * fy - self.r * self.r;
143            let disc = (b * b - 4.0 * a * c).max(0.0);
144            let k = (-b + disc.sqrt()) / (2.0 * a);
145            if k > f64::EPSILON {
146                1.0 / k
147            } else {
148                0.0
149            }
150        };
151        sample_stops(&self.stops, apply_spread(t, self.spread))
152    }
153}
154
155/// Repeating raster pattern paint expressed in SVG/user drawing coordinates.
156#[derive(Clone, Debug, PartialEq)]
157pub struct PatternPaint {
158    pub x: f64,
159    pub y: f64,
160    pub width: f64,
161    pub height: f64,
162    pub transform: TransAffine,
163    /// Straight-alpha RGBA tile pixels in bottom-up row order.
164    pub pixels: Arc<Vec<u8>>,
165    pub pixel_width: u32,
166    pub pixel_height: u32,
167}
168
169impl PatternPaint {
170    pub fn sample(&self, mut x: f64, mut y: f64) -> Color {
171        if self.width <= f64::EPSILON
172            || self.height <= f64::EPSILON
173            || self.pixel_width == 0
174            || self.pixel_height == 0
175            || self.pixels.is_empty()
176        {
177            return Color::transparent();
178        }
179
180        self.transform.inverse_transform(&mut x, &mut y);
181        let tx = (x - self.x).rem_euclid(self.width);
182        let ty_down = (y - self.y).rem_euclid(self.height);
183        let px = ((tx / self.width) * self.pixel_width as f64)
184            .floor()
185            .clamp(0.0, self.pixel_width.saturating_sub(1) as f64) as usize;
186        let py = (((self.height - ty_down) / self.height) * self.pixel_height as f64)
187            .floor()
188            .clamp(0.0, self.pixel_height.saturating_sub(1) as f64) as usize;
189        let i = (py * self.pixel_width as usize + px) * 4;
190        if i + 3 >= self.pixels.len() {
191            return Color::transparent();
192        }
193
194        Color::rgba(
195            self.pixels[i] as f32 / 255.0,
196            self.pixels[i + 1] as f32 / 255.0,
197            self.pixels[i + 2] as f32 / 255.0,
198            self.pixels[i + 3] as f32 / 255.0,
199        )
200    }
201}
202
203fn apply_spread(t: f64, spread: GradientSpread) -> f64 {
204    match spread {
205        GradientSpread::Pad => t.clamp(0.0, 1.0),
206        GradientSpread::Repeat => t - t.floor(),
207        GradientSpread::Reflect => {
208            let period = t.rem_euclid(2.0);
209            if period <= 1.0 {
210                period
211            } else {
212                2.0 - period
213            }
214        }
215    }
216}
217
218fn sample_stops(stops: &[GradientStop], t: f64) -> Color {
219    if t <= stops[0].offset {
220        return stops[0].color;
221    }
222    for pair in stops.windows(2) {
223        let a = pair[0];
224        let b = pair[1];
225        if t <= b.offset {
226            let span = (b.offset - a.offset).max(f64::EPSILON);
227            let u = ((t - a.offset) / span).clamp(0.0, 1.0) as f32;
228            return lerp_color(a.color, b.color, u);
229        }
230    }
231    stops[stops.len() - 1].color
232}
233
234fn lerp_color(a: Color, b: Color, t: f32) -> Color {
235    Color::rgba(
236        a.r + (b.r - a.r) * t,
237        a.g + (b.g - a.g) * t,
238        a.b + (b.b - a.b) * t,
239        a.a + (b.a - a.a) * t,
240    )
241}