libshimmer 1.0.1

Animated status lines for terminal UIs, paced by the host's work
Documentation
//! Patterns the stock shaders share, kept bit-for-bit with the design swatches.

use crate::paint::Rgb;
use crate::stage::TICK_SECS;

pub(crate) const PINK: Rgb = Rgb(255.0, 78.0, 160.0);
pub(crate) const TANGERINE: Rgb = Rgb(255.0, 140.0, 50.0);
pub(crate) const SUN: Rgb = Rgb(255.0, 214.0, 64.0);
pub(crate) const LIME: Rgb = Rgb(170.0, 240.0, 60.0);
pub(crate) const MINT: Rgb = Rgb(60.0, 240.0, 170.0);
pub(crate) const CYAN: Rgb = Rgb(50.0, 215.0, 255.0);
pub(crate) const AZURE: Rgb = Rgb(70.0, 130.0, 255.0);
pub(crate) const VIOLET: Rgb = Rgb(165.0, 105.0, 255.0);
pub(crate) const MAGENTA: Rgb = Rgb(230.0, 70.0, 230.0);
pub(crate) const CORAL: Rgb = Rgb(255.0, 95.0, 95.0);
pub(crate) const GOLD: Rgb = Rgb(255.0, 190.0, 40.0);
pub(crate) const ICE: Rgb = Rgb(180.0, 230.0, 255.0);
pub(crate) const STEEL: Rgb = Rgb(120.0, 140.0, 165.0);
/// A hot white, brighter than most terminals' default foreground.
pub(crate) const WHITE: Rgb = Rgb(245.0, 245.0, 245.0);

/// A deterministic draw in [0, 1) for `seed`.
pub(crate) fn hash(seed: i64) -> f64 {
    let bytes = seed.to_le_bytes();
    let low = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
    let mut x = (low ^ 0x9e37_79b9).wrapping_mul(0x85eb_ca6b);
    x ^= x >> 13;
    x = x.wrapping_mul(0xc2b2_ae35);
    f64::from(x ^ (x >> 16)) / 4_294_967_296.0
}

pub(crate) fn chance(seed: i64, odds: f64) -> bool {
    hash(seed) < odds
}

#[expect(
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    clippy::cast_precision_loss,
    reason = "The hash is in [0, 1); flooring its scaled value deliberately selects a nonnegative sample, clamped to the slice's last index."
)]
pub(crate) fn pick<T: Copy>(seed: i64, list: &[T]) -> T {
    list[((hash(seed) * list.len() as f64) as usize).min(list.len() - 1)]
}

/// Every `every` seconds, one draw at `odds`; a won draw lasts `span` seconds.
pub(crate) struct Lottery {
    pub(crate) every: f64,
    pub(crate) odds: f64,
    pub(crate) span: f64,
    pub(crate) seed: i64,
}

impl Lottery {
    /// Progress through the won draw under way at `t`, if one is.
    #[expect(
        clippy::cast_possible_truncation,
        reason = "A lottery draw is deliberately identified by its whole animation-clock bucket."
    )]
    pub(crate) fn at(&self, t: f64) -> Option<f64> {
        let draw = (t / self.every).floor();
        let into = t - draw * self.every;
        (into < self.span && chance(self.seed + 7919 * draw as i64, self.odds))
            .then_some(into / self.span)
    }
}

pub(crate) fn frac(x: f64) -> f64 {
    x - x.floor()
}

/// A cosine ease from 0 to 1 over [0, 1].
pub(crate) fn ease(x: f64) -> f64 {
    0.5 - 0.5 * (std::f64::consts::PI * x).cos()
}

/// A smoothstep from `from` to `to`.
pub(crate) fn smooth(from: f64, to: f64, x: f64) -> f64 {
    let t = ((x - from) / (to - from)).clamp(0.0, 1.0);
    t * t * (3.0 - 2.0 * t)
}

/// A raised-cosine bump of half-width `width` around zero.
pub(crate) fn bump(distance: f64, width: f64) -> f64 {
    if distance.abs() < width {
        0.5 + 0.5 * (std::f64::consts::PI * distance / width).cos()
    } else {
        0.0
    }
}

/// One tick of a first-order lag from `value` toward `target` with time constant `tau` seconds.
pub(crate) fn lag(value: f64, target: f64, tau: f64) -> f64 {
    value + (target - value) * (1.0 - (-TICK_SECS / tau).exp())
}

/// An inverse-square falloff: 1 at no distance, ยฝ at `radius`, and never a division by zero.
pub(crate) fn glow(distance: f64, radius: f64) -> f64 {
    let r = distance / radius;
    1.0 / (1.0 + r * r)
}

