Skip to main content

hayro_interpret/
encode.rs

1//! Encoding shading patterns for easy sampling.
2
3use crate::color::{AlphaColor, ColorComponents, ColorSpace};
4use crate::interpret::state::ActiveTransferFunction;
5use crate::pattern::ShadingPattern;
6use crate::shading::{ShadingFunction, ShadingType, Triangle};
7use crate::util::x_y_advances;
8use kurbo::{Affine, Point, Rect};
9use rustc_hash::FxHashMap;
10use smallvec::{ToSmallVec, smallvec};
11use std::sync::Arc;
12
13/// A shading pattern that was encoded so it can be sampled.
14#[derive(Clone, Debug)]
15pub struct EncodedShadingPattern {
16    /// The base transform of the shading pattern.
17    pub base_transform: Affine,
18    pub(crate) color_space: ColorSpace,
19    pub(crate) background_color: AlphaColor,
20    /// The encoded shading type.
21    pub shading_type: EncodedShadingType,
22    pub(crate) opacity: f32,
23    pub(crate) transfer_function: Option<ActiveTransferFunction>,
24}
25
26/// The encoded shading type.
27#[derive(Clone, Debug)]
28pub enum EncodedShadingType {
29    /// Function-based shading.
30    FunctionBased(EncodedFunctionBasedShading),
31    /// Axial or radial shading.
32    RadialAxial(EncodedRadialAxialShading),
33    /// Mesh-based shading sampled into device pixels.
34    Sampled(EncodedSampledShading),
35    /// Empty dummy shading.
36    Dummy(EncodedDummyShading),
37}
38
39/// Encoded function-based shading.
40#[derive(Clone, Debug)]
41pub struct EncodedFunctionBasedShading {
42    pub(crate) domain: Rect,
43    pub(crate) function: ShadingFunction,
44}
45
46/// Encoded axial or radial shading.
47#[derive(Clone, Debug)]
48pub struct EncodedRadialAxialShading {
49    pub(crate) function: ShadingFunction,
50    pub(crate) params: RadialAxialParams,
51    pub(crate) domain: [f32; 2],
52    pub(crate) extend: [bool; 2],
53}
54
55/// Encoded sampled mesh shading.
56#[derive(Clone, Debug)]
57pub struct EncodedSampledShading {
58    pub(crate) samples: Arc<FxHashMap<(u16, u16), ColorComponents>>,
59    pub(crate) function: Option<ShadingFunction>,
60}
61
62/// Encoded dummy shading.
63#[derive(Clone, Debug)]
64pub struct EncodedDummyShading;
65
66/// A shading that was rasterized into a texture.
67pub struct ShadingTexture {
68    /// The straight (non-premultiplied) RGBA8 pixel data, row-major.
69    pub data: Vec<u8>,
70    /// The width of the texture in pixels.
71    pub width: u32,
72    /// The height of the texture in pixels.
73    pub height: u32,
74    /// The transform from texture space to the space of the rasterized
75    /// bounding box.
76    pub transform: Affine,
77    /// Whether any pixel of the texture is not fully opaque.
78    pub has_transparency: bool,
79}
80
81/// Return the texture dimensions [`EncodedShadingPattern::sample_texture`]
82/// uses for the given bounding box and scale factor.
83///
84/// Exposed so that callers can bound the size of the resulting texture
85/// before sampling.
86pub fn texture_dimensions(bbox: Rect, scale: f32) -> (u32, u32) {
87    (
88        (bbox.width() as f32 * scale).max(1.0).ceil() as u32,
89        (bbox.height() as f32 * scale).max(1.0).ceil() as u32,
90    )
91}
92
93impl EncodedShadingPattern {
94    /// Sample the shading at the given position.
95    #[inline]
96    pub fn sample(&self, pos: Point) -> [f32; 4] {
97        self.shading_type
98            .eval(pos, self.background_color, &self.color_space)
99            .map(|v| {
100                let mut components = v.components();
101                components[3] *= self.opacity;
102
103                if let Some(tf) = &self.transfer_function {
104                    return tf.apply(&AlphaColor::new(components)).components();
105                }
106
107                components
108            })
109            .unwrap_or([0.0, 0.0, 0.0, 0.0])
110    }
111
112    /// Sample the shading on a pixel grid covering `bbox`, with `scale`
113    /// pixels per unit of `bbox`.
114    ///
115    /// The callback is invoked once per pixel in row-major order with the
116    /// straight (non-premultiplied) RGBA sample, letting callers apply their
117    /// own conversion. Returns the dimensions of the grid (see
118    /// [`texture_dimensions`]) and the transform from grid space to the space
119    /// of `bbox`. No size cap is applied — callers that need to bound the
120    /// texture size should check [`texture_dimensions`] first.
121    pub fn sample_texture(
122        &self,
123        bbox: Rect,
124        scale: f32,
125        mut f: impl FnMut([f32; 4]),
126    ) -> (u32, u32, Affine) {
127        let (width, height) = texture_dimensions(bbox, scale);
128        let inv_scale = 1.0 / scale as f64;
129
130        let (x_advance, y_advance) =
131            x_y_advances(&(Affine::scale(inv_scale) * self.base_transform));
132
133        let mut start_point = self.base_transform
134            * Affine::translate((0.5 * inv_scale, 0.5 * inv_scale))
135            * Point::new(bbox.x0, bbox.y0);
136
137        for _ in 0..height {
138            let mut point = start_point;
139
140            for _ in 0..width {
141                f(self.sample(point));
142
143                point += x_advance;
144            }
145
146            start_point += y_advance;
147        }
148
149        (
150            width,
151            height,
152            Affine::translate((bbox.x0, bbox.y0)) * Affine::scale(inv_scale),
153        )
154    }
155
156    /// Rasterize the shading into a straight (non-premultiplied) RGBA8
157    /// texture covering `bbox`, with `scale` pixels per unit of `bbox`.
158    ///
159    /// See [`sample_texture`](Self::sample_texture) for the grid semantics.
160    pub fn render_texture(&self, bbox: Rect, scale: f32) -> ShadingTexture {
161        let (width, height) = texture_dimensions(bbox, scale);
162        let mut data = Vec::with_capacity(width as usize * height as usize * 4);
163        let mut has_transparency = false;
164
165        let (_, _, transform) = self.sample_texture(bbox, scale, |sample| {
166            let converted = [
167                (sample[0] * 255.0 + 0.5) as u8,
168                (sample[1] * 255.0 + 0.5) as u8,
169                (sample[2] * 255.0 + 0.5) as u8,
170                (sample[3] * 255.0 + 0.5) as u8,
171            ];
172
173            has_transparency |= converted[3] != 255;
174            data.extend_from_slice(&converted);
175        });
176
177        ShadingTexture {
178            data,
179            width,
180            height,
181            transform,
182            has_transparency,
183        }
184    }
185}
186
187impl ShadingPattern {
188    /// Encode the shading pattern.
189    pub fn encode(&self) -> EncodedShadingPattern {
190        let base_transform;
191
192        let shading_type = match self.shading.shading_type.as_ref() {
193            ShadingType::FunctionBased {
194                domain,
195                matrix,
196                function,
197            } => {
198                base_transform = (self.matrix * *matrix).inverse();
199                encode_function_shading(domain, function)
200            }
201            ShadingType::RadialAxial {
202                coords,
203                domain,
204                function,
205                extend,
206                axial,
207            } => {
208                let (encoded, initial_transform) =
209                    encode_axial_shading(*coords, *domain, function, *extend, *axial);
210
211                base_transform = initial_transform * self.matrix.inverse();
212
213                encoded
214            }
215            ShadingType::TriangleMesh {
216                triangles,
217                function,
218            } => {
219                let full_transform = self.matrix;
220                let samples = sample_triangles(triangles, full_transform);
221
222                base_transform = Affine::IDENTITY;
223
224                EncodedShadingType::Sampled(EncodedSampledShading {
225                    samples: Arc::new(samples),
226                    function: function.clone(),
227                })
228            }
229            ShadingType::CoonsPatchMesh { patches, function } => {
230                let mut triangles = vec![];
231                for patch in patches {
232                    patch.to_triangles(&mut triangles);
233                }
234
235                let full_transform = self.matrix;
236                let samples = sample_triangles(&triangles, full_transform);
237
238                base_transform = Affine::IDENTITY;
239
240                EncodedShadingType::Sampled(EncodedSampledShading {
241                    samples: Arc::new(samples),
242                    function: function.clone(),
243                })
244            }
245            ShadingType::TensorProductPatchMesh { patches, function } => {
246                let mut triangles = vec![];
247                for patch in patches {
248                    patch.to_triangles(&mut triangles);
249                }
250
251                let full_transform = self.matrix;
252                let samples = sample_triangles(&triangles, full_transform);
253
254                base_transform = Affine::IDENTITY;
255
256                EncodedShadingType::Sampled(EncodedSampledShading {
257                    samples: Arc::new(samples),
258                    function: function.clone(),
259                })
260            }
261            ShadingType::Dummy => {
262                base_transform = Affine::IDENTITY;
263
264                EncodedShadingType::Dummy(EncodedDummyShading)
265            }
266        };
267
268        let color_space = self.shading.color_space.clone();
269
270        let background_color = self
271            .shading
272            .background
273            .as_ref()
274            .map(|b| color_space.to_rgba(b, 1.0))
275            .unwrap_or(AlphaColor::TRANSPARENT);
276
277        EncodedShadingPattern {
278            color_space,
279            background_color,
280            shading_type,
281            base_transform,
282            opacity: self.opacity,
283            transfer_function: self.transfer_function.clone(),
284        }
285    }
286}
287
288fn encode_axial_shading(
289    coords: [f32; 6],
290    domain: [f32; 2],
291    function: &ShadingFunction,
292    extend: [bool; 2],
293    is_axial: bool,
294) -> (EncodedShadingType, Affine) {
295    let initial_transform;
296
297    let params = if is_axial {
298        let [x_0, y_0, x_1, y_1, _, _] = coords;
299
300        initial_transform = ts_from_line_to_line(
301            Point::new(x_0 as f64, y_0 as f64),
302            Point::new(x_1 as f64, y_1 as f64),
303            Point::ZERO,
304            Point::new(1.0, 0.0),
305        );
306
307        RadialAxialParams::Axial
308    } else {
309        let [x_0, y_0, r0, x_1, y_1, r_1] = coords;
310
311        initial_transform = Affine::translate((-x_0 as f64, -y_0 as f64));
312        let new_x1 = x_1 - x_0;
313        let new_y1 = y_1 - y_0;
314
315        let p1 = Point::new(new_x1 as f64, new_y1 as f64);
316        let r = Point::new(r0 as f64, r_1 as f64);
317
318        RadialAxialParams::Radial { p1, r }
319    };
320
321    (
322        EncodedShadingType::RadialAxial(EncodedRadialAxialShading {
323            function: function.clone(),
324            params,
325            domain,
326            extend,
327        }),
328        initial_transform,
329    )
330}
331
332fn sample_triangles(
333    triangles: &[Triangle],
334    transform: Affine,
335) -> FxHashMap<(u16, u16), ColorComponents> {
336    let mut map = FxHashMap::default();
337
338    for t in triangles {
339        let t = {
340            let p0 = transform * t.p0.point;
341            let p1 = transform * t.p1.point;
342            let p2 = transform * t.p2.point;
343
344            let mut v0 = t.p0.clone();
345            v0.point = p0;
346            let mut v1 = t.p1.clone();
347            v1.point = p1;
348            let mut v2 = t.p2.clone();
349            v2.point = p2;
350
351            Triangle::new(v0, v1, v2)
352        };
353
354        let bbox = t.bounding_box();
355
356        for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) {
357            for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) {
358                let point = Point::new(x as f64, y as f64);
359                if t.contains_point(point) {
360                    map.insert((x, y), t.interpolate(point));
361                }
362            }
363        }
364    }
365
366    map
367}
368
369fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType {
370    let domain = Rect::new(
371        domain[0] as f64,
372        domain[2] as f64,
373        domain[1] as f64,
374        domain[3] as f64,
375    );
376
377    EncodedShadingType::FunctionBased(EncodedFunctionBasedShading {
378        domain,
379        function: function.clone(),
380    })
381}
382
383#[derive(Clone, Debug)]
384pub(crate) enum RadialAxialParams {
385    Axial,
386    Radial { p1: Point, r: Point },
387}
388
389impl EncodedShadingType {
390    pub(crate) fn eval(
391        &self,
392        pos: Point,
393        bg_color: AlphaColor,
394        color_space: &ColorSpace,
395    ) -> Option<AlphaColor> {
396        match self {
397            Self::FunctionBased(EncodedFunctionBasedShading { domain, function }) => {
398                if !domain.contains(pos) {
399                    Some(bg_color)
400                } else {
401                    let out = function.eval(&smallvec![pos.x as f32, pos.y as f32])?;
402                    // TODO: Clamp out-of-range values.
403                    Some(color_space.to_rgba(&out, 1.0))
404                }
405            }
406            Self::RadialAxial(EncodedRadialAxialShading {
407                function,
408                params,
409                domain,
410                extend,
411            }) => {
412                let (t0, t1) = (domain[0], domain[1]);
413
414                let mut t = match params {
415                    RadialAxialParams::Axial => pos.x as f32,
416                    RadialAxialParams::Radial { p1, r } => {
417                        radial_pos(&pos, p1, *r, extend[0], extend[1]).unwrap_or(f32::MIN)
418                    }
419                };
420
421                if t == f32::MIN {
422                    return Some(bg_color);
423                }
424
425                if t < 0.0 {
426                    if extend[0] {
427                        t = 0.0;
428                    } else {
429                        return Some(bg_color);
430                    }
431                } else if t > 1.0 {
432                    if extend[1] {
433                        t = 1.0;
434                    } else {
435                        return Some(bg_color);
436                    }
437                }
438
439                let t = t0 + (t1 - t0) * t;
440
441                let val = function.eval(&smallvec![t])?;
442
443                Some(color_space.to_rgba(&val, 1.0))
444            }
445            Self::Sampled(EncodedSampledShading { samples, function }) => {
446                let sample_point = (pos.x as u16, pos.y as u16);
447
448                if let Some(color) = samples.get(&sample_point) {
449                    if let Some(function) = function {
450                        let val = function.eval(&color.to_smallvec())?;
451                        Some(color_space.to_rgba(&val, 1.0))
452                    } else {
453                        Some(color_space.to_rgba(color, 1.0))
454                    }
455                } else {
456                    Some(bg_color)
457                }
458            }
459            Self::Dummy(_) => Some(AlphaColor::TRANSPARENT),
460        }
461    }
462}
463
464fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine {
465    let unit_to_line1 = unit_to_line(src1, src2);
466    let line1_to_unit = unit_to_line1.inverse();
467    let unit_to_line2 = unit_to_line(dst1, dst2);
468
469    unit_to_line2 * line1_to_unit
470}
471
472fn unit_to_line(p0: Point, p1: Point) -> Affine {
473    Affine::new([
474        p1.y - p0.y,
475        p0.x - p1.x,
476        p1.x - p0.x,
477        p1.y - p0.y,
478        p0.x,
479        p0.y,
480    ])
481}
482
483fn radial_pos(
484    pos: &Point,
485    p1: &Point,
486    r: Point,
487    min_extend: bool,
488    max_extend: bool,
489) -> Option<f32> {
490    let r0 = r.x as f32;
491    let dx = p1.x as f32;
492    let dy = p1.y as f32;
493    let dr = r.y as f32 - r0;
494
495    let px = pos.x as f32;
496    let py = pos.y as f32;
497
498    let a = dx * dx + dy * dy - dr * dr;
499    let b = -2.0 * (px * dx + py * dy + r0 * dr);
500    let c = px * px + py * py - r0 * r0;
501
502    let discriminant = b * b - 4.0 * a * c;
503
504    // No solution available.
505    if discriminant < 0.0 {
506        return None;
507    }
508
509    if a.abs() < 1e-6 {
510        if b.abs() < 1e-6 {
511            return None;
512        }
513
514        let t = -c / b;
515
516        if (!min_extend && t < 0.0) || (!max_extend && t > 1.0) {
517            return None;
518        }
519
520        let r_t = r0 + dr * t;
521        if r_t < 0.0 {
522            return None;
523        }
524
525        return Some(t);
526    }
527
528    let sqrt_d = discriminant.sqrt();
529    let t1 = (-b - sqrt_d) / (2.0 * a);
530    let t2 = (-b + sqrt_d) / (2.0 * a);
531
532    let max = t1.max(t2);
533    let mut take_max = Some(max);
534    let min = t1.min(t2);
535    let mut take_min = Some(min);
536
537    if (!min_extend && min < 0.0) || r0 + dr * min < 0.0 {
538        take_min = None;
539    }
540
541    if (!max_extend && max > 1.0) || r0 + dr * max < 0.0 {
542        take_max = None;
543    }
544
545    match (take_min, take_max) {
546        (Some(_), Some(max)) => Some(max),
547        (Some(min), None) => Some(min),
548        (None, Some(max)) => Some(max),
549        (None, None) => None,
550    }
551}