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);
pub(crate) const WHITE: Rgb = Rgb(245.0, 245.0, 245.0);
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)]
}
pub(crate) struct Lottery {
pub(crate) every: f64,
pub(crate) odds: f64,
pub(crate) span: f64,
pub(crate) seed: i64,
}
impl Lottery {
#[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()
}
pub(crate) fn ease(x: f64) -> f64 {
0.5 - 0.5 * (std::f64::consts::PI * x).cos()
}
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)
}
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
}
}
pub(crate) fn lag(value: f64, target: f64, tau: f64) -> f64 {
value + (target - value) * (1.0 - (-TICK_SECS / tau).exp())
}
#[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)
}
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))
}
pub(crate) const QUADRANTS: [char; 16] = [
' ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ', 'โ',
];
pub(crate) const OCTANTS: [char; 256] = [
' ', '๐บจ', '๐บซ', '๐ฎ', '๐ด', 'โ', '๐ด', '๐ด', '๐ด', '๐ด', 'โ', '๐ด
', '๐ด', '๐ด', '๐ด', 'โ', '๐ด', '๐ด', '๐ด',
'๐ด', '๐ฏฆ', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด', '๐ด',
'๐ด', '๐ด', '๐ฏง', '๐ด ', '๐ดก', '๐ดข', '๐ดฃ', '๐ดค', '๐ดฅ', '๐ดฆ', '๐ดง', '๐ดจ', '๐ดฉ', '๐ดช', '๐ดซ', '๐ดฌ', '๐ดญ', '๐ดฎ', '๐ดฏ',
'๐ดฐ', '๐ดฑ', '๐ดฒ', '๐ดณ', '๐ดด', '๐ดต', '๐ฎ
', '๐บฃ', '๐ดถ', '๐ดท', '๐ดธ', '๐ดน', '๐ดบ', '๐ดป', '๐ดผ', '๐ดฝ', '๐ดพ', '๐ดฟ', '๐ต',
'๐ต', '๐ต', '๐ต', '๐ต', 'โ', '๐ต
', '๐ต', '๐ต', '๐ต', 'โ', '๐ต', '๐ต', '๐ต', '๐ต', 'โ', '๐ต', '๐ต', '๐ต', '๐ต',
'โ', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต', '๐ต ', '๐ตก', '๐ตข',
'๐ตฃ', '๐ตค', '๐ตฅ', '๐ตฆ', '๐ตง', '๐ตจ', '๐ตฉ', '๐ตช', '๐ตซ', '๐ตฌ', '๐ตญ', '๐ตฎ', '๐ตฏ', '๐ตฐ', '๐บ ', '๐ตฑ', '๐ตฒ', '๐ตณ', '๐ตด',
'๐ตต', '๐ตถ', '๐ตท', '๐ตธ', '๐ตน', '๐ตบ', '๐ตป', '๐ตผ', '๐ตฝ', '๐ตพ', '๐ตฟ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ
', '๐ถ', '๐ถ',
'๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', 'โ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', 'โ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', 'โ',
'๐ถ', '๐ถ', '๐ถ', '๐ถ', 'โ', '๐ถ', '๐ถ', '๐ถ', '๐ถ', '๐ถ ', '๐ถก', '๐ถข', '๐ถฃ', '๐ถค', '๐ถฅ', '๐ถฆ', '๐ถง', '๐ถจ', '๐ถฉ',
'๐ถช', '๐ถซ', 'โ', '๐ถฌ', '๐ถญ', '๐ถฎ', '๐ถฏ', '๐ถฐ', '๐ถฑ', '๐ถฒ', '๐ถณ', '๐ถด', '๐ถต', '๐ถถ', '๐ถท', '๐ถธ', '๐ถน', '๐ถบ', '๐ถป',
'๐ถผ', '๐ถฝ', '๐ถพ', '๐ถฟ', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท
', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท',
'๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', '๐ท', 'โ', '๐ท', '๐ท', '๐ท', '๐ท', 'โ', '๐ท',
'๐ท ', '๐ทก', '๐ทข', 'โ', '๐ทฃ', 'โ', '๐ทค', '๐ทฅ', 'โ',
];
#[derive(Clone, Debug)]
pub(crate) struct Rng(u64);
impl Rng {
pub(crate) const fn new(seed: u64) -> Self {
Self(seed)
}
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)
}
#[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
}
#[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
}
pub(crate) fn chance(&mut self, odds: f64) -> bool {
self.unit() < odds
}
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
}
}