1use crate::color::AlphaColor;
8use crate::encode::{EncodedRadialAxialShading, EncodedShadingPattern, RadialAxialParams};
9use crate::function::StitchingBounds;
10use kurbo::{Affine, Circle, Point, Rect, Shape};
11use smallvec::smallvec;
12
13const MAX_GRADIENT_SUBDIVISION_DEPTH: u8 = 10;
14const RADIAL_EPSILON: f64 = 1.0e-6;
15
16pub struct SvgGradient {
18 pub kind: SvgGradientKind,
20 pub transform: Affine,
22 pub stops: Vec<SvgGradientStop>,
24}
25
26pub enum SvgGradientKind {
28 Linear {
30 start: Point,
32 end: Point,
34 },
35 Radial {
37 start_center: Point,
39 start_radius: f32,
41 end_center: Point,
43 end_radius: f32,
45 },
46}
47
48pub struct SvgGradientStop {
50 pub offset: f32,
52 pub color: [f32; 4],
54}
55
56impl EncodedRadialAxialShading {
57 pub fn as_svg_gradient(
59 &self,
60 pattern: &EncodedShadingPattern,
61 path_bbox: Rect,
62 tolerance: f32,
63 ) -> Option<SvgGradient> {
64 match self.params {
66 RadialAxialParams::Axial => {
67 if !self.axial_extend_covers_bbox(pattern, path_bbox) {
68 return None;
69 }
70 }
71 RadialAxialParams::Radial { .. } => {
72 if !self.radial_extend_covers_bbox(pattern, path_bbox)
73 || pattern.background_color.components()[3] > 0.0
74 {
75 return None;
76 }
77 }
78 }
79
80 Some(SvgGradient {
81 kind: self.native_gradient_kind(),
82 transform: pattern.base_transform.inverse(),
83 stops: approximate_gradient_stops(
84 |t| self.sample_t(pattern, t),
85 &self.normalized_stitching_bounds(),
86 tolerance,
87 ),
88 })
89 }
90
91 fn native_gradient_kind(&self) -> SvgGradientKind {
92 match &self.params {
93 RadialAxialParams::Axial => SvgGradientKind::Linear {
94 start: Point::ZERO,
95 end: Point::new(1.0, 0.0),
96 },
97 RadialAxialParams::Radial { p1, r } => SvgGradientKind::Radial {
98 start_center: Point::ZERO,
99 start_radius: r.x as f32,
100 end_center: *p1,
101 end_radius: r.y as f32,
102 },
103 }
104 }
105
106 fn sample_t(&self, pattern: &EncodedShadingPattern, t: f32) -> [f32; 4] {
107 let (t0, t1) = (self.domain[0], self.domain[1]);
108 let t = t0 + (t1 - t0) * t;
109 let Some(out) = self.function.eval(&smallvec![t]) else {
110 return [0.0, 0.0, 0.0, 0.0];
111 };
112 let mut components = pattern.color_space.to_rgba(&out, 1.0).components();
113 components[3] *= pattern.opacity;
114
115 if let Some(tf) = &pattern.transfer_function {
116 return tf.apply(&AlphaColor::new(components)).components();
117 }
118
119 components
120 }
121
122 fn axial_extend_covers_bbox(&self, pattern: &EncodedShadingPattern, path_bbox: Rect) -> bool {
123 let path_bbox = pattern.base_transform.transform_rect_bbox(path_bbox);
124
125 let min_x = path_bbox.min_x();
126 let max_x = path_bbox.max_x();
127
128 (self.extend[0] || min_x >= 0.0) && (self.extend[1] || max_x <= 1.0)
129 }
130
131 fn radial_extend_covers_bbox(&self, pattern: &EncodedShadingPattern, path_bbox: Rect) -> bool {
132 let RadialAxialParams::Radial { p1, r } = self.params else {
133 return false;
134 };
135
136 let path_bbox = pattern.base_transform.transform_rect_bbox(path_bbox);
137
138 if p1.x.abs() <= RADIAL_EPSILON && p1.y.abs() <= RADIAL_EPSILON {
139 return self.same_center_radial_extend_covers_bbox(path_bbox, r.x, r.y);
140 }
141
142 let start_extend_ok = self.extend[0] || r.x.abs() <= RADIAL_EPSILON;
143 let end_extend_ok = self.extend[1] || circle_covers_bbox(Circle::new(p1, r.y), path_bbox);
144
145 start_extend_ok && end_extend_ok
146 }
147
148 fn same_center_radial_extend_covers_bbox(&self, path_bbox: Rect, r0: f64, r1: f64) -> bool {
149 let mut inner_circle = Circle::new(Point::ZERO, r0);
150 let mut outer_circle = Circle::new(Point::ZERO, r1);
151 let mut inner_extend = self.extend[0];
152 let mut outer_extend = self.extend[1];
153
154 if inner_circle.radius > outer_circle.radius {
155 std::mem::swap(&mut inner_circle, &mut outer_circle);
156 std::mem::swap(&mut inner_extend, &mut outer_extend);
157 }
158
159 let inner_extend_ok = inner_extend
160 || inner_circle.radius <= RADIAL_EPSILON
161 || circle_outside_bbox(inner_circle, path_bbox);
162 let outer_extend_ok = outer_extend || circle_covers_bbox(outer_circle, path_bbox);
163
164 inner_extend_ok && outer_extend_ok
165 }
166
167 fn normalized_stitching_bounds(&self) -> StitchingBounds {
168 let [t0, t1] = self.domain;
169 let dt = t1 - t0;
170 if dt.abs() <= f32::EPSILON {
171 return StitchingBounds::new();
172 }
173
174 let mut bounds = self
175 .function
176 .stitching_bounds()
177 .into_iter()
178 .map(|bound| (bound - t0) / dt)
179 .filter(|bound| *bound > 0.0 && *bound < 1.0)
180 .collect::<StitchingBounds>();
181
182 if dt < 0.0 {
183 bounds.reverse();
184 }
185
186 bounds
187 }
188}
189
190fn circle_covers_bbox(circle: Circle, bbox: Rect) -> bool {
191 if circle.radius < 0.0 {
192 return false;
193 }
194
195 let radius_squared = circle.radius * circle.radius;
196 [
197 Point::new(bbox.x0, bbox.y0),
198 Point::new(bbox.x1, bbox.y0),
199 Point::new(bbox.x0, bbox.y1),
200 Point::new(bbox.x1, bbox.y1),
201 ]
202 .into_iter()
203 .all(|point| {
204 let delta = point - circle.center;
205 delta.hypot2() <= radius_squared
206 })
207}
208
209fn circle_outside_bbox(circle: Circle, bbox: Rect) -> bool {
210 if circle.radius <= 0.0 {
211 return true;
212 }
213
214 let closest = Point::new(
215 circle.center.x.clamp(bbox.x0, bbox.x1),
216 circle.center.y.clamp(bbox.y0, bbox.y1),
217 );
218
219 !circle.contains(closest)
220}
221
222fn approximate_gradient_stops(
223 mut sample: impl FnMut(f32) -> [f32; 4],
224 breakpoints: &[f32],
225 tolerance: f32,
226) -> Vec<SvgGradientStop> {
227 let mut interval_bounds = Vec::with_capacity(breakpoints.len() + 2);
228 interval_bounds.push(0.0);
229 interval_bounds.extend(breakpoints.iter().copied());
230 interval_bounds.push(1.0);
231
232 let mut stops = Vec::new();
233 for interval in interval_bounds.windows(2) {
234 let start_offset = interval[0];
235 let end_offset = interval[1];
236
237 let start_color = sample(start_offset);
238 stops.push(SvgGradientStop {
239 offset: start_offset,
240 color: start_color,
241 });
242
243 let end_color = sample(end_offset);
244
245 approximate_gradient_interval(
246 &mut sample,
247 &mut stops,
248 GradientSample {
249 offset: start_offset,
250 color: start_color,
251 },
252 GradientSample {
253 offset: end_offset,
254 color: end_color,
255 },
256 tolerance.max(0.0),
257 0,
258 );
259 }
260
261 stops
262}
263
264#[derive(Clone, Copy)]
265struct GradientSample {
266 offset: f32,
267 color: [f32; 4],
268}
269
270fn approximate_gradient_interval(
271 sample: &mut impl FnMut(f32) -> [f32; 4],
272 stops: &mut Vec<SvgGradientStop>,
273 start: GradientSample,
274 end: GradientSample,
275 tolerance: f32,
276 depth: u8,
277) {
278 let offset_delta = end.offset - start.offset;
279 let quarter_offset = start.offset + offset_delta * 0.25;
280 let mid_offset = start.offset + offset_delta * 0.5;
281 let three_quarter_offset = start.offset + offset_delta * 0.75;
282
283 let quarter_color = sample(quarter_offset);
284 let mid_color = sample(mid_offset);
285 let three_quarter_color = sample(three_quarter_offset);
286
287 if color_error(quarter_color, lerp_color(start.color, end.color, 0.25)) <= tolerance
288 && color_error(mid_color, lerp_color(start.color, end.color, 0.5)) <= tolerance
289 && color_error(
290 three_quarter_color,
291 lerp_color(start.color, end.color, 0.75),
292 ) <= tolerance
293 || depth == MAX_GRADIENT_SUBDIVISION_DEPTH
294 {
295 stops.push(SvgGradientStop {
296 offset: end.offset,
297 color: end.color,
298 });
299
300 return;
301 }
302
303 let mid = GradientSample {
304 offset: mid_offset,
305 color: mid_color,
306 };
307
308 approximate_gradient_interval(sample, stops, start, mid, tolerance, depth + 1);
309 approximate_gradient_interval(sample, stops, mid, end, tolerance, depth + 1);
310}
311
312fn lerp_color(c0: [f32; 4], c1: [f32; 4], t: f32) -> [f32; 4] {
313 [
314 c0[0] + (c1[0] - c0[0]) * t,
315 c0[1] + (c1[1] - c0[1]) * t,
316 c0[2] + (c1[2] - c0[2]) * t,
317 c0[3] + (c1[3] - c0[3]) * t,
318 ]
319}
320
321fn color_error(c0: [f32; 4], c1: [f32; 4]) -> f32 {
322 c0.iter()
323 .zip(c1)
324 .map(|(a, b)| (a - b).abs())
325 .fold(0.0, f32::max)
326}