Files
mspaint/src/paint.rs
2026-02-28 16:17:17 +08:00

809 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use ::image::{ImageBuffer, ImageError, Rgba};
use iced::Theme;
use iced::padding;
use iced::time::{self, Instant, milliseconds};
use iced::widget::container;
use iced::widget::{Column, button, column, image, pick_list, row, text};
use iced::{Border, Color, Element, Length, Point, Renderer, Task};
use iced::{Subscription, color, event, mouse};
use std::thread;
use crate::image_button::image_button;
use crate::mouse_area::mouse_area;
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)]
enum Message {
MousePressed(Point),
MouseReleased(Point),
MouseMoved(Point),
Clear,
SavePNG,
/// 内部消息:请求刷新图像
RefreshImage,
ClickTool(Tool),
Increment(ConfigOption),
Decrement(ConfigOption),
BrushSelected(BrushKind),
/// 全局鼠标释放
WindowMouseRelease,
Tick(Instant),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Tool {
FreeFormSelect,
Select,
Eraser,
FillWithColor,
PickColor,
Magnifier,
Pencil,
Brush,
Airbrush,
Text,
Line,
Curve,
Rectangle,
Polygon,
Ellipse,
RoundedRectangle,
Count,
}
impl From<usize> for Tool {
fn from(tool: usize) -> Self {
match tool {
0 => Self::FreeFormSelect,
1 => Self::Select,
2 => Self::Eraser,
3 => Self::FillWithColor,
4 => Self::PickColor,
5 => Self::Magnifier,
6 => Self::Pencil,
7 => Self::Brush,
8 => Self::Airbrush,
9 => Self::Text,
10 => Self::Line,
11 => Self::Curve,
12 => Self::Rectangle,
13 => Self::Polygon,
14 => Self::Ellipse,
15 => Self::RoundedRectangle,
_ => Self::Count,
}
}
}
impl From<Tool> for usize {
fn from(tool: Tool) -> usize {
match tool {
Tool::FreeFormSelect => 0,
Tool::Select => 1,
Tool::Eraser => 2,
Tool::FillWithColor => 3,
Tool::PickColor => 4,
Tool::Magnifier => 5,
Tool::Pencil => 6,
Tool::Brush => 7,
Tool::Airbrush => 8,
Tool::Text => 9,
Tool::Line => 10,
Tool::Curve => 11,
Tool::Rectangle => 12,
Tool::Polygon => 13,
Tool::Ellipse => 14,
Tool::RoundedRectangle => 15,
Tool::Count => 16,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConfigOption {
EraserWidth,
LineWidth,
AirbrushRadius,
AirbrushDensity,
}
#[derive(Debug, Clone, Copy)]
struct Config {
eraser_width: i32,
line_width: i32,
airbrush_radius: i32,
airbrush_density: i32,
}
impl Default for Config {
fn default() -> Self {
Self::DEFAULT.clone()
}
}
impl Config {
const DEFAULT: Config = Config {
line_width: 1,
eraser_width: 4,
airbrush_radius: 4,
airbrush_density: 16,
};
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;
}
}
ConfigOption::AirbrushRadius => {
self.airbrush_radius += step;
if self.airbrush_radius < Self::DEFAULT.airbrush_radius {
self.airbrush_radius = Self::DEFAULT.airbrush_radius;
}
}
ConfigOption::AirbrushDensity => {
self.airbrush_density += step;
if self.airbrush_density < Self::DEFAULT.airbrush_density {
self.airbrush_density = Self::DEFAULT.airbrush_density;
}
}
}
}
}
struct PaintApp {
tool_states: [bool; Tool::Count as usize],
tool_selected: Tool,
canvas: MSCanvas,
/// 是否正在绘制
is_drawing: bool,
/// pencil brush line curve rectangle
begin_point: Point,
/// curve
end_point: Point,
/// 贝塞尔曲线控制
is_controlling: bool,
control_points: Vec<Point>,
/// 用于显示的图像句柄缓存
/// 每次像素变化后需要重新生成
image_handle: image::Handle,
/// 标记像素是否被修改,用于优化图像句柄的生成
dirty: bool,
config: Config,
brush_selected: Option<BrushKind>,
}
impl PaintApp {
// region iced application
pub fn new() -> Self {
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,
end_point: Point::ORIGIN,
is_controlling: false,
control_points: Vec::with_capacity(2),
image_handle: image::Handle::from_rgba(width as u32, height as u32, pixels),
dirty: false,
config,
brush_selected: None,
}
}
pub fn view(&self) -> Column<'_, Message> {
// 创建显示图像的 Widget
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));
let canvas_area = mouse_area(image_widget)
.on_press(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义
.on_release(|pos| Message::MouseReleased(pos))
.on_move(|pos| Message::MouseMoved(pos));
// 注意mouse_area 的 on_move 给出的坐标通常是相对于 widget 左上角的,这正是我们需要的!
let canvas_area = container(canvas_area)
.width(Length::Fill)
.height(Length::Fill)
.padding(padding::left(5).top(5).right(5))
.style(|_| container::Style {
background: Some(color!(0x808080).into()),
..container::Style::default()
});
let mut columns: Vec<Element<'_, Message, Theme, Renderer>> = Vec::new();
for i in (0..(Tool::Count as usize)).step_by(2) {
let btn1 = image_button(
format!("image/normal/normal_{:02}.jpg", i + 1),
format!("image/selected/selected_{:02}.jpg", i + 1),
self.tool_states[i],
)
.on_press(Message::ClickTool(Tool::from(i)));
let btn2 = image_button(
format!("image/normal/normal_{:02}.jpg", i + 2),
format!("image/selected/selected_{:02}.jpg", i + 2),
self.tool_states[i + 1],
)
.on_press(Message::ClickTool(Tool::from(i + 1)));
columns.push(row![btn1, btn2].into());
}
let tool_config_area = container("").width(90).height(200).style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
background: Some(Color::from_rgb8(192, 192, 192).into()),
border: Border {
width: 1.0,
radius: 5.0.into(),
color: palette.background.weak.color,
},
..container::Style::default()
}
});
let tool_config_area = container(tool_config_area).padding(padding::top(5).left(5));
columns.push(tool_config_area.into());
let grid = Column::from_vec(columns);
let tool_area = container(grid)
.padding(padding::top(5).left(5).right(5).bottom(10))
.style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
background: Some(Color::from_rgb8(192, 192, 192).into()),
border: Border {
width: 1.0,
radius: 5.0.into(),
color: palette.background.weak.color,
},
..container::Style::default()
}
});
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)),
],
]
.padding(padding::right(5)),
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)),
],
]
.padding(padding::right(5)),
pick_list(
&BrushKind::ALL[..],
self.brush_selected,
Message::BrushSelected,
)
.placeholder("Brush..."),
column![
text("Airbrush Radius"),
row![
button("+").on_press(Message::Increment(ConfigOption::AirbrushRadius)),
text(self.config.airbrush_radius).size(20).center(),
button("-").on_press(Message::Decrement(ConfigOption::AirbrushRadius)),
],
]
.padding(padding::right(5)),
column![
text("Airbrush Density"),
row![
button("+").on_press(Message::Increment(ConfigOption::AirbrushDensity)),
text(self.config.airbrush_density).size(20).center(),
button("-").on_press(Message::Decrement(ConfigOption::AirbrushDensity)),
],
]
.padding(padding::right(5)),
];
debug_area = debug_area.padding(padding::top(10).left(5).bottom(10));
column![
row![
button("CLEAR").on_press(Message::Clear),
button("PNG").on_press(Message::SavePNG),
],
row![tool_area, canvas_area],
debug_area,
]
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match self.tool_selected {
Tool::Eraser => {
self.update_with_eraser(message);
}
Tool::FillWithColor => {
self.update_with_fill_with_color(message);
}
Tool::Pencil => {
self.update_with_pencil(message);
}
Tool::Brush => {
self.update_with_brush(message);
}
Tool::Airbrush => {
self.update_with_airbrush(message);
}
Tool::Line => {
self.update_with_line(message);
}
Tool::Curve => {
self.update_with_curve(message);
}
Tool::Rectangle => {
self.update_with_rectangle(message);
}
_ => {}
}
match message {
Message::Clear => {
// 重置为白色
self.canvas.clear();
self.dirty = true;
}
Message::SavePNG => {
let (width, height) = self.canvas.size();
let pixels = self.canvas.get_pixels();
let path = "mspaint.png";
save_pixels_async(pixels, width as u32, height as u32, 4, path.to_string());
}
Message::RefreshImage => {
if self.dirty {
self.update_image_handle();
self.dirty = false;
}
}
Message::ClickTool(tool) => {
self.update_tool_states(tool);
}
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);
}
}
Message::BrushSelected(kind) => {
self.brush_selected = Some(kind);
}
Message::WindowMouseRelease => {
// 处理鼠标在 canvas_area 外面释放
self.is_drawing = false;
}
Message::Tick(_) => {
if self.is_drawing && self.tool_selected == Tool::Airbrush {
self.canvas.spray_paint(
self.begin_point,
self.config.airbrush_radius,
self.config.airbrush_density as u32,
);
self.dirty = true;
}
}
_ => {}
}
// 如果像素被修改了,我们需要触发一次 RefreshImage 来更新 UI
// 在实际复杂应用中,可能需要防抖或异步处理,这里为了实时性直接同步触发
if self.dirty {
// 像素变了,安排下一帧刷新图像句柄
// 注意:频繁生成 Handle 可能消耗 CPU生产环境建议加节流
return Task::perform(async { Message::RefreshImage }, |msg| msg);
}
Task::none()
}
pub fn subscription(&self) -> Subscription<Message> {
let ev = event::listen().filter_map(|event| match event {
// 只关心鼠标释放事件
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
Some(Message::WindowMouseRelease)
}
_ => None,
});
let tick = time::every(milliseconds(50)).map(Message::Tick);
Subscription::batch(vec![ev, tick])
}
// endregion
// 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) => {
self.canvas.fill_scanline(pos);
self.dirty = true;
}
_ => {}
}
}
pub fn update_with_pencil(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
self.is_drawing = true;
self.canvas.draw_pixel_at(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(self.begin_point, pos);
self.begin_point = pos;
self.dirty = true;
}
}
_ => {}
}
}
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_airbrush(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
self.is_drawing = true;
self.canvas.spray_paint(
pos,
self.config.airbrush_radius,
self.config.airbrush_density as u32,
);
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.spray_paint(
pos,
self.config.airbrush_radius,
self.config.airbrush_density as u32,
);
self.begin_point = pos;
self.dirty = true;
}
}
_ => {}
}
}
pub fn update_with_line(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
self.is_drawing = true;
self.canvas.brush_circle(pos);
self.canvas.save_pixels();
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.restore_pixels();
self.canvas
.draw_line_with_circle_brush(self.begin_point, pos);
self.dirty = true;
}
}
_ => {}
}
}
pub fn update_with_curve(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
if self.is_controlling {
if self.control_points.len() == 0 {
self.canvas.restore_pixels();
self.canvas
.quadratic_bezier(self.begin_point, self.end_point, pos);
self.control_points.push(pos);
} else {
self.canvas.restore_pixels();
self.canvas.cubic_bezier(
self.begin_point,
self.end_point,
self.control_points[0],
pos,
);
self.control_points.push(pos);
}
self.dirty = true;
} else {
self.is_drawing = true;
self.canvas.save_pixels();
self.begin_point = pos;
}
}
Message::MouseReleased(pos) => {
if self.control_points.len() == 0 {
self.is_drawing = false;
self.end_point = pos;
self.is_controlling = true;
} else if self.control_points.len() == 2 {
self.control_points.clear();
self.is_controlling = false;
}
}
Message::MouseMoved(pos) => {
if self.is_drawing {
self.canvas.restore_pixels();
self.canvas
.draw_line_with_circle_brush(self.begin_point, pos);
self.dirty = true;
}
}
_ => {}
}
}
pub fn update_with_rectangle(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
self.is_drawing = true;
self.canvas.save_pixels();
self.begin_point = pos;
}
Message::MouseReleased(pos) => {
self.is_drawing = false;
self.begin_point = pos;
}
Message::MouseMoved(pos) => {
if self.is_drawing {
self.canvas.restore_pixels();
self.canvas.stroke_rect1(self.begin_point, pos);
self.dirty = true;
}
}
_ => {}
}
}
// endregion
pub fn update_tool_states(&mut self, tool: Tool) {
let idx = tool as usize;
if idx >= self.tool_states.len() {
return;
}
let old_value = self.tool_states[idx];
for i in 0..(Tool::Count as usize) {
self.tool_states[i] = false;
}
self.tool_states[idx] = !old_value;
self.tool_selected = idx.into();
}
/// 将原始字节转换为 Iced 的图像句柄
fn update_image_handle(&mut self) {
// 克隆数据以避免所有权问题,或者使用 Arc 如果数据量大
// 这里为了简单直接 clone对于 800x600 (约 2MB) 来说很快
let data = self.canvas.get_pixels();
self.image_handle = image::Handle::from_rgba(WIDTH, HEIGHT, data);
}
}
pub fn main() -> iced::Result {
iced::application(PaintApp::new, PaintApp::update, PaintApp::view)
.theme(Theme::Dark)
.subscription(PaintApp::subscription)
.run()
}
fn save_rgba_to_png(
rgba_data: Vec<u8>,
width: u32,
height: u32,
path: &str,
) -> Result<(), ImageError> {
// Each pixel is 4 bytes (R, G, B, A)
let img: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::from_raw(width, height, rgba_data)
.expect("Failed to create ImageBuffer: data size mismatch");
img.save(path)?;
Ok(())
}
fn scale_pixels(pixels: Vec<u8>, width: u32, height: u32, scale: u32) -> Vec<u8> {
if scale <= 1 {
return pixels;
}
let dst_width = width * scale;
let dst_height = height * scale;
let mut dst = vec![0; (dst_width * dst_height * 4) as usize]; // RGBA
for y in 0..height {
for x in 0..width {
// 源像素索引
let src_idx = ((y * width + x) * 4) as usize;
// 源像素颜色
let r = pixels[src_idx];
let g = pixels[src_idx + 1];
let b = pixels[src_idx + 2];
let a = 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
}
fn save_pixels_async(pixels: Vec<u8>, width: u32, height: u32, scale: u32, path: String) {
// 用 Arc 共享数据(或直接 move 进去)
thread::spawn(move || {
let pixels = scale_pixels(pixels, width, height, scale);
match save_rgba_to_png(pixels, width, height, &path) {
Ok(()) => println!("✅ Image saved to {}", path),
Err(e) => eprintln!("❌ Failed to save image: {}", e),
}
});
}