diff --git a/.gitignore b/.gitignore index 6abfe1b..5db355e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /.direnv +*.png diff --git a/Cargo.lock b/Cargo.lock index 96407fe..fa18bf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,84 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "imgsyn" version = "0.1.0" +dependencies = [ + "png", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" diff --git a/Cargo.toml b/Cargo.toml index 3a9694c..2485187 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +png = "0.18.0" diff --git a/shell.nix b/shell.nix index 9457e4f..137c9b3 100644 --- a/shell.nix +++ b/shell.nix @@ -8,5 +8,6 @@ pkgs.mkShell { ffmpeg imagemagick rust-analyzer + cargo-watch ]; } diff --git a/src/assist.rs b/src/assist.rs new file mode 100644 index 0000000..f1a6d4b --- /dev/null +++ b/src/assist.rs @@ -0,0 +1,25 @@ +use std::f32::consts::*; + +use crate::Canvas; + +pub fn polar(x: f32, y: f32, meta: Canvas) -> (f32, f32) { + let x = x - 0.5; + let y = y - 0.5; + let r = (x * x + y * y).sqrt(); + let mut phi = f32::atan2(y, x) + PI / 2.; + phi = phi.rem_euclid(TAU); + (r * 2. / corner_distance(meta), phi / TAU) +} + +pub fn axial(r: f32, phi: f32, meta: Canvas) -> (f32, f32) { + let phi = phi * TAU - PI / 2.; + let r = r / 2. * corner_distance(meta); + let x = r * phi.cos(); + let y = r * phi.sin(); + (x + 0.5, y + 0.5) +} + +fn corner_distance(meta: Canvas) -> f32 { + let corner_distance = (meta.width * meta.width + meta.height * meta.height).sqrt(); + corner_distance / meta.width.max(meta.height) +} diff --git a/src/canvas.rs b/src/canvas.rs index f099c44..43f898a 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -2,19 +2,18 @@ use crate::{ImgSyn, pixel::Pixel}; #[derive(Clone, Copy, PartialEq)] pub struct Canvas { - pub width: u32, - pub height: u32, + pub width: f32, + pub height: f32, pub default_px: Pixel, } #[derive(Clone, PartialEq)] pub struct PaintedCanvas { - pub meta: Canvas, pub array: Vec>, } impl Canvas { - pub fn new(width: u32, height: u32, default_px: Pixel) -> Self { + pub fn new(width: f32, height: f32, default_px: Pixel) -> Self { Self { width, height, @@ -32,25 +31,34 @@ impl ImgSyn for Canvas { impl PaintedCanvas { pub fn new(meta @ Canvas { width, height, .. }: Canvas, function: impl ImgSyn) -> Self { Self { - meta, - array: (0..width) - .map(|x| x as f32 / width as f32) - .map(|x| { - (0..height) - .map(|y| y as f32 / height as f32) - .map(|y| function.getpx(meta, x, y)) + array: (0..height as u32) + .map(|y| y as f32 / height) + .map(|y| { + (0..width as u32) + .map(|x| x as f32 / width) + .map(|x| function.getpx(meta, x, y)) .collect() }) .collect(), } } + + pub fn rgba255(&self) -> Vec { + self.array + .iter() + .flat_map(|row| row.iter().flat_map(|pix| pix.rgba255())) + .collect() + } } impl ImgSyn for PaintedCanvas { fn getpx(&self, meta: Canvas, x: f32, y: f32) -> Pixel { // float coords -> int coords - let x = (x * meta.width as f32).round() as usize; - let y = (y * meta.height as f32).round() as usize; - self.array[x][y] + let x = (x * meta.width).round() as usize; + let y = (y * meta.height).round() as usize; + self.array[y][x] + } + fn paint(self, _meta: Canvas) -> PaintedCanvas { + self } } diff --git a/src/lib.rs b/src/lib.rs index 23cee6b..ac35104 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ +pub mod assist; mod canvas; -pub mod ops; +mod ops; mod pixel; mod pixfn; @@ -7,30 +8,171 @@ pub use canvas::*; pub use pixel::*; pub use pixfn::*; -use crate::ops::ImgSynMap; +use crate::ops::{ + ImgSynArea, ImgSynAxial, ImgSynMap, ImgSynMapArea, ImgSynPolar, ImgSynRect, ImgSynStretched, + ImgSynUnmapRect, +}; -pub trait ImgSynPixFn: Clone { - fn run(&self, meta: Canvas, x: f32, y: f32, pixel: Pixel) -> Pixel; +pub trait ImgSynPixFn: Clone { + fn run(&self, meta: Canvas, x: f32, y: f32, pixel: Pixel) -> T; } pub trait ImgSyn: Clone { fn getpx(&self, meta: Canvas, x: f32, y: f32) -> Pixel; - fn map(self, f: F) -> ImgSynMap { + fn map>(self, f: F) -> ImgSynMap { ImgSynMap(self, f) } + fn area, ImgB: ImgSyn, F: FnOnce(Self) -> ImgB>( + self, + condition: C, + f: F, + ) -> ImgSynArea { + ImgSynArea(self.clone(), f(self), condition) + } + fn mapped_rect) -> ImgB>( + self, + begin_x: f32, + begin_y: f32, + size_x: f32, + size_y: f32, + f: F, + ) -> ImgSynRect { + let rect = (begin_x, begin_y, size_x, size_y); + ImgSynRect(self.clone(), f(ImgSynUnmapRect(self, rect)), rect) + } + fn add_image( + self, + begin_x: f32, + begin_y: f32, + size_x: f32, + size_y: f32, + img: ImgB, + ) -> ImgSynRect { + let rect = (begin_x, begin_y, size_x, size_y); + ImgSynRect(self, img, rect) + } + fn map_area, F: ImgSynPixFn>( + self, + condition: C, + f: F, + ) -> ImgSynMapArea { + ImgSynMapArea(self, condition, f) + } + /// Makes future operations work on polar coordinates when terminated by .axial() + /// + /// **MUST** be used before a subsequent .axial() + fn polar(self) -> ImgSynAxial { + ImgSynAxial(self) + } + /// Makes future operations work on axial coordinates when .polar() was used previously + /// + /// **MUST** be used after .polar() + fn axial(self) -> ImgSynPolar { + ImgSynPolar(self) + } + fn stretched_by(self, x: f32, y: f32) -> ImgSynStretched { + ImgSynStretched(self, (x, y)) + } fn paint(self, meta: Canvas) -> PaintedCanvas { PaintedCanvas::new(meta, self) } + fn rgba255(self, meta: Canvas) -> Vec { + let painted = self.paint(meta); + PaintedCanvas::rgba255(&painted) + } + fn png(self, meta: Canvas) -> Vec { + let painted = self.paint(meta); + let mut data = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut data, meta.width as u32, meta.height as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().unwrap(); + writer.write_image_data(&painted.rgba255(meta)).unwrap(); + writer.finish().unwrap(); + } + data + } } #[cfg(test)] mod test { + use std::fs; + use crate::{Canvas, ImgSyn, PixFnFloat, Pixel}; #[test] fn test() { - Canvas::new(100, 100, Pixel::new_black()) - .map(PixFnFloat(|_, x, y, pix| pix.with_r(x).with_g(y))) - .map(PixFnFloat(|_, _, _, pix| pix.with_b(1.0))); + let canvas = Canvas::new(1000., 1000., Pixel::new_black()); + fs::write( + "test.png", + canvas + .polar() + .map(|_, y: f32, pix: Pixel| pix.with_r((y - 0.5).abs() * 2.)) + .map(|_, y, pix: Pixel| pix.with_b(y)) + .axial() + .map(PixFnFloat(|_, _, y, pix| pix.with_g(y))) + .map_area( + PixFnFloat(|_, x, y, _| x < 0.2 && y < 0.2), + PixFnFloat(|_, _, _, pix| pix.with_g(1.0)), + ) + .area(PixFnFloat(|_, _, _, px| px.g > 0.8 && px.r < 0.3), |img| { + img.polar() + .map(PixFnFloat(|_, x, y, pix| pix.with_r(x).with_b(1. - y))) + .axial() + }) + .mapped_rect(0.3, 0.3, 0.4, 0.4, |img| { + img.map(PixFnFloat(|_, x, y, px| { + let thickness = 0.01; + if !(thickness..=1. - thickness).contains(&x) + || !(thickness..=1. - thickness).contains(&y) + { + return Pixel::new_white(); + } + px + })) + }) + .paint(canvas) + .png(canvas), + ) + .unwrap(); + } + + #[test] + fn graph() { + let canvas = Canvas::new(1000., 1000., Pixel::new_white()); + fs::write( + "graph.png", + canvas + .add_image( + 0.1, + 0.0, + 0.9, + 0.9, + Canvas::new(900., 900., Pixel::new_black()).map_area( + |x, y, _| x < 0.7 || y > 0.3, + |x, y, px: Pixel| px.with_r(x).with_g(1. - y), + ), + ) + .map_area( + |x, y, _| { + let t = 0.004; + (0.1 - t..0.1 + t).contains(&x) || (0.9 - t..0.9 + t).contains(&y) + }, + |_, _, px: Pixel| px.with_rgb(1.0, 0.0, 0.0), + ) + .mapped_rect(0.05, 0.90, 0.05, 0.05, |img| { + img.polar() + .map_area( + |r: f32, phi, _| (r - 0.3).abs() < 0.04, + |_, _, px: Pixel| px.with_rgb_of(0.0), + ) + .axial() + .stretched_by(1.0, 1.5) + }) + .paint(canvas) + .png(canvas), + ) + .unwrap(); } } diff --git a/src/ops/area.rs b/src/ops/area.rs new file mode 100644 index 0000000..335331e --- /dev/null +++ b/src/ops/area.rs @@ -0,0 +1,91 @@ +use crate::{Canvas, ImgSyn, ImgSynPixFn, Pixel}; + +#[derive(Clone, Copy)] +pub struct ImgSynMapArea, F: ImgSynPixFn>( + pub(crate) Prev, + pub(crate) C, + pub(crate) F, +); + +impl ImgSyn for ImgSynMapArea +where + Prev: ImgSyn, + C: ImgSynPixFn, + F: ImgSynPixFn, +{ + fn getpx(&self, meta: Canvas, x: f32, y: f32) -> crate::Pixel { + let px = self.0.getpx(meta, x, y); + if !self.1.run(meta, x, y, px) { + return px; + } + self.2.run(meta, x, y, px) + } +} + +#[derive(Clone, Copy)] +pub struct ImgSynArea>( + pub(crate) Prev, + pub(crate) ImgB, + pub(crate) C, +); + +impl ImgSyn for ImgSynArea +where + Prev: ImgSyn, + ImgB: ImgSyn, + C: ImgSynPixFn, +{ + fn getpx(&self, meta: Canvas, x: f32, y: f32) -> crate::Pixel { + let px = self.0.getpx(meta, x, y); + if !self.2.run(meta, x, y, px) { + return px; + } + self.1.getpx(meta, x, y) + } +} + +#[derive(Clone, Copy)] +pub struct ImgSynRect( + pub(crate) Prev, + pub(crate) ImgB, + pub(crate) (f32, f32, f32, f32), +); + +impl ImgSyn for ImgSynRect +where + Prev: ImgSyn, + ImgB: ImgSyn, +{ + fn getpx(&self, meta: Canvas, x: f32, y: f32) -> crate::Pixel { + let px = self.0.getpx(meta, x, y); + let (startx, starty, sizex, sizey) = self.2; + if x < startx || y < starty { + return px; + } + if x - startx >= sizex || y - starty >= sizey { + return px; + } + self.1.getpx( + Canvas::new(sizex * meta.width, sizey * meta.height, meta.default_px), + (x - startx) / sizex, + (y - starty) / sizey, + ) + } +} + +#[derive(Clone, Copy)] +pub struct ImgSynUnmapRect(pub(crate) Prev, pub(crate) (f32, f32, f32, f32)); + +impl ImgSyn for ImgSynUnmapRect +where + Prev: ImgSyn, +{ + fn getpx(&self, meta: Canvas, x: f32, y: f32) -> crate::Pixel { + let (startx, starty, sizex, sizey) = self.1; + self.0.getpx( + Canvas::new(meta.width / sizex, meta.height / sizey, meta.default_px), + x * sizex + startx, + y * sizey + starty, + ) + } +} diff --git a/src/ops/axial.rs b/src/ops/axial.rs new file mode 100644 index 0000000..4e3f689 --- /dev/null +++ b/src/ops/axial.rs @@ -0,0 +1,29 @@ +use crate::{ + Canvas, ImgSyn, + assist::{axial, polar}, +}; + +#[derive(Clone, Copy)] +pub struct ImgSynAxial(pub(crate) Prev); + +impl ImgSyn for ImgSynAxial +where + Prev: ImgSyn, +{ + fn getpx(&self, meta: Canvas, x: f32, y: f32) -> crate::Pixel { + let (x, y) = axial(x, y, meta); + self.0.getpx(meta, x, y) + } +} +#[derive(Clone, Copy)] +pub struct ImgSynPolar(pub(crate) Prev); + +impl ImgSyn for ImgSynPolar +where + Prev: ImgSyn, +{ + fn getpx(&self, meta: Canvas, x: f32, y: f32) -> crate::Pixel { + let (x, y) = polar(x, y, meta); + self.0.getpx(meta, x, y) + } +} diff --git a/src/ops/map.rs b/src/ops/map.rs index 5f1399f..6127d29 100644 --- a/src/ops/map.rs +++ b/src/ops/map.rs @@ -1,9 +1,9 @@ -use crate::{ImgSyn, ImgSynPixFn}; +use crate::{ImgSyn, ImgSynPixFn, Pixel}; #[derive(Clone, Copy)] -pub struct ImgSynMap(pub(crate) Prev, pub(crate) F); +pub struct ImgSynMap>(pub(crate) Prev, pub(crate) F); -impl ImgSyn for ImgSynMap { +impl> ImgSyn for ImgSynMap { fn getpx(&self, meta: crate::Canvas, x: f32, y: f32) -> crate::Pixel { self.1.run(meta, x, y, self.0.getpx(meta, x, y)) } diff --git a/src/ops/mod.rs b/src/ops/mod.rs index aa2efd9..a00d84c 100644 --- a/src/ops/mod.rs +++ b/src/ops/mod.rs @@ -1,2 +1,8 @@ mod map; pub use map::*; +mod area; +pub use area::*; +mod axial; +pub use axial::*; +mod stretch; +pub use stretch::*; diff --git a/src/ops/stretch.rs b/src/ops/stretch.rs new file mode 100644 index 0000000..b43d81f --- /dev/null +++ b/src/ops/stretch.rs @@ -0,0 +1,14 @@ +use crate::ImgSyn; + +#[derive(Clone, Copy)] +pub struct ImgSynStretched(pub(crate) Prev, pub(crate) (f32, f32)); + +impl ImgSyn for ImgSynStretched { + fn getpx(&self, meta: crate::Canvas, x: f32, y: f32) -> crate::Pixel { + self.0.getpx( + meta, + x / self.1.0 - 0.5 / self.1.0 + 0.5, + y / self.1.1 - 0.5 / self.1.1 + 0.5, + ) + } +} diff --git a/src/pixel.rs b/src/pixel.rs index cb35b9e..0ac37c0 100644 --- a/src/pixel.rs +++ b/src/pixel.rs @@ -1,9 +1,9 @@ #[derive(Clone, Copy, PartialEq)] pub struct Pixel { - r: f32, - g: f32, - b: f32, - a: f32, + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, } impl Pixel { @@ -33,6 +33,10 @@ impl Pixel { } } + pub fn of(r: f32, g: f32, b: f32, a: f32) -> Pixel { + Self { r, g, b, a } + } + pub fn with_r(self, x: f32) -> Pixel { Self { r: x, ..self } } @@ -45,6 +49,17 @@ impl Pixel { pub fn with_a(self, x: f32) -> Pixel { Self { a: x, ..self } } + pub fn with_rgb(self, r: f32, g: f32, b: f32) -> Pixel { + Self { r, g, b, ..self } + } + pub fn with_rgb_of(self, x: f32) -> Pixel { + Self { + r: x, + g: x, + b: x, + ..self + } + } pub fn map(self, f: impl FnOnce(Self) -> Pixel) -> Pixel { f(self) @@ -66,4 +81,13 @@ impl Pixel { self.r = f(self.r); self } + + pub fn rgba255(self) -> [u8; 4] { + [ + (self.r.clamp(0.0, 1.0) * 255.) as u8, + (self.g.clamp(0.0, 1.0) * 255.) as u8, + (self.b.clamp(0.0, 1.0) * 255.) as u8, + (self.a.clamp(0.0, 1.0) * 255.) as u8, + ] + } } diff --git a/src/pixfn.rs b/src/pixfn.rs index 8381d3a..83ad80a 100644 --- a/src/pixfn.rs +++ b/src/pixfn.rs @@ -1,28 +1,37 @@ use crate::{Canvas, ImgSynPixFn, Pixel}; #[derive(Clone, Copy)] -pub struct PixFnInt Pixel) + Copy>(pub T); -impl ImgSynPixFn for PixFnInt +pub struct PixFnInt T) + Copy>(pub F); +impl ImgSynPixFn for PixFnInt where - T: (Fn(Canvas, u32, u32, Pixel) -> Pixel) + Copy, + F: (Fn(Canvas, u32, u32, Pixel) -> T) + Copy, { - fn run(&self, meta: Canvas, x: f32, y: f32, pixel: Pixel) -> Pixel { + fn run(&self, meta: Canvas, x: f32, y: f32, pixel: Pixel) -> T { self.0( meta, - (x * meta.width as f32).round() as u32, - (y * meta.height as f32).round() as u32, + (x * meta.width).round() as u32, + (y * meta.height).round() as u32, pixel, ) } } #[derive(Clone, Copy)] -pub struct PixFnFloat Pixel) + Copy>(pub T); -impl ImgSynPixFn for PixFnFloat +pub struct PixFnFloat T) + Copy>(pub F); +impl ImgSynPixFn for PixFnFloat where - T: (Fn(Canvas, f32, f32, Pixel) -> Pixel) + Copy, + F: (Fn(Canvas, f32, f32, Pixel) -> T) + Copy, { - fn run(&self, meta: Canvas, x: f32, y: f32, pixel: Pixel) -> Pixel { + fn run(&self, meta: Canvas, x: f32, y: f32, pixel: Pixel) -> T { self.0(meta, x, y, pixel) } } + +impl ImgSynPixFn for F +where + F: (Fn(f32, f32, Pixel) -> T) + Copy, +{ + fn run(&self, _meta: Canvas, x: f32, y: f32, px: Pixel) -> T { + self(x, y, px) + } +}