/// One-dimensional value noise in [0, 1) for `seed`, smooth in `x`.
#[expect(
    clippy::cast_possible_truncation,
    reason = "A lattice point is deliberately identified by its floored coordinate, far inside i64."
)]
pub(crate) fn noise(x: f64, seed: i64) -> f64 {
    let floor = x.floor();
    let u = smooth(0.0, 1.0, x - floor);
    let point = floor as i64;
    let salt = seed.wrapping_mul(0x27d4_eb2d);
    hash(point ^ salt) * (1.0 - u) + hash(point.wrapping_add(1) ^ salt) * u
}

/// `n` as a coordinate.
#[expect(
    clippy::cast_precision_loss,
    reason = "Counts of letters and cells are far below 2^53, where the conversion is exact."
)]
pub(crate) fn real(n: usize) -> f64 {
    n as f64
}

/// The color at `x` in [0, 1] along evenly spaced `stops`.
#[expect(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "A clamped unit coordinate deliberately selects adjacent color stops; the floored nonnegative index is capped below the last stop."
)]
pub(crate) fn ramp(stops: &[Rgb], x: f64) -> Rgb {
    let y = x.clamp(0.0, 1.0) * (stops.len() - 1) as f64;
    let i = (y.floor() as usize).min(stops.len() - 2);
    stops[i + 1].blend(stops[i], y - i as f64)
}

/// A color from `hue` in turns, `saturation` and `lightness`.
pub(crate) fn hsl(hue: f64, saturation: f64, lightness: f64) -> Rgb {
    let a = saturation * lightness.min(1.0 - lightness);
    let channel = |n: f64| {
        let k = (n + 12.0 * frac(hue)) % 12.0;
        255.0 * (lightness - a * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0))
    };
    Rgb(channel(0.0), channel(8.0), channel(4.0))
}

/// Quadrant blocks by bits: upper left 8, upper right 4, lower left 2, lower right 1.
pub(crate) const QUADRANTS: [char; 16] = [
    ' ', 'โ–—', 'โ––', 'โ–„', 'โ–', 'โ–', 'โ–ž', 'โ–Ÿ', 'โ–˜', 'โ–š', 'โ–Œ', 'โ–™', 'โ–€', 'โ–œ', 'โ–›', 'โ–ˆ',
];

