From a9243c498fc609e21bb813fd0519cbcedf35a532 Mon Sep 17 00:00:00 2001 From: yeqing Date: Sat, 28 Feb 2026 14:32:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=96=B7=E6=9E=AA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 85 ++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 3 +- src/mscanvas.rs | 19 ++++++++- src/paint.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 197 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df3c876..ee7544c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 0553841..8e34482 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/mscanvas.rs b/src/mscanvas.rs index 7e4e8a7..db823be 100644 --- a/src/mscanvas.rs +++ b/src/mscanvas.rs @@ -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); + } + } + } } diff --git a/src/paint.rs b/src/paint.rs index f8b34bc..df666e8 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -1,6 +1,7 @@ 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}; @@ -67,9 +68,11 @@ enum Message { /// 全局鼠标释放 WindowMouseRelease, + + Tick(Instant), } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Tool { FreeFormSelect, Select, @@ -143,12 +146,16 @@ impl From 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 { @@ -161,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 { @@ -176,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; + } + } } } } @@ -297,27 +318,47 @@ impl PaintApp { let mut debug_area = row![ column![ - text("Eraser Width "), + 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 "), + 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)); @@ -345,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); } @@ -394,6 +438,16 @@ impl PaintApp { // 处理鼠标在 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; + } + } _ => {} } @@ -409,13 +463,15 @@ impl PaintApp { } pub fn subscription(&self) -> Subscription { - event::listen().filter_map(|event| match event { + 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 @@ -468,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; @@ -516,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) => {