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}