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