spin.rs
6.1 kB · rust · 209 lines
1use crate::{code_of, Fault, Grid, Pixels};2use mrlyrs::core::{json, Colorizer};3use mrlyrs::math::bang::Code;4use mrlyrs::math::moire::{presets, render, Field};5use mrlyrs::math::six;6use mrlyrs::math::spin;7use wasm_bindgen::prelude::*;89pub(crate) fn ramp_of(ramp: &str) -> Colorizer {10 match ramp {11 "heat" => Colorizer::heat(),12 "diverge" => Colorizer::diverge(),13 _ => Colorizer::fire(),14 }15}1617fn slice_raster(18 code: &str,19 number: usize,20 level: usize,21 base: usize,22 size: usize,23) -> Result<Vec<f32>, Fault> {24 let cell = six::cut_design(Code::from(code_of(code)?), number, level, base)?;25 Ok(six::raster(&cell, size)?)26}2728/// Spins a square field about its centre: the exact circle means at the steps radii from the centre to the corner.29#[wasm_bindgen]30pub fn profile(field: &[f32], size: usize, steps: usize) -> Result<Vec<f32>, Fault> {31 square(field, size)?;32 Ok(spin::profile(field, size, steps)?)33}3435/// Rasterizes the diagonal slice of the cube the code names on a square of the size: one byte per pixel, one on a fill.36#[wasm_bindgen]37pub fn slice_grid(38 code: &str,39 number: usize,40 level: usize,41 base: usize,42 size: usize,43) -> Result<Grid, Fault> {44 let data = slice_raster(code, number, level, base, size)?;45 Ok(Grid {46 width: size as u32,47 height: size as u32,48 types: data.iter().map(|&v| v as u8).collect(),49 })50}5152fn painted(53 data: Vec<f32>,54 size: usize,55 ramp: &str,56 levels: usize,57 invert: bool,58) -> Result<Pixels, Fault> {59 if size == 0 || data.len() != size * size {60 return Err(Fault::new(61 "the field must be size by size with size at least 1.",62 ));63 }64 let field = Field::from_data(data, size)?;65 let png = render(&field, &ramp_of(ramp), levels, false, invert, 1)?;66 let (width, height, colors) = mrlyrs::core::unpng(&png)?;67 Ok(Pixels::of(width, height, colors))68}6970/// Paints a ring profile back over a square of the size, quantized into levels through the fire, heat or diverge ramp.71#[wasm_bindgen]72pub fn wheel(73 profile: &[f32],74 size: usize,75 ramp: &str,76 levels: usize,77 invert: bool,78) -> Result<Pixels, Fault> {79 if size == 0 {80 return Err(Fault::new("size must be at least 1."));81 }82 painted(spin::wheel(profile, size), size, ramp, levels, invert)83}8485/// Paints a square field, quantized into levels through the fire, heat or diverge ramp.86#[wasm_bindgen]87pub fn sheet(88 field: &[f32],89 size: usize,90 ramp: &str,91 levels: usize,92 invert: bool,93) -> Result<Pixels, Fault> {94 painted(field.to_vec(), size, ramp, levels, invert)95}9697/// Samples a moire preset up to the scale limit on a square of the size: the raw field, row by row.98#[wasm_bindgen]99pub fn moire_field(name: &str, limit: usize, size: usize) -> Result<Vec<f32>, Fault> {100 Ok(presets::named(name, limit)?.field(size)?.data)101}102103fn square(field: &[f32], size: usize) -> Result<(), Fault> {104 if size == 0 || field.len() != size * size {105 return Err(Fault::new(106 "the field must be size by size with size at least 1.",107 ));108 }109 Ok(())110}111112/// 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.113#[wasm_bindgen]114pub fn radial(115 field: &[f32],116 size: usize,117 out: usize,118 copies: usize,119 step: f64,120 blend: &str,121 samples: usize,122) -> Result<Vec<f32>, Fault> {123 square(field, size)?;124 if out == 0 {125 return Err(Fault::new("out must be at least 1."));126 }127 let blend = spin::Blend::named(blend).ok_or_else(|| {128 Fault::new(format!(129 "blend {blend:?} is not mean, sum, union, meet, parity or difference."130 ))131 })?;132 Ok(spin::radial(133 field,134 size,135 out,136 copies,137 step / 360.0,138 blend,139 samples,140 )?)141}142143/// 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.144#[wasm_bindgen]145pub fn harmonics(146 field: &[f32],147 size: usize,148 rings: usize,149 orders: usize,150) -> Result<Vec<f64>, Fault> {151 square(field, size)?;152 Ok(spin::harmonics(field, size, rings, orders)?)153}154155/// The rotation order a harmonic power spectrum reveals: the gcd of the live orders, zero when none lives.156#[wasm_bindgen]157pub fn turns(power: &[f64]) -> usize {158 spin::turns(power)159}160161/// The share of the harmonic power order zero carries, in percent.162#[wasm_bindgen]163pub fn radial_share(power: &[f64]) -> f64 {164 let total: f64 = power.iter().sum();165 if total > 0.0 {166 power[0] / total * 100.0167 } else {168 0.0169 }170}171172/// The step in degrees that shares one full turn over the copies.173#[wasm_bindgen]174pub fn full_turn(copies: usize) -> f64 {175 360.0 / copies.max(1) as f64176}177178/// The degrees a turntable at the rpm turns between two frames at the frame rate.179#[wasm_bindgen]180pub fn frame_step(rpm: f64, fps: f64) -> f64 {181 rpm * 6.0 / fps.max(1e-9)182}183184/// The petals a full radial stack of the copies shows on a design of the rotation order: their least common multiple.185#[wasm_bindgen]186pub fn petals(copies: usize, order: usize) -> usize {187 spin::petals(copies, order)188}189190/// 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.191#[wasm_bindgen]192pub fn spin_stats(profile: &[f32], size: usize) -> String {193 let last = profile.len().saturating_sub(1).max(1) as f64;194 let reach = spin::reach(size);195 let disc = profile196 .iter()197 .position(|&v| v > 0.0)198 .map(|k| k as f64 / last * reach)199 .unwrap_or(reach);200 let peak = profile.iter().cloned().fold(f32::NEG_INFINITY, f32::max);201 json!({202 "mass": spin::mass(profile, size),203 "reach": reach,204 "inner": size as f64 / 2.0,205 "disc": disc,206 "peak": peak as f64,207 })208 .to_string()209}