1pub mod gpu;
2pub mod shader;
3#[cfg(test)]
4mod tests;
5
6pub use gpu::LookGpu;
7
8pub const GRADIENT_STOPS: usize = 8;
9pub const PALETTE_MIN: usize = 2;
10pub const PALETTE_MAX: usize = 256;
11
12fn encode(c: f32) -> f32 {
13 pfx_materials::encode_channel(c.clamp(0.0, 1.0))
14}
15
16pub fn oklab(encoded: [f32; 3]) -> [f32; 3] {
17 let [r, g, b] = encoded.map(pfx_materials::linear_channel);
18 let l = (0.412_221_46 * r + 0.536_332_55 * g + 0.051_445_995 * b).cbrt();
19 let m = (0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b).cbrt();
20 let s = (0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b).cbrt();
21 [
22 0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s,
23 1.977_998_5 * l - 2.428_592_2 * m + 0.450_593_7 * s,
24 0.025_904_037 * l + 0.782_771_77 * m - 0.808_675_77 * s,
25 ]
26}
27
28pub fn srgb(lab: [f32; 3]) -> [f32; 3] {
29 let [l, a, b] = lab;
30 let l_ = l + 0.396_337_78 * a + 0.215_803_76 * b;
31 let m_ = l - 0.105_561_346 * a - 0.063_854_17 * b;
32 let s_ = l - 0.089_484_18 * a - 1.291_485_5 * b;
33 let (l, m, s) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);
34 [
35 4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s,
36 -1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s,
37 -0.004_196_086_3 * l - 0.703_418_6 * m + 1.707_614_7 * s,
38 ]
39 .map(encode)
40}
41
42pub fn mix_oklab(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
43 let la = oklab([a[0], a[1], a[2]]);
44 let lb = oklab([b[0], b[1], b[2]]);
45 let [r, g, bl] = srgb(std::array::from_fn(|i| la[i] + (lb[i] - la[i]) * t));
46 [r, g, bl, a[3] + (b[3] - a[3]) * t]
47}
48
49fn premultiply(c: [f32; 4]) -> [f32; 4] {
50 [c[0] * c[3], c[1] * c[3], c[2] * c[3], c[3]]
51}
52
53fn over(top: [f32; 4], under: [f32; 4]) -> [f32; 4] {
54 std::array::from_fn(|i| top[i] + under[i] * (1.0 - top[3]))
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct Place {
59 pub layout: [f32; 2],
60 pub scale: f32,
61 pub offset: [f32; 2],
62}
63
64impl Place {
65 pub fn area(&self, size: [u32; 2]) -> [f32; 4] {
66 let [x0, y0] = self.to_layout([0.0, 0.0]);
67 let [x1, y1] = self.to_layout([size[0] as f32, size[1] as f32]);
68 [x0, y0, x1, y1]
69 }
70
71 pub fn to_layout(&self, pixel: [f32; 2]) -> [f32; 2] {
72 [
73 (pixel[0] - self.offset[0]) / self.scale,
74 (pixel[1] - self.offset[1]) / self.scale,
75 ]
76 }
77
78 pub fn to_target(&self, point: [f32; 2]) -> [f32; 2] {
79 [
80 point[0] * self.scale + self.offset[0],
81 point[1] * self.scale + self.offset[1],
82 ]
83 }
84}
85
86#[derive(Clone, Debug, PartialEq)]
87pub struct Palette {
88 colours: Vec<[f32; 3]>,
89 lab: Vec<[f32; 3]>,
90}
91
92impl Palette {
93 pub fn new(colours: Vec<[f32; 3]>) -> Result<Self, String> {
94 if !(PALETTE_MIN..=PALETTE_MAX).contains(&colours.len()) {
95 return Err(format!(
96 "a palette holds {PALETTE_MIN} to {PALETTE_MAX} colours, not {}",
97 colours.len()
98 ));
99 }
100 if colours
101 .iter()
102 .flatten()
103 .any(|value| !(0.0..=1.0).contains(value))
104 {
105 return Err("palette colours are sRGB-encoded values in 0..=1".into());
106 }
107 let lab = colours.iter().map(|c| oklab(*c)).collect();
108 Ok(Self { colours, lab })
109 }
110
111 pub fn hex(colours: &[u32]) -> Result<Self, String> {
112 Self::new(
113 colours
114 .iter()
115 .map(|rgb| {
116 [
117 ((rgb >> 16) & 255) as f32 / 255.0,
118 ((rgb >> 8) & 255) as f32 / 255.0,
119 (rgb & 255) as f32 / 255.0,
120 ]
121 })
122 .collect(),
123 )
124 }
125
126 pub fn cube(levels: u32) -> Result<Self, String> {
127 if !(2..=6).contains(&levels) {
128 return Err("a colour cube takes 2 to 6 levels a channel".into());
129 }
130 let step = |i: u32| i as f32 / (levels - 1) as f32;
131 let mut colours = Vec::new();
132 for r in 0..levels {
133 for g in 0..levels {
134 for b in 0..levels {
135 colours.push([step(r), step(g), step(b)]);
136 }
137 }
138 }
139 Self::new(colours)
140 }
141
142 pub fn colours(&self) -> &[[f32; 3]] {
143 &self.colours
144 }
145
146 pub fn len(&self) -> usize {
147 self.colours.len()
148 }
149
150 pub fn is_empty(&self) -> bool {
151 self.colours.is_empty()
152 }
153
154 pub fn nearest_lab(&self, lab: [f32; 3]) -> usize {
155 let mut best = 0;
156 let mut best_distance = f32::MAX;
157 for (index, entry) in self.lab.iter().enumerate() {
158 let d = [entry[0] - lab[0], entry[1] - lab[1], entry[2] - lab[2]];
159 let distance = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
160 if distance < best_distance {
161 best_distance = distance;
162 best = index;
163 }
164 }
165 best
166 }
167
168 pub fn nearest(&self, rgb: [f32; 3]) -> usize {
169 self.nearest_lab(oklab(rgb))
170 }
171
172 pub fn packed(&self) -> Vec<[f32; 4]> {
173 let mut order: Vec<usize> = (0..self.colours.len()).collect();
174 order.sort_by(|&a, &b| self.lab[a][0].total_cmp(&self.lab[b][0]).then(a.cmp(&b)));
175 let mut out = vec![[0.0; 4]; 2 * PALETTE_MAX];
176 for (at, &index) in order.iter().enumerate() {
177 let lab = self.lab[index];
178 let rgb = self.colours[index];
179 out[at] = [lab[0], lab[1], lab[2], index as f32];
180 out[PALETTE_MAX + at] = [rgb[0], rgb[1], rgb[2], 1.0];
181 }
182 out
183 }
184}
185
186pub fn bayer_value(order: u32, x: u32, y: u32) -> u32 {
187 let bits = order.trailing_zeros();
188 let mut value = 0;
189 for i in 0..bits {
190 let xb = (x >> i) & 1;
191 let yb = (y >> i) & 1;
192 value += ((xb ^ yb) * 2 + yb) << (2 * (bits - 1 - i));
193 }
194 value
195}
196
197pub fn bayer_matrix(order: u32) -> Option<Vec<u32>> {
198 matches!(order, 2 | 4 | 8).then(|| {
199 (0..order * order)
200 .map(|i| bayer_value(order, i % order, i / order))
201 .collect()
202 })
203}
204
205pub fn threshold(order: u32, x: u32, y: u32) -> f32 {
206 (bayer_value(order, x % order, y % order) as f32 + 0.5) / (order * order) as f32
207}
208
209#[derive(Clone, Copy, Debug, Default, PartialEq)]
210pub enum Dither {
211 #[default]
212 None,
213 Bayer {
214 order: u32,
215 spread: f32,
216 },
217}
218
219#[derive(Clone, Debug, PartialEq)]
220pub struct Quantise {
221 pub palette: Palette,
222 pub dither: Dither,
223}
224
225impl Quantise {
226 pub fn new(palette: Palette, dither: Dither) -> Result<Self, String> {
227 if let Dither::Bayer { order, spread } = dither {
228 if !matches!(order, 2 | 4 | 8) {
229 return Err("a Bayer dither has order 2, 4 or 8".into());
230 }
231 if !(spread.is_finite() && spread >= 0.0) {
232 return Err("a dither's spread is finite and nonnegative".into());
233 }
234 }
235 Ok(Self { palette, dither })
236 }
237
238 pub fn pixel(&self, premultiplied: [f32; 4], at: [u32; 2]) -> [f32; 4] {
239 let a = premultiplied[3];
240 if a <= 0.0 {
241 return [0.0; 4];
242 }
243 let mut lab = oklab([
244 premultiplied[0] / a,
245 premultiplied[1] / a,
246 premultiplied[2] / a,
247 ]);
248 if let Dither::Bayer { order, spread } = self.dither {
249 lab[0] += (threshold(order, at[0], at[1]) - 0.5) * spread;
250 }
251 let [r, g, b] = self.palette.colours[self.palette.nearest_lab(lab)];
252 [r * a, g * a, b * a, a]
253 }
254
255 pub fn image(&self, pixels: &[[f32; 4]], size: [u32; 2]) -> Vec<[f32; 4]> {
256 pixels
257 .iter()
258 .enumerate()
259 .map(|(i, p)| self.pixel(*p, [i as u32 % size[0], i as u32 / size[0]]))
260 .collect()
261 }
262}
263
264#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
265pub enum Upscale {
266 #[default]
267 Integer,
268 Fit,
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct UpscaleMap {
273 pub source: [u32; 2],
274 pub target: [u32; 2],
275 pub offset: [u32; 2],
276 pub extent: [u32; 2],
277}
278
279impl UpscaleMap {
280 pub fn new(source: [u32; 2], target: [u32; 2], mode: Upscale) -> Result<Self, String> {
281 if source.contains(&0) || target.contains(&0) {
282 return Err("an upscale needs a nonempty source and target".into());
283 }
284 let whole = (target[0] / source[0]).min(target[1] / source[1]);
285 let extent = if mode == Upscale::Integer && whole >= 1 {
286 [source[0] * whole, source[1] * whole]
287 } else if u64::from(target[0]) * u64::from(source[1])
288 <= u64::from(target[1]) * u64::from(source[0])
289 {
290 let h = (2 * u64::from(source[1]) * u64::from(target[0]) + u64::from(source[0]))
291 / (2 * u64::from(source[0]));
292 [target[0], (h as u32).clamp(1, target[1])]
293 } else {
294 let w = (2 * u64::from(source[0]) * u64::from(target[1]) + u64::from(source[1]))
295 / (2 * u64::from(source[1]));
296 [(w as u32).clamp(1, target[0]), target[1]]
297 };
298 Ok(Self {
299 source,
300 target,
301 offset: [(target[0] - extent[0]) / 2, (target[1] - extent[1]) / 2],
302 extent,
303 })
304 }
305
306 pub fn source_pixel(&self, pixel: [u32; 2]) -> Option<[u32; 2]> {
307 let inside = |axis: usize| {
308 pixel[axis] >= self.offset[axis] && pixel[axis] < self.offset[axis] + self.extent[axis]
309 };
310 if !(inside(0) && inside(1)) {
311 return None;
312 }
313 Some(std::array::from_fn(|axis| {
314 (pixel[axis] - self.offset[axis]) * self.source[axis] / self.extent[axis]
315 }))
316 }
317
318 pub fn scale(&self) -> [f32; 2] {
319 [
320 self.extent[0] as f32 / self.source[0] as f32,
321 self.extent[1] as f32 / self.source[1] as f32,
322 ]
323 }
324
325 pub fn image(&self, pixels: &[[f32; 4]], clear: [f32; 4]) -> Vec<[f32; 4]> {
326 let mut out = Vec::with_capacity((self.target[0] * self.target[1]) as usize);
327 for y in 0..self.target[1] {
328 for x in 0..self.target[0] {
329 out.push(self.source_pixel([x, y]).map_or(clear, |[sx, sy]| {
330 pixels[(sy * self.source[0] + sx) as usize]
331 }));
332 }
333 }
334 out
335 }
336}
337
338#[derive(Clone, Copy, Debug, PartialEq)]
339pub enum GradientShape {
340 Linear { from: [f32; 2], to: [f32; 2] },
341 Radial { centre: [f32; 2], radius: [f32; 2] },
342}
343
344#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
345pub enum Interpolation {
346 #[default]
347 Srgb,
348 Oklab,
349}
350
351#[derive(Clone, Copy, Debug, PartialEq)]
352pub struct Gradient {
353 pub shape: GradientShape,
354 pub space: Interpolation,
355 count: u8,
356 offsets: [f32; GRADIENT_STOPS],
357 colours: [[f32; 4]; GRADIENT_STOPS],
358}
359
360impl Gradient {
361 pub fn new(shape: GradientShape, stops: &[(f32, [f32; 4])]) -> Result<Self, String> {
362 if stops.is_empty() || stops.len() > GRADIENT_STOPS {
363 return Err(format!(
364 "a gradient takes 1 to {GRADIENT_STOPS} stops, not {}",
365 stops.len()
366 ));
367 }
368 let finite = |v: &[f32]| v.iter().all(|value| value.is_finite());
369 let geometry_ok = match shape {
370 GradientShape::Linear { from, to } => finite(&from) && finite(&to),
371 GradientShape::Radial { centre, radius } => {
372 finite(¢re) && finite(&radius) && radius[0] > 0.0 && radius[1] > 0.0
373 }
374 };
375 if !geometry_ok {
376 return Err("a gradient's geometry must be finite, with positive radii".into());
377 }
378 let mut offsets = [1.0; GRADIENT_STOPS];
379 let mut colours = [[0.0; 4]; GRADIENT_STOPS];
380 let mut previous = 0.0f32;
381 for (index, (offset, colour)) in stops.iter().enumerate() {
382 if !offset.is_finite() || !finite(colour) {
383 return Err("gradient stops must be finite".into());
384 }
385 previous = offset.clamp(0.0, 1.0).max(previous);
386 offsets[index] = previous;
387 colours[index] = colour.map(|value| value.clamp(0.0, 1.0));
388 }
389 Ok(Self {
390 shape,
391 space: Interpolation::Srgb,
392 count: stops.len() as u8,
393 offsets,
394 colours,
395 })
396 }
397
398 pub fn linear(from: [f32; 2], to: [f32; 2], stops: &[(f32, [f32; 4])]) -> Result<Self, String> {
399 Self::new(GradientShape::Linear { from, to }, stops)
400 }
401
402 pub fn radial(
403 centre: [f32; 2],
404 radius: [f32; 2],
405 stops: &[(f32, [f32; 4])],
406 ) -> Result<Self, String> {
407 Self::new(GradientShape::Radial { centre, radius }, stops)
408 }
409
410 pub fn space(mut self, space: Interpolation) -> Self {
411 self.space = space;
412 self
413 }
414
415 pub fn stops(&self) -> impl Iterator<Item = (f32, [f32; 4])> + '_ {
416 (0..usize::from(self.count)).map(|i| (self.offsets[i], self.colours[i]))
417 }
418
419 pub fn max_alpha(&self) -> f32 {
420 self.stops().map(|(_, c)| c[3]).fold(0.0, f32::max)
421 }
422
423 pub fn geometry(&self) -> [f32; 4] {
424 match self.shape {
425 GradientShape::Linear { from, to } => [from[0], from[1], to[0], to[1]],
426 GradientShape::Radial { centre, radius } => {
427 [centre[0], centre[1], radius[0], radius[1]]
428 }
429 }
430 }
431
432 pub fn header(&self) -> [f32; 4] {
433 [
434 f32::from(self.count),
435 match self.shape {
436 GradientShape::Linear { .. } => 0.0,
437 GradientShape::Radial { .. } => 1.0,
438 },
439 match self.space {
440 Interpolation::Srgb => 0.0,
441 Interpolation::Oklab => 1.0,
442 },
443 self.max_alpha(),
444 ]
445 }
446
447 pub fn packed(&self) -> [[f32; 4]; 12] {
448 let mut out = [[0.0; 4]; 12];
449 out[..GRADIENT_STOPS].copy_from_slice(&self.colours);
450 out[8] = [
451 self.offsets[0],
452 self.offsets[1],
453 self.offsets[2],
454 self.offsets[3],
455 ];
456 out[9] = [
457 self.offsets[4],
458 self.offsets[5],
459 self.offsets[6],
460 self.offsets[7],
461 ];
462 out[10] = self.geometry();
463 out[11] = self.header();
464 out
465 }
466
467 pub fn position(&self, p: [f32; 2]) -> f32 {
468 let t = match self.shape {
469 GradientShape::Linear { from, to } => {
470 let d = [to[0] - from[0], to[1] - from[1]];
471 ((p[0] - from[0]) * d[0] + (p[1] - from[1]) * d[1])
472 / (d[0] * d[0] + d[1] * d[1]).max(1e-12)
473 }
474 GradientShape::Radial { centre, radius } => {
475 let q = [
476 (p[0] - centre[0]) / radius[0],
477 (p[1] - centre[1]) / radius[1],
478 ];
479 (q[0] * q[0] + q[1] * q[1]).sqrt()
480 }
481 };
482 t.clamp(0.0, 1.0)
483 }
484
485 pub fn colour(&self, t: f32) -> [f32; 4] {
486 let n = usize::from(self.count);
487 if t <= self.offsets[0] {
488 return self.colours[0];
489 }
490 for i in 0..n - 1 {
491 if t < self.offsets[i + 1] {
492 let f = (t - self.offsets[i]) / (self.offsets[i + 1] - self.offsets[i]);
493 let (a, b) = (self.colours[i], self.colours[i + 1]);
494 return match self.space {
495 Interpolation::Srgb => std::array::from_fn(|c| a[c] + (b[c] - a[c]) * f),
496 Interpolation::Oklab => mix_oklab(a, b, f),
497 };
498 }
499 }
500 self.colours[n - 1]
501 }
502
503 pub fn paint(&self, p: [f32; 2]) -> [f32; 4] {
504 premultiply(self.colour(self.position(p)))
505 }
506}
507
508#[derive(Clone, Copy, Debug, PartialEq)]
509pub struct Major {
510 pub every: u32,
511 pub width: f32,
512 pub colour: [f32; 4],
513}
514
515#[derive(Clone, Copy, Debug, PartialEq)]
516pub struct Grid {
517 pub spacing: [f32; 2],
518 pub origin: [f32; 2],
519 pub width: f32,
520 pub colour: [f32; 4],
521 pub major: Option<Major>,
522}
523
524impl Grid {
525 pub fn new(spacing: f32, width: f32, colour: [f32; 4]) -> Self {
526 Self {
527 spacing: [spacing; 2],
528 origin: [0.0; 2],
529 width,
530 colour,
531 major: None,
532 }
533 }
534
535 pub fn major(mut self, every: u32, width: f32, colour: [f32; 4]) -> Self {
536 self.major = Some(Major {
537 every,
538 width,
539 colour,
540 });
541 self
542 }
543
544 fn cover(distance: f32, width: f32, scale: f32) -> f32 {
545 (width * scale * 0.5 - distance * scale + 0.5).clamp(0.0, 1.0)
546 }
547
548 pub fn paint(&self, p: [f32; 2], scale: f32) -> [f32; 4] {
549 let mut minor = 0.0f32;
550 let mut major = 0.0f32;
551 for ((&at, &spacing), &origin) in p.iter().zip(&self.spacing).zip(&self.origin) {
552 if spacing.is_nan() || spacing <= 0.0 {
553 continue;
554 }
555 let u = (at - origin) / spacing;
556 let d = (u - (u + 0.5).floor()).abs() * spacing;
557 minor = minor.max(Self::cover(d, self.width, scale));
558 if let Some(m) = self.major.filter(|m| m.every > 0) {
559 let period = spacing * m.every as f32;
560 let v = (at - origin) / period;
561 let d = (v - (v + 0.5).floor()).abs() * period;
562 major = major.max(Self::cover(d, m.width, scale));
563 }
564 }
565 let paint = premultiply(self.colour).map(|value| value * minor);
566 match self.major {
567 Some(m) => over(premultiply(m.colour).map(|value| value * major), paint),
568 None => paint,
569 }
570 }
571}
572
573#[derive(Clone, Copy, Debug, Default, PartialEq)]
574pub struct Backdrop {
575 pub colour: Option<[f32; 4]>,
576 pub gradient: Option<Gradient>,
577 pub grid: Option<Grid>,
578}
579
580impl Backdrop {
581 pub fn paint(&self, p: [f32; 2], scale: f32) -> [f32; 4] {
582 let mut base = self.colour.map_or([0.0; 4], premultiply);
583 if let Some(gradient) = &self.gradient {
584 base = over(gradient.paint(p), base);
585 }
586 if let Some(grid) = &self.grid {
587 base = over(grid.paint(p, scale), base);
588 }
589 base
590 }
591
592 pub fn under(&self, pixels: &[[f32; 4]], size: [u32; 2], place: &Place) -> Vec<[f32; 4]> {
593 pixels
594 .iter()
595 .enumerate()
596 .map(|(i, top)| {
597 let pixel = [
598 (i as u32 % size[0]) as f32 + 0.5,
599 (i as u32 / size[0]) as f32 + 0.5,
600 ];
601 over(*top, self.paint(place.to_layout(pixel), place.scale))
602 })
603 .collect()
604 }
605}
606
607#[derive(Clone, Copy, Debug, PartialEq)]
608pub struct Vignette {
609 pub centre: [f32; 2],
610 pub radius: [f32; 2],
611 pub falloff: f32,
612 pub floor: f32,
613 pub colour: [f32; 4],
614}
615
616impl Vignette {
617 pub fn light(&self, p: [f32; 2]) -> f32 {
618 let q = [
619 (p[0] - self.centre[0]) / self.radius[0].max(1e-6),
620 (p[1] - self.centre[1]) / self.radius[1].max(1e-6),
621 ];
622 let e = (q[0] * q[0] + q[1] * q[1]).sqrt();
623 let t = ((e - 1.0) / self.falloff.max(1e-6)).clamp(0.0, 1.0);
624 let s = t * t * (3.0 - 2.0 * t);
625 let floor = self.floor.clamp(0.0, 1.0);
626 1.0 - (1.0 - floor) * s * self.colour[3].clamp(0.0, 1.0)
627 }
628
629 pub fn pixel(&self, c: [f32; 4], p: [f32; 2]) -> [f32; 4] {
630 let light = self.light(p);
631 [
632 c[0] * light + self.colour[0] * c[3] * (1.0 - light),
633 c[1] * light + self.colour[1] * c[3] * (1.0 - light),
634 c[2] * light + self.colour[2] * c[3] * (1.0 - light),
635 c[3],
636 ]
637 }
638
639 pub fn image(&self, pixels: &[[f32; 4]], size: [u32; 2], place: &Place) -> Vec<[f32; 4]> {
640 pixels
641 .iter()
642 .enumerate()
643 .map(|(i, c)| {
644 let pixel = [
645 (i as u32 % size[0]) as f32 + 0.5,
646 (i as u32 / size[0]) as f32 + 0.5,
647 ];
648 self.pixel(*c, place.to_layout(pixel))
649 })
650 .collect()
651 }
652}
653
654#[derive(Clone, Copy, Debug, PartialEq)]
655pub struct Scanlines {
656 pub period: f32,
657 pub strength: f32,
658}
659
660impl Scanlines {
661 pub fn gain(&self, y: f32) -> f32 {
662 let phase = std::f32::consts::TAU * y / self.period.max(1e-6);
663 1.0 - self.strength.clamp(0.0, 1.0) * (0.5 + 0.5 * phase.cos())
664 }
665}
666
667#[derive(Clone, Copy, Debug, PartialEq)]
668pub struct Glow {
669 pub threshold: f32,
670 pub radius: f32,
671 pub strength: f32,
672 pub tint: [f32; 4],
673}
674
675#[derive(Clone, Copy, Debug, PartialEq)]
676pub struct Curvature {
677 pub amount: f32,
678 pub corner: f32,
679 pub bezel: [f32; 4],
680}
681
682impl Curvature {
683 pub fn source(&self, pixel: [f32; 2], size: [u32; 2]) -> Option<[f32; 2]> {
684 let size = [size[0] as f32, size[1] as f32];
685 let uv = [
686 pixel[0] / size[0] * 2.0 - 1.0,
687 pixel[1] / size[1] * 2.0 - 1.0,
688 ];
689 let w = [
690 uv[0] * (1.0 + self.amount * uv[1] * uv[1]),
691 uv[1] * (1.0 + self.amount * uv[0] * uv[0]),
692 ];
693 (w[0].abs() <= 1.0 && w[1].abs() <= 1.0)
694 .then(|| [(w[0] * 0.5 + 0.5) * size[0], (w[1] * 0.5 + 0.5) * size[1]])
695 }
696}
697
698#[derive(Clone, Copy, Debug, PartialEq)]
699pub struct Aberration {
700 pub shift: f32,
701}
702
703#[derive(Clone, Copy, Debug, Default, PartialEq)]
704pub struct Crt {
705 pub scanlines: Option<Scanlines>,
706 pub glow: Option<Glow>,
707 pub curvature: Option<Curvature>,
708 pub aberration: Option<Aberration>,
709}
710
711impl Crt {
712 pub fn is_empty(&self) -> bool {
713 self.scanlines.is_none()
714 && self.glow.is_none()
715 && self.curvature.is_none()
716 && self.aberration.is_none()
717 }
718
719 pub fn scanlines_image(
720 &self,
721 pixels: &[[f32; 4]],
722 size: [u32; 2],
723 place: &Place,
724 ) -> Vec<[f32; 4]> {
725 let Some(lines) = self.scanlines else {
726 return pixels.to_vec();
727 };
728 pixels
729 .iter()
730 .enumerate()
731 .map(|(i, c)| {
732 let y = (i as u32 / size[0]) as f32 + 0.5;
733 let gain = lines.gain(place.to_layout([0.0, y])[1]);
734 [c[0] * gain, c[1] * gain, c[2] * gain, c[3]]
735 })
736 .collect()
737 }
738}
739
740#[derive(Clone, Copy, Debug, Default, PartialEq)]
741pub enum Blend {
742 #[default]
743 Crossfade,
744 Wipe {
745 angle: f32,
746 softness: f32,
747 },
748}
749
750impl Blend {
751 pub fn weight(&self, progress: f32, p: [f32; 2], area: [f32; 4]) -> f32 {
752 let progress = progress.clamp(0.0, 1.0);
753 match *self {
754 Blend::Crossfade => progress,
755 Blend::Wipe { angle, softness } => {
756 let (front, along) = wipe_front(angle, softness, progress, area);
757 let s = p[0] * along[0] + p[1] * along[1];
758 let soft = softness.max(1e-4);
759 ((front - s) / soft + 0.5).clamp(0.0, 1.0)
760 }
761 }
762 }
763}
764
765pub fn wipe_front(angle: f32, softness: f32, progress: f32, area: [f32; 4]) -> (f32, [f32; 2]) {
766 let along = [angle.cos(), angle.sin()];
767 let corners = [
768 [area[0], area[1]],
769 [area[2], area[1]],
770 [area[0], area[3]],
771 [area[2], area[3]],
772 ]
773 .map(|c| c[0] * along[0] + c[1] * along[1]);
774 let low = corners.iter().copied().fold(f32::MAX, f32::min);
775 let high = corners.iter().copied().fold(f32::MIN, f32::max);
776 let soft = softness.max(1e-4);
777 (low - soft * 0.5 + (high - low + soft) * progress, along)
778}