Initial commit
This commit is contained in:
commit
0b9243dc4c
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
test_images
|
5034
Cargo.lock
generated
Normal file
5034
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "imflow"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = { version = "0.13.1", features = ["debug", "canvas", "tokio", "image"]}
|
||||||
|
image = "0.25.6"
|
||||||
|
|
||||||
|
itertools = "0.12"
|
||||||
|
memmap2 = "0.9.5"
|
||||||
|
# rustc-hash.workspace = true
|
||||||
|
tokio = { version = "1.44.1", features = ["sync"] }
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
zune-image = {version = "0.4.15", features = ["all"]}
|
||||||
|
[profile.dev.package.zune-jpeg]
|
||||||
|
opt-level = 3
|
326
src/main.rs
Normal file
326
src/main.rs
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
//! This example showcases an interactive version of the Game of Life, invented
|
||||||
|
//! by John Conway. It leverages a `Canvas` together with other widgets.
|
||||||
|
// use grid::Grid;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{self, Duration};
|
||||||
|
|
||||||
|
use iced::futures::AsyncReadExt;
|
||||||
|
use iced::widget::shader::wgpu::core::command::LoadOp;
|
||||||
|
// use iced::time::milliseconds;
|
||||||
|
use iced::widget::image::FilterMethod;
|
||||||
|
use iced::widget::{
|
||||||
|
Column, Container, button, center, checkbox, column, container, pick_list, row, slider, text,
|
||||||
|
};
|
||||||
|
use iced::{Center, Element, Fill, Length, Size, Subscription, Task, Theme};
|
||||||
|
use image::{self, DynamicImage, EncodableLayout, ImageBuffer, ImageReader};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use zune_image::codecs::qoi::zune_core::options::DecoderOptions; // for general image operations
|
||||||
|
// use image::io::Reader as ImageReader; // specifically for Reader
|
||||||
|
|
||||||
|
pub fn main() -> iced::Result {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
iced::application("Game of Life - Iced", GameOfLife::update, GameOfLife::view)
|
||||||
|
.subscription(GameOfLife::subscription)
|
||||||
|
.theme(|_| Theme::Dark)
|
||||||
|
.antialiasing(true)
|
||||||
|
.centered()
|
||||||
|
.window_size(Size::new(1500.0, 1000.0))
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GameOfLife {
|
||||||
|
is_playing: bool,
|
||||||
|
queued_ticks: usize,
|
||||||
|
speed: usize,
|
||||||
|
next_speed: Option<usize>,
|
||||||
|
version: usize,
|
||||||
|
image_filter_method: FilterMethod,
|
||||||
|
current_image: Option<PathBuf>,
|
||||||
|
width: u32,
|
||||||
|
available_images: Vec<PathBuf>,
|
||||||
|
current_image_id: usize,
|
||||||
|
loaded_images: HashMap<PathBuf, iced::widget::image::Handle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
TogglePlayback,
|
||||||
|
ToggleGrid(bool),
|
||||||
|
Clear,
|
||||||
|
SpeedChanged(f32),
|
||||||
|
Tick,
|
||||||
|
Next(i32),
|
||||||
|
ImageWidthChanged(u32),
|
||||||
|
ImageUseNearestToggled(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameOfLife {
|
||||||
|
fn new() -> Self {
|
||||||
|
let mut dir: Vec<PathBuf> = fs::read_dir(Path::new("./test_images"))
|
||||||
|
.unwrap()
|
||||||
|
.map(|f| f.unwrap().path())
|
||||||
|
.collect();
|
||||||
|
dir.sort();
|
||||||
|
let mut res = Self {
|
||||||
|
is_playing: false,
|
||||||
|
queued_ticks: 0,
|
||||||
|
speed: 5,
|
||||||
|
next_speed: None,
|
||||||
|
version: 0,
|
||||||
|
image_filter_method: FilterMethod::Nearest,
|
||||||
|
width: 1400,
|
||||||
|
current_image: Some(dir.first().unwrap().clone()),
|
||||||
|
available_images: dir,
|
||||||
|
current_image_id: 0,
|
||||||
|
loaded_images: HashMap::new(),
|
||||||
|
};
|
||||||
|
let _ = res.update(Message::Next(0));
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::Tick => {
|
||||||
|
self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
|
||||||
|
|
||||||
|
// if let Some(task) = self.grid.tick(self.queued_ticks) {
|
||||||
|
// if let Some(speed) = self.next_speed.take() {
|
||||||
|
// self.speed = speed;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// self.queued_ticks = 0;
|
||||||
|
|
||||||
|
// let version = self.version;
|
||||||
|
|
||||||
|
// // return Task::perform(task, Message::Grid.with(version));
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
Message::TogglePlayback => {
|
||||||
|
self.is_playing = !self.is_playing;
|
||||||
|
}
|
||||||
|
Message::ToggleGrid(show_grid_lines) => {
|
||||||
|
// self.grid.toggle_lines(show_grid_lines);
|
||||||
|
}
|
||||||
|
Message::Clear => {
|
||||||
|
// self.grid.clear();
|
||||||
|
self.version += 1;
|
||||||
|
}
|
||||||
|
Message::SpeedChanged(speed) => {
|
||||||
|
if self.is_playing {
|
||||||
|
self.next_speed = Some(speed.round() as usize);
|
||||||
|
} else {
|
||||||
|
self.speed = speed.round() as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::ImageWidthChanged(image_width) => {
|
||||||
|
self.width = image_width;
|
||||||
|
}
|
||||||
|
Message::ImageUseNearestToggled(use_nearest) => {
|
||||||
|
self.image_filter_method = if use_nearest {
|
||||||
|
FilterMethod::Nearest
|
||||||
|
} else {
|
||||||
|
FilterMethod::Linear
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Message::Next(change) => {
|
||||||
|
let elements = self.available_images.len() as i32;
|
||||||
|
let new_id = (self.current_image_id as i32 + change).clamp(0, elements - 1);
|
||||||
|
println!(
|
||||||
|
"updated id: {} from {} total {}",
|
||||||
|
new_id, self.current_image_id, elements
|
||||||
|
);
|
||||||
|
self.current_image_id = new_id as usize;
|
||||||
|
let path = self
|
||||||
|
.available_images
|
||||||
|
.get(self.current_image_id)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
self.current_image = Some(path.clone());
|
||||||
|
if !self.loaded_images.contains_key(&path.to_path_buf()) {
|
||||||
|
self.loaded_images.insert(
|
||||||
|
path.to_path_buf(),
|
||||||
|
load_thumbnail(path.to_str().unwrap()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
if self.is_playing {
|
||||||
|
iced::time::every(Duration::from_millis(1000 / self.speed as u64))
|
||||||
|
.map(|_| Message::Tick)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Element<'_, Message> {
|
||||||
|
let version = self.version;
|
||||||
|
let selected_speed = self.next_speed.unwrap_or(self.speed);
|
||||||
|
let controls = view_controls(
|
||||||
|
self.is_playing,
|
||||||
|
true,
|
||||||
|
// self.grid.are_lines_visible(),
|
||||||
|
selected_speed,
|
||||||
|
// self.grid.preset(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let content = column![
|
||||||
|
// image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into(),
|
||||||
|
// self.grid.view().map(Message::Grid.with(version)),
|
||||||
|
self.image(),
|
||||||
|
controls,
|
||||||
|
]
|
||||||
|
.height(Fill);
|
||||||
|
|
||||||
|
container(content).width(Fill).height(Fill).into()
|
||||||
|
// image("/media/nfs/sphotos/Images/24-08-11-Copenhagen/24-08-12/20240812-175614_DSC03844.JPG").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn image(&self) -> Column<Message> {
|
||||||
|
let width = self.width;
|
||||||
|
let filter_method = self.image_filter_method;
|
||||||
|
|
||||||
|
Self::container("Image")
|
||||||
|
.push("An image that tries to keep its aspect ratio.")
|
||||||
|
.push(self.ferris(
|
||||||
|
width,
|
||||||
|
filter_method,
|
||||||
|
self.current_image.as_ref().unwrap().as_ref(),
|
||||||
|
))
|
||||||
|
.push(slider(100..=1500, width, Message::ImageWidthChanged))
|
||||||
|
.push(text!("Width: {width} px").width(Fill).align_x(Center))
|
||||||
|
.push(
|
||||||
|
checkbox(
|
||||||
|
"Use nearest interpolation",
|
||||||
|
filter_method == FilterMethod::Nearest,
|
||||||
|
)
|
||||||
|
.on_toggle(Message::ImageUseNearestToggled),
|
||||||
|
)
|
||||||
|
.align_x(Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container(title: &str) -> Column<'_, Message> {
|
||||||
|
column![text(title).size(50)].spacing(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ferris<'a>(
|
||||||
|
&self,
|
||||||
|
width: u32,
|
||||||
|
filter_method: iced::widget::image::FilterMethod,
|
||||||
|
path: &Path,
|
||||||
|
) -> Container<'a, Message> {
|
||||||
|
if self.loaded_images.get(path).is_none() {
|
||||||
|
return center(text("loading"));
|
||||||
|
}
|
||||||
|
let img = iced::widget::image::Image::new(self.loaded_images.get(path).unwrap());
|
||||||
|
center(
|
||||||
|
// This should go away once we unify resource loading on native
|
||||||
|
// platforms
|
||||||
|
img.filter_method(filter_method)
|
||||||
|
.width(Length::Fixed(width as f32)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameOfLife {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_controls<'a>(
|
||||||
|
is_playing: bool,
|
||||||
|
is_grid_enabled: bool,
|
||||||
|
speed: usize,
|
||||||
|
// preset: Preset,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let playback_controls = row![
|
||||||
|
button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback),
|
||||||
|
button("Previous")
|
||||||
|
.on_press(Message::Next(-1))
|
||||||
|
.style(button::secondary),
|
||||||
|
button("Next")
|
||||||
|
.on_press(Message::Next(1))
|
||||||
|
.style(button::secondary),
|
||||||
|
]
|
||||||
|
.spacing(10);
|
||||||
|
|
||||||
|
let speed_controls = row![
|
||||||
|
slider(1.0..=1000.0, speed as f32, Message::SpeedChanged),
|
||||||
|
text!("x{speed}").size(16),
|
||||||
|
]
|
||||||
|
.align_y(Center)
|
||||||
|
.spacing(10);
|
||||||
|
|
||||||
|
row![
|
||||||
|
playback_controls,
|
||||||
|
speed_controls,
|
||||||
|
// checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid),
|
||||||
|
// row![
|
||||||
|
// pick_list(preset::ALL, Some(preset), Message::PresetPicked),
|
||||||
|
// button("Clear")
|
||||||
|
// .on_press(Message::Clear)
|
||||||
|
// .style(button::danger)
|
||||||
|
// ]
|
||||||
|
// .spacing(10)
|
||||||
|
]
|
||||||
|
.padding(10)
|
||||||
|
.spacing(20)
|
||||||
|
.align_y(Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_thumbnail(path: &str) -> Result<iced::widget::image::Handle, String> {
|
||||||
|
// let file = File::open(path).map_err(|e| e.to_string())?;
|
||||||
|
// let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(|e| e.to_string())?;
|
||||||
|
// println!("mapped file");
|
||||||
|
// let img = zune_image::image::Image::read(&*mmap, DecoderOptions::default()).unwrap();
|
||||||
|
let img = zune_image::image::Image::open_with_options(path, DecoderOptions::default()).unwrap();
|
||||||
|
// let img = image::load_from_memory(&mmap)
|
||||||
|
// .map_err(|e| e.to_string())?
|
||||||
|
// .decode()
|
||||||
|
// .map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// let thumbnail = img.thumbnail(128, 128);
|
||||||
|
let width = img.dimensions().0 as u32;
|
||||||
|
let height = img.dimensions().1 as u32;
|
||||||
|
println!("loaded");
|
||||||
|
let flat = img.flatten_to_u8();
|
||||||
|
println!("flattened");
|
||||||
|
let rgba = convert_rgb_to_rgba(flat);
|
||||||
|
println!("rgbad");
|
||||||
|
// let leaked_slice = Box::leak(rgba[0].clone().into_boxed_slice());
|
||||||
|
// let slice = rgba[0].as_slice().clone();
|
||||||
|
let conv = iced::widget::image::Handle::from_rgba(
|
||||||
|
width, height, // leaked_slice.as_bytes(),
|
||||||
|
rgba,
|
||||||
|
);
|
||||||
|
println!("iced");
|
||||||
|
|
||||||
|
Ok((conv))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_rgb_to_rgba(rgb_data: Vec<Vec<u8>>) -> Vec<u8> {
|
||||||
|
let r_channel = &rgb_data[0];
|
||||||
|
|
||||||
|
let num_pixels = r_channel.len() / 3;
|
||||||
|
let mut rgba_data: Vec<u8> = Vec::with_capacity(num_pixels * 4);
|
||||||
|
|
||||||
|
for i in 0..num_pixels {
|
||||||
|
rgba_data.push(r_channel[i * 3]);
|
||||||
|
rgba_data.push(r_channel[i * 3 + 1]);
|
||||||
|
rgba_data.push(r_channel[i * 3 + 2]);
|
||||||
|
rgba_data.push(255); // Fully opaque Alpha value
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba_data
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user