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",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
185
src/paint.rs
185
src/paint.rs
@@ -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);
|
||||
@@ -293,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));
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user