Skip to main content

motion_content/vector/ink/
paper.rs

1//! Paper models shared by ink fills and brush strokes.
2
3use crate::ImageAsset;
4use motion_core::{GradientExtend, ImagePattern, Point, Transform, ValidationError};
5use std::sync::Arc;
6
7/// The sheet an [`Ink`](super::Ink) is absorbed into, set with [`Ink::paper`](super::Ink::paper).
8///
9/// Both variants only attenuate pigment coverage and are sampled in paper space, which is local
10/// coordinates mapped by [`Ink::paper_transform`](super::Ink::paper_transform).
11#[derive(Clone, Debug)]
12pub enum Paper {
13    /// Seeded procedural fibers; needs no resources.
14    Procedural {
15        /// Seed of the fiber noise.
16        seed: u32,
17        /// Coverage attenuation in `0..=1`.
18        strength: f32,
19        /// Finite positive paper-space pixels per noise cell.
20        scale: f32,
21    },
22    /// Repeating immutable paper height map.
23    Texture {
24        /// Already resolved image; no implicit file loading during rendering.
25        texture: Arc<ImageAsset>,
26        /// Paper-space units per texel; finite and positive.
27        grain_scale: f64,
28        /// Coverage attenuation by the paper height, in `0..=1`.
29        absorbency: f64,
30    },
31}
32
33impl Paper {
34    pub(super) fn is_valid(&self) -> bool {
35        match self {
36            Self::Procedural {
37                strength, scale, ..
38            } => {
39                strength.is_finite()
40                    && (0.0..=1.0).contains(strength)
41                    && scale.is_finite()
42                    && *scale > 0.0
43            }
44            Self::Texture {
45                grain_scale,
46                absorbency,
47                ..
48            } => {
49                grain_scale.is_finite()
50                    && *grain_scale > 0.0
51                    && absorbency.is_finite()
52                    && (0.0..=1.0).contains(absorbency)
53            }
54        }
55    }
56}
57
58/// A texture paper resolved once for a baked material.
59pub(super) struct PaperSampler {
60    pattern: ImagePattern,
61    absorbency: f64,
62}
63
64impl PaperSampler {
65    pub(super) fn new(texture: Arc<ImageAsset>, grain_scale: f64, absorbency: f64) -> Self {
66        Self {
67            pattern: ImagePattern::from_asset(texture)
68                .transform(Transform::scale(grain_scale))
69                .extend(GradientExtend::Repeat),
70            absorbency,
71        }
72    }
73
74    /// Returns the coverage multiplier at a paper-space point.
75    pub(super) fn factor(&self, point: Point) -> Result<f64, ValidationError> {
76        let sample = image_sample(&self.pattern, point)?;
77        let height = if sample[3] > 0.0 {
78            (sample[0] * 0.2126 + sample[1] * 0.7152 + sample[2] * 0.0722) / sample[3]
79        } else {
80            0.0
81        };
82        Ok(1.0 - self.absorbency * height.clamp(0.0, 1.0))
83    }
84}
85
86/// Bilinear premultiplied sample of an image pattern at an absolute point.
87pub(super) fn image_sample(
88    pattern: &ImagePattern,
89    point: Point,
90) -> Result<[f64; 4], ValidationError> {
91    let inverse = pattern.transform.inverse().ok_or_else(|| {
92        ValidationError::invalid("brush_stroke", "paint", "Noninvertible paint transform")
93    })?;
94    let point = inverse.apply(point);
95    let width = pattern.image.width() as usize;
96    let height = pattern.image.height() as usize;
97    if pattern.decal
98        && (point.x < 0.0 || point.y < 0.0 || point.x >= width as f64 || point.y >= height as f64)
99    {
100        return Ok([0.0; 4]);
101    }
102    let horizontal = point.x - 0.5;
103    let vertical = point.y - 0.5;
104    let column = horizontal.floor();
105    let row = vertical.floor();
106    let fraction = [horizontal - column, vertical - row];
107    let address = |index: i64, length: usize| match pattern.extend {
108        GradientExtend::Pad => index.clamp(0, length as i64 - 1) as usize,
109        GradientExtend::Repeat => index.rem_euclid(length as i64) as usize,
110        GradientExtend::Reflect => {
111            let index = index.rem_euclid(length as i64 * 2) as usize;
112            if index >= length {
113                length * 2 - index - 1
114            } else {
115                index
116            }
117        }
118    };
119    let mut result = [0.0; 4];
120    for vertical in 0..2 {
121        for horizontal in 0..2 {
122            let weight = (if horizontal == 0 {
123                1.0 - fraction[0]
124            } else {
125                fraction[0]
126            }) * (if vertical == 0 {
127                1.0 - fraction[1]
128            } else {
129                fraction[1]
130            });
131            let texel = pattern.image.texel(
132                0,
133                address(column as i64 + horizontal, width),
134                address(row as i64 + vertical, height),
135            );
136            for channel in 0..4 {
137                result[channel] += f64::from(texel[channel]) * weight;
138            }
139        }
140    }
141    Ok(result)
142}