feat: Implement eraser

This commit is contained in:
2026-02-27 22:29:17 +08:00
parent da99cfc765
commit 1f8a2859f9
2 changed files with 263 additions and 59 deletions

View File

@@ -40,16 +40,15 @@ pub struct MSCanvas {
width: i32, width: i32,
height: i32, height: i32,
// 原始像素数据RGBA 格式 /// 原始像素数据RGBA 格式
// 长度 = width * height * 4 /// 长度 = width * height * 4
pixels: Vec<u8>, pixels: Vec<u8>,
pixels_bak: Vec<u8>, pixels_bak: Vec<u8>,
// 当前笔画颜色 /// 当前笔画颜色
color: MSColor, color: MSColor,
// brush 大小 line_width: i32,
brush_radius: f32,
} }
#[allow(unused)] #[allow(unused)]
@@ -61,7 +60,7 @@ impl MSCanvas {
pixels: vec![255; (width * height * 4) as usize], pixels: vec![255; (width * height * 4) as usize],
pixels_bak: Vec::new(), pixels_bak: Vec::new(),
color: MSColor::BLACK, color: MSColor::BLACK,
brush_radius: 0.5, line_width: 1,
} }
} }
@@ -69,13 +68,17 @@ impl MSCanvas {
self.color = color; self.color = color;
} }
pub fn set_brush_radius(&mut self, brush_radius: f32) { pub fn set_line_width(&mut self, line_width: i32) {
self.brush_radius = brush_radius; self.line_width = line_width;
} }
} }
#[allow(unused)] #[allow(unused)]
impl MSCanvas { impl MSCanvas {
pub fn size(&self) -> (i32, i32) {
(self.width, self.height)
}
pub fn get_pixels(&self) -> Vec<u8> { pub fn get_pixels(&self) -> Vec<u8> {
self.pixels.clone() self.pixels.clone()
} }
@@ -157,15 +160,16 @@ impl MSCanvas {
} }
} }
pub fn draw_brush_at(&mut self, center: Point) { pub fn brush_circle(&mut self, center: Point) {
if self.brush_radius < 1.0 { if self.line_width <= 1 {
self.draw_pixel_at(center); self.draw_pixel_at(center);
return; return;
} }
let square = self.brush_radius * self.brush_radius; let square = (self.line_width as f32) * (self.line_width as f32) / 4.0;
let r = self.brush_radius.floor() as i32; let l = self.line_width / 2;
for dy in -r..=r { let r = self.line_width - l;
for dx in -r..=r { for dy in -l..r {
for dx in -l..r {
if (dx * dx + dy * dy) as f32 <= square { if (dx * dx + dy * dy) as f32 <= square {
self.draw_pixel_at(Point::new(center.x + dx as f32, center.y + dy as f32)); 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<Point> { fn bresenham_line(&mut self, begin: Point, end: Point) -> Vec<Point> {
let x1 = begin.x; let x1 = begin.x;
let y1 = begin.y; 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<F>(&mut self, begin: Point, end: Point, brush_fn: F)
where
F: Fn(&mut MSCanvas, Point) -> (),
{
let points = self.bresenham_line(begin, end); let points = self.bresenham_line(begin, end);
for point in points { for point in points {
self.draw_brush_at(point); brush_fn(self, point);
} }
} }
@@ -387,22 +420,65 @@ impl MSCanvas {
(iter_count, fill_count) (iter_count, fill_count)
} }
pub fn stroke_rect(&mut self, x: f32, y: f32, width: f32, height: f32) { pub fn clear_rect(&mut self, x: i32, y: i32, width: i32, height: i32) {
self.draw_line_thick(Point::new(x, y), Point::new(x, y + height)); for yi in y..(y + height) {
self.draw_line_thick(Point::new(x, y + height), Point::new(x + width, y + height)); self.clear_row(x, x + width, yi);
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 stroke_rect1(&mut self, left_top: Point, right_bottom: Point) { pub fn fill_rect(&mut self, x: i32, y: i32, width: i32, height: i32) {
let x = left_top.x; for yi in y..(y + height) {
let y = left_top.y; self.draw_pixel_row(x, x + width, yi);
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 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) { pub fn clear(&mut self) {
self.pixels.fill(255); 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
}
}
} }

View File

@@ -3,7 +3,7 @@ use crate::mouse_area::mouse_area;
use iced::Theme; use iced::Theme;
use iced::padding; use iced::padding;
use iced::widget::container; 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::{Border, Color, Element, Length, Point, Renderer, Task};
use iced_core::color; use iced_core::color;
@@ -12,8 +12,41 @@ use crate::mscanvas::MSCanvas;
const WIDTH: u32 = 800; const WIDTH: u32 = 800;
const HEIGHT: u32 = 600; 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)] #[derive(Debug, Clone, Copy)]
pub enum Message { enum Message {
MousePressed(Point), MousePressed(Point),
MouseReleased(Point), MouseReleased(Point),
MouseMoved(Point), MouseMoved(Point),
@@ -23,12 +56,14 @@ pub enum Message {
ClickTool(Tool), ClickTool(Tool),
Increment, Increment(ConfigOption),
Decrement, Decrement(ConfigOption),
BrushSelected(BrushKind),
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Tool { enum Tool {
FreeFormSelect, FreeFormSelect,
Select, Select,
Eraser, Eraser,
@@ -97,6 +132,47 @@ impl From<Tool> 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 { struct PaintApp {
tool_states: [bool; Tool::Count as usize], tool_states: [bool; Tool::Count as usize],
tool_selected: Tool, tool_selected: Tool,
@@ -115,34 +191,39 @@ struct PaintApp {
// 标记像素是否被修改,用于优化图像句柄的生成 // 标记像素是否被修改,用于优化图像句柄的生成
dirty: bool, dirty: bool,
value: i32, config: Config,
brush_selected: Option<BrushKind>,
} }
impl PaintApp { impl PaintApp {
// region iced application (new view update) // region iced application (new view update)
pub fn new() -> Self { 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 pixels = canvas.get_pixels();
let config = Config::default();
canvas.set_line_width(config.line_width);
Self { Self {
tool_states: [false; Tool::Count as usize], tool_states: [false; Tool::Count as usize],
tool_selected: Tool::Count, tool_selected: Tool::Count,
canvas, canvas,
is_drawing: false, is_drawing: false,
begin_point: Point::ORIGIN, 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, dirty: false,
config,
value: 1, brush_selected: None,
} }
} }
pub fn view(&self) -> Column<'_, Message> { pub fn view(&self) -> Column<'_, Message> {
// 创建显示图像的 Widget // 创建显示图像的 Widget
// 如果 handle 还没准备好,显示一个占位符 let (width, height) = self.canvas.size();
let image_widget = image(self.image_handle.clone()) let image_widget = image(self.image_handle.clone())
.width(Length::Fixed(WIDTH as f32)) .width(Length::Fixed(width as f32))
.height(Length::Fixed(HEIGHT as f32)); .height(Length::Fixed(height as f32));
let canvas_area = mouse_area(image_widget) let canvas_area = mouse_area(image_widget)
.on_press(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义 .on_press(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义
@@ -207,24 +288,39 @@ impl PaintApp {
} }
}); });
let debug_area = column![ let mut debug_area = column![
text("brush"), text("Eraser Width"),
row![ row![
button("+").on_press(Message::Increment), button("+").on_press(Message::Increment(ConfigOption::EraserWidth)),
text(self.value), text(self.config.eraser_width).size(20).center(),
button("-").on_press(Message::Decrement), 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![ column![
button("CLEAR").on_press(Message::Clear), button("CLEAR").on_press(Message::Clear),
row![tool_area, canvas_area], row![tool_area, canvas_area, debug_area],
debug_area,
] ]
} }
pub fn update(&mut self, message: Message) -> Task<Message> { pub fn update(&mut self, message: Message) -> Task<Message> {
match self.tool_selected { match self.tool_selected {
Tool::Eraser => {
self.update_with_eraser(message);
}
Tool::FillWithColor => { Tool::FillWithColor => {
self.update_with_fill_with_color(message); self.update_with_fill_with_color(message);
} }
@@ -255,16 +351,17 @@ impl PaintApp {
Message::ClickTool(tool) => { Message::ClickTool(tool) => {
self.update_tool_states(tool); self.update_tool_states(tool);
} }
Message::Increment => { Message::Increment(opt) => {
self.value = self.value + 1; self.config.incr(opt, 1);
self.canvas.set_brush_radius(self.value as f32 / 2.0); if opt == ConfigOption::LineWidth {
self.canvas.set_line_width(self.config.line_width);
} }
Message::Decrement => {
self.value = self.value - 1;
if self.value < 1 {
self.value = 1;
} }
self.canvas.set_brush_radius(self.value as f32 / 2.0); Message::Decrement(opt) => {
self.config.incr(opt, 1);
if opt == ConfigOption::LineWidth {
self.canvas.set_line_width(self.config.line_width);
}
} }
_ => {} _ => {}
} }
@@ -284,6 +381,36 @@ impl PaintApp {
// region tool update // 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) { pub fn update_with_fill_with_color(&mut self, message: Message) {
match message { match message {
Message::MousePressed(pos) => { Message::MousePressed(pos) => {
@@ -320,7 +447,7 @@ impl PaintApp {
match message { match message {
Message::MousePressed(pos) => { Message::MousePressed(pos) => {
self.is_drawing = true; self.is_drawing = true;
self.canvas.draw_brush_at(pos); self.canvas.brush_circle(pos);
self.canvas.save_pixels(); self.canvas.save_pixels();
self.begin_point = pos; self.begin_point = pos;
self.dirty = true; self.dirty = true;
@@ -332,7 +459,8 @@ impl PaintApp {
Message::MouseMoved(pos) => { Message::MouseMoved(pos) => {
if self.is_drawing { if self.is_drawing {
self.canvas.restore_pixels(); 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; self.dirty = true;
} }
} }