Compare commits

...

2 Commits

Author SHA1 Message Date
a9243c498f feat: 实现喷枪功能 2026-02-28 14:32:39 +08:00
3ed3c12f47 feat: 监听全局鼠标释放和异步保存图片 2026-02-28 12:57:19 +08:00
4 changed files with 269 additions and 23 deletions

85
Cargo.lock generated
View File

@@ -197,6 +197,17 @@ dependencies = [
"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]]
name = "async-io"
version = "2.6.0"
@@ -226,6 +237,17 @@ dependencies = [
"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]]
name = "async-process"
version = "2.5.0"
@@ -549,6 +571,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "clipboard-win"
version = "5.4.1"
@@ -727,6 +760,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1222,6 +1264,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core 0.10.0",
"wasip2",
"wasip3",
]
@@ -1458,6 +1501,7 @@ dependencies = [
"iced_core",
"log",
"rustc-hash 2.1.1",
"smol",
"wasm-bindgen-futures",
"wasmtimer",
]
@@ -1954,6 +1998,7 @@ dependencies = [
"iced",
"iced_core",
"image",
"rand 0.10.0",
]
[[package]]
@@ -2795,7 +2840,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"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]]
@@ -2805,7 +2861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.5",
]
[[package]]
@@ -2817,6 +2873,12 @@ dependencies = [
"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]]
name = "range-alloc"
version = "0.1.4"
@@ -2856,7 +2918,7 @@ dependencies = [
"num-traits",
"paste",
"profiling",
"rand",
"rand 0.9.2",
"rand_chacha",
"simd_helpers",
"thiserror 2.0.18",
@@ -3230,6 +3292,23 @@ dependencies = [
"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]]
name = "smol_str"
version = "0.2.2"

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
iced = {version = "0.14.0", features = ["advanced", "image"]}
iced = {version = "0.14.0", features = ["advanced", "image", "smol"]}
iced_core = "0.14.0"
image = "0.25.9"
rand = "0.10.0"

View File

@@ -1,4 +1,5 @@
use iced::Point;
use rand::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MSColor {
@@ -540,7 +541,6 @@ 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);
@@ -582,4 +582,21 @@ impl MSCanvas {
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);
}
}
}
}

View File

@@ -1,10 +1,12 @@
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_core::color;
use iced::{Subscription, color, event, mouse};
use std::thread;
use crate::image_button::image_button;
use crate::mouse_area::mouse_area;
@@ -54,7 +56,7 @@ enum Message {
Clear,
SavePNG,
// 内部消息:请求刷新图像
/// 内部消息:请求刷新图像
RefreshImage,
ClickTool(Tool),
@@ -63,9 +65,14 @@ enum Message {
Decrement(ConfigOption),
BrushSelected(BrushKind),
/// 全局鼠标释放
WindowMouseRelease,
Tick(Instant),
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Tool {
FreeFormSelect,
Select,
@@ -139,12 +146,16 @@ impl From<Tool> for usize {
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 {
@@ -157,6 +168,8 @@ 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 {
@@ -172,6 +185,18 @@ impl Config {
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 {
// region iced application (new view update)
// region iced application
pub fn new() -> Self {
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(),
button("-").on_press(Message::Decrement(ConfigOption::EraserWidth)),
],
],
]
.padding(padding::right(5)),
column![
text("Line Width"),
row![
@@ -307,13 +333,32 @@ impl PaintApp {
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));
@@ -341,6 +386,9 @@ impl PaintApp {
Tool::Brush => {
self.update_with_brush(message);
}
Tool::Airbrush => {
self.update_with_airbrush(message);
}
Tool::Line => {
self.update_with_line(message);
}
@@ -357,17 +405,10 @@ impl PaintApp {
self.dirty = true;
}
Message::SavePNG => {
let scale = 4;
let (width, height) = self.canvas.size();
let pixels = self.canvas.get_pixels_scale(4);
save_rgba_to_png(
pixels,
(width * scale) as u32,
(height * scale) as u32,
"mspaint.png",
)
.unwrap();
println!("save png");
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 {
@@ -393,6 +434,20 @@ impl PaintApp {
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;
}
}
_ => {}
}
@@ -407,6 +462,18 @@ impl PaintApp {
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
// region tool update
@@ -457,6 +524,7 @@ impl PaintApp {
self.is_drawing = true;
self.canvas.draw_pixel_at(pos);
self.begin_point = pos;
self.dirty = true;
}
Message::MouseReleased(pos) => {
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) {
match message {
Message::MousePressed(pos) => {
@@ -580,6 +679,7 @@ impl PaintApp {
pub fn main() -> iced::Result {
iced::application(PaintApp::new, PaintApp::update, PaintApp::view)
.theme(Theme::Dark)
.subscription(PaintApp::subscription)
.run()
}
@@ -596,3 +696,52 @@ fn save_rgba_to_png(
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),
}
});
}