chladni.rs
5.8 kB · rust · 203 lines
1#![allow(clippy::too_many_arguments)]23use crate::life::life_noise;4use crate::{code_of, Fault, Grid};5use mrlycore::json;6use mrlymath::life;7use mrlynum::fft::{8 convolve_with, embed_kernel, log_spectrum, peak_ring, radial_profile, transform,9};10use wasm_bindgen::prelude::*;1112fn checked_size(size: usize) -> Result<(), Fault> {13 if size == 0 || !size.is_power_of_two() {14 return Err(Fault::new("the field size must be a power of two."));15 }16 Ok(())17}1819fn field_of(types: &[u8], size: usize) -> Result<Vec<f64>, Fault> {20 checked_size(size)?;21 if types.len() != size * size {22 return Err(Fault::new("the grid bytes do not match size squared."));23 }24 Ok(types25 .iter()26 .map(|&t| if t != 0 { 1.0 } else { 0.0 })27 .collect())28}2930struct Mask {31 span: usize,32 cells: Vec<u8>,33 budget: usize,34}3536fn mask_of(code: &str, side: usize, level: usize, size: usize) -> Result<Mask, Fault> {37 checked_size(size)?;38 let mask = life::design_mask(2, code_of(code)?, side, level)?;39 let span = mask.shape[0];40 if span > size {41 return Err(Fault::new("the mask does not fit the field."));42 }43 let budget = mask.sum() as usize;44 if budget == 0 {45 return Err(Fault::new("the mask is empty."));46 }47 Ok(Mask {48 span,49 cells: mask.bytes().to_vec(),50 budget,51 })52}5354fn window(lo: f64, hi: f64, budget: usize) -> Vec<bool> {55 (0..=budget)56 .map(|k| {57 let share = k as f64 / budget as f64;58 lo <= share && share <= hi59 })60 .collect()61}6263struct Engine {64 size: usize,65 budget: usize,66 kernel_re: Vec<f64>,67 kernel_im: Vec<f64>,68 born: Vec<bool>,69 kept: Vec<bool>,70 field: Vec<f64>,71}7273impl Engine {74 fn new(75 code: &str,76 side: usize,77 level: usize,78 b_lo: f64,79 b_hi: f64,80 s_lo: f64,81 s_hi: f64,82 size: usize,83 ) -> Result<Engine, Fault> {84 let mask = mask_of(code, side, level, size)?;85 let (kernel_re, kernel_im) = transform(&embed_kernel(&mask.cells, mask.span, size), size);86 Ok(Engine {87 size,88 budget: mask.budget,89 kernel_re,90 kernel_im,91 born: window(b_lo, b_hi, mask.budget),92 kept: window(s_lo, s_hi, mask.budget),93 field: vec![0.0; size * size],94 })95 }96 fn step(&mut self, types: &mut [u8]) {97 for (slot, &t) in self.field.iter_mut().zip(types.iter()) {98 *slot = if t != 0 { 1.0 } else { 0.0 };99 }100 let counts = convolve_with(&self.field, &self.kernel_re, &self.kernel_im, self.size);101 for (slot, &count) in types.iter_mut().zip(&counts) {102 let n = (count.round().max(0.0) as usize).min(self.budget);103 let lives = if *slot != 0 {104 self.kept[n]105 } else {106 self.born[n]107 };108 *slot = u8::from(lives);109 }110 }111}112113/// Runs a seeded soup on a design mask for the given steps on a wrapped power-of-two torus: a dead cell is born and a live one kept when its neighbour count over the mask budget lies in the closed birth or survive window, the count read by FFT convolution.114#[wasm_bindgen]115pub fn chladni_run(116 code: &str,117 side: usize,118 level: usize,119 b_lo: f64,120 b_hi: f64,121 s_lo: f64,122 s_hi: f64,123 size: usize,124 steps: usize,125 density: f64,126 seed: u32,127) -> Result<Grid, Fault> {128 let mut engine = Engine::new(code, side, level, b_lo, b_hi, s_lo, s_hi, size)?;129 let mut types = life_noise(size, size, density, seed);130 for _ in 0..steps {131 engine.step(&mut types);132 }133 Ok(Grid {134 width: size as u32,135 height: size as u32,136 types,137 })138}139140/// Advances a size-square grid one generation on a design mask under the closed birth and survive windows, wrapping the edges.141#[wasm_bindgen]142pub fn chladni_next(143 types: &[u8],144 size: usize,145 code: &str,146 side: usize,147 level: usize,148 b_lo: f64,149 b_hi: f64,150 s_lo: f64,151 s_hi: f64,152) -> Result<Vec<u8>, Fault> {153 field_of(types, size)?;154 let mut engine = Engine::new(code, side, level, b_lo, b_hi, s_lo, s_hi, size)?;155 let mut next = types.to_vec();156 engine.step(&mut next);157 Ok(next)158}159160/// Draws a design mask centred in a size-square picture, the mask centre at size over two.161#[wasm_bindgen]162pub fn chladni_kernel(code: &str, side: usize, level: usize, size: usize) -> Result<Grid, Fault> {163 let mask = mask_of(code, side, level, size)?;164 let start = size / 2 - mask.span / 2;165 let mut types = vec![0u8; size * size];166 for r in 0..mask.span {167 for c in 0..mask.span {168 types[(start + r) * size + start + c] = mask.cells[r * mask.span + c];169 }170 }171 Ok(Grid {172 width: size as u32,173 height: size as u32,174 types,175 })176}177178/// Reads the centred log magnitude spectrum of a size-square 0/1 field, scaled to at most one by its peak.179#[wasm_bindgen]180pub fn chladni_spectrum(types: &[u8], size: usize) -> Result<Vec<f32>, Fault> {181 let spectrum = log_spectrum(&field_of(types, size)?, size);182 let top = spectrum.iter().cloned().fold(0.0f64, f64::max);183 let scale = if top > 0.0 { 1.0 / top } else { 0.0 };184 Ok(spectrum.iter().map(|&v| (v * scale) as f32).collect())185}186187/// Reads the ring means of a size-square 0/1 field's log spectrum, the peak ring past the centre and its wavelength in cells, as JSON.188#[wasm_bindgen]189pub fn chladni_profile(types: &[u8], size: usize) -> Result<String, Fault> {190 let profile = radial_profile(&log_spectrum(&field_of(types, size)?, size), size);191 let ring = peak_ring(&profile);192 let wavelength = if ring == 0 {193 0.0194 } else {195 size as f64 / ring as f64196 };197 Ok(json!({198 "profile": profile,199 "peak_ring": ring,200 "wavelength": wavelength,201 })202 .to_string())203}