hayro_interpret/
encode.rs

1//! Encoding shading patterns for easy sampling.
2
3use crate::color::{AlphaColor, ColorComponents, ColorSpace};
4use crate::pattern::ShadingPattern;
5use crate::shading::{ShadingFunction, ShadingType, Triangle};
6use kurbo::{Affine, Point};
7use rustc_hash::FxHashMap;
8use smallvec::{ToSmallVec, smallvec};
9
10/// A shading pattern that was encoded so it can be sampled.
11#[derive(Debug)]
12pub struct EncodedShadingPattern {
13    /// The base transform of the shading pattern.
14    pub base_transform: Affine,
15    pub(crate) color_space: ColorSpace,
16    pub(crate) background_color: AlphaColor,
17    pub(crate) shading_type: EncodedShadingType,
18}
19
20impl EncodedShadingPattern {
21    /// Sample the shading at the given position.
22    pub fn sample(&self, pos: Point) -> [f32; 4] {
23        self.shading_type
24            .eval(pos, self.background_color, &self.color_space)
25            .map(|v| v.components())
26            .unwrap_or([0.0, 0.0, 0.0, 0.0])
27    }
28}
29
30impl ShadingPattern {
31    /// Encode the shading pattern.
32    pub fn encode(&self) -> EncodedShadingPattern {
33        let base_transform;
34
35        let shading_type = match self.shading.shading_type.as_ref() {
36            ShadingType::FunctionBased {
37                domain,
38                matrix,
39                function,
40            } => {
41                base_transform = (self.matrix * *matrix).inverse();
42                encode_function_shading(domain, function)
43            }
44            ShadingType::RadialAxial {
45                coords,
46                domain,
47                function,
48                extend,
49                axial,
50            } => {
51                let (encoded, initial_transform) =
52                    encode_axial_shading(*coords, *domain, function, *extend, *axial);
53
54                base_transform = initial_transform * self.matrix.inverse();
55
56                encoded
57            }
58            ShadingType::TriangleMesh {
59                triangles,
60                function,
61            } => {
62                let full_transform = self.matrix;
63                let samples = sample_triangles(triangles, full_transform);
64
65                base_transform = Affine::IDENTITY;
66
67                EncodedShadingType::Sampled {
68                    samples,
69                    function: function.clone(),
70                }
71            }
72            ShadingType::CoonsPatchMesh { patches, function } => {
73                let triangles = patches
74                    .iter()
75                    .flat_map(|p| p.to_triangles())
76                    .collect::<Vec<_>>();
77
78                let full_transform = self.matrix;
79                let samples = sample_triangles(&triangles, full_transform);
80
81                base_transform = Affine::IDENTITY;
82
83                EncodedShadingType::Sampled {
84                    samples,
85                    function: function.clone(),
86                }
87            }
88            ShadingType::TensorProductPatchMesh { patches, function } => {
89                let triangles = patches
90                    .iter()
91                    .flat_map(|p| p.to_triangles())
92                    .collect::<Vec<_>>();
93
94                let full_transform = self.matrix;
95                let samples = sample_triangles(&triangles, full_transform);
96
97                base_transform = Affine::IDENTITY;
98
99                EncodedShadingType::Sampled {
100                    samples,
101                    function: function.clone(),
102                }
103            }
104            ShadingType::Dummy => {
105                base_transform = Affine::IDENTITY;
106
107                EncodedShadingType::Dummy
108            }
109        };
110
111        let color_space = self.shading.color_space.clone();
112
113        let background_color = self
114            .shading
115            .background
116            .as_ref()
117            .map(|b| color_space.to_rgba(b, 1.0))
118            .unwrap_or(AlphaColor::TRANSPARENT);
119
120        EncodedShadingPattern {
121            color_space,
122            background_color,
123            shading_type,
124            base_transform,
125        }
126    }
127}
128
129fn encode_axial_shading(
130    coords: [f32; 6],
131    domain: [f32; 2],
132    function: &ShadingFunction,
133    extend: [bool; 2],
134    is_axial: bool,
135) -> (EncodedShadingType, Affine) {
136    let initial_transform;
137
138    let params = if is_axial {
139        let [x_0, y_0, x_1, y_1, _, _] = coords;
140
141        initial_transform = ts_from_line_to_line(
142            Point::new(x_0 as f64, y_0 as f64),
143            Point::new(x_1 as f64, y_1 as f64),
144            Point::ZERO,
145            Point::new(1.0, 0.0),
146        );
147
148        RadialAxialParams::Axial
149    } else {
150        let [x_0, y_0, r0, x_1, y_1, r_1] = coords;
151
152        initial_transform = Affine::translate((-x_0 as f64, -y_0 as f64));
153        let new_x1 = x_1 - x_0;
154        let new_y1 = y_1 - y_0;
155
156        let p1 = Point::new(new_x1 as f64, new_y1 as f64);
157        let r = Point::new(r0 as f64, r_1 as f64);
158
159        RadialAxialParams::Radial { p1, r }
160    };
161
162    (
163        EncodedShadingType::RadialAxial {
164            function: function.clone(),
165            params,
166            domain,
167            extend,
168        },
169        initial_transform,
170    )
171}
172
173fn sample_triangles(
174    triangles: &[Triangle],
175    transform: Affine,
176) -> FxHashMap<(u16, u16), ColorComponents> {
177    let mut map = FxHashMap::default();
178
179    for t in triangles {
180        let t = {
181            let p0 = transform * t.p0.point;
182            let p1 = transform * t.p1.point;
183            let p2 = transform * t.p2.point;
184
185            let mut v0 = t.p0.clone();
186            v0.point = p0;
187            let mut v1 = t.p1.clone();
188            v1.point = p1;
189            let mut v2 = t.p2.clone();
190            v2.point = p2;
191
192            Triangle::new(v0, v1, v2)
193        };
194
195        let bbox = t.bounding_box();
196
197        for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) {
198            for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) {
199                let point = Point::new(x as f64, y as f64);
200                if t.contains_point(point) {
201                    map.insert((x, y), t.interpolate(point));
202                }
203            }
204        }
205    }
206
207    map
208}
209
210fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType {
211    let domain = kurbo::Rect::new(
212        domain[0] as f64,
213        domain[2] as f64,
214        domain[1] as f64,
215        domain[3] as f64,
216    );
217
218    EncodedShadingType::FunctionBased {
219        domain,
220        function: function.clone(),
221    }
222}
223
224#[derive(Debug)]
225pub(crate) enum RadialAxialParams {
226    Axial,
227    Radial { p1: Point, r: Point },
228}
229
230#[derive(Debug)]
231pub(crate) enum EncodedShadingType {
232    FunctionBased {
233        domain: kurbo::Rect,
234        function: ShadingFunction,
235    },
236    RadialAxial {
237        function: ShadingFunction,
238        params: RadialAxialParams,
239        domain: [f32; 2],
240        extend: [bool; 2],
241    },
242    Sampled {
243        samples: FxHashMap<(u16, u16), ColorComponents>,
244        function: Option<ShadingFunction>,
245    },
246    Dummy,
247}
248
249impl EncodedShadingType {
250    pub(crate) fn eval(
251        &self,
252        pos: Point,
253        bg_color: AlphaColor,
254        color_space: &ColorSpace,
255    ) -> Option<AlphaColor> {
256        match self {
257            EncodedShadingType::FunctionBased { domain, function } => {
258                if !domain.contains(pos) {
259                    Some(bg_color)
260                } else {
261                    let out = function.eval(&smallvec![pos.x as f32, pos.y as f32])?;
262                    // TODO: Clamp out-of-range values.
263                    Some(color_space.to_rgba(&out, 1.0))
264                }
265            }
266            EncodedShadingType::RadialAxial {
267                function,
268                params,
269                domain,
270                extend,
271            } => {
272                let (t0, t1) = (domain[0], domain[1]);
273
274                let mut t = match params {
275                    RadialAxialParams::Axial => pos.x as f32,
276                    RadialAxialParams::Radial { p1, r } => {
277                        radial_pos(&pos, p1, *r, extend[0], extend[1]).unwrap_or(f32::MIN)
278                    }
279                };
280
281                if t == f32::MIN {
282                    return Some(bg_color);
283                }
284
285                if t < 0.0 {
286                    if extend[0] {
287                        t = 0.0;
288                    } else {
289                        return Some(bg_color);
290                    }
291                } else if t > 1.0 {
292                    if extend[1] {
293                        t = 1.0;
294                    } else {
295                        return Some(bg_color);
296                    }
297                }
298
299                let t = t0 + (t1 - t0) * t;
300
301                let val = function.eval(&smallvec![t])?;
302
303                Some(color_space.to_rgba(&val, 1.0))
304            }
305            EncodedShadingType::Sampled { samples, function } => {
306                let sample_point = (pos.x as u16, pos.y as u16);
307
308                if let Some(color) = samples.get(&sample_point) {
309                    if let Some(function) = function {
310                        let val = function.eval(&color.to_smallvec())?;
311                        Some(color_space.to_rgba(&val, 1.0))
312                    } else {
313                        Some(color_space.to_rgba(color, 1.0))
314                    }
315                } else {
316                    Some(bg_color)
317                }
318            }
319            EncodedShadingType::Dummy => Some(AlphaColor::TRANSPARENT),
320        }
321    }
322}
323
324fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine {
325    let unit_to_line1 = unit_to_line(src1, src2);
326    let line1_to_unit = unit_to_line1.inverse();
327    let unit_to_line2 = unit_to_line(dst1, dst2);
328
329    unit_to_line2 * line1_to_unit
330}
331
332fn unit_to_line(p0: Point, p1: Point) -> Affine {
333    Affine::new([
334        p1.y - p0.y,
335        p0.x - p1.x,
336        p1.x - p0.x,
337        p1.y - p0.y,
338        p0.x,
339        p0.y,
340    ])
341}
342
343fn radial_pos(
344    pos: &Point,
345    p1: &Point,
346    r: Point,
347    min_extend: bool,
348    max_extend: bool,
349) -> Option<f32> {
350    let r0 = r.x as f32;
351    let dx = p1.x as f32;
352    let dy = p1.y as f32;
353    let dr = r.y as f32 - r0;
354
355    let px = pos.x as f32;
356    let py = pos.y as f32;
357
358    let a = dx * dx + dy * dy - dr * dr;
359    let b = -2.0 * (px * dx + py * dy + r0 * dr);
360    let c = px * px + py * py - r0 * r0;
361
362    let discriminant = b * b - 4.0 * a * c;
363
364    // No solution available.
365    if discriminant < 0.0 {
366        return None;
367    }
368
369    if a.abs() < 1e-6 {
370        if b.abs() < 1e-6 {
371            return None;
372        }
373
374        let t = -c / b;
375
376        if (!min_extend && t < 0.0) || (!max_extend && t > 1.0) {
377            return None;
378        }
379
380        let r_t = r0 + dr * t;
381        if r_t < 0.0 {
382            return None;
383        }
384
385        return Some(t);
386    }
387
388    let sqrt_d = discriminant.sqrt();
389    let t1 = (-b - sqrt_d) / (2.0 * a);
390    let t2 = (-b + sqrt_d) / (2.0 * a);
391
392    let max = t1.max(t2);
393    let mut take_max = Some(max);
394    let min = t1.min(t2);
395    let mut take_min = Some(min);
396
397    if (!min_extend && min < 0.0) || r0 + dr * min < 0.0 {
398        take_min = None;
399    }
400
401    if (!max_extend && max > 1.0) || r0 + dr * max < 0.0 {
402        take_max = None;
403    }
404
405    match (take_min, take_max) {
406        (Some(_), Some(max)) => Some(max),
407        (Some(min), None) => Some(min),
408        (None, Some(max)) => Some(max),
409        (None, None) => None,
410    }
411}