motion_core/drawing/mesh/
evaluate.rs1use crate::{MeshVertex, Point, ValidationError, color};
4
5const MAX_POSITION: f64 = 1e6;
7
8const ROOT_SLACK: f64 = 1e-6;
10
11const MAX_LAB_COMPONENT: f64 = 1e10;
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct MeshPatch {
17 positions: [Point; 4],
18 colors: [[f64; 4]; 4],
19}
20
21impl MeshPatch {
22 pub(super) fn new(vertices: [MeshVertex; 4]) -> Result<Self, ValidationError> {
23 let positions = vertices.map(|vertex| vertex.position);
24 let colors = vertices.map(|vertex| {
25 let [red, green, blue, alpha] = vertex.color.to_rgba_float();
26 let [lightness, horizontal, vertical] = color::srgb_to_oklab([red, green, blue]);
27 [lightness, horizontal, vertical, alpha]
28 });
29 if positions.iter().any(|point| {
30 !point.x.is_finite()
31 || !point.y.is_finite()
32 || point.x.abs() > MAX_POSITION
33 || point.y.abs() > MAX_POSITION
34 }) || colors
35 .iter()
36 .flatten()
37 .any(|value| !value.is_finite() || value.abs() > MAX_LAB_COMPONENT)
38 {
39 return Err(ValidationError::invalid(
40 "mesh_gradient",
41 "patch",
42 "Analytic patch coordinates and colors exceed finite working support",
43 ));
44 }
45 Ok(Self { positions, colors })
46 }
47
48 pub fn positions(&self) -> &[Point; 4] {
50 &self.positions
51 }
52
53 pub fn colors(&self) -> &[[f64; 4]; 4] {
55 &self.colors
56 }
57
58 pub fn sample(&self, point: Point) -> Result<Option<[f32; 4]>, ValidationError> {
64 if !point.x.is_finite() || !point.y.is_finite() {
65 return Err(ValidationError::invalid(
66 "mesh_gradient",
67 "point",
68 "Mesh samples must be finite",
69 ));
70 }
71 let [a, b, c, d] = self.positions;
72 let b = [b.x - a.x, b.y - a.y];
73 let c = [c.x - a.x, c.y - a.y];
74 let d = [d.x - a.x - b[0] - c[0], d.y - a.y - b[1] - c[1]];
75 let q = [point.x - a.x, point.y - a.y];
76 let aa = -cross(b, d);
77 let bb = cross(q, d) - cross(b, c);
78 let cc = cross(q, c);
79 let scale = aa.abs().max(bb.abs()).max(cc.abs());
80 if scale == 0.0 {
81 return Ok(None);
82 }
83 let roots = if aa.abs() <= scale * 1e-12 {
84 if bb.abs() <= scale * 1e-12 {
85 return Ok(None);
86 }
87 [-cc / bb; 2]
88 } else {
89 let discriminant = bb * bb - 4.0 * aa * cc;
90 if discriminant < 0.0 {
91 return Ok(None);
92 }
93 let root = libm::sqrt(discriminant);
94 let quotient = -0.5 * (bb + if bb >= 0.0 { root } else { -root });
96 if quotient == 0.0 {
97 [0.0; 2]
98 } else if bb >= 0.0 {
99 [cc / quotient, quotient / aa]
100 } else {
101 [quotient / aa, cc / quotient]
102 }
103 };
104 for u in roots {
105 let direction = [c[0] + d[0] * u, c[1] + d[1] * u];
106 let axis = usize::from(direction[1].abs() > direction[0].abs());
107 if direction[axis] == 0.0 {
108 continue;
109 }
110 let v = (q[axis] - b[axis] * u) / direction[axis];
111 if !(-ROOT_SLACK..=1.0 + ROOT_SLACK).contains(&u)
112 || !(-ROOT_SLACK..=1.0 + ROOT_SLACK).contains(&v)
113 {
114 continue;
115 }
116 let smooth = |value: f64| {
117 let value = value.clamp(0.0, 1.0);
118 value * value * (3.0 - 2.0 * value)
119 };
120 let (u, v) = (smooth(u), smooth(v));
121 let mixed: [f64; 4] = std::array::from_fn(|channel| {
122 let top = self.colors[0][channel]
123 + (self.colors[1][channel] - self.colors[0][channel]) * u;
124 let bottom = self.colors[2][channel]
125 + (self.colors[3][channel] - self.colors[2][channel]) * u;
126 top + (bottom - top) * v
127 });
128 let rgb =
129 color::oklab_to_srgb([mixed[0], mixed[1], mixed[2]]).map(color::srgb_to_linear);
130 let result = [
131 rgb[0] * mixed[3],
132 rgb[1] * mixed[3],
133 rgb[2] * mixed[3],
134 mixed[3],
135 ]
136 .map(|value| value as f32);
137 if result.iter().any(|value| !value.is_finite()) {
138 return Err(ValidationError::invalid(
139 "mesh_gradient",
140 "color",
141 "Mesh sample exceeds finite working storage",
142 ));
143 }
144 return Ok(Some(result));
145 }
146 Ok(None)
147 }
148}
149
150fn cross(first: [f64; 2], second: [f64; 2]) -> f64 {
151 first[0] * second[1] - first[1] * second[0]
152}