spin.rs
6.1 kB · rust · 208 lines
1use crate::{code_of, Fault, Grid, Pixels};2use mrlycore::{json, Colorizer};3use mrlylab::moire::{presets, render, Field};4use mrlymath::six;5use mrlynum::spin;6use wasm_bindgen::prelude::*;78pub(crate) fn ramp_of(ramp: &str) -> Colorizer {9 match ramp {10 "heat" => Colorizer::heat(),11 "diverge" => Colorizer::diverge(),12 _ => Colorizer::fire(),13 }14}1516fn slice_raster(17 code: &str,18 number: usize,19 level: usize,20 base: usize,21 size: usize,22) -> Result<Vec<f32>, Fault> {23 let cell = six::cut_design(code_of(code)?, number, level, base)?;24 Ok(six::raster(&cell, size)?)25}2627/// Spins a square field about its centre: the exact circle means at the steps radii from the centre to the corner.28#[wasm_bindgen]29pub fn profile(field: &[f32], size: usize, steps: usize) -> Result<Vec<f32>, Fault> {30 square(field, size)?;31 Ok(spin::profile(field, size, steps))32}3334/// Rasterizes the diagonal slice of the cube the code names on a square of the size: one byte per pixel, one on a fill.35#[wasm_bindgen]36pub fn slice_grid(37 code: &str,38 number: usize,39 level: usize,40 base: usize,41 size: usize,42) -> Result<Grid, Fault> {43 let data = slice_raster(code, number, level, base, size)?;44 Ok(Grid {45 width: size as u32,46 height: size as u32,47 types: data.iter().map(|&v| v as u8).collect(),48 })49}5051fn painted(52 data: Vec<f32>,53 size: usize,54 ramp: &str,55 levels: usize,56 invert: bool,57) -> Result<Pixels, Fault> {58 if size == 0 || data.len() != size * size {59 return Err(Fault::new(60 "the field must be size by size with size at least 1.",61 ));62 }63 let field = Field::from_data(data, size);64 let png = render(&field, &ramp_of(ramp), levels, false, invert, 1)?;65 let (width, height, colors) = mrlycore::unpng(&png)?;66 Ok(Pixels::of(width, height, colors))67}6869/// Paints a ring profile back over a square of the size, quantized into levels through the fire, heat or diverge ramp.70#[wasm_bindgen]71pub fn wheel(72 profile: &[f32],73 size: usize,74 ramp: &str,75 levels: usize,76 invert: bool,77) -> Result<Pixels, Fault> {78 if size == 0 {79 return Err(Fault::new("size must be at least 1."));80 }81 painted(spin::wheel(profile, size), size, ramp, levels, invert)82}8384/// Paints a square field, quantized into levels through the fire, heat or diverge ramp.85#[wasm_bindgen]86pub fn sheet(87 field: &[f32],88 size: usize,89 ramp: &str,90 levels: usize,91 invert: bool,92) -> Result<Pixels, Fault> {93 painted(field.to_vec(), size, ramp, levels, invert)94}9596/// Samples a moire preset up to the scale limit on a square of the size: the raw field, row by row.97#[wasm_bindgen]98pub fn moire_field(name: &str, limit: usize, size: usize) -> Result<Vec<f32>, Fault> {99 Ok(presets::named(name, limit)?.field(size)?.data)100}101102fn square(field: &[f32], size: usize) -> Result<(), Fault> {103 if size == 0 || field.len() != size * size {104 return Err(Fault::new(105 "the field must be size by size with size at least 1.",106 ));107 }108 Ok(())109}110111/// Stacks a square field radially: copies turned by multiples of the step in degrees about the centre, merged by the named blend, on an output square of the out side whose inscribed circle is the field's corner circle, each pixel the mean of samples squared points.112#[wasm_bindgen]113pub fn radial(114 field: &[f32],115 size: usize,116 out: usize,117 copies: usize,118 step: f64,119 blend: &str,120 samples: usize,121) -> Result<Vec<f32>, Fault> {122 square(field, size)?;123 if out == 0 {124 return Err(Fault::new("out must be at least 1."));125 }126 let blend = spin::Blend::named(blend).ok_or_else(|| {127 Fault::new(format!(128 "blend {blend:?} is not mean, sum, union, meet, parity or difference."129 ))130 })?;131 Ok(spin::radial(132 field,133 size,134 out,135 copies,136 step / 360.0,137 blend,138 samples,139 ))140}141142/// The circular-harmonic power of a square field over rings radii: one energy per order from zero to the last, each ring's coefficients exact from its arcs.143#[wasm_bindgen]144pub fn harmonics(145 field: &[f32],146 size: usize,147 rings: usize,148 orders: usize,149) -> Result<Vec<f64>, Fault> {150 square(field, size)?;151 Ok(spin::harmonics(field, size, rings, orders))152}153154/// The rotation order a harmonic power spectrum reveals: the gcd of the live orders, zero when none lives.155#[wasm_bindgen]156pub fn turns(power: &[f64]) -> usize {157 spin::turns(power)158}159160/// The share of the harmonic power order zero carries, in percent.161#[wasm_bindgen]162pub fn radial_share(power: &[f64]) -> f64 {163 let total: f64 = power.iter().sum();164 if total > 0.0 {165 power[0] / total * 100.0166 } else {167 0.0168 }169}170171/// The step in degrees that shares one full turn over the copies.172#[wasm_bindgen]173pub fn full_turn(copies: usize) -> f64 {174 360.0 / copies.max(1) as f64175}176177/// The degrees a turntable at the rpm turns between two frames at the frame rate.178#[wasm_bindgen]179pub fn frame_step(rpm: f64, fps: f64) -> f64 {180 rpm * 6.0 / fps.max(1e-9)181}182183/// The petals a full radial stack of the copies shows on a design of the rotation order: their least common multiple.184#[wasm_bindgen]185pub fn petals(copies: usize, order: usize) -> usize {186 spin::petals(copies, order)187}188189/// Reads a ring profile against the raster side it came from: the mass `2 pi r F(r)` integrates to, the reach of the last radius, the radius of the inscribed circle, the radius the first ring opens at and the brightest mean, as JSON.190#[wasm_bindgen]191pub fn spin_stats(profile: &[f32], size: usize) -> String {192 let last = profile.len().saturating_sub(1).max(1) as f64;193 let reach = spin::reach(size);194 let disc = profile195 .iter()196 .position(|&v| v > 0.0)197 .map(|k| k as f64 / last * reach)198 .unwrap_or(reach);199 let peak = profile.iter().cloned().fold(f32::NEG_INFINITY, f32::max);200 json!({201 "mass": spin::mass(profile, size),202 "reach": reach,203 "inner": size as f64 / 2.0,204 "disc": disc,205 "peak": peak as f64,206 })207 .to_string()208}