Skip to main content

pfx_post/look/
mod.rs

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(&centre) && 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}