Initial commit
This commit is contained in:
commit
34e989adbf
9 changed files with 3579 additions and 0 deletions
3109
Cargo.lock
generated
Normal file
3109
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "s_it_paint"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.23.0"
|
||||
egui = "0.23.0"
|
||||
egui_file = "0.11.0"
|
||||
image = "0.24.7"
|
||||
micro_ndarray = "0.6.1"
|
||||
49
src/color.rs
Normal file
49
src/color.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use egui::*;
|
||||
|
||||
use crate::App;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DrawColor {
|
||||
Black,
|
||||
White,
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
Yellow,
|
||||
Orange,
|
||||
Brown,
|
||||
Aqua,
|
||||
Purple,
|
||||
}
|
||||
|
||||
use DrawColor::*;
|
||||
|
||||
impl DrawColor {
|
||||
pub fn menu(app: &mut App, ui: &mut Ui) {
|
||||
ui.radio_value(&mut app.color, Black, "Black");
|
||||
ui.radio_value(&mut app.color, White, "White");
|
||||
ui.radio_value(&mut app.color, Red, "Red");
|
||||
ui.radio_value(&mut app.color, Green, "Green");
|
||||
ui.radio_value(&mut app.color, Blue, "Blue");
|
||||
ui.radio_value(&mut app.color, Yellow, "Yellow");
|
||||
ui.radio_value(&mut app.color, Orange, "Orange");
|
||||
ui.radio_value(&mut app.color, Brown, "Brown");
|
||||
ui.radio_value(&mut app.color, Aqua, "Aqua");
|
||||
ui.radio_value(&mut app.color, Purple, "Purple");
|
||||
}
|
||||
|
||||
pub fn into_color(self) -> u32 {
|
||||
match self {
|
||||
Black => 0,
|
||||
White => 0xffffff,
|
||||
Red => 0xff0000,
|
||||
Green => 0x00ff00,
|
||||
Blue => 0x0000ff,
|
||||
Yellow => 0xffff00,
|
||||
Orange => 0xff8000,
|
||||
Brown => 0x654321,
|
||||
Aqua => 0x00ffff,
|
||||
Purple => 0xff00ff,
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/dialog.rs
Normal file
46
src/dialog.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use egui::*;
|
||||
|
||||
use crate::{App, DialogAction};
|
||||
|
||||
impl App {
|
||||
pub fn open_file(&mut self) {
|
||||
let mut dialog = egui_file::FileDialog::open_file(None);
|
||||
dialog.open();
|
||||
self.dialog_action = Some(DialogAction::Open);
|
||||
self.dialog = Some(dialog);
|
||||
}
|
||||
|
||||
pub fn save_file(&mut self, ask_name: bool) {
|
||||
if ask_name || self.filename.is_none() {
|
||||
let mut dialog = egui_file::FileDialog::save_file(None);
|
||||
dialog.open();
|
||||
self.dialog_action = Some(DialogAction::Save);
|
||||
self.dialog = Some(dialog);
|
||||
} else {
|
||||
self.save();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_dialogs(&mut self, ctx: &Context) {
|
||||
if let Some(ref mut d) = self.dialog {
|
||||
if d.show(ctx).selected() {
|
||||
if let Some(file) = d.path() {
|
||||
match self.dialog_action.as_ref().unwrap() {
|
||||
DialogAction::Open => {
|
||||
self.filename =
|
||||
Some(file.to_str().expect("invalid file name").to_owned());
|
||||
self.load();
|
||||
}
|
||||
DialogAction::Save => {
|
||||
self.filename =
|
||||
Some(file.to_str().expect("invalid file name").to_owned());
|
||||
self.save();
|
||||
}
|
||||
};
|
||||
self.dialog_action = None;
|
||||
self.dialog = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/draw.rs
Normal file
79
src/draw.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
use crate::App;
|
||||
|
||||
impl App {
|
||||
pub fn set_px(&mut self, x: usize, y: usize, px: u32) {
|
||||
let size = self.image.size();
|
||||
if size[1] <= x || size[2] <= y {
|
||||
return; // just ignore
|
||||
}
|
||||
self.image[[0, x, y]] = ((px >> 16) & 0xff) as u8;
|
||||
self.image[[1, x, y]] = ((px >> 8) & 0xff) as u8;
|
||||
self.image[[2, x, y]] = ((px) & 0xff) as u8;
|
||||
}
|
||||
|
||||
pub fn draw_dot(&mut self, x: usize, y: usize) {
|
||||
let color = self.color.into_color();
|
||||
self.set_px(x, y + 1, color);
|
||||
self.set_px(x - 1, y, color);
|
||||
self.set_px(x, y, color);
|
||||
self.set_px(x + 1, y, color);
|
||||
self.set_px(x, y - 1, color);
|
||||
}
|
||||
|
||||
pub fn draw_line(
|
||||
&mut self,
|
||||
x1: usize,
|
||||
y1: usize,
|
||||
x2: usize,
|
||||
y2: usize,
|
||||
func: fn(&mut Self, usize, usize),
|
||||
) {
|
||||
let dx = x2 as f32 - x1 as f32;
|
||||
let dy = y2 as f32 - y1 as f32;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
let step_x = dx / dist;
|
||||
let step_y = dy / dist;
|
||||
let mut fx = x1 as f32;
|
||||
let mut fy = y1 as f32;
|
||||
for _ in 0..(dist + 1.0) as usize {
|
||||
func(self, fx as usize, fy as usize);
|
||||
if fx as usize == x2 && fy as usize == y2 {
|
||||
break;
|
||||
}
|
||||
fx += step_x;
|
||||
fy += step_y;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_mouse(&mut self, x: usize, y: usize, func: fn(&mut Self, usize, usize)) {
|
||||
let [last_x, last_y] = self.last_mouse_pos.unwrap_or([x, y]);
|
||||
self.draw_line(last_x, last_y, x, y, func);
|
||||
self.last_mouse_pos = Some([x, y]);
|
||||
}
|
||||
|
||||
pub fn draw_ngon(&mut self, x: usize, y: usize, n: usize, radius: f32, begin_angle: f32) {
|
||||
let begin_angle = (begin_angle - 90.0) / 180.0 * PI;
|
||||
let angle_increment = PI * 2.0 / n as f32;
|
||||
let mut current_angle = angle_increment + begin_angle;
|
||||
let fx = x as f32;
|
||||
let fy = y as f32;
|
||||
let mut last_x = begin_angle.cos() * radius;
|
||||
let mut last_y = begin_angle.sin() * radius;
|
||||
for _ in 0..n {
|
||||
let new_x = current_angle.cos() * radius;
|
||||
let new_y = current_angle.sin() * radius;
|
||||
self.draw_line(
|
||||
(fx + last_x) as usize,
|
||||
(fy + last_y) as usize,
|
||||
(fx + new_x) as usize,
|
||||
(fy + new_y) as usize,
|
||||
Self::draw_dot,
|
||||
);
|
||||
last_x = new_x;
|
||||
last_y = new_y;
|
||||
current_angle += angle_increment;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/io.rs
Normal file
38
src/io.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use image::{io::Reader as ImageReader, DynamicImage, ImageBuffer};
|
||||
use micro_ndarray::Array;
|
||||
|
||||
use crate::App;
|
||||
|
||||
impl App {
|
||||
/// SAFETY: Call only when self.filename is present
|
||||
pub fn load(&mut self) {
|
||||
if let Ok(x) = ImageReader::open(self.filename.as_ref().unwrap())
|
||||
.expect("This file can't be opened due to an IO error")
|
||||
.decode()
|
||||
{
|
||||
self.image = Array::from_flat(
|
||||
x.to_rgb8().into_vec(),
|
||||
[3, x.width() as usize, x.height() as usize],
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
self.filename = None;
|
||||
println!("Unable to load this image.");
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: Call only when self.filename is present
|
||||
pub fn save(&mut self) {
|
||||
let size = self.image.size();
|
||||
DynamicImage::ImageRgb8(
|
||||
ImageBuffer::from_vec(
|
||||
size[1] as u32,
|
||||
size[2] as u32,
|
||||
self.image.clone().into_flattened(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.save(self.filename.as_ref().unwrap())
|
||||
.expect("This file can't be saved to due to an IO error");
|
||||
}
|
||||
}
|
||||
158
src/main.rs
Normal file
158
src/main.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use std::f32::consts::PI;
|
||||
use std::{process, sync::Arc, time::Duration};
|
||||
|
||||
use color::DrawColor;
|
||||
use eframe::CreationContext;
|
||||
use egui::load::SizedTexture;
|
||||
use egui::*;
|
||||
|
||||
use egui_file::FileDialog;
|
||||
use micro_ndarray::Array;
|
||||
use mode::Mode;
|
||||
|
||||
mod color;
|
||||
mod dialog;
|
||||
mod draw;
|
||||
mod io;
|
||||
mod mode;
|
||||
mod tex;
|
||||
|
||||
fn main() {
|
||||
let native_options = eframe::NativeOptions::default();
|
||||
eframe::run_native(
|
||||
"Zeichenprogramm",
|
||||
native_options,
|
||||
Box::new(|cc| Box::new(App::new(cc))),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub enum DialogAction {
|
||||
Open,
|
||||
Save,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub image: Array<u8, 3>,
|
||||
pub tex: TextureId,
|
||||
pub filename: Option<String>,
|
||||
pub dialog_action: Option<DialogAction>,
|
||||
pub dialog: Option<FileDialog>,
|
||||
pub last_mouse_pos: Option<[usize; 2]>,
|
||||
pub mode: Mode,
|
||||
pub color: DrawColor,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(cc: &CreationContext) -> App {
|
||||
Self {
|
||||
image: Array::new_with([3, 100, 100], 0xff),
|
||||
tex: cc.egui_ctx.tex_manager().write().alloc(
|
||||
"canvas".to_owned(),
|
||||
ImageData::Color(Arc::new(ColorImage::new(
|
||||
[100, 100],
|
||||
Color32::TEMPORARY_COLOR,
|
||||
))),
|
||||
TextureOptions {
|
||||
magnification: TextureFilter::Nearest,
|
||||
minification: TextureFilter::Linear,
|
||||
},
|
||||
),
|
||||
filename: None,
|
||||
dialog_action: None,
|
||||
dialog: None,
|
||||
last_mouse_pos: None,
|
||||
mode: Mode::Paintbrush,
|
||||
color: DrawColor::Black,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
ctx.request_repaint_after(Duration::from_millis(1000 / 120));
|
||||
self.handle_dialogs(ctx);
|
||||
let f = Frame::none()
|
||||
.inner_margin(Margin::same(2.0))
|
||||
.fill(Color32::from_rgb(0x00, 0x00, 0x00));
|
||||
TopBottomPanel::top("menubar").frame(f).show(ctx, |ui| {
|
||||
menu::bar(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
if self.filename.is_some() && ui.button("Reload from file").clicked() {
|
||||
self.load();
|
||||
}
|
||||
if ui.button("Clear").clicked() {
|
||||
self.image = Array::new_with([3, 0, 0], 0xff); // expanded automatically
|
||||
}
|
||||
if ui.button("Open...").clicked() {
|
||||
self.open_file();
|
||||
}
|
||||
if ui.button("Save").clicked() {
|
||||
self.save_file(false);
|
||||
}
|
||||
if ui.button("Save as...").clicked() {
|
||||
self.save_file(true);
|
||||
}
|
||||
if ui.button("Close").clicked() {
|
||||
process::exit(0);
|
||||
}
|
||||
});
|
||||
ui.menu_button("Tools", |ui| {
|
||||
Mode::menu(self, ui);
|
||||
});
|
||||
ui.menu_button("Colors", |ui| {
|
||||
DrawColor::menu(self, ui);
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
CentralPanel::default().frame(f).show(ctx, |ui| {
|
||||
let size = ui.available_size();
|
||||
self.correct_tex_size(
|
||||
&mut ctx.tex_manager().write(),
|
||||
[size.x as usize, size.y as usize],
|
||||
);
|
||||
self.image_to_texture(&mut ctx.tex_manager().write());
|
||||
let r = ui.add(egui::Image::from_texture(SizedTexture::new(self.tex, size)));
|
||||
ui.input(|inp| {
|
||||
if !r.hovered() {
|
||||
return;
|
||||
}
|
||||
let Some(pointer_pos) = r.hover_pos().map(|x| x - r.rect.min) else {
|
||||
return;
|
||||
};
|
||||
if inp.key_down(Key::D) {
|
||||
self.draw_ngon(pointer_pos.x as usize, pointer_pos.y as usize, 3, 30.0, 0.0);
|
||||
}
|
||||
if inp.key_down(Key::Q) {
|
||||
self.draw_ngon(
|
||||
pointer_pos.x as usize,
|
||||
pointer_pos.y as usize,
|
||||
4,
|
||||
30.0,
|
||||
45.0,
|
||||
);
|
||||
}
|
||||
if inp.key_down(Key::K) {
|
||||
self.draw_ngon(
|
||||
pointer_pos.x as usize,
|
||||
pointer_pos.y as usize,
|
||||
(30.0 * PI) as usize,
|
||||
30.0,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
if inp.pointer.primary_down() {
|
||||
self.draw_mouse(
|
||||
pointer_pos.x as usize,
|
||||
pointer_pos.y as usize,
|
||||
self.mode.into_fn(),
|
||||
);
|
||||
} else {
|
||||
self.last_mouse_pos = None;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
33
src/mode.rs
Normal file
33
src/mode.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use std::f32::consts::PI;
|
||||
|
||||
use egui::*;
|
||||
|
||||
use crate::App;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Paintbrush,
|
||||
Triangle,
|
||||
Square,
|
||||
Circle,
|
||||
}
|
||||
|
||||
use Mode::*;
|
||||
|
||||
impl Mode {
|
||||
pub fn menu(app: &mut App, ui: &mut Ui) {
|
||||
ui.radio_value(&mut app.mode, Paintbrush, "Paintbrush");
|
||||
ui.radio_value(&mut app.mode, Triangle, "Triangle");
|
||||
ui.radio_value(&mut app.mode, Square, "Square");
|
||||
ui.radio_value(&mut app.mode, Circle, "Circle");
|
||||
}
|
||||
|
||||
pub fn into_fn(self) -> fn(&mut App, usize, usize) {
|
||||
match self {
|
||||
Paintbrush => App::draw_dot,
|
||||
Triangle => |this, x, y| this.draw_ngon(x, y, 3, 30.0, 0.0),
|
||||
Square => |this, x, y| this.draw_ngon(x, y, 4, 30.0, 45.0),
|
||||
Circle => |this, x, y| this.draw_ngon(x, y, (30.0 * PI) as usize, 30.0, 0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/tex.rs
Normal file
54
src/tex.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use egui::*;
|
||||
use epaint::{ImageDelta, TextureManager};
|
||||
use micro_ndarray::Array;
|
||||
|
||||
use crate::App;
|
||||
|
||||
impl App {
|
||||
pub fn correct_tex_size(&mut self, texman: &mut TextureManager, window_size: [usize; 2]) {
|
||||
if self.image.size()[1..3] == window_size {
|
||||
return;
|
||||
}
|
||||
let mut new_image = Array::new_with([3, window_size[0], window_size[1]], 0xff);
|
||||
for (pos, pixel) in self.image.iter() {
|
||||
if let Some(px) = new_image.get_mut(pos) {
|
||||
*px = *pixel;
|
||||
}
|
||||
}
|
||||
self.image = new_image;
|
||||
|
||||
let cimg = ColorImage::from_rgb(
|
||||
self.image.size()[1..3].try_into().unwrap(),
|
||||
self.image.as_flattened(),
|
||||
);
|
||||
texman.free(self.tex);
|
||||
self.tex = texman.alloc(
|
||||
"canvas".to_owned(),
|
||||
ImageData::Color(Arc::new(cimg)),
|
||||
TextureOptions {
|
||||
magnification: TextureFilter::Nearest,
|
||||
minification: TextureFilter::Linear,
|
||||
},
|
||||
);
|
||||
self.image_to_texture(texman);
|
||||
}
|
||||
|
||||
pub fn image_to_texture(&mut self, texman: &mut TextureManager) {
|
||||
let cimg = ColorImage::from_rgb(
|
||||
self.image.size()[1..3].try_into().unwrap(),
|
||||
self.image.as_flattened(),
|
||||
);
|
||||
texman.set(
|
||||
self.tex,
|
||||
ImageDelta::full(
|
||||
cimg,
|
||||
TextureOptions {
|
||||
magnification: TextureFilter::Nearest,
|
||||
minification: TextureFilter::Linear,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue