Factored out crates for web version
This commit is contained in:
111
mandelia_renderer/src/lib.rs
Normal file
111
mandelia_renderer/src/lib.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//#![feature(portable_simd)]
|
||||
|
||||
use clap::Parser;
|
||||
use image::{ImageBuffer, Rgb};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod transform;
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[clap(short, long, default_value_t = 3840)]
|
||||
pub width: u32,
|
||||
#[clap(short, long, default_value_t = 2160)]
|
||||
pub height: u32,
|
||||
|
||||
#[clap(short, long, default_value_t = 100)]
|
||||
pub maxiter: usize,
|
||||
|
||||
#[clap(short, long, default_value="out.png")]
|
||||
pub output: PathBuf,
|
||||
|
||||
pub transform: Vec<String>
|
||||
}
|
||||
|
||||
pub type Float = f64;
|
||||
pub type Point = Complex<Float>;
|
||||
use num::complex::Complex;
|
||||
pub type Transform = [(Point, Point); 3];
|
||||
|
||||
trait ComplexExt<T> {
|
||||
fn norm2(self) -> T;
|
||||
}
|
||||
|
||||
impl ComplexExt<Float> for Complex<Float> {
|
||||
fn norm2(self) -> Float {
|
||||
self.im * self.im + self.re * self.re
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_point(transform: &Transform, point: Point) -> (Point, Point) {
|
||||
let start = transform[0].0 * point.re + transform[1].0 * point.im + transform[2].0;
|
||||
let delta = transform[0].1 * point.re + transform[1].1 * point.im + transform[2].1;
|
||||
(start, delta)
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub transform: Transform,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub maxiter: usize,
|
||||
// We represent escape as the square of its magnitude
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Config {
|
||||
fn render_point(&self, (base, delta): (Point, Point)) -> Float {
|
||||
const ESCAPE: Float = 2.;
|
||||
const ESCAPE2: Float = ESCAPE * ESCAPE;
|
||||
|
||||
let result =
|
||||
std::iter::successors(Some(base), |i| Some(*i * *i + delta))
|
||||
.take(self.maxiter)
|
||||
.enumerate()
|
||||
.skip_while(|(_, i)| i.norm2() <= ESCAPE2)
|
||||
.next()
|
||||
.map_or(0.,
|
||||
|(iter, val)|
|
||||
iter as Float + 1. - val.norm().ln().ln() / ESCAPE.ln()
|
||||
);
|
||||
result as Float
|
||||
}
|
||||
|
||||
pub fn render_image(&self) -> ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||
let scale = 2.0 / std::cmp::min(self.width, self.height) as Float;
|
||||
|
||||
ImageBuffer::<image::Rgb<u8>, _>::from_par_fn(self.width, self.height, |x, y| {
|
||||
let x = (x as Float) * scale - 1.;
|
||||
let y = (y as Float) * scale - 1.;
|
||||
let point = transform_point(&self.transform, Complex::new(x, y));
|
||||
map_palette(self.render_point(point) / self.maxiter as Float)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_palette(val: Float) -> image::Rgb<u8> {
|
||||
let magnitude = Float::clamp(val * 255., 0., 255.) as u8;
|
||||
image::Rgb([magnitude, magnitude, magnitude])
|
||||
}
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
|
||||
// let transform = [
|
||||
// (Point::new(0., 0.), Point::new(1., 0.)),
|
||||
// (Point::new(0., 0.), Point::new(0., 1.)),
|
||||
// (Point::new(0., 0.), Point::new(0., 0.)),
|
||||
// ];
|
||||
|
||||
let args = <Args as clap::Parser>::parse();
|
||||
|
||||
let config = Config {
|
||||
transform: transform::parse_transforms(args.transform.iter())?,
|
||||
width: args.width,
|
||||
height: args.height,
|
||||
maxiter: args.maxiter,
|
||||
};
|
||||
|
||||
let img = config.render_image();
|
||||
|
||||
img.save(&args.output)?;
|
||||
Ok(())
|
||||
}
|
||||
130
mandelia_renderer/src/transform.rs
Normal file
130
mandelia_renderer/src/transform.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use num::Num;
|
||||
use super::{Float, Point};
|
||||
pub type MTransform = nalgebra::Matrix5<Float>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Type {
|
||||
Rotation(u8, u8, Float),
|
||||
Translation(u8, Float),
|
||||
CoordSwap(u8, u8),
|
||||
Scale(Float),
|
||||
}
|
||||
|
||||
impl From<Type> for MTransform {
|
||||
fn from(ty: Type) -> Self {
|
||||
match ty {
|
||||
Type::Rotation(a0, a1, _) if a0 == a1=> MTransform::identity(),
|
||||
Type::Rotation(a0, a1, delta) => {
|
||||
let a0 = a0 as usize;
|
||||
let a1 = a1 as usize;
|
||||
let delta = delta.to_radians();
|
||||
let s = delta.sin();
|
||||
let c = delta.cos();
|
||||
let mut m = MTransform::identity();
|
||||
m[(a0,a0)] = c;
|
||||
m[(a0,a1)] = -s;
|
||||
m[(a1,a0)] = s;
|
||||
m[(a1,a1)] = c;
|
||||
m
|
||||
}
|
||||
Type::Translation(a, delta) => {
|
||||
let mut m = MTransform::identity();
|
||||
m[(4, a as usize)] = -delta;
|
||||
m
|
||||
}
|
||||
Type::CoordSwap(a, b) => {
|
||||
let mut m = MTransform::identity();
|
||||
let a = a as usize;
|
||||
let b = b as usize;
|
||||
m[(a,a)] = 0.;
|
||||
m[(b,b)] = 0.;
|
||||
m[(a,b)] = 1.;
|
||||
m[(b,a)] = 1.;
|
||||
m
|
||||
}
|
||||
Type::Scale(delta) => {
|
||||
let mut xf = MTransform::identity() / delta;
|
||||
xf[(4,4)] = 1.;
|
||||
xf
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn parse_axis(c: char) -> Option<u8> {
|
||||
Some(match c {
|
||||
'x' => 0,
|
||||
'y' => 1,
|
||||
'X' => 2,
|
||||
'Y' => 3,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// We take a type and distance.
|
||||
/// We have four axes, x,y,X,Y. The first two adjust the base point; the second two adjust the delta.
|
||||
///
|
||||
/// Given that, a plane can be defined with two axes, and a translation can be defined with one.
|
||||
///
|
||||
/// TODO: replace this with a nom parser
|
||||
pub fn parse_step(s: &str) -> Option<Type> {
|
||||
if let Some(r) = s.strip_prefix("z") {
|
||||
return Float::from_str_radix(r, 10).ok().map(Type::Scale);
|
||||
}
|
||||
if s.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut chars = s.chars();
|
||||
let axis0 = chars.next().and_then(parse_axis)?;
|
||||
Some(match chars.clone().next()? {
|
||||
'=' => {
|
||||
chars.next();
|
||||
let axis1 = chars.next().and_then(parse_axis)?;
|
||||
Type::CoordSwap(axis0, axis1)
|
||||
}
|
||||
'x' | 'X' | 'y' | 'Y' => {
|
||||
chars.next();
|
||||
let axis1 = chars.next().and_then(parse_axis)?;
|
||||
if axis0 == axis1 {
|
||||
return None;
|
||||
}
|
||||
let val = Float::from_str_radix(chars.as_str().trim(), 10).ok()?;
|
||||
Type::Rotation(axis0, axis1, val)
|
||||
}
|
||||
'0' ..= '9' => {
|
||||
let val = Float::from_str_radix(chars.as_str().trim(), 10).ok()?;
|
||||
Type::Translation(axis0, val)
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transform_to_coords(mat: &MTransform) -> super::Transform {
|
||||
let bx = Point::new(mat[(0,0)], mat[(0,1)]);
|
||||
let by = Point::new(mat[(1,0)], mat[(1,1)]);
|
||||
let bd = Point::new(mat[(4,0)], mat[(4,1)]);
|
||||
|
||||
let cx = Point::new(mat[(0,2)], mat[(0,3)]);
|
||||
let cy = Point::new(mat[(1,2)], mat[(1,3)]);
|
||||
let cd = Point::new(mat[(4,2)], mat[(4,3)]);
|
||||
[
|
||||
(bx, cx),
|
||||
(by, cy),
|
||||
(bd, cd),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn parse_transforms<'a>(transforms: impl Iterator<Item=impl AsRef<str>>) ->anyhow::Result<super::Transform> {
|
||||
let mat = transforms
|
||||
.map(|s| parse_step(s.as_ref())
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid step: {:?}", s.as_ref())))
|
||||
.try_fold(MTransform::identity(),
|
||||
|acc, new| new.map(|new| acc * MTransform::from(new)))?;
|
||||
|
||||
|
||||
Ok(transform_to_coords(&mat))
|
||||
|
||||
}
|
||||
|
||||
pub fn parse_transform(s: &str) -> anyhow::Result<super::Transform> {
|
||||
parse_transforms(s.split_whitespace())
|
||||
}
|
||||
Reference in New Issue
Block a user