Compare commits

...

2 commits

Author SHA1 Message Date
Logan 2e467aca96 Changed into a CGI script 2024-08-25 01:20:52 -05:00
Logan 042055273f test 2024-06-09 01:05:23 -05:00
17 changed files with 814 additions and 606 deletions

1158
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,19 +6,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
log = "0.4" image = "0.25"
image = "0.24" gif = "0.13"
gif = "0.12"
futures = "0.3" futures = "0.3"
wgpu = {version="0.18", features=["webgl"]} tokio = {version="1.37", features=["full"]}
wgpu = {version="22"}
bytemuck = {version="1.14", features=["derive"]} bytemuck = {version="1.14", features=["derive"]}
futures-intrusive = "0.5" futures-intrusive = "0.5"
pollster = "0.3" rust-cgi = "0.7"
clap = { version = "4.4.16", features = ["derive"] }
indicatif = "0.17"
[dev-dependencies]
flexi_logger = "0.27"
[profile.dev] [profile.dev]
opt-level=1 opt-level=1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

BIN
resources/cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
resources/dog.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
resources/goback.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
resources/skull.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
resources/starry.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

BIN
resources/starved.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
resources/stoic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.62 107.62"><defs><style>
.cls-1 {
fill: #654ff0;
}
</style></defs><title>web-assembly-icon</title><g id="Layer_2" data-name="Layer 2"><g id="Notch_-_Purple" data-name="Notch - Purple"><g id="icon"><path class="cls-1" d="M66.12,0c0,.19,0,.38,0,.58a12.34,12.34,0,1,1-24.68,0c0-.2,0-.39,0-.58H0V107.62H107.62V0ZM51.38,96.1,46.14,70.17H46L40.39,96.1H33.18L25,58h7.13L37,83.93h.09L42.94,58h6.67L54.9,84.25H55L60.55,58h7L58.46,96.1Zm39.26,0-2.43-8.48H75.4L73.53,96.1H66.36L75.59,58H86.83L98,96.1Z"/><polygon class="cls-1" points="79.87 67.39 76.76 81.37 86.44 81.37 82.87 67.39 79.87 67.39"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 685 B

BIN
resources/wimpy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

View file

