some basic operations

This commit is contained in:
Tove 2026-02-09 16:10:08 +01:00
parent 7e974d42ae
commit 4285b7a966
14 changed files with 468 additions and 39 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
/.direnv
*.png

78
Cargo.lock generated
View file

@ -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"

View file

@ -4,3 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
png = "0.18.0"

View file

@ -8,5 +8,6 @@ pkgs.mkShell {
ffmpeg
imagemagick
rust-analyzer
cargo-watch
];
}

25
src/assist.rs Normal file
View file

@ -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)
}

View file

@ -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<Vec<Pixel>>,
}
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<u8> {
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
}
}

View file

@ -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<T>: 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<F: ImgSynPixFn>(self, f: F) -> ImgSynMap<Self, F> {
fn map<F: ImgSynPixFn<Pixel>>(self, f: F) -> ImgSynMap<Self, F> {
ImgSynMap(self, f)
}
fn area<C: ImgSynPixFn<bool>, ImgB: ImgSyn, F: FnOnce(Self) -> ImgB>(
self,
condition: C,
f: F,
) -> ImgSynArea<Self, ImgB, C> {
ImgSynArea(self.clone(), f(self), condition)
}
fn mapped_rect<ImgB: ImgSyn, F: FnOnce(ImgSynUnmapRect<Self>) -> ImgB>(
self,
begin_x: f32,
begin_y: f32,
size_x: f32,
size_y: f32,
f: F,
) -> ImgSynRect<Self, ImgB> {
let rect = (begin_x, begin_y, size_x, size_y);
ImgSynRect(self.clone(), f(ImgSynUnmapRect(self, rect)), rect)
}
fn add_image<ImgB: ImgSyn>(
self,
begin_x: f32,
begin_y: f32,
size_x: f32,
size_y: f32,
img: ImgB,
) -> ImgSynRect<Self, ImgB> {
let rect = (begin_x, begin_y, size_x, size_y);
ImgSynRect(self, img, rect)
}
fn map_area<C: ImgSynPixFn<bool>, F: ImgSynPixFn<Pixel>>(
self,
condition: C,
f: F,
) -> ImgSynMapArea<Self, C, F> {
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<Self> {
ImgSynAxial(self)
}
/// Makes future operations work on axial coordinates when .polar() was used previously
///
/// **MUST** be used after .polar()
fn axial(self) -> ImgSynPolar<Self> {
ImgSynPolar(self)
}
fn stretched_by(self, x: f32, y: f32) -> ImgSynStretched<Self> {
ImgSynStretched(self, (x, y))
}
fn paint(self, meta: Canvas) -> PaintedCanvas {
PaintedCanvas::new(meta, self)
}
fn rgba255(self, meta: Canvas) -> Vec<u8> {
let painted = self.paint(meta);
PaintedCanvas::rgba255(&painted)
}
fn png(self, meta: Canvas) -> Vec<u8> {
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();
}
}

91
src/ops/area.rs Normal file
View file

@ -0,0 +1,91 @@
use crate::{Canvas, ImgSyn, ImgSynPixFn, Pixel};
#[derive(Clone, Copy)]
pub struct ImgSynMapArea<Prev: ImgSyn, C: ImgSynPixFn<bool>, F: ImgSynPixFn<Pixel>>(
pub(crate) Prev,
pub(crate) C,
pub(crate) F,
);
impl<Prev, C, F> ImgSyn for ImgSynMapArea<Prev, C, F>
where
Prev: ImgSyn,
C: ImgSynPixFn<bool>,
F: ImgSynPixFn<Pixel>,
{
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<Prev: ImgSyn, ImgB: ImgSyn, C: ImgSynPixFn<bool>>(
pub(crate) Prev,
pub(crate) ImgB,
pub(crate) C,
);
impl<Prev, ImgB, C> ImgSyn for ImgSynArea<Prev, ImgB, C>
where
Prev: ImgSyn,
ImgB: ImgSyn,
C: ImgSynPixFn<bool>,
{
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<Prev: ImgSyn, ImgB: ImgSyn>(
pub(crate) Prev,
pub(crate) ImgB,
pub(crate) (f32, f32, f32, f32),
);
impl<Prev, ImgB> ImgSyn for ImgSynRect<Prev, ImgB>
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<Prev: ImgSyn>(pub(crate) Prev, pub(crate) (f32, f32, f32, f32));
impl<Prev> ImgSyn for ImgSynUnmapRect<Prev>
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,
)
}
}

29
src/ops/axial.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{
Canvas, ImgSyn,
assist::{axial, polar},
};
#[derive(Clone, Copy)]
pub struct ImgSynAxial<Prev: ImgSyn>(pub(crate) Prev);
impl<Prev> ImgSyn for ImgSynAxial<Prev>
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<Prev: ImgSyn>(pub(crate) Prev);
impl<Prev> ImgSyn for ImgSynPolar<Prev>
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)
}
}

View file

@ -1,9 +1,9 @@
use crate::{ImgSyn, ImgSynPixFn};
use crate::{ImgSyn, ImgSynPixFn, Pixel};
#[derive(Clone, Copy)]
pub struct ImgSynMap<Prev: ImgSyn, F: ImgSynPixFn>(pub(crate) Prev, pub(crate) F);
pub struct ImgSynMap<Prev: ImgSyn, F: ImgSynPixFn<Pixel>>(pub(crate) Prev, pub(crate) F);
impl<Prev: ImgSyn, F: ImgSynPixFn> ImgSyn for ImgSynMap<Prev, F> {
impl<Prev: ImgSyn, F: ImgSynPixFn<Pixel>> ImgSyn for ImgSynMap<Prev, F> {
fn getpx(&self, meta: crate::Canvas, x: f32, y: f32) -> crate::Pixel {
self.1.run(meta, x, y, self.0.getpx(meta, x, y))
}

View file

@ -1,2 +1,8 @@
mod map;
pub use map::*;
mod area;
pub use area::*;
mod axial;
pub use axial::*;
mod stretch;
pub use stretch::*;

14
src/ops/stretch.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::ImgSyn;
#[derive(Clone, Copy)]
pub struct ImgSynStretched<Prev: ImgSyn>(pub(crate) Prev, pub(crate) (f32, f32));
impl<Prev: ImgSyn> ImgSyn for ImgSynStretched<Prev> {
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,
)
}
}

View file

@ -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,
]
}
}

View file

@ -1,28 +1,37 @@
use crate::{Canvas, ImgSynPixFn, Pixel};
#[derive(Clone, Copy)]
pub struct PixFnInt<T: (Fn(Canvas, u32, u32, Pixel) -> Pixel) + Copy>(pub T);
impl<T> ImgSynPixFn for PixFnInt<T>
pub struct PixFnInt<T: Clone + Copy, F: (Fn(Canvas, u32, u32, Pixel) -> T) + Copy>(pub F);
impl<T: Clone + Copy, F> ImgSynPixFn<T> for PixFnInt<T, F>
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<T: (Fn(Canvas, f32, f32, Pixel) -> Pixel) + Copy>(pub T);
impl<T> ImgSynPixFn for PixFnFloat<T>
pub struct PixFnFloat<T: Clone + Copy, F: (Fn(Canvas, f32, f32, Pixel) -> T) + Copy>(pub F);
impl<T: Clone + Copy, F> ImgSynPixFn<T> for PixFnFloat<T, F>
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<T: Clone + Copy, F> ImgSynPixFn<T> 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)
}
}