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}