/// Octant blocks by bits: pixel `2 * y + x` of a cell's 2ร—4 grid is bit `2 * y + x`. Pixels are
/// square on a 1:2 cell. Patterns Unicode encodes outside the octant block map to those characters.
pub(crate) const OCTANTS: [char; 256] = [
    ' ', '๐œบจ', '๐œบซ', '๐Ÿฎ‚', '๐œด€', 'โ–˜', '๐œด', '๐œด‚', '๐œดƒ', '๐œด„', 'โ–', '๐œด…', '๐œด†', '๐œด‡', '๐œดˆ', 'โ–€', '๐œด‰', '๐œดŠ', '๐œด‹',
    '๐œดŒ', '๐Ÿฏฆ', '๐œด', '๐œดŽ', '๐œด', '๐œด', '๐œด‘', '๐œด’', '๐œด“', '๐œด”', '๐œด•', '๐œด–', '๐œด—', '๐œด˜', '๐œด™', '๐œดš', '๐œด›', '๐œดœ', '๐œด',
    '๐œดž', '๐œดŸ', '๐Ÿฏง', '๐œด ', '๐œดก', '๐œดข', '๐œดฃ', '๐œดค', '๐œดฅ', '๐œดฆ', '๐œดง', '๐œดจ', '๐œดฉ', '๐œดช', '๐œดซ', '๐œดฌ', '๐œดญ', '๐œดฎ', '๐œดฏ',
    '๐œดฐ', '๐œดฑ', '๐œดฒ', '๐œดณ', '๐œดด', '๐œดต', '๐Ÿฎ…', '๐œบฃ', '๐œดถ', '๐œดท', '๐œดธ', '๐œดน', '๐œดบ', '๐œดป', '๐œดผ', '๐œดฝ', '๐œดพ', '๐œดฟ', '๐œต€',
    '๐œต', '๐œต‚', '๐œตƒ', '๐œต„', 'โ––', '๐œต…', '๐œต†', '๐œต‡', '๐œตˆ', 'โ–Œ', '๐œต‰', '๐œตŠ', '๐œต‹', '๐œตŒ', 'โ–ž', '๐œต', '๐œตŽ', '๐œต', '๐œต',
    'โ–›', '๐œต‘', '๐œต’', '๐œต“', '๐œต”', '๐œต•', '๐œต–', '๐œต—', '๐œต˜', '๐œต™', '๐œตš', '๐œต›', '๐œตœ', '๐œต', '๐œตž', '๐œตŸ', '๐œต ', '๐œตก', '๐œตข',
    '๐œตฃ', '๐œตค', '๐œตฅ', '๐œตฆ', '๐œตง', '๐œตจ', '๐œตฉ', '๐œตช', '๐œตซ', '๐œตฌ', '๐œตญ', '๐œตฎ', '๐œตฏ', '๐œตฐ', '๐œบ ', '๐œตฑ', '๐œตฒ', '๐œตณ', '๐œตด',
    '๐œตต', '๐œตถ', '๐œตท', '๐œตธ', '๐œตน', '๐œตบ', '๐œตป', '๐œตผ', '๐œตฝ', '๐œตพ', '๐œตฟ', '๐œถ€', '๐œถ', '๐œถ‚', '๐œถƒ', '๐œถ„', '๐œถ…', '๐œถ†', '๐œถ‡',
    '๐œถˆ', '๐œถ‰', '๐œถŠ', '๐œถ‹', '๐œถŒ', '๐œถ', '๐œถŽ', '๐œถ', 'โ–—', '๐œถ', '๐œถ‘', '๐œถ’', '๐œถ“', 'โ–š', '๐œถ”', '๐œถ•', '๐œถ–', '๐œถ—', 'โ–',
    '๐œถ˜', '๐œถ™', '๐œถš', '๐œถ›', 'โ–œ', '๐œถœ', '๐œถ', '๐œถž', '๐œถŸ', '๐œถ ', '๐œถก', '๐œถข', '๐œถฃ', '๐œถค', '๐œถฅ', '๐œถฆ', '๐œถง', '๐œถจ', '๐œถฉ',
    '๐œถช', '๐œถซ', 'โ–‚', '๐œถฌ', '๐œถญ', '๐œถฎ', '๐œถฏ', '๐œถฐ', '๐œถฑ', '๐œถฒ', '๐œถณ', '๐œถด', '๐œถต', '๐œถถ', '๐œถท', '๐œถธ', '๐œถน', '๐œถบ', '๐œถป',
    '๐œถผ', '๐œถฝ', '๐œถพ', '๐œถฟ', '๐œท€', '๐œท', '๐œท‚', '๐œทƒ', '๐œท„', '๐œท…', '๐œท†', '๐œท‡', '๐œทˆ', '๐œท‰', '๐œทŠ', '๐œท‹', '๐œทŒ', '๐œท', '๐œทŽ',
    '๐œท', '๐œท', '๐œท‘', '๐œท’', '๐œท“', '๐œท”', '๐œท•', '๐œท–', '๐œท—', '๐œท˜', '๐œท™', '๐œทš', 'โ–„', '๐œท›', '๐œทœ', '๐œท', '๐œทž', 'โ–™', '๐œทŸ',
    '๐œท ', '๐œทก', '๐œทข', 'โ–Ÿ', '๐œทฃ', 'โ–†', '๐œทค', '๐œทฅ', 'โ–ˆ',
];

/// A `SplitMix64` stream: a shader's seeded randomness.
#[derive(Clone, Debug)]
pub(crate) struct Rng(u64);

impl Rng {
    pub(crate) const fn new(seed: u64) -> Self {
        Self(seed)
    }

    /// The next 64 random bits.
    pub(crate) fn bits(&mut self) -> u64 {
        self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
        let mut z = self.0;
        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
        z ^ (z >> 31)
    }

    /// A draw from `0..n`, or 0 when `n` is 0.
    #[expect(
        clippy::cast_possible_truncation,
        reason = "The product's high half is below n, which fits a usize."
    )]
    pub(crate) fn below(&mut self, n: usize) -> usize {
        ((u128::from(self.bits()) * n as u128) >> 64) as usize
    }

    /// A draw from [0, 1).
    #[expect(
        clippy::cast_precision_loss,
        reason = "53 random bits convert to f64 exactly."
    )]
    pub(crate) fn unit(&mut self) -> f64 {
        (self.bits() >> 11) as f64 / (1_u64 << 53) as f64
    }

    /// Whether a draw at `odds` wins.
    pub(crate) fn chance(&mut self, odds: f64) -> bool {
        self.unit() < odds
    }

    /// `amount` distinct draws from `0..n`, in draw order; all of `0..n` when `amount` exceeds `n`.
    pub(crate) fn sample(&mut self, n: usize, amount: usize) -> Vec<usize> {
        let mut pool: Vec<usize> = (0..n).collect();
        let amount = amount.min(n);
        for i in 0..amount {
            let j = i + self.below(n - i);
            pool.swap(i, j);
        }
        pool.truncate(amount);
        pool
    }
}