From 1f8a2859f9cbbd13f7a9985be0ca6e1ac243e85d Mon Sep 17 00:00:00 2001 From: yeqing Date: Fri, 27 Feb 2026 22:29:17 +0800 Subject: [PATCH] feat: Implement eraser --- src/mscanvas.rs | 130 +++++++++++++++++++++++++------- src/paint.rs | 192 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 263 insertions(+), 59 deletions(-) diff --git a/src/mscanvas.rs b/src/mscanvas.rs index 24951a9..6edf543 100644 --- a/src/mscanvas.rs +++ b/src/mscanvas.rs @@ -40,16 +40,15 @@ pub struct MSCanvas { width: i32, height: i32, - // 原始像素数据:RGBA 格式 - // 长度 = width * height * 4 + /// 原始像素数据:RGBA 格式 + /// 长度 = width * height * 4 pixels: Vec, pixels_bak: Vec, - // 当前笔画颜色 + /// 当前笔画颜色 color: MSColor, - // brush 大小 - brush_radius: f32, + line_width: i32, } #[allow(unused)] @@ -61,7 +60,7 @@ impl MSCanvas { pixels: vec![255; (width * height * 4) as usize], pixels_bak: Vec::new(), color: MSColor::BLACK, - brush_radius: 0.5, + line_width: 1, } } @@ -69,13 +68,17 @@ impl MSCanvas { self.color = color; } - pub fn set_brush_radius(&mut self, brush_radius: f32) { - self.brush_radius = brush_radius; + pub fn set_line_width(&mut self, line_width: i32) { + self.line_width = line_width; } } #[allow(unused)] impl MSCanvas { + pub fn size(&self) -> (i32, i32) { + (self.width, self.height) + } + pub fn get_pixels(&self) -> Vec { self.pixels.clone() } @@ -157,15 +160,16 @@ impl MSCanvas { } } - pub fn draw_brush_at(&mut self, center: Point) { - if self.brush_radius < 1.0 { + pub fn brush_circle(&mut self, center: Point) { + if self.line_width <= 1 { self.draw_pixel_at(center); return; } - let square = self.brush_radius * self.brush_radius; - let r = self.brush_radius.floor() as i32; - for dy in -r..=r { - for dx in -r..=r { + let square = (self.line_width as f32) * (self.line_width as f32) / 4.0; + let l = self.line_width / 2; + let r = self.line_width - l; + for dy in -l..r { + for dx in -l..r { if (dx * dx + dy * dy) as f32 <= square { self.draw_pixel_at(Point::new(center.x + dx as f32, center.y + dy as f32)); } @@ -173,6 +177,20 @@ impl MSCanvas { } } + pub fn brush_square(&mut self, center: Point) { + if self.line_width <= 1 { + self.draw_pixel_at(center); + return; + } + let l = self.line_width / 2; + let r = self.line_width - l; + for dy in -l..r { + for dx in -l..r { + self.draw_pixel_at(Point::new(center.x + dx as f32, center.y + dy as f32)); + } + } + } + fn bresenham_line(&mut self, begin: Point, end: Point) -> Vec { let x1 = begin.x; let y1 = begin.y; @@ -259,10 +277,25 @@ impl MSCanvas { } } - pub fn draw_line_thick(&mut self, begin: Point, end: Point) { + pub fn draw_line_with_circle_brush(&mut self, begin: Point, end: Point) { + // let points = self.bresenham_line(begin, end); + // for point in points { + // self.draw_brush_at(point); + // } + self.draw_line_with(begin, end, MSCanvas::brush_circle); + } + + pub fn draw_line_with_square_brush(&mut self, begin: Point, end: Point) { + self.draw_line_with(begin, end, MSCanvas::brush_square); + } + + pub fn draw_line_with(&mut self, begin: Point, end: Point, brush_fn: F) + where + F: Fn(&mut MSCanvas, Point) -> (), + { let points = self.bresenham_line(begin, end); for point in points { - self.draw_brush_at(point); + brush_fn(self, point); } } @@ -387,22 +420,65 @@ impl MSCanvas { (iter_count, fill_count) } - pub fn stroke_rect(&mut self, x: f32, y: f32, width: f32, height: f32) { - self.draw_line_thick(Point::new(x, y), Point::new(x, y + height)); - self.draw_line_thick(Point::new(x, y + height), Point::new(x + width, y + height)); - self.draw_line_thick(Point::new(x + width, y + height), Point::new(x + width, y)); - self.draw_line_thick(Point::new(x, y), Point::new(x + width, y)); + pub fn clear_rect(&mut self, x: i32, y: i32, width: i32, height: i32) { + for yi in y..(y + height) { + self.clear_row(x, x + width, yi); + } } - pub fn stroke_rect1(&mut self, left_top: Point, right_bottom: Point) { - let x = left_top.x; - let y = left_top.y; - let width = (right_bottom.x - left_top.x); - let height = (right_bottom.y - left_top.y); - self.stroke_rect(x, y, width, height); + pub fn fill_rect(&mut self, x: i32, y: i32, width: i32, height: i32) { + for yi in y..(y + height) { + self.draw_pixel_row(x, x + width, yi); + } + } + + pub fn stroke_rect(&mut self, x: i32, y: i32, width: i32, height: i32) { + 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); + self.fill_rect(x + width - self.line_width, y, self.line_width, height); + } + + pub fn stroke_rect1(&mut self, p1: Point, p2: Point) { + let mut x = p1.x; + let mut y = p1.y; + let mut width = (p2.x - p1.x); + let mut height = (p2.y - p1.y); + if width < 0.0 && height < 0.0 { + x = p2.x; + y = p2.y; + width = (p1.x - p2.x); + height = (p1.y - p2.y); + } else if width < 0.0 { + x += width; + width = -width; + } else if height < 0.0 { + y += height; + height = -height; + } + self.stroke_rect(x as i32, y as i32, width as i32, height as i32); } pub fn clear(&mut self) { self.pixels.fill(255); } + + pub fn clear_row(&mut self, xs: i32, xe: i32, y: i32) { + if y < 0 || y >= self.height { + return; + } + + let xs = xs.clamp(0, self.width - 1); + let xe = xe.clamp(0, self.width as i32); + for x in xs..xe { + let index = ((y * self.width + x) * 4) as usize; + + // 写入 RGBA 数据 + // 注意:Color 的 r, g, b, a 是 0.0 - 1.0,需要转为 0 - 255 + self.pixels[index] = 255; // R + self.pixels[index + 1] = 255; // G + self.pixels[index + 2] = 255; // B + self.pixels[index + 3] = 255; // A + } + } } diff --git a/src/paint.rs b/src/paint.rs index 1584a46..f86740d 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -3,7 +3,7 @@ use crate::mouse_area::mouse_area; use iced::Theme; use iced::padding; use iced::widget::container; -use iced::widget::{Column, button, column, image, row, text}; +use iced::widget::{Column, button, column, image, pick_list, row, text}; use iced::{Border, Color, Element, Length, Point, Renderer, Task}; use iced_core::color; @@ -12,8 +12,41 @@ use crate::mscanvas::MSCanvas; const WIDTH: u32 = 800; const HEIGHT: u32 = 600; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum BrushKind { + #[default] + Circle, + Square, + Slash, + Backslash, +} + +impl BrushKind { + const ALL: [BrushKind; 4] = [ + BrushKind::Circle, + BrushKind::Square, + BrushKind::Slash, + BrushKind::Backslash, + ]; +} + +impl std::fmt::Display for BrushKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + BrushKind::Circle => "Circle", + BrushKind::Square => "Square", + BrushKind::Slash => "Slash", + BrushKind::Backslash => "Backslash", + } + ) + } +} + #[derive(Debug, Clone, Copy)] -pub enum Message { +enum Message { MousePressed(Point), MouseReleased(Point), MouseMoved(Point), @@ -23,12 +56,14 @@ pub enum Message { ClickTool(Tool), - Increment, - Decrement, + Increment(ConfigOption), + Decrement(ConfigOption), + + BrushSelected(BrushKind), } #[derive(Clone, Copy, Debug)] -pub enum Tool { +enum Tool { FreeFormSelect, Select, Eraser, @@ -97,6 +132,47 @@ impl From for usize { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ConfigOption { + EraserWidth, + LineWidth, +} + +#[derive(Debug, Clone, Copy)] +struct Config { + eraser_width: i32, + line_width: i32, +} + +impl Default for Config { + fn default() -> Self { + Self::DEFAULT.clone() + } +} + +impl Config { + const DEFAULT: Config = Config { + line_width: 1, + eraser_width: 4, + }; + fn incr(&mut self, option: ConfigOption, step: i32) { + match option { + ConfigOption::EraserWidth => { + self.eraser_width += step; + if self.eraser_width < Self::DEFAULT.eraser_width { + self.eraser_width = Self::DEFAULT.eraser_width; + } + } + ConfigOption::LineWidth => { + self.line_width += step; + if self.line_width < Self::DEFAULT.line_width { + self.line_width = Self::DEFAULT.line_width; + } + } + } + } +} + struct PaintApp { tool_states: [bool; Tool::Count as usize], tool_selected: Tool, @@ -115,34 +191,39 @@ struct PaintApp { // 标记像素是否被修改,用于优化图像句柄的生成 dirty: bool, - value: i32, + config: Config, + + brush_selected: Option, } impl PaintApp { // region iced application (new view update) pub fn new() -> Self { - let canvas = MSCanvas::new(WIDTH as i32, HEIGHT as i32); + let mut canvas = MSCanvas::new(WIDTH as i32, HEIGHT as i32); + let (width, height) = canvas.size(); let pixels = canvas.get_pixels(); + let config = Config::default(); + canvas.set_line_width(config.line_width); Self { tool_states: [false; Tool::Count as usize], tool_selected: Tool::Count, canvas, is_drawing: false, begin_point: Point::ORIGIN, - image_handle: image::Handle::from_rgba(WIDTH, HEIGHT, pixels), + image_handle: image::Handle::from_rgba(width as u32, height as u32, pixels), dirty: false, - - value: 1, + config, + brush_selected: None, } } pub fn view(&self) -> Column<'_, Message> { // 创建显示图像的 Widget - // 如果 handle 还没准备好,显示一个占位符 + let (width, height) = self.canvas.size(); let image_widget = image(self.image_handle.clone()) - .width(Length::Fixed(WIDTH as f32)) - .height(Length::Fixed(HEIGHT as f32)); + .width(Length::Fixed(width as f32)) + .height(Length::Fixed(height as f32)); let canvas_area = mouse_area(image_widget) .on_press(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义 @@ -207,24 +288,39 @@ impl PaintApp { } }); - let debug_area = column![ - text("brush"), + let mut debug_area = column![ + text("Eraser Width"), row![ - button("+").on_press(Message::Increment), - text(self.value), - button("-").on_press(Message::Decrement), - ] + 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)), + ], + pick_list( + &BrushKind::ALL[..], + self.brush_selected, + Message::BrushSelected, + ) + .placeholder("Brush..."), ]; + debug_area = debug_area.padding(padding::left(5).right(5)); column![ button("CLEAR").on_press(Message::Clear), - row![tool_area, canvas_area], - debug_area, + row![tool_area, canvas_area, debug_area], ] } pub fn update(&mut self, message: Message) -> Task { match self.tool_selected { + Tool::Eraser => { + self.update_with_eraser(message); + } Tool::FillWithColor => { self.update_with_fill_with_color(message); } @@ -255,16 +351,17 @@ impl PaintApp { Message::ClickTool(tool) => { self.update_tool_states(tool); } - Message::Increment => { - self.value = self.value + 1; - self.canvas.set_brush_radius(self.value as f32 / 2.0); - } - Message::Decrement => { - self.value = self.value - 1; - if self.value < 1 { - self.value = 1; + Message::Increment(opt) => { + self.config.incr(opt, 1); + if opt == ConfigOption::LineWidth { + self.canvas.set_line_width(self.config.line_width); + } + } + Message::Decrement(opt) => { + self.config.incr(opt, 1); + if opt == ConfigOption::LineWidth { + self.canvas.set_line_width(self.config.line_width); } - self.canvas.set_brush_radius(self.value as f32 / 2.0); } _ => {} } @@ -284,6 +381,36 @@ impl PaintApp { // region tool update + pub fn update_with_eraser(&mut self, message: Message) { + match message { + Message::MousePressed(pos) => { + self.is_drawing = true; + self.canvas.clear_rect( + (pos.x as i32) - self.config.eraser_width / 2, + (pos.y as i32) - self.config.eraser_width / 2, + self.config.eraser_width, + self.config.eraser_width, + ); + self.dirty = true; + } + Message::MouseReleased(_) => { + self.is_drawing = false; + } + Message::MouseMoved(pos) => { + if self.is_drawing { + self.canvas.clear_rect( + (pos.x as i32) - self.config.eraser_width / 2, + (pos.y as i32) - self.config.eraser_width / 2, + self.config.eraser_width, + self.config.eraser_width, + ); + self.dirty = true; + } + } + _ => {} + } + } + pub fn update_with_fill_with_color(&mut self, message: Message) { match message { Message::MousePressed(pos) => { @@ -320,7 +447,7 @@ impl PaintApp { match message { Message::MousePressed(pos) => { self.is_drawing = true; - self.canvas.draw_brush_at(pos); + self.canvas.brush_circle(pos); self.canvas.save_pixels(); self.begin_point = pos; self.dirty = true; @@ -332,7 +459,8 @@ impl PaintApp { Message::MouseMoved(pos) => { if self.is_drawing { self.canvas.restore_pixels(); - self.canvas.draw_line_thick(self.begin_point, pos); + self.canvas + .draw_line_with_circle_brush(self.begin_point, pos); self.dirty = true; } }