Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_math/
color.rs

1//! RGB arithmetic with explicit rounding contracts.
2/// Scale each channel by a signed ratio, truncating toward zero, then clamp.
3/// `den` must be nonzero, each channel product must fit `i32`, and a
4/// product of `i32::MIN` must not be divided by -1.
5#[inline]
6pub fn scale_rgb(rgb: (u8, u8, u8), num: i32, den: i32) -> (u8, u8, u8) {
7    let channel = |v: u8| ((i32::from(v) * num) / den).clamp(0, 255) as u8;
8    (channel(rgb.0), channel(rgb.1), channel(rgb.2))
9}
10/// Interpolate using signed division toward zero. Clamp the numerator to the
11/// original denominator before replacing a zero divisor with one. Therefore
12/// a zero denominator always returns `a`, matching the caller contract.
13#[inline]
14pub fn lerp_rgb(a: (u8, u8, u8), b: (u8, u8, u8), num: u16, den: u16) -> (u8, u8, u8) {
15    let num = i32::from(num.min(den));
16    let den = den.max(1);
17    let channel =
18        |x: u8, y: u8| (i32::from(x) + (i32::from(y) - i32::from(x)) * num / i32::from(den)) as u8;
19    (channel(a.0, b.0), channel(a.1, b.1), channel(a.2, b.2))
20}
21/// Q8 interpolation with arithmetic shift (rounding negative deltas down).
22/// `t` is a fraction over 256; 255 deliberately does not quite reach `b`.
23#[inline]
24pub fn lerp_rgb_q8(a: (u8, u8, u8), b: (u8, u8, u8), t: u8) -> (u8, u8, u8) {
25    let channel =
26        |x: u8, y: u8| (i32::from(x) + (((i32::from(y) - i32::from(x)) * i32::from(t)) >> 8)) as u8;
27    (channel(a.0, b.0), channel(a.1, b.1), channel(a.2, b.2))
28}
29#[cfg(test)]
30mod tests {
31    use super::*;
32    #[test]
33    fn ratio_clamping_matches_legacy_formula_including_zero_denominator() {
34        for den in [0u16, 1, 2, 255, u16::MAX] {
35            for num in [0u16, 1, 9, 255, u16::MAX] {
36                for (a, b) in [((3, 4, 5), (9, 8, 7)), ((255, 0, 1), (0, 255, 0))] {
37                    let (n, d) = (num.min(den) as i32, den.max(1) as i32);
38                    let channel = |x: u8, y: u8| (x as i32 + (y as i32 - x as i32) * n / d) as u8;
39                    assert_eq!(
40                        lerp_rgb(a, b, num, den),
41                        (channel(a.0, b.0), channel(a.1, b.1), channel(a.2, b.2))
42                    );
43                }
44            }
45        }
46    }
47    #[test]
48    fn descending_rounding_is_explicit() {
49        assert_eq!(lerp_rgb((3, 0, 255), (0, 3, 0), 1, 2), (2, 1, 128));
50        assert_eq!(lerp_rgb_q8((3, 0, 255), (0, 3, 0), 128), (1, 1, 127));
51        assert_eq!(lerp_rgb((3, 4, 5), (9, 8, 7), 9, 0), (3, 4, 5));
52        assert_eq!(scale_rgb((1, 128, 255), 3, 2), (1, 192, 255));
53        assert_eq!(scale_rgb((1, 128, 255), -1, 2), (0, 0, 0));
54    }
55}