1use crate::color::{AlphaColor, ColorComponents, ColorSpace};
4use crate::interpret::state::ActiveTransferFunction;
5use crate::pattern::ShadingPattern;
6use crate::shading::{ShadingFunction, ShadingType, Triangle};
7use kurbo::{Affine, Point};
8use rustc_hash::FxHashMap;
9use smallvec::{ToSmallVec, smallvec};
10
11#[derive(Debug)]
13pub struct EncodedShadingPattern {
14 pub base_transform: Affine,
16 pub(crate) color_space: ColorSpace,
17 pub(crate) background_color: AlphaColor,
18 pub(crate) shading_type: EncodedShadingType,
19 pub(crate) opacity: f32,
20 pub(crate) transfer_function: Option<ActiveTransferFunction>,
21}
22
23impl EncodedShadingPattern {
24 #[inline]
26 pub fn sample(&self, pos: Point) -> [f32; 4] {
27 self.shading_type
28 .eval(pos, self.background_color, &self.color_space)
29 .map(|v| {
30 let mut components = v.components();
31 components[3] *= self.opacity;
32
33 if let Some(tf) = &self.transfer_function {
34 return tf.apply(&AlphaColor::new(components)).components();
35 }
36
37 components
38 })
39 .unwrap_or([0.0, 0.0, 0.0, 0.0])
40 }
41}
42
43impl ShadingPattern {
44 pub fn encode(&self) -> EncodedShadingPattern {
46 let base_transform;
47
48 let shading_type = match self.shading.shading_type.as_ref() {
49 ShadingType::FunctionBased {
50 domain,
51 matrix,
52 function,
53 } => {
54 base_transform = (self.matrix * *matrix).inverse();
55 encode_function_shading(domain, function)
56 }
57 ShadingType::RadialAxial {
58 coords,
59 domain,
60 function,
61 extend,
62 axial,
63 } => {
64 let (encoded, initial_transform) =
65 encode_axial_shading(*coords, *domain, function, *extend, *axial);
66
67 base_transform = initial_transform * self.matrix.inverse();
68
69 encoded
70 }
71 ShadingType::TriangleMesh {
72 triangles,
73 function,
74 } => {
75 let full_transform = self.matrix;
76 let samples = sample_triangles(triangles, full_transform);
77
78 base_transform = Affine::IDENTITY;
79
80 EncodedShadingType::Sampled {
81 samples,
82 function: function.clone(),
83 }
84 }
85 ShadingType::CoonsPatchMesh { patches, function } => {
86 let mut triangles = vec![];
87 for patch in patches {
88 patch.to_triangles(&mut triangles);
89 }
90
91 let full_transform = self.matrix;
92 let samples = sample_triangles(&triangles, full_transform);
93
94 base_transform = Affine::IDENTITY;
95
96 EncodedShadingType::Sampled {
97 samples,
98 function: function.clone(),
99 }
100 }
101 ShadingType::TensorProductPatchMesh { patches, function } => {
102 let mut triangles = vec![];
103 for patch in patches {
104 patch.to_triangles(&mut triangles);
105 }
106
107 let full_transform = self.matrix;
108 let samples = sample_triangles(&triangles, full_transform);
109
110 base_transform = Affine::IDENTITY;
111
112 EncodedShadingType::Sampled {
113 samples,
114 function: function.clone(),
115 }
116 }
117 ShadingType::Dummy => {
118 base_transform = Affine::IDENTITY;
119
120 EncodedShadingType::Dummy
121 }
122 };
123
124 let color_space = self.shading.color_space.clone();
125
126 let background_color = self
127 .shading
128 .background
129 .as_ref()
130 .map(|b| color_space.to_rgba(b, 1.0, false))
131 .unwrap_or(AlphaColor::TRANSPARENT);
132
133 EncodedShadingPattern {
134 color_space,
135 background_color,
136 shading_type,
137 base_transform,
138 opacity: self.opacity,
139 transfer_function: self.transfer_function.clone(),
140 }
141 }
142}
143
144fn encode_axial_shading(
145 coords: [f32; 6],
146 domain: [f32; 2],
147 function: &ShadingFunction,
148 extend: [bool; 2],
149 is_axial: bool,
150) -> (EncodedShadingType, Affine) {
151 let initial_transform;
152
153 let params = if is_axial {
154 let [x_0, y_0, x_1, y_1, _, _] = coords;
155
156 initial_transform = ts_from_line_to_line(
157 Point::new(x_0 as f64, y_0 as f64),
158 Point::new(x_1 as f64, y_1 as f64),
159 Point::ZERO,
160 Point::new(1.0, 0.0),
161 );
162
163 RadialAxialParams::Axial
164 } else {
165 let [x_0, y_0, r0, x_1, y_1, r_1] = coords;
166
167 initial_transform = Affine::translate((-x_0 as f64, -y_0 as f64));
168 let new_x1 = x_1 - x_0;
169 let new_y1 = y_1 - y_0;
170
171 let p1 = Point::new(new_x1 as f64, new_y1 as f64);
172 let r = Point::new(r0 as f64, r_1 as f64);
173
174 RadialAxialParams::Radial { p1, r }
175 };
176
177 (
178 EncodedShadingType::RadialAxial {
179 function: function.clone(),
180 params,
181 domain,
182 extend,
183 },
184 initial_transform,
185 )
186}
187
188fn sample_triangles(
189 triangles: &[Triangle],
190 transform: Affine,
191) -> FxHashMap<(i32, i32), ColorComponents> {
192 let mut map = FxHashMap::default();
193
194 for t in triangles {
195 let t = {
196 let p0 = transform * t.p0.point;
197 let p1 = transform * t.p1.point;
198 let p2 = transform * t.p2.point;
199
200 let mut v0 = t.p0.clone();
201 v0.point = p0;
202 let mut v1 = t.p1.clone();
203 v1.point = p1;
204 let mut v2 = t.p2.clone();
205 v2.point = p2;
206
207 Triangle::new(v0, v1, v2)
208 };
209
210 let bbox = t.bounding_box();
211
212 for y in (bbox.y0.floor() as i32)..(bbox.y1.ceil() as i32) {
216 for x in (bbox.x0.floor() as i32)..(bbox.x1.ceil() as i32) {
217 let point = Point::new(x as f64, y as f64);
218 if t.contains_point(point) {
219 map.insert((x, y), t.interpolate(point));
220 }
221 }
222 }
223 }
224
225 map
226}
227
228fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType {
229 let domain = kurbo::Rect::new(
230 domain[0] as f64,
231 domain[2] as f64,
232 domain[1] as f64,
233 domain[3] as f64,
234 );
235
236 EncodedShadingType::FunctionBased {
237 domain,
238 function: function.clone(),
239 }
240}
241
242#[derive(Debug)]
243pub(crate) enum RadialAxialParams {
244 Axial,
245 Radial { p1: Point, r: Point },
246}
247
248#[derive(Debug)]
249pub(crate) enum EncodedShadingType {
250 FunctionBased {
251 domain: kurbo::Rect,
252 function: ShadingFunction,
253 },
254 RadialAxial {
255 function: ShadingFunction,
256 params: RadialAxialParams,
257 domain: [f32; 2],
258 extend: [bool; 2],
259 },
260 Sampled {
261 samples: FxHashMap<(i32, i32), ColorComponents>,
262 function: Option<ShadingFunction>,
263 },
264 Dummy,
265}
266
267impl EncodedShadingType {
268 pub(crate) fn eval(
269 &self,
270 pos: Point,
271 bg_color: AlphaColor,
272 color_space: &ColorSpace,
273 ) -> Option<AlphaColor> {
274 match self {
275 Self::FunctionBased { domain, function } => {
276 if !domain.contains(pos) {
277 Some(bg_color)
278 } else {
279 let out = function.eval(&smallvec![pos.x as f32, pos.y as f32])?;
280 Some(color_space.to_rgba(&out, 1.0, false))
282 }
283 }
284 Self::RadialAxial {
285 function,
286 params,
287 domain,
288 extend,
289 } => {
290 let (t0, t1) = (domain[0], domain[1]);
291
292 let mut t = match params {
293 RadialAxialParams::Axial => pos.x as f32,
294 RadialAxialParams::Radial { p1, r } => {
295 radial_pos(&pos, p1, *r, extend[0], extend[1]).unwrap_or(f32::MIN)
296 }
297 };
298
299 if t == f32::MIN {
300 return Some(bg_color);
301 }
302
303 if t < 0.0 {
304 if extend[0] {
305 t = 0.0;
306 } else {
307 return Some(bg_color);
308 }
309 } else if t > 1.0 {
310 if extend[1] {
311 t = 1.0;
312 } else {
313 return Some(bg_color);
314 }
315 }
316
317 let t = t0 + (t1 - t0) * t;
318
319 let val = function.eval(&smallvec![t])?;
320
321 Some(color_space.to_rgba(&val, 1.0, false))
322 }
323 Self::Sampled { samples, function } => {
324 let sample_point = (pos.x.round() as i32, pos.y.round() as i32);
329
330 if let Some(color) = samples.get(&sample_point) {
331 if let Some(function) = function {
332 let val = function.eval(&color.to_smallvec())?;
333 Some(color_space.to_rgba(&val, 1.0, false))
334 } else {
335 Some(color_space.to_rgba(color, 1.0, false))
336 }
337 } else {
338 Some(bg_color)
339 }
340 }
341 Self::Dummy => Some(AlphaColor::TRANSPARENT),
342 }
343 }
344}
345
346fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine {
347 let unit_to_line1 = unit_to_line(src1, src2);
348 let line1_to_unit = unit_to_line1.inverse();
349 let unit_to_line2 = unit_to_line(dst1, dst2);
350
351 unit_to_line2 * line1_to_unit
352}
353
354fn unit_to_line(p0: Point, p1: Point) -> Affine {
355 Affine::new([
356 p1.y - p0.y,
357 p0.x - p1.x,
358 p1.x - p0.x,
359 p1.y - p0.y,
360 p0.x,
361 p0.y,
362 ])
363}
364
365fn radial_pos(
366 pos: &Point,
367 p1: &Point,
368 r: Point,
369 min_extend: bool,
370 max_extend: bool,
371) -> Option<f32> {
372 let r0 = r.x as f32;
373 let dx = p1.x as f32;
374 let dy = p1.y as f32;
375 let dr = r.y as f32 - r0;
376
377 let px = pos.x as f32;
378 let py = pos.y as f32;
379
380 let a = dx * dx + dy * dy - dr * dr;
381 let b = -2.0 * (px * dx + py * dy + r0 * dr);
382 let c = px * px + py * py - r0 * r0;
383
384 let discriminant = b * b - 4.0 * a * c;
385
386 if discriminant < 0.0 {
388 return None;
389 }
390
391 if a.abs() < 1e-6 {
392 if b.abs() < 1e-6 {
393 return None;
394 }
395
396 let t = -c / b;
397
398 if (!min_extend && t < 0.0) || (!max_extend && t > 1.0) {
399 return None;
400 }
401
402 let r_t = r0 + dr * t;
403 if r_t < 0.0 {
404 return None;
405 }
406
407 return Some(t);
408 }
409
410 let sqrt_d = discriminant.sqrt();
411 let t1 = (-b - sqrt_d) / (2.0 * a);
412 let t2 = (-b + sqrt_d) / (2.0 * a);
413
414 let max = t1.max(t2);
415 let mut take_max = Some(max);
416 let min = t1.min(t2);
417 let mut take_min = Some(min);
418
419 if (!min_extend && min < 0.0) || r0 + dr * min < 0.0 {
420 take_min = None;
421 }
422
423 if (!max_extend && max > 1.0) || r0 + dr * max < 0.0 {
424 take_max = None;
425 }
426
427 match (take_min, take_max) {
428 (Some(_), Some(max)) => Some(max),
429 (Some(min), None) => Some(min),
430 (None, Some(max)) => Some(max),
431 (None, None) => None,
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::shading::{Triangle, TriangleVertex};
439 use kurbo::{Affine, Point};
440 use rustc_hash::FxHashMap;
441 use smallvec::smallvec;
442
443 fn make_vertex(x: f64, y: f64, color: f32) -> TriangleVertex {
444 TriangleVertex::new(0, Point::new(x, y), smallvec![color])
445 }
446
447 #[test]
451 fn sample_triangles_negative_coords() {
452 let v0 = make_vertex(-2.0, -2.0, 0.0);
453 let v1 = make_vertex(2.0, -2.0, 1.0);
454 let v2 = make_vertex(0.0, 2.0, 0.5);
455 let tri = Triangle::new(v0, v1, v2);
456
457 let map = sample_triangles(&[tri], Affine::IDENTITY);
458
459 assert!(
461 map.keys().any(|(x, _)| *x < 0),
462 "expected negative x keys in sample map"
463 );
464 assert!(
465 map.keys().any(|(_, y)| *y < 0),
466 "expected negative y keys in sample map"
467 );
468 }
469
470 #[test]
473 fn sampled_eval_roundtrip() {
474 use crate::color::ColorSpace;
475
476 let mut samples: FxHashMap<(i32, i32), ColorComponents> = FxHashMap::default();
477 samples.insert((10, 20), smallvec![0.5]);
478
479 let stype = EncodedShadingType::Sampled {
480 samples,
481 function: None,
482 };
483
484 let cs = ColorSpace::device_gray();
485 let bg = AlphaColor::TRANSPARENT;
486
487 let hit = stype.eval(Point::new(10.0, 20.0), bg, &cs);
489 assert!(hit.is_some(), "exact integer lookup should find sample");
490 let color = hit.unwrap();
491 assert!(color.components()[3] > 0.0, "sample should be opaque");
492
493 let hit2 = stype.eval(Point::new(10.4, 20.4), bg, &cs);
495 assert!(
496 hit2.is_some(),
497 "nearby point (0.4 offset) should hit same bucket"
498 );
499
500 let miss = stype.eval(Point::new(10.6, 20.6), bg, &cs);
502 assert_eq!(
503 miss.map(|c| c.components()[3]),
504 Some(bg.components()[3]),
505 "point rounding to (11,21) should return bg"
506 );
507 }
508}