use std::{fmt, time::SystemTime}; use actix_web::{http, middleware, server, App, HttpRequest, HttpResponse, Path, Query, Result}; use askama::Template; use log::info; use rand::SeedableRng; use rand_xorshift::XorShiftRng; use serde_derive::Deserialize; use structopt::StructOpt; use renderer::{ noise, noise::{lode::Lode, perlin::Perlin, NoiseType}, texture::{NoiseTexture, Texture}, vec3::Vec3, }; #[derive(StructOpt)] #[structopt(name = "noise_explorer", about = "CLI for exploring Perlin noise")] struct Opt { /// HTTP listen address #[structopt(long = "addr", default_value = "0.0.0.0:8889")] pub addr: String, } #[derive(Copy, Clone, Debug, Deserialize, PartialEq)] enum NoiseSource { #[serde(rename = "perlin")] Perlin, #[serde(rename = "lode")] Lode, } impl fmt::Display for NoiseSource { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { NoiseSource::Perlin => write!(f, "perlin"), NoiseSource::Lode => write!(f, "lode"), } } } #[derive(Copy, Clone, Debug, Deserialize)] struct OptionalParams { pixel_scale: f32, } #[derive(Debug, Deserialize)] struct NoiseParams { width: u32, height: u32, noise_source: NoiseSource, noise_type: NoiseType, optional: Option, } #[derive(Debug, Deserialize)] struct NoiseParamsScale { width: u32, height: u32, noise_source: NoiseSource, scale: f32, } #[derive(Debug, Deserialize)] struct NoiseParamsTurbulence { width: u32, height: u32, noise_source: NoiseSource, turbulence: usize, } #[derive(Debug, Deserialize)] struct NoiseParamsMarble { width: u32, height: u32, noise_source: NoiseSource, x: f32, y: f32, z: f32, power: f32, size: usize, scale: f32, } impl NoiseParams { fn url(&self) -> String { let mut s = format!( "/noise/{}/{}/{}/{}", self.noise_source, self.width, self.height, self.noise_type.to_url(), ); if let Some(optional) = self.optional { s += &format!("?pixel_scale={}", optional.pixel_scale); }; s } fn big_url(&self) -> String { let mut s = format!( "/noise/{}/1024/1024/{}", self.noise_source, self.noise_type.to_url() ); if let Some(optional) = self.optional { s += &format!("?pixel_scale={}", optional.pixel_scale); }; s } } fn render_noise(noise_params: NoiseParams) -> image::GrayImage { let options = noise_params .optional .unwrap_or(OptionalParams { pixel_scale: 1.0 }); const SEED: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; let mut rng: XorShiftRng = SeedableRng::from_seed(SEED); let mut img = image::GrayImage::new(noise_params.width, noise_params.height); let tex: NoiseTexture> = match noise_params.noise_source { NoiseSource::Perlin => { NoiseTexture::new(Box::new(Perlin::new(&mut rng)), noise_params.noise_type) } NoiseSource::Lode => { NoiseTexture::new(Box::new(Lode::new(&mut rng)), noise_params.noise_type) } }; info!("{:?}", noise_params); for x in 0..img.width() { for y in 0..img.height() { let u = x as f32 / options.pixel_scale; let v = y as f32 / options.pixel_scale; let p = Vec3::new(u, v, 1.); let luma = tex.value(u, v, p).x; let luma = (luma * 255.) as u8; img.put_pixel(x, y, image::Luma([luma])); } } img } struct Specimen { title: String, params: Vec, } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { render_time: u64, noise_source: NoiseSource, specimens: Vec, } fn render_html( noise_source: NoiseSource, specimens: Vec, ) -> Result { let index = IndexTemplate { render_time: SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(), noise_source, specimens, }; index.render() } fn build_specimens( noise_source: NoiseSource, width: u32, height: u32, optional: Option, ) -> Vec { let mut specimens = Vec::new(); let mut specimen = Specimen { title: "Unperturbed".into(), params: Vec::new(), }; { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Scale(1.), optional, }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Varying Scale Factor <1".into(), params: Vec::new(), }; let debug = false; if !debug { for scale in 1..10 { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Scale(scale as f32 / 10.), optional, }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Varying Scale Factor >1".into(), params: Vec::new(), }; for scale in 1..10 { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Scale(scale as f32), optional, }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Varying Turbulence Factor".into(), params: Vec::new(), }; for turbulence in 0..9 { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Turbulence(1 << turbulence), optional, }; specimen.params.push(params); } specimens.push(specimen); let max_size = 8; for power in 1..12 { let mut specimen = Specimen { title: format!( "Marble power {}, varying size 2-{}", power, 1 << (max_size - 1) ), params: Vec::new(), }; for size in 1..max_size { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Marble { period: Vec3::new(5., 10., 0.), power: power as f32, size: 1 << size, scale: 1., }, optional, }; specimen.params.push(params); } specimens.push(specimen); } let mut specimen = Specimen { title: "Marble varying power, size = 32".into(), params: Vec::new(), }; for power in 1..10 { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Marble { period: Vec3::new(0., 1., 0.), power: power as f32, size: 32, scale: 1., }, optional, }; specimen.params.push(params); } specimens.push(specimen); let mut specimen = Specimen { title: "Marble varying scale, size = 32".into(), params: Vec::new(), }; for scale in (1..4).rev() { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Marble { period: Vec3::new(0., 1., 0.), power: 1., size: 32, scale: 1. / (1 << scale) as f32, }, optional, }; specimen.params.push(params); } for scale in 1..4 { let params = NoiseParams { width, height, noise_source, noise_type: NoiseType::Marble { period: Vec3::new(0., 1., 0.), power: 1., size: 32, scale: (1 << scale) as f32, }, optional, }; specimen.params.push(params); } specimens.push(specimen); } specimens } fn style(_req: &HttpRequest) -> Result { let bytes = include_bytes!("../templates/style.css"); Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..])) } fn index(source: NoiseSource, optional: Option>) -> Result { let width = 128; let height = 128; let specimens = build_specimens(source, width, height, optional.map(|o| o.into_inner())); Ok(HttpResponse::Ok() .content_type("text/html") .body(render_html(source, specimens).unwrap())) } fn index_perlin(optional: Option>) -> Result { index(NoiseSource::Perlin, optional) } fn index_lode(optional: Option>) -> Result { index(NoiseSource::Lode, optional) } fn noise(np: NoiseParams) -> Result { let img = render_noise(np); let mut buf = Vec::new(); let img = image::DynamicImage::ImageLuma8(img); // TODO(wathiede): remove unwrap with proper error propagation. img.write_to(&mut buf, image::ImageOutputFormat::PNG) .unwrap(); Ok(HttpResponse::Ok().content_type("image/png").body(buf)) } #[allow(clippy::needless_pass_by_value)] fn noise_scale( np: Path, optional: Option>, ) -> Result { noise(NoiseParams { width: np.width, height: np.height, noise_source: np.noise_source, noise_type: NoiseType::Scale(np.scale), optional: optional.map(|o| o.into_inner()), }) } #[allow(clippy::needless_pass_by_value)] fn noise_turbulence( np: Path, optional: Option>, ) -> Result { noise(NoiseParams { width: np.width, height: np.height, noise_source: np.noise_source, noise_type: NoiseType::Turbulence(np.turbulence), optional: optional.map(|o| o.into_inner()), }) } #[allow(clippy::needless_pass_by_value)] fn noise_marble( np: Path, optional: Option>, ) -> Result { noise(NoiseParams { width: np.width, height: np.height, noise_source: np.noise_source, noise_type: NoiseType::Marble { period: Vec3::new(np.x, np.y, np.z), power: np.power, size: np.size, scale: np.scale, }, optional: optional.map(|o| o.into_inner()), }) } fn main() -> Result<(), std::io::Error> { stderrlog::new() .verbosity(2) .timestamp(stderrlog::Timestamp::Millisecond) .init() .unwrap(); let opt = Opt::from_args(); server::new(|| { App::new() .resource("/noise/{noise_source}/{width}/{height}/scale/{scale}", |r| { r.with(noise_scale); }).resource("/noise/{noise_source}/{width}/{height}/turbulence/{turbulence}", |r| { r.with(noise_turbulence); }).resource( "/noise/{noise_source}/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}/scale/{scale}", |r| { r.with(noise_marble); }, ).resource("/style.css", |r| r.f(style)) .resource("/lode", |r| r.with(index_lode)) .resource("/perlin", |r| r.with(index_perlin)) .resource("/", |r| r.f(|_| HttpResponse::Found().header(http::header::LOCATION, "/lode").finish())) .middleware(middleware::Logger::default()) }).bind(opt.addr) .unwrap() .run(); Ok(()) } #[cfg(test)] mod tests { use std::str; use actix_web::{http::Method, test::TestServer, HttpMessage, Path, Query}; use super::{NoiseParamsMarble, NoiseParamsScale, NoiseParamsTurbulence, OptionalParams}; #[test] fn noise_param_from_req() { let mut srv = TestServer::build().start(|app| { app.resource("/noise/{noise_source}/{width}/{height}/scale/{scale}", |r| { r.with(|p: Path, q:Option>| format!("{:?} {:?}", p,q)); }); app.resource("/noise/{noise_source}/{width}/{height}/turbulence/{turbulence}", |r| { r.with(|p: Path| format!("{:?}", p)); }); app.resource( "/noise/{noise_source}/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}/scale/{scale}", |r| { r.with(|p: Path| format!("{:?}", p)); }, ); }); let request = srv .client(Method::GET, "/noise/perlin/32/32/scale/1") .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsScale { width: 32, height: 32, noise_source: Perlin, scale: 1.0 } None" ); let request = srv .client(Method::GET, "/noise/perlin/32/32/scale/1?pixel_scale=1") .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsScale { width: 32, height: 32, noise_source: Perlin, scale: 1.0 } Some(OptionalParams { pixel_scale: 1.0 })" ); let request = srv .client(Method::GET, "/noise/lode/32/32/scale/1") .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsScale { width: 32, height: 32, noise_source: Lode, scale: 1.0 } None" ); let request = srv .client(Method::GET, "/noise/lode/32/32/turbulence/5") .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsTurbulence { width: 32, height: 32, noise_source: Lode, turbulence: 5 }" ); let request = srv .client( Method::GET, "/noise/lode/32/32/marble/period/1.,2.,3./power/32./size/4/scale/255.", ) .finish() .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success(), "Response {:?}", response); let bytes = srv.execute(response.body()).unwrap(); let body = str::from_utf8(&bytes).unwrap(); assert_eq!( body, "NoiseParamsMarble { width: 32, height: 32, noise_source: Lode, x: 1.0, y: 2.0, z: 3.0, power: 32.0, size: 4, scale: 255.0 }" ); } }