@ -118,7 +118,7 @@ pub struct QuickDraw {
} }
impl QuickDraw { impl QuickDraw {
pub async fn new(width: u32, height: u32, max_circles: u64) -> Self { pub async fn new(width: u32, height: u32, max_circles: u64) -> Option<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(), backends: wgpu::Backends::all(),
..Default::default() ..Default::default()
@ -135,7 +135,7 @@ impl QuickDraw {
Some(a) => a, Some(a) => a,
None => { None => {
eprintln!("Could not find WGPU adapter"); eprintln!("Could not find WGPU adapter");
std::process::exit(1); return None;
}, },
}; };
let (device, queue) = let (device, queue) =
@ -144,7 +144,7 @@ impl QuickDraw {
Err(e) => { Err(e) => {
eprintln!("Could not find WGPU device:"); eprintln!("Could not find WGPU device:");
eprintln!("{}", e); eprintln!("{}", e);
std::process::exit(1); return None;
}, },
}; };
@ -244,14 +244,17 @@ impl QuickDraw {
multiview: None, multiview: None,
label: Some("Render Pipeline"), label: Some("Render Pipeline"),
layout: Some(&render_pipeline_layout), layout: Some(&render_pipeline_layout),
cache: None,
vertex: wgpu::VertexState { vertex: wgpu::VertexState {
module: &vert, module: &vert,
entry_point: "vs_main", entry_point: "vs_main",
buffers: &[Vertex::desc(), Circle::desc()], buffers: &[Vertex::desc(), Circle::desc()],
compilation_options: PipelineCompilationOptions::default(),
}, },
fragment: Some(wgpu::FragmentState { fragment: Some(wgpu::FragmentState {
module: &frag, module: &frag,
entry_point: "fs_main", entry_point: "fs_main",
compilation_options: PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState { targets: &[Some(wgpu::ColorTargetState {
format: texture_desc.format, format: texture_desc.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING), blend: Some(wgpu::BlendState::ALPHA_BLENDING),
@ -277,7 +280,7 @@ impl QuickDraw {
}, },
}); });
Self { Some(Self {
device, device,
queue, queue,
width, width,
@ -292,7 +295,7 @@ impl QuickDraw {
output_buffer, output_buffer,
vertex_buffer, vertex_buffer,
pipeline, pipeline,
} })
} }
pub async fn resize(&mut self, width: u32, height: u32, max_circles: usize) { pub async fn resize(&mut self, width: u32, height: u32, max_circles: usize) {

View file

@ -1,23 +1,15 @@
#![feature(future_join)] #![feature(future_join)]
use std::{cell::RefCell, future::join, io::Write, path::Path, sync::Arc}; use std::{io::Cursor, sync::Arc};
use tokio::sync::Mutex;
use draw::QuickDraw; use draw::QuickDraw;
use gif::Frame; use gif::Frame;
use image::{ImageBuffer, Rgb}; use image::{ImageBuffer, Rgb};
use indicatif::{ProgressBar, ProgressStyle};
use log::debug;
pub mod draw; pub mod draw;
pub mod helper; pub mod helper;
pub mod sim; pub mod sim;
pub mod tests;
pub fn make_progress(msg: &'static str, max: u64) -> ProgressBar {
let progress = ProgressBar::new(max);
progress.set_style(ProgressStyle::with_template("{msg} :: {bar}").unwrap());
progress.set_message(msg);
progress
}
const WIDTH: f32 = 512.0; const WIDTH: f32 = 512.0;
const HEIGHT: f32 = 512.0; const HEIGHT: f32 = 512.0;
@ -27,21 +19,21 @@ pub async fn preprocess(
radius: f32, radius: f32,
) -> (sim::Simulation, usize, usize) { ) -> (sim::Simulation, usize, usize) {
let (sim, it, max_circles) = let (sim, it, max_circles) =
sim::Simulation::simulate_image(WIDTH, HEIGHT, radius, image).await; sim::Simulation::simulate_image(WIDTH, HEIGHT, radius, image);
(sim, it, max_circles) (sim, it, max_circles)
} }
pub async fn simulate( pub async fn simulate(
draw: &mut QuickDraw, draw: Arc<Mutex<QuickDraw>>,
mut sim: Simulation, mut sim: Simulation,
it: usize, it: usize,
step: usize, step: usize,
max_circles: usize, max_circles: usize,
) -> Vec<Frame<'static>> { ) -> Vec<Frame<'static>> {
let _draw = draw;
let mut draw = _draw.lock().await;
draw.resize(WIDTH as u32, HEIGHT as u32, max_circles).await; draw.resize(WIDTH as u32, HEIGHT as u32, max_circles).await;
let mut frames = vec![]; let mut frames = vec![];
let progress =
make_progress("Simulating ", ((it - sim.clock) / sim.substeps) as u64);
while sim.clock < it { while sim.clock < it {
let circles = &sim let circles = &sim
.circles .circles
@ -52,14 +44,12 @@ pub async fn simulate(
color: [c.color.0, c.color.1, c.color.2, 255], color: [c.color.0, c.color.1, c.color.2, 255],
}) })
.collect::<Vec<draw::Circle>>(); .collect::<Vec<draw::Circle>>();
let bytes_future = draw.draw_circles(circles); sim.steps(step);
let steps = sim.steps(step); let mut bytes = draw.draw_circles(circles).await;
let mut bytes = join!(bytes_future, steps).await.0; let frame =
let frame = gif::Frame::from_rgba(WIDTH as u16, HEIGHT as u16, &mut bytes); gif::Frame::from_rgba_speed(WIDTH as u16, HEIGHT as u16, &mut bytes, 10);
frames.push(frame); frames.push(frame);
progress.inc(step as u64);
} }
progress.finish();
frames frames
} }
@ -67,13 +57,10 @@ pub async fn encode(frames: Vec<Frame<'static>>, repeat: bool) -> Vec<u8> {
let mut buffer = Vec::<u8>::new(); let mut buffer = Vec::<u8>::new();
let mut encoder = let mut encoder =
gif::Encoder::new(&mut buffer, WIDTH as u16, HEIGHT as u16, &[]).unwrap(); gif::Encoder::new(&mut buffer, WIDTH as u16, HEIGHT as u16, &[]).unwrap();
let progress = make_progress("Encoding ", frames.len() as u64);
for mut frame in frames { for mut frame in frames {
frame.delay = 1; frame.delay = 1;
encoder.write_frame(&frame).unwrap(); encoder.write_frame(&frame).unwrap();
progress.inc(1);
} }
progress.finish();
if repeat { if repeat {
encoder.set_repeat(gif::Repeat::Infinite).unwrap(); encoder.set_repeat(gif::Repeat::Infinite).unwrap();
} }
@ -81,96 +68,49 @@ pub async fn encode(frames: Vec<Frame<'static>>, repeat: bool) -> Vec<u8> {
buffer buffer
} }
use clap::Parser;
use sim::Simulation; use sim::Simulation;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Input file to process. All common image types are
/// supported, see the `image` crate docs for specific
/// compatibility
#[arg(short = 'i', value_hint = clap::ValueHint::DirPath)]
input: std::path::PathBuf,
/// Output file path ('./output.gif' by default) #[derive(Debug, Clone)]
#[arg(short = 'o', value_hint = clap::ValueHint::DirPath)] pub enum Error {
output: Option<std::path::PathBuf>, CantGuessFormat,
DecodeError,
/// How many physics-steps between frames. Affects the
/// speed and length of the animation
#[arg(short = 's')]
step: Option<u32>,
/// Radius of the circle, smaller means more detailed but
/// slower to compute (8.0 by default, must be between
/// 1.0 and 50.0 inclusive)
#[arg(short = 'r')]
radius: Option<f32>,
/// Loop the GIF
#[arg(short = 'l', long = "loop")]
looping: bool,
} }
fn open_image<P: AsRef<Path>>(path: P) -> ImageBuffer<Rgb<u8>, Vec<u8>> { pub async fn make_quickdraw() -> Option<Arc<Mutex<QuickDraw>>> {
let image = match image::io::Reader::open(path) { Some(Arc::new(Mutex::new(QuickDraw::new(512, 512, 1000).await?)))
Ok(i) => i, }
Err(e) => {
eprintln!("Error opening file for processing: "); pub async fn make_gif(
eprintln!("{}", e); input: &[u8],
std::process::exit(1); draw: Arc<Mutex<QuickDraw>>,
}, ) -> Result<Vec<u8>, Error> {
const STEP: usize = 20;
const RADIUS: f32 = 7.0;
let image = image::io::Reader::new(Cursor::new(input.to_vec().as_slice()))
.with_guessed_format()
.map_err(|_| Error::CantGuessFormat)?
.decode()
.map_err(|_| Error::DecodeError)?
.to_rgb8();
let (sim, it, max) = preprocess(image, RADIUS).await;
let frames = simulate(draw, sim, it, STEP, max).await;
let gif = encode(frames, true).await;
Ok(gif)
}
use rust_cgi as cgi;
const CONTENT_TYPE: &'static str = "Content-Type: image/gif";
rust_cgi::cgi_main!(|req: cgi::http::Request<Vec<u8>>| -> rust_cgi::Response {
let qd = match futures::executor::block_on(make_quickdraw()) {
Some(qd) => qd,
None => return cgi::text_response(500, "Failed to initialize WGPU"),
}; };
let image = req.into_body();
let decoded = match image.decode() { let gif = match futures::executor::block_on(make_gif(&image, qd)) {
Ok(d) => d, Ok(gif) => gif,
Err(e) => { Err(_) => return cgi::text_response(500, "Failed to process image"),
eprintln!("Error processing file: ");
eprintln!("{}", e);
std::process::exit(1);
},
}; };
decoded.to_rgb8() rust_cgi::binary_response(200, CONTENT_TYPE, gif)
} });
fn main() {
let args = Args::parse();
let output = args.output.unwrap_or("output.gif".into());
let step = args.step.unwrap_or(20) as usize;
if step == 0 {
eprintln!("Physics step cannot be 0");
std::process::exit(1);
}
let radius = args.radius.unwrap_or(8.0);
if radius < 1.0 || radius > 50.0 {
eprintln!("Invalid radius {}", radius);
eprintln!("Must be between 1.0 and 50.0 inclusive");
std::process::exit(1);
}
let image = open_image(args.input);
let (sim, it, max) = pollster::block_on(preprocess(image, radius));
let mut draw = pollster::block_on(QuickDraw::new(512, 512, 1000));
let frames = pollster::block_on(simulate(&mut draw, sim, it, step, max));
let gif = pollster::block_on(encode(frames, args.looping));
let mut file = match std::fs::File::create(output.clone()) {
Ok(f) => f,
Err(e) => {
eprintln!("Error opening file for writing:");
eprintln!("{}", e);
std::process::exit(1);
},
};
match file.write(&gif) {
Ok(_) => {
println!("Successfully created file at '{}'", output.display());
},
Err(e) => {
eprintln!("Error writing output file: ");
eprintln!("{}", e);
std::process::exit(1);
},
};
}

View file

@ -1,6 +1,6 @@
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use crate::{helper::*, make_progress}; use crate::helper::*;
use image::{ImageBuffer, Rgb}; use image::{ImageBuffer, Rgb};
pub struct Circle { pub struct Circle {
@ -55,10 +55,7 @@ impl Simulation {
} }
#[inline] #[inline]
async fn assign_colors_from_image( fn assign_colors_from_image(&mut self, img: ImageBuffer<Rgb<u8>, Vec<u8>>) {
&mut self,
img: ImageBuffer<Rgb<u8>, Vec<u8>>,
) {
let (width, height) = (img.width() as f32 - 1.0, img.height() as f32 - 1.0); let (width, height) = (img.width() as f32 - 1.0, img.height() as f32 - 1.0);
for (pos, index) in self.circles.iter().map(|c| (c.position, c.index)) { for (pos, index) in self.circles.iter().map(|c| (c.position, c.index)) {
let img_x = let img_x =
@ -73,7 +70,7 @@ impl Simulation {
} }
#[inline] #[inline]
pub async fn simulate_image( pub fn simulate_image(
width: f32, width: f32,
height: f32, height: f32,
circle_radius: f32, circle_radius: f32,
@ -85,22 +82,22 @@ impl Simulation {
s.finish() s.finish()
} % 1204) as usize; } % 1204) as usize;
let mut sim = Simulation::new(width, height, circle_radius, image_hash); let mut sim = Simulation::new(width, height, circle_radius, image_hash);
let progress = make_progress( // let progress = make_progress(
"Preprocessing", // "Preprocessing",
(sim.max_circles + Self::POST_PROCESS) as u64, // (sim.max_circles + Self::POST_PROCESS) as u64,
); // );
while sim.circles() < sim.max_circles { while sim.circles() < sim.max_circles {
sim.step().await; sim.step();
progress.inc(2); // progress.inc(2);
} }
for _ in 0..Self::POST_PROCESS { for _ in 0..Self::POST_PROCESS {
sim.step().await; sim.step();
progress.inc(1); // progress.inc(1);
} }
progress.finish(); // progress.finish();
let total_iterations = sim.clock; let total_iterations = sim.clock;
let max_circles = sim.circles.len(); let max_circles = sim.circles.len();
sim.assign_colors_from_image(img).await; sim.assign_colors_from_image(img);
sim.circles.clear(); sim.circles.clear();
sim.clock = sim.rand_seed; sim.clock = sim.rand_seed;
(sim, total_iterations, max_circles) (sim, total_iterations, max_circles)
@ -144,7 +141,7 @@ impl Simulation {
// Insertion sort // Insertion sort
#[inline] #[inline]
async fn sort(&mut self) { fn sort(&mut self) {
if self.circles.len() == 1 { if self.circles.len() == 1 {
return; return;
} }
@ -159,7 +156,7 @@ impl Simulation {
} }
#[inline] #[inline]
async fn integrate(&mut self) { fn integrate(&mut self) {
let delta = self.timescale * (1.0 / self.substeps as f32); let delta = self.timescale * (1.0 / self.substeps as f32);
let gravity = Vector2::new(0.0, self.gravity) * delta.powi(2); let gravity = Vector2::new(0.0, self.gravity) * delta.powi(2);
self.circles.iter_mut().for_each(|circle| { self.circles.iter_mut().for_each(|circle| {
@ -170,7 +167,7 @@ impl Simulation {
} }
#[inline] #[inline]
async fn collide(&mut self) { fn collide(&mut self) {
for i in 0..self.circles.len() { for i in 0..self.circles.len() {
// Apply gravity // Apply gravity
for j in i..self.circles.len() { for j in i..self.circles.len() {
@ -218,7 +215,7 @@ impl Simulation {
} }
#[inline] #[inline]
pub async fn step(&mut self) { pub fn step(&mut self) {
if self.circles.len() < self.max_circles { if self.circles.len() < self.max_circles {
self.launch(); self.launch();
} }
@ -228,21 +225,19 @@ impl Simulation {
for _ in 0..self.substeps { for _ in 0..self.substeps {
self.constrain_rect(); self.constrain_rect();
self.sort().await; self.sort();
self.collide().await; self.collide();
self.integrate().await; self.integrate();
self.clock += 1; self.clock += 1;
} }
} }
#[inline] #[inline]
pub async fn steps(&mut self, steps: usize) { pub fn steps(&mut self, steps: usize) {
for _ in 0..steps { for _ in 0..steps {
self.step().await; self.step();
} }
} }
pub fn circles(&self) -> usize { pub fn circles(&self) -> usize { self.circles.len() }
self.circles.len()
}
} }

View file

@ -1,19 +0,0 @@
#[cfg(test)]
#[test]
fn bench_async() {
use std::io::Write;
flexi_logger::Logger::try_with_str("debug").unwrap();
let image = image::io::Reader::open(
"resources/mona
lisa.png",
)
.unwrap()
.decode()
.unwrap()
.to_rgb8();
// let gif = pollster::block_on(crate::generate_async(image, None));
// let mut file =
// std::fs::File::create("test.gif").unwrap();
// file.write(&gif).unwrap();
}

BIN
test.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB