use std::fmt;
use serde::{Deserialize, Serialize};
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NoiseSeed(u64);
impl NoiseSeed {
#[must_use]
pub fn random() -> Self {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::time::{Duration, SystemTime};
std::thread_local! {
static COUNTER: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
let count = COUNTER.with(|c| {
let v = c.get().wrapping_add(1);
c.set(v);
v
});
let nanos = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.subsec_nanos();
let mut hasher = DefaultHasher::new();
nanos.hash(&mut hasher);
count.hash(&mut hasher);
Self(hasher.finish())
}
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}
}
impl From<u64> for NoiseSeed {
fn from(v: u64) -> Self {
Self(v)
}
}
impl fmt::Display for NoiseSeed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[inline]
fn mix(seed: u64, operation: &str, a: u32, b: u32) -> u64 {
const M: u64 = 0x9e37_79b9_7f4a_7c15;
let mut h = seed;
for byte in operation.bytes() {
h = h.wrapping_mul(M).wrapping_add(u64::from(byte));
h ^= h >> 33;
}
h = h.wrapping_mul(M).wrapping_add(u64::from(a));
h ^= h >> 33;
h = h.wrapping_mul(M).wrapping_add(u64::from(b));
h ^= h >> 33;
h
}
#[inline]
const fn bounded_bytes(h: u64) -> (i8, i8, i8, i8) {
let red = ((h & 0xFFFF) % 7) as i8 - 3;
let green = (((h >> 16) & 0xFFFF) % 7) as i8 - 3;
let blue = (((h >> 32) & 0xFFFF) % 7) as i8 - 3;
let alpha = (((h >> 48) & 0xFFFF) % 7) as i8 - 3;
(red, green, blue, alpha)
}
#[derive(Debug, Clone)]
pub struct NoiseEngine {
seed: NoiseSeed,
}
impl NoiseEngine {
#[must_use]
pub const fn new(seed: NoiseSeed) -> Self {
Self { seed }
}
#[must_use]
pub const fn seed(&self) -> NoiseSeed {
self.seed
}
#[must_use]
pub fn pixel_noise(&self, operation: &str, x: u32, y: u32) -> (i8, i8, i8, i8) {
bounded_bytes(mix(self.seed.0, operation, x, y))
}
#[must_use]
pub fn float_noise(&self, operation: &str, index: u32) -> f64 {
let h = mix(self.seed.0, operation, index, 0);
let upper53 = h >> 11;
let high = ((upper53 >> 21) & 0xFFFF_FFFF) as u32;
let low = (upper53 & ((1_u64 << 21) - 1)) as u32;
let normalized = (f64::from(high) * 2_097_152.0 + f64::from(low)) / 9_007_199_254_740_991.0;
(normalized - 0.5) * 2.0e-5 }
#[must_use]
pub fn rect_noise(&self, operation: &str, index: u32) -> (f64, f64, f64, f64) {
let hash = mix(self.seed.0, operation, index, 0xDEAD_BEEF);
let (red, green, blue, alpha) = bounded_bytes(hash);
let scale = 1.0_f64 / 6.0;
(
f64::from(red) * scale,
f64::from(green) * scale,
f64::from(blue) * scale,
f64::from(alpha) * scale,
)
}
#[must_use]
pub fn webgl_noise(&self, operation: &str, x: u32, y: u32) -> (i8, i8, i8, i8) {
bounded_bytes(mix(self.seed.0, operation, x, y ^ 0xCAFE_BABE))
}
#[must_use]
pub fn js_noise_fn(&self) -> String {
let seed = self.seed.0;
format!(
r"(function() {{
const _SEED = {seed}n;
const _M = 0x9e3779b97f4a7c15n;
const _MASK = 0xFFFFFFFFFFFFFFFFn;
function _mix(seed, op, a, b) {{
let h = BigInt(seed);
for (let i = 0; i < op.length; i++) {{
h = ((h * _M) + BigInt(op.charCodeAt(i))) & _MASK;
h = (h ^ (h >> 33n)) & _MASK;
}}
h = ((h * _M) + BigInt(a)) & _MASK;
h = (h ^ (h >> 33n)) & _MASK;
h = ((h * _M) + BigInt(b)) & _MASK;
h = (h ^ (h >> 33n)) & _MASK;
return h;
}}
function _bb(h) {{
const r = Number((h & 0xFFFFn) % 7n) - 3;
const g = Number(((h >> 16n) & 0xFFFFn) % 7n) - 3;
const b = Number(((h >> 32n) & 0xFFFFn) % 7n) - 3;
const a = Number(((h >> 48n) & 0xFFFFn) % 7n) - 3;
return [r, g, b, a];
}}
globalThis.__stygian_noise = function(operation, x, y) {{
return _bb(_mix(_SEED, operation, x >>> 0, y >>> 0));
}};
globalThis.__stygian_float_noise = function(operation, index) {{
const h = _mix(_SEED, operation, index >>> 0, 0);
const upper53 = h >> 11n;
const normalized = Number(upper53) / 9007199254740991;
return (normalized - 0.5) * 2e-5;
}};
globalThis.__stygian_rect_noise = function(operation, index) {{
const h = _mix(_SEED, operation, index >>> 0, 0xDEADBEEF);
const [r, g, b, a] = _bb(h);
return [r / 6, g / 6, b / 6, a / 6];
}};
globalThis.__stygian_webgl_noise = function(operation, x, y) {{
return _bb(_mix(_SEED, operation, x >>> 0, (y >>> 0) ^ 0xCAFEBABE));
}};
}})();"
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NoiseConfig {
pub seed: Option<NoiseSeed>,
pub canvas_enabled: bool,
pub webgl_enabled: bool,
pub audio_enabled: bool,
pub rects_enabled: bool,
}
impl Default for NoiseConfig {
fn default() -> Self {
Self {
seed: None,
canvas_enabled: true,
webgl_enabled: true,
audio_enabled: true,
rects_enabled: true,
}
}
}
impl NoiseConfig {
#[must_use]
pub fn build_engine(&self) -> NoiseEngine {
let seed = self.seed.unwrap_or_else(NoiseSeed::random);
NoiseEngine::new(seed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_seed_same_args_deterministic() {
let e = NoiseEngine::new(NoiseSeed::from(42_u64));
assert_eq!(
e.pixel_noise("canvas.toDataURL", 10, 20),
e.pixel_noise("canvas.toDataURL", 10, 20)
);
}
#[test]
fn different_seeds_different_outputs() {
let e1 = NoiseEngine::new(NoiseSeed::from(1_u64));
let e2 = NoiseEngine::new(NoiseSeed::from(2_u64));
assert_ne!(
e1.pixel_noise("canvas.toDataURL", 10, 20),
e2.pixel_noise("canvas.toDataURL", 10, 20)
);
}
#[test]
fn pixel_noise_bounded() {
let e = NoiseEngine::new(NoiseSeed::from(0xDEAD_BEEF_u64));
for px in 0u32..16 {
for py in 0u32..16 {
let (red, green, blue, alpha) = e.pixel_noise("canvas", px, py);
assert!((-3..=3).contains(&red), "r={red} out of range");
assert!((-3..=3).contains(&green), "g={green} out of range");
assert!((-3..=3).contains(&blue), "b={blue} out of range");
assert!((-3..=3).contains(&alpha), "a={alpha} out of range");
}
}
}
#[test]
fn webgl_noise_bounded() {
let e = NoiseEngine::new(NoiseSeed::from(0xCAFE_u64));
for px in 0u32..8 {
for py in 0u32..8 {
let (red, green, blue, alpha) = e.webgl_noise("readPixels", px, py);
assert!((-3..=3).contains(&red));
assert!((-3..=3).contains(&green));
assert!((-3..=3).contains(&blue));
assert!((-3..=3).contains(&alpha));
}
}
}
#[test]
fn float_noise_bounded() {
let e = NoiseEngine::new(NoiseSeed::from(7_u64));
for i in 0u32..32 {
let v = e.float_noise("AudioBuffer", i);
assert!(
v.abs() <= 1e-5 + f64::EPSILON,
"float_noise {v} out of range"
);
}
}
#[test]
fn rect_noise_bounded() {
let e = NoiseEngine::new(NoiseSeed::from(99_u64));
for i in 0u32..16 {
let (dx, dy, dw, dh) = e.rect_noise("getBoundingClientRect", i);
assert!(dx.abs() <= 0.5 + f64::EPSILON);
assert!(dy.abs() <= 0.5 + f64::EPSILON);
assert!(dw.abs() <= 0.5 + f64::EPSILON);
assert!(dh.abs() <= 0.5 + f64::EPSILON);
}
}
#[test]
fn noise_config_serde_round_trip() {
let cfg = NoiseConfig {
seed: Some(NoiseSeed::from(555_u64)),
canvas_enabled: true,
webgl_enabled: false,
audio_enabled: true,
rects_enabled: false,
};
let json_result = serde_json::to_string(&cfg);
assert!(json_result.is_ok(), "serialize failed: {json_result:?}");
let Ok(json) = json_result else {
return;
};
let back_result: Result<NoiseConfig, _> = serde_json::from_str(&json);
assert!(back_result.is_ok(), "deserialize failed: {back_result:?}");
let Ok(back) = back_result else {
return;
};
assert_eq!(back.seed, cfg.seed);
assert_eq!(back.canvas_enabled, cfg.canvas_enabled);
assert_eq!(back.webgl_enabled, cfg.webgl_enabled);
assert_eq!(back.audio_enabled, cfg.audio_enabled);
assert_eq!(back.rects_enabled, cfg.rects_enabled);
}
#[test]
fn js_noise_fn_contains_seed() {
let seed = 98_765_u64;
let e = NoiseEngine::new(NoiseSeed::from(seed));
let js = e.js_noise_fn();
assert!(js.contains(&seed.to_string()), "seed not embedded in JS");
assert!(js.contains("__stygian_noise"), "missing __stygian_noise");
assert!(
js.contains("__stygian_float_noise"),
"missing __stygian_float_noise"
);
}
#[test]
fn noise_seed_random_unique() {
let seeds: Vec<NoiseSeed> = (0..100).map(|_| NoiseSeed::random()).collect();
let unique: std::collections::HashSet<u64> = seeds.iter().map(|s| s.as_u64()).collect();
assert_eq!(unique.len(), 100, "random seeds collided");
}
#[test]
fn noise_config_build_engine_uses_seed() {
let cfg = NoiseConfig {
seed: Some(NoiseSeed::from(77_u64)),
..Default::default()
};
let engine = cfg.build_engine();
assert_eq!(engine.seed().as_u64(), 77);
}
#[test]
fn noise_config_build_engine_random_when_none() {
let cfg = NoiseConfig::default();
let e1 = cfg.build_engine();
let e2 = cfg.build_engine();
assert_ne!(e1.seed().as_u64(), e2.seed().as_u64());
}
}