From 6f6e6f326b66f7c04a6fe8bf3441006c96b620ee Mon Sep 17 00:00:00 2001 From: yeqing Date: Sat, 28 Feb 2026 01:09:51 +0800 Subject: [PATCH] feat: Implement crush and save to png --- Cargo.lock | 1 + Cargo.toml | 1 + src/mscanvas.rs | 94 ++++++++++++++++++++++++++++++++++++++ src/paint.rs | 118 ++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 196 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41360f6..df3c876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1953,6 +1953,7 @@ version = "0.1.0" dependencies = [ "iced", "iced_core", + "image", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 401b81d..0553841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] iced = {version = "0.14.0", features = ["advanced", "image"]} iced_core = "0.14.0" +image = "0.25.9" diff --git a/src/mscanvas.rs b/src/mscanvas.rs index 6edf543..2c578cc 100644 --- a/src/mscanvas.rs +++ b/src/mscanvas.rs @@ -83,6 +83,44 @@ impl MSCanvas { self.pixels.clone() } + pub fn get_pixels_scale(&self, scale: i32) -> Vec { + if scale <= 1 { + return self.pixels.clone(); + } + let dst_width = self.width * scale; + let dst_height = self.width * scale; + let mut dst = vec![0; (self.width * self.width * 4) as usize]; // RGBA + + for y in 0..self.height { + for x in 0..self.width { + // 源像素索引 + let src_idx = ((y * self.width + x) * 4) as usize; + + // 源像素颜色 + let r = self.pixels[src_idx]; + let g = self.pixels[src_idx + 1]; + let b = self.pixels[src_idx + 2]; + let a = self.pixels[src_idx + 3]; + + // 在目标图像中填充 scale×scale 区域 + for dy in 0..scale { + for dx in 0..scale { + let dst_x = x * scale + dx; + let dst_y = y * scale + dy; + let dst_idx = ((dst_y * dst_width + dst_x) * 4) as usize; + + dst[dst_idx] = r; + dst[dst_idx + 1] = g; + dst[dst_idx + 2] = b; + dst[dst_idx + 3] = a; + } + } + } + } + + dst + } + pub fn pixel_at(&self, x: i32, y: i32) -> MSColor { // 边界检查 if x < 0 || x >= self.width || y < 0 || y >= self.height as i32 { @@ -118,6 +156,25 @@ impl MSCanvas { self.pixels[index + 3] = self.color.a; // A } + pub fn draw_pixel_color_at(&mut self, point: Point, color: MSColor) { + let Point { x, y } = point; + let x = x as i32; + let y = y as i32; + // 边界检查 + if x < 0 || x >= self.width || y < 0 || y >= self.height { + return; + } + // 计算索引:(y * width + x) * 4 + let index = ((y * self.width + x) * 4) as usize; + + // 写入 RGBA 数据 + // 注意:Color 的 r, g, b, a 是 0.0 - 1.0,需要转为 0 - 255 + self.pixels[index] = color.r; // R + self.pixels[index + 1] = color.g; // G + self.pixels[index + 2] = color.b; // B + self.pixels[index + 3] = color.a; // A + } + fn draw_pixel_at1(&mut self, x: i32, y: i32) { // 边界检查 if x < 0 || x >= self.width || y < 0 || y >= self.height { @@ -160,6 +217,17 @@ impl MSCanvas { } } + pub fn draw_cross_color(&mut self, point: Point, color: MSColor) { + let Point { x, y } = point; + let r = 5; + for dy in -r..=r { + self.draw_pixel_color_at(Point::new(point.x, point.y + dy as f32), color); + } + for dx in -r..=r { + self.draw_pixel_color_at(Point::new(point.x + dx as f32, point.y), color); + } + } + pub fn brush_circle(&mut self, center: Point) { if self.line_width <= 1 { self.draw_pixel_at(center); @@ -191,6 +259,27 @@ impl MSCanvas { } } + pub fn brush_slash(&mut self, center: Point) { + let r = self.line_width / 2; + let l = self.line_width - r; + // for dx in -1..1 { + // for d in -l..r { + // self.draw_pixel_at(Point::new(center.x - (d as f32) + (dx as f32), center.y - d as f32)); + // } + // } + for d in -l..r { + self.draw_pixel_at(Point::new(center.x - (d as f32), center.y - d as f32)); + } + } + + pub fn brush_backslash(&mut self, center: Point) { + let r = self.line_width / 2; + let l = self.line_width - r; + for d in -l..r { + self.draw_pixel_at(Point::new(center.x + d as f32, center.y - d as f32)); + } + } + fn bresenham_line(&mut self, begin: Point, end: Point) -> Vec { let x1 = begin.x; let y1 = begin.y; @@ -433,6 +522,10 @@ impl MSCanvas { } pub fn stroke_rect(&mut self, x: i32, y: i32, width: i32, height: i32) { + if width < 2 * self.line_width || height < 2 * self.line_width { + self.fill_rect(x, y, width, height); + return; + } self.fill_rect(x, y, width, self.line_width); self.fill_rect(x, y + height - self.line_width, width, self.line_width); self.fill_rect(x, y, self.line_width, height); @@ -440,6 +533,7 @@ impl MSCanvas { } pub fn stroke_rect1(&mut self, p1: Point, p2: Point) { + self.draw_cross_color(p1, MSColor::new(255, 0, 0, 255)); let mut x = p1.x; let mut y = p1.y; let mut width = (p2.x - p1.x); diff --git a/src/paint.rs b/src/paint.rs index f86740d..d3815b8 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -1,5 +1,4 @@ -use crate::image_button::image_button; -use crate::mouse_area::mouse_area; +use ::image::RgbaImage; use iced::Theme; use iced::padding; use iced::widget::container; @@ -7,6 +6,8 @@ use iced::widget::{Column, button, column, image, pick_list, row, text}; use iced::{Border, Color, Element, Length, Point, Renderer, Task}; use iced_core::color; +use crate::image_button::image_button; +use crate::mouse_area::mouse_area; use crate::mscanvas::MSCanvas; const WIDTH: u32 = 800; @@ -50,7 +51,9 @@ enum Message { MousePressed(Point), MouseReleased(Point), MouseMoved(Point), + Clear, + SavePNG, // 内部消息:请求刷新图像 RefreshImage, @@ -233,7 +236,7 @@ impl PaintApp { let canvas_area = container(canvas_area) .width(Length::Fill) .height(Length::Fill) - .padding(padding::left(5).top(5)) + .padding(padding::left(5).top(5).right(5)) .style(|_| container::Style { background: Some(color!(0x808080).into()), ..container::Style::default() @@ -288,18 +291,22 @@ impl PaintApp { } }); - let mut debug_area = column![ - text("Eraser Width"), - row![ - button("+").on_press(Message::Increment(ConfigOption::EraserWidth)), - text(self.config.eraser_width).size(20).center(), - button("-").on_press(Message::Decrement(ConfigOption::EraserWidth)), + let mut debug_area = row![ + column![ + text("Eraser Width "), + row![ + button("+").on_press(Message::Increment(ConfigOption::EraserWidth)), + text(self.config.eraser_width).size(20).center(), + button("-").on_press(Message::Decrement(ConfigOption::EraserWidth)), + ], ], - text("Line Width"), - row![ - button("+").on_press(Message::Increment(ConfigOption::LineWidth)), - text(self.config.line_width).size(20).center(), - button("-").on_press(Message::Decrement(ConfigOption::LineWidth)), + column![ + text("Line Width "), + row![ + button("+").on_press(Message::Increment(ConfigOption::LineWidth)), + text(self.config.line_width).size(20).center(), + button("-").on_press(Message::Decrement(ConfigOption::LineWidth)), + ], ], pick_list( &BrushKind::ALL[..], @@ -308,11 +315,15 @@ impl PaintApp { ) .placeholder("Brush..."), ]; - debug_area = debug_area.padding(padding::left(5).right(5)); + debug_area = debug_area.padding(padding::top(10).left(5).bottom(10)); column![ - button("CLEAR").on_press(Message::Clear), - row![tool_area, canvas_area, debug_area], + row![ + button("CLEAR").on_press(Message::Clear), + button("PNG").on_press(Message::SavePNG), + ], + row![tool_area, canvas_area], + debug_area, ] } @@ -327,6 +338,9 @@ impl PaintApp { Tool::Pencil => { self.update_with_pencil(message); } + Tool::Brush => { + self.update_with_brush(message); + } Tool::Line => { self.update_with_line(message); } @@ -342,6 +356,18 @@ impl PaintApp { self.canvas.clear(); self.dirty = true; } + Message::SavePNG => { + let scale = 4; + let (width, height) = self.canvas.size(); + let pixels = self.canvas.get_pixels_scale(4); + save_rgba_as_png( + pixels, + (width * scale) as u32, + (height * scale) as u32, + "mspaint.png", + ) + .unwrap(); + } Message::RefreshImage => { if self.dirty { self.update_image_handle(); @@ -358,11 +384,14 @@ impl PaintApp { } } Message::Decrement(opt) => { - self.config.incr(opt, 1); + self.config.incr(opt, -1); if opt == ConfigOption::LineWidth { self.canvas.set_line_width(self.config.line_width); } } + Message::BrushSelected(kind) => { + self.brush_selected = Some(kind); + } _ => {} } @@ -443,6 +472,38 @@ impl PaintApp { } } + pub fn update_with_brush(&mut self, message: Message) { + let mut brush_fn: fn(&mut MSCanvas, Point) = MSCanvas::brush_circle; + if let Some(kind) = self.brush_selected { + brush_fn = match kind { + BrushKind::Circle => MSCanvas::brush_circle, + BrushKind::Square => MSCanvas::brush_square, + BrushKind::Slash => MSCanvas::brush_slash, + BrushKind::Backslash => MSCanvas::brush_backslash, + } + } + match message { + Message::MousePressed(pos) => { + self.is_drawing = true; + brush_fn(&mut self.canvas, pos); + self.begin_point = pos; + self.dirty = true + } + Message::MouseReleased(pos) => { + self.is_drawing = false; + self.begin_point = pos; + } + Message::MouseMoved(pos) => { + if self.is_drawing { + self.canvas.draw_line_with(self.begin_point, pos, brush_fn); + self.begin_point = pos; + self.dirty = true; + } + } + _ => {} + } + } + pub fn update_with_line(&mut self, message: Message) { match message { Message::MousePressed(pos) => { @@ -520,3 +581,24 @@ pub fn main() -> iced::Result { .theme(Theme::Dark) .run() } + +fn save_rgba_as_png( + pixels: Vec, + width: u32, + height: u32, + path: &str, +) -> Result<(), Box> { + // 验证像素数量是否匹配 + if pixels.len() != (width * height * 4) as usize { + return Err("Pixel buffer size does not match width × height × 4".into()); + } + + // 创建 RgbaImage + let img: RgbaImage = RgbaImage::from_vec(width, height, pixels) + .ok_or("Failed to create image from pixel data")?; + + // 保存为 PNG(保留 Alpha 通道) + img.save(path)?; + + Ok(()) +}