Compare commits
2 Commits
ae095946f0
...
a9243c498f
| Author | SHA1 | Date | |
|---|---|---|---|
| a9243c498f | |||
| 3ed3c12f47 |
85
Cargo.lock
generated
85
Cargo.lock
generated
@@ -197,6 +197,17 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-fs"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
||||||
|
dependencies = [
|
||||||
|
"async-lock",
|
||||||
|
"blocking",
|
||||||
|
"futures-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-io"
|
name = "async-io"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@@ -226,6 +237,17 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-net"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"blocking",
|
||||||
|
"futures-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-process"
|
name = "async-process"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -549,6 +571,17 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"rand_core 0.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -727,6 +760,15 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1222,6 +1264,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
|
"rand_core 0.10.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
@@ -1458,6 +1501,7 @@ dependencies = [
|
|||||||
"iced_core",
|
"iced_core",
|
||||||
"log",
|
"log",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
|
"smol",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasmtimer",
|
"wasmtimer",
|
||||||
]
|
]
|
||||||
@@ -1954,6 +1998,7 @@ dependencies = [
|
|||||||
"iced",
|
"iced",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"image",
|
"image",
|
||||||
|
"rand 0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2795,7 +2840,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.1",
|
||||||
|
"rand_core 0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2805,7 +2861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2817,6 +2873,12 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -2856,7 +2918,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"paste",
|
"paste",
|
||||||
"profiling",
|
"profiling",
|
||||||
"rand",
|
"rand 0.9.2",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"simd_helpers",
|
"simd_helpers",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -3230,6 +3292,23 @@ dependencies = [
|
|||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smol"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-executor",
|
||||||
|
"async-fs",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-net",
|
||||||
|
"async-process",
|
||||||
|
"blocking",
|
||||||
|
"futures-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smol_str"
|
name = "smol_str"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced = {version = "0.14.0", features = ["advanced", "image"]}
|
iced = {version = "0.14.0", features = ["advanced", "image", "smol"]}
|
||||||
iced_core = "0.14.0"
|
iced_core = "0.14.0"
|
||||||
image = "0.25.9"
|
image = "0.25.9"
|
||||||
|
rand = "0.10.0"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use iced::Point;
|
use iced::Point;
|
||||||
|
use rand::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct MSColor {
|
pub struct MSColor {
|
||||||
@@ -540,7 +541,6 @@ impl MSCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn stroke_rect1(&mut self, p1: Point, p2: Point) {
|
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 x = p1.x;
|
||||||
let mut y = p1.y;
|
let mut y = p1.y;
|
||||||
let mut width = (p2.x - p1.x);
|
let mut width = (p2.x - p1.x);
|
||||||
@@ -582,4 +582,21 @@ impl MSCanvas {
|
|||||||
self.pixels[index + 3] = 255; // A
|
self.pixels[index + 3] = 255; // A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 实现喷枪效果的函数
|
||||||
|
pub fn spray_paint(&mut self, center: Point, radius: i32, density: u32) {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
|
for _ in 0..density {
|
||||||
|
// 在给定半径内随机产生偏移量
|
||||||
|
let offset_x = rng.random_range(-radius..=radius);
|
||||||
|
let offset_y = rng.random_range(-radius..=radius);
|
||||||
|
|
||||||
|
// 确保我们只在圆形区域内绘制,而非整个正方形区域
|
||||||
|
if (offset_x * offset_x + offset_y * offset_y) <= (radius * radius) {
|
||||||
|
let point = Point::new(center.x + offset_x as f32, center.y + offset_y as f32);
|
||||||
|
self.draw_pixel_at(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
181
src/paint.rs
181
src/paint.rs
@@ -1,10 +1,12 @@
|
|||||||
use ::image::{ImageBuffer, ImageError, Rgba};
|
use ::image::{ImageBuffer, ImageError, Rgba};
|
||||||
use iced::Theme;
|
use iced::Theme;
|
||||||
use iced::padding;
|
use iced::padding;
|
||||||
|
use iced::time::{self, Instant, milliseconds};
|
||||||
use iced::widget::container;
|
use iced::widget::container;
|
||||||
use iced::widget::{Column, button, column, image, pick_list, 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::{Subscription, color, event, mouse};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
use crate::image_button::image_button;
|
use crate::image_button::image_button;
|
||||||
use crate::mouse_area::mouse_area;
|
use crate::mouse_area::mouse_area;
|
||||||
@@ -54,7 +56,7 @@ enum Message {
|
|||||||
|
|
||||||
Clear,
|
Clear,
|
||||||
SavePNG,
|
SavePNG,
|
||||||
// 内部消息:请求刷新图像
|
/// 内部消息:请求刷新图像
|
||||||
RefreshImage,
|
RefreshImage,
|
||||||
|
|
||||||
ClickTool(Tool),
|
ClickTool(Tool),
|
||||||
@@ -63,9 +65,14 @@ enum Message {
|
|||||||
Decrement(ConfigOption),
|
Decrement(ConfigOption),
|
||||||
|
|
||||||
BrushSelected(BrushKind),
|
BrushSelected(BrushKind),
|
||||||
|
|
||||||
|
/// 全局鼠标释放
|
||||||
|
WindowMouseRelease,
|
||||||
|
|
||||||
|
Tick(Instant),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum Tool {
|
enum Tool {
|
||||||
FreeFormSelect,
|
FreeFormSelect,
|
||||||
Select,
|
Select,
|
||||||
@@ -139,12 +146,16 @@ impl From<Tool> for usize {
|
|||||||
pub enum ConfigOption {
|
pub enum ConfigOption {
|
||||||
EraserWidth,
|
EraserWidth,
|
||||||
LineWidth,
|
LineWidth,
|
||||||
|
AirbrushRadius,
|
||||||
|
AirbrushDensity,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct Config {
|
struct Config {
|
||||||
eraser_width: i32,
|
eraser_width: i32,
|
||||||
line_width: i32,
|
line_width: i32,
|
||||||
|
airbrush_radius: i32,
|
||||||
|
airbrush_density: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -157,6 +168,8 @@ impl Config {
|
|||||||
const DEFAULT: Config = Config {
|
const DEFAULT: Config = Config {
|
||||||
line_width: 1,
|
line_width: 1,
|
||||||
eraser_width: 4,
|
eraser_width: 4,
|
||||||
|
airbrush_radius: 4,
|
||||||
|
airbrush_density: 16,
|
||||||
};
|
};
|
||||||
fn incr(&mut self, option: ConfigOption, step: i32) {
|
fn incr(&mut self, option: ConfigOption, step: i32) {
|
||||||
match option {
|
match option {
|
||||||
@@ -172,6 +185,18 @@ impl Config {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +225,7 @@ struct PaintApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PaintApp {
|
impl PaintApp {
|
||||||
// region iced application (new view update)
|
// region iced application
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut canvas = MSCanvas::new(WIDTH as i32, HEIGHT as i32);
|
let mut canvas = MSCanvas::new(WIDTH as i32, HEIGHT as i32);
|
||||||
@@ -299,7 +324,8 @@ impl PaintApp {
|
|||||||
text(self.config.eraser_width).size(20).center(),
|
text(self.config.eraser_width).size(20).center(),
|
||||||
button("-").on_press(Message::Decrement(ConfigOption::EraserWidth)),
|
button("-").on_press(Message::Decrement(ConfigOption::EraserWidth)),
|
||||||
],
|
],
|
||||||
],
|
]
|
||||||
|
.padding(padding::right(5)),
|
||||||
column![
|
column![
|
||||||
text("Line Width"),
|
text("Line Width"),
|
||||||
row![
|
row![
|
||||||
@@ -307,13 +333,32 @@ impl PaintApp {
|
|||||||
text(self.config.line_width).size(20).center(),
|
text(self.config.line_width).size(20).center(),
|
||||||
button("-").on_press(Message::Decrement(ConfigOption::LineWidth)),
|
button("-").on_press(Message::Decrement(ConfigOption::LineWidth)),
|
||||||
],
|
],
|
||||||
],
|
]
|
||||||
|
.padding(padding::right(5)),
|
||||||
pick_list(
|
pick_list(
|
||||||
&BrushKind::ALL[..],
|
&BrushKind::ALL[..],
|
||||||
self.brush_selected,
|
self.brush_selected,
|
||||||
Message::BrushSelected,
|
Message::BrushSelected,
|
||||||
)
|
)
|
||||||
.placeholder("Brush..."),
|
.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));
|
debug_area = debug_area.padding(padding::top(10).left(5).bottom(10));
|
||||||
|
|
||||||
@@ -341,6 +386,9 @@ impl PaintApp {
|
|||||||
Tool::Brush => {
|
Tool::Brush => {
|
||||||
self.update_with_brush(message);
|
self.update_with_brush(message);
|
||||||
}
|
}
|
||||||
|
Tool::Airbrush => {
|
||||||
|
self.update_with_airbrush(message);
|
||||||
|
}
|
||||||
Tool::Line => {
|
Tool::Line => {
|
||||||
self.update_with_line(message);
|
self.update_with_line(message);
|
||||||
}
|
}
|
||||||
@@ -357,17 +405,10 @@ impl PaintApp {
|
|||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
Message::SavePNG => {
|
Message::SavePNG => {
|
||||||
let scale = 4;
|
|
||||||
let (width, height) = self.canvas.size();
|
let (width, height) = self.canvas.size();
|
||||||
let pixels = self.canvas.get_pixels_scale(4);
|
let pixels = self.canvas.get_pixels();
|
||||||
save_rgba_to_png(
|
let path = "mspaint.png";
|
||||||
pixels,
|
save_pixels_async(pixels, width as u32, height as u32, 4, path.to_string());
|
||||||
(width * scale) as u32,
|
|
||||||
(height * scale) as u32,
|
|
||||||
"mspaint.png",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
println!("save png");
|
|
||||||
}
|
}
|
||||||
Message::RefreshImage => {
|
Message::RefreshImage => {
|
||||||
if self.dirty {
|
if self.dirty {
|
||||||
@@ -393,6 +434,20 @@ impl PaintApp {
|
|||||||
Message::BrushSelected(kind) => {
|
Message::BrushSelected(kind) => {
|
||||||
self.brush_selected = Some(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +462,18 @@ impl PaintApp {
|
|||||||
Task::none()
|
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);
|
||||||
|
return Subscription::batch(vec![ev, tick]);
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region tool update
|
// region tool update
|
||||||
@@ -457,6 +524,7 @@ impl PaintApp {
|
|||||||
self.is_drawing = true;
|
self.is_drawing = true;
|
||||||
self.canvas.draw_pixel_at(pos);
|
self.canvas.draw_pixel_at(pos);
|
||||||
self.begin_point = pos;
|
self.begin_point = pos;
|
||||||
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
Message::MouseReleased(pos) => {
|
Message::MouseReleased(pos) => {
|
||||||
self.is_drawing = false;
|
self.is_drawing = false;
|
||||||
@@ -505,6 +573,37 @@ impl PaintApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
pub fn update_with_line(&mut self, message: Message) {
|
||||||
match message {
|
match message {
|
||||||
Message::MousePressed(pos) => {
|
Message::MousePressed(pos) => {
|
||||||
@@ -580,6 +679,7 @@ impl PaintApp {
|
|||||||
pub fn main() -> iced::Result {
|
pub fn main() -> iced::Result {
|
||||||
iced::application(PaintApp::new, PaintApp::update, PaintApp::view)
|
iced::application(PaintApp::new, PaintApp::update, PaintApp::view)
|
||||||
.theme(Theme::Dark)
|
.theme(Theme::Dark)
|
||||||
|
.subscription(PaintApp::subscription)
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,3 +696,52 @@ fn save_rgba_to_png(
|
|||||||
img.save(path)?;
|
img.save(path)?;
|
||||||
Ok(())
|
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),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user