Initial commit

This commit is contained in:
Tove 2023-11-02 21:18:38 +01:00
commit 34e989adbf
Signed by: TudbuT
GPG key ID: B3CF345217F202D3
9 changed files with 3579 additions and 0 deletions

3109
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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,
},
),
);
}
}