crapify 0.4.0

Deep-fry your images, and other crimes against pixels.
// Xorshift64* — a small, dependency-free PRNG shared across crapifiers that
// need a handful of random draws per invocation. Pulling `rand` in as a direct
// dep would pin a version that could conflict with future `imageproc` bumps;
// the draws here aren't quality-critical (toner speckle, scanline drops,
// per-pass skew angles) so the inline xorshift64* is the right trade-off.

pub struct Xorshift64 {
    state: u64,
}

impl Xorshift64 {
    pub fn new(seed: u64) -> Self {
        // state == 0 is a fixed point of xorshift; the max(1) guard means a
        // zero seed gets bumped to 1 instead of producing an infinite zero
        // stream. xorshift_zero_seed_is_handled is the regression test.
        Self { state: seed.max(1) }
    }

    pub fn next_u64(&mut self) -> u64 {
        // Canonical xorshift64* (Marsaglia, 2003) — shift sequence plus a
        // final multiply by a Knuth-Lemire mixer to break the low-bit
        // correlations that bare xorshift64 has.
        let mut x = self.state;
        x ^= x >> 12;
        x ^= x << 25;
        x ^= x >> 27;
        self.state = x;
        x.wrapping_mul(0x2545F4914F6CDD1D)
    }

    pub fn next_f32(&mut self) -> f32 {
        // Top 24 bits → uniform [0, 1). 24 mantissa bits is the f32 precision
        // ceiling; dividing by 2^24 keeps us strictly inside [0.0, 1.0).
        ((self.next_u64() >> 40) as f32) / ((1u32 << 24) as f32)
    }

    pub fn next_signed_unit(&mut self) -> f32 {
        self.next_f32() * 2.0 - 1.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn xorshift_zero_seed_is_handled() {
        // Naive xorshift gets stuck at 0 forever. The state.max(1) in
        // Xorshift64::new is the load-bearing guard against that.
        let mut rng = Xorshift64::new(0);
        let a = rng.next_u64();
        let b = rng.next_u64();
        assert_ne!(a, 0);
        assert_ne!(a, b);
    }

    #[test]
    fn next_f32_stays_in_unit_interval() {
        let mut rng = Xorshift64::new(0xCAFEF00D);
        for _ in 0..10_000 {
            let v = rng.next_f32();
            assert!((0.0..1.0).contains(&v), "next_f32 escaped [0, 1): {v}");
        }
    }

    #[test]
    fn next_signed_unit_stays_in_signed_unit_interval() {
        let mut rng = Xorshift64::new(0xDEADBEEF);
        for _ in 0..10_000 {
            let v = rng.next_signed_unit();
            assert!(
                (-1.0..1.0).contains(&v),
                "next_signed_unit escaped [-1, 1): {v}"
            );
        }
    }
}