feat: Implement crush and save to png
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1953,6 +1953,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"iced",
|
||||
"iced_core",
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -6,3 +6,4 @@ edition = "2024"
|
||||
[dependencies]
|
||||
iced = {version = "0.14.0", features = ["advanced", "image"]}
|
||||
iced_core = "0.14.0"
|
||||
image = "0.25.9"
|
||||
|
||||
@@ -83,6 +83,44 @@ impl MSCanvas {
|
||||
self.pixels.clone()
|
||||
}
|
||||
|
||||
pub fn get_pixels_scale(&self, scale: i32) -> Vec<u8> {
|
||||
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<Point> {
|
||||
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);
|
||||
|
||||
100
src/paint.rs
100
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,19 +291,23 @@ impl PaintApp {
|
||||
}
|
||||
});
|
||||
|
||||
let mut debug_area = column![
|
||||
text("Eraser Width"),
|
||||
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"),
|
||||
],
|
||||
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[..],
|
||||
self.brush_selected,
|
||||
@@ -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![
|
||||
row![
|
||||
button("CLEAR").on_press(Message::Clear),
|
||||
row![tool_area, canvas_area, debug_area],
|
||||
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<u8>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
path: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 验证像素数量是否匹配
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user