feat: 实现圆角矩形功能
This commit is contained in:
216
src/mscanvas.rs
216
src/mscanvas.rs
@@ -237,7 +237,7 @@ impl MSCanvas {
|
|||||||
|
|
||||||
pub fn draw_cross_color(&mut self, point: Point, color: MSColor) {
|
pub fn draw_cross_color(&mut self, point: Point, color: MSColor) {
|
||||||
let Point { x, y } = point;
|
let Point { x, y } = point;
|
||||||
let r = 5;
|
let r = 10;
|
||||||
for dy in -r..=r {
|
for dy in -r..=r {
|
||||||
self.draw_pixel_color_at(Point::new(point.x, point.y + dy as f32), color);
|
self.draw_pixel_color_at(Point::new(point.x, point.y + dy as f32), color);
|
||||||
}
|
}
|
||||||
@@ -392,10 +392,6 @@ impl MSCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_line_with_circle_brush(&mut self, begin: Point, end: Point) {
|
pub fn draw_line_with_circle_brush(&mut self, begin: Point, end: Point) {
|
||||||
// let points = self.bresenham_line(begin, end);
|
|
||||||
// for point in points {
|
|
||||||
// self.draw_brush_at(point);
|
|
||||||
// }
|
|
||||||
self.draw_line_with(begin, end, MSCanvas::brush_circle);
|
self.draw_line_with(begin, end, MSCanvas::brush_circle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,6 +530,118 @@ impl MSCanvas {
|
|||||||
(iter_count, fill_count)
|
(iter_count, fill_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn arc_1px(
|
||||||
|
&mut self,
|
||||||
|
center: Point,
|
||||||
|
radius: f32,
|
||||||
|
start_angle: f32,
|
||||||
|
end_angle: f32,
|
||||||
|
counterclockwise: bool,
|
||||||
|
) {
|
||||||
|
if radius <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据半径确定步长,步长越小曲线越平滑
|
||||||
|
// 圆周长约为 2πr,每像素对应的角度步长约为 1/r 弧度
|
||||||
|
let step = (1.0 / radius).max(0.001_f32);
|
||||||
|
|
||||||
|
// 将角度规范化,确保 start_angle 和 end_angle 在合理范围内
|
||||||
|
let (start, end) = if counterclockwise {
|
||||||
|
// 逆时针:角度递减,将区间转成递增处理
|
||||||
|
// 逆时针从 start_angle 到 end_angle,等价于顺时针从 end_angle 到 start_angle
|
||||||
|
let mut s = end_angle;
|
||||||
|
let mut e = start_angle;
|
||||||
|
// 保证 e >= s
|
||||||
|
while e < s {
|
||||||
|
e += std::f32::consts::TAU;
|
||||||
|
}
|
||||||
|
(s, e)
|
||||||
|
} else {
|
||||||
|
// 顺时针:角度递增
|
||||||
|
let mut s = start_angle;
|
||||||
|
let mut e = end_angle;
|
||||||
|
// 保证 e >= s
|
||||||
|
while e < s {
|
||||||
|
e += std::f32::consts::TAU;
|
||||||
|
}
|
||||||
|
(s, e)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 沿角度步进,绘制每个像素点
|
||||||
|
let mut angle = start;
|
||||||
|
while angle <= end + step {
|
||||||
|
let a = angle.min(end);
|
||||||
|
let x = center.x + radius * a.cos();
|
||||||
|
let y = center.y + radius * a.sin();
|
||||||
|
self.draw_pixel_at(Point::new(x, y));
|
||||||
|
angle += step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 圆弧从 x 轴方向开始计算, start_angle end_angle 为弧度,counterclockwise 是否逆时针方向
|
||||||
|
pub fn arc(
|
||||||
|
&mut self,
|
||||||
|
center: Point,
|
||||||
|
radius: f32,
|
||||||
|
start_angle: f32,
|
||||||
|
end_angle: f32,
|
||||||
|
counterclockwise: bool,
|
||||||
|
) {
|
||||||
|
if radius <= 1.0 {
|
||||||
|
self.draw_pixel_at(center);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_angle = normalize_radian(start_angle);
|
||||||
|
let end_angle = normalize_radian(end_angle);
|
||||||
|
let full_circle = (start_angle - end_angle).abs() < f32::EPSILON;
|
||||||
|
|
||||||
|
let min_x = ((center.x - radius) as i32).max(0);
|
||||||
|
let max_x = ((center.x + radius) as i32).min(self.width - 1);
|
||||||
|
let min_y = ((center.y - radius) as i32).max(0);
|
||||||
|
let max_y = ((center.y + radius) as i32).min(self.height - 1);
|
||||||
|
|
||||||
|
let min_r = (radius - self.line_width as f32).max(0.0);
|
||||||
|
let max_r = radius + 0.05;
|
||||||
|
|
||||||
|
let min_sq = min_r * min_r;
|
||||||
|
let max_sq = max_r * max_r;
|
||||||
|
|
||||||
|
let color = MSColor::new(
|
||||||
|
(255 + self.color.r) / 2,
|
||||||
|
self.color.g,
|
||||||
|
self.color.b,
|
||||||
|
self.color.a,
|
||||||
|
);
|
||||||
|
|
||||||
|
let center_x = center.x as i32;
|
||||||
|
let center_y = center.y as i32;
|
||||||
|
for y in min_y..=max_y {
|
||||||
|
for x in min_x..=max_x {
|
||||||
|
let dx = (x - center_x) as f32;
|
||||||
|
let dy = (y - center_y) as f32;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
// 判断点是不是在扇区内
|
||||||
|
if dist_sq > max_sq || dist_sq <= min_sq {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断角度是不是在扇区内
|
||||||
|
let theta = dy.atan2(dx);
|
||||||
|
let theta = normalize_radian(theta);
|
||||||
|
|
||||||
|
if full_circle
|
||||||
|
|| (counterclockwise && (theta >= end_angle || theta <= start_angle))
|
||||||
|
|| (!counterclockwise && theta >= start_angle && theta <= end_angle)
|
||||||
|
{
|
||||||
|
self.draw_pixel_at(Point::new(x as f32, y as f32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_rect(&mut self, x: i32, y: i32, width: i32, height: i32) {
|
pub fn clear_rect(&mut self, x: i32, y: i32, width: i32, height: i32) {
|
||||||
for yi in y..(y + height) {
|
for yi in y..(y + height) {
|
||||||
self.clear_row(x, x + width, yi);
|
self.clear_row(x, x + width, yi);
|
||||||
@@ -546,6 +654,91 @@ impl MSCanvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn round_rect(&mut self, x: f32, y: f32, width: f32, height: f32, radius: f32) {
|
||||||
|
if (width as i32) < 2 * self.line_width
|
||||||
|
|| (height as i32) < 2 * self.line_width
|
||||||
|
|| width < 2.0 * radius
|
||||||
|
|| height < 2.0 * radius
|
||||||
|
{
|
||||||
|
self.fill_rect(x as i32, y as i32, width as i32, height as i32);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fill_rect(
|
||||||
|
(x + radius) as i32,
|
||||||
|
y as i32,
|
||||||
|
(width - 2.0 * radius) as i32,
|
||||||
|
self.line_width,
|
||||||
|
);
|
||||||
|
self.fill_rect(
|
||||||
|
(x + radius) as i32,
|
||||||
|
(y + height) as i32 - self.line_width,
|
||||||
|
(width - 2.0 * radius) as i32,
|
||||||
|
self.line_width,
|
||||||
|
);
|
||||||
|
self.fill_rect(
|
||||||
|
x as i32,
|
||||||
|
(y + radius) as i32,
|
||||||
|
self.line_width,
|
||||||
|
(height - 2.0 * radius) as i32,
|
||||||
|
);
|
||||||
|
self.fill_rect(
|
||||||
|
(x + width) as i32 - self.line_width,
|
||||||
|
(y + radius) as i32,
|
||||||
|
self.line_width,
|
||||||
|
(height - 2.0 * radius) as i32,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.arc(
|
||||||
|
Point::new(x + radius, y + radius),
|
||||||
|
radius,
|
||||||
|
std::f32::consts::PI,
|
||||||
|
std::f32::consts::PI + std::f32::consts::FRAC_PI_2,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.arc(
|
||||||
|
Point::new(x + width - 1.0 - radius, y + radius),
|
||||||
|
radius,
|
||||||
|
0.0,
|
||||||
|
std::f32::consts::PI + std::f32::consts::FRAC_PI_2,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
self.arc(
|
||||||
|
Point::new(x + radius, y + height - 1.0 - radius),
|
||||||
|
radius,
|
||||||
|
std::f32::consts::FRAC_PI_2,
|
||||||
|
std::f32::consts::PI,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.arc(
|
||||||
|
Point::new(x + width - 1.0 - radius, y + height - 1.0 - radius),
|
||||||
|
radius,
|
||||||
|
0.0,
|
||||||
|
std::f32::consts::FRAC_PI_2,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn round_rect1(&mut self, p1: Point, p2: Point, radius: f32) {
|
||||||
|
let mut x = p1.x;
|
||||||
|
let mut y = p1.y;
|
||||||
|
let mut width = (p2.x - p1.x);
|
||||||
|
let mut height = (p2.y - p1.y);
|
||||||
|
if width < 0.0 && height < 0.0 {
|
||||||
|
x = p2.x;
|
||||||
|
y = p2.y;
|
||||||
|
width = (p1.x - p2.x);
|
||||||
|
height = (p1.y - p2.y);
|
||||||
|
} else if width < 0.0 {
|
||||||
|
x += width;
|
||||||
|
width = -width;
|
||||||
|
} else if height < 0.0 {
|
||||||
|
y += height;
|
||||||
|
height = -height;
|
||||||
|
}
|
||||||
|
self.round_rect(x, y, width, height, radius);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stroke_rect(&mut self, x: i32, y: i32, width: i32, height: i32) {
|
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 {
|
if width < 2 * self.line_width || height < 2 * self.line_width {
|
||||||
self.fill_rect(x, y, width, height);
|
self.fill_rect(x, y, width, height);
|
||||||
@@ -759,3 +952,16 @@ fn point_muln(point: Point, t: f32) -> Point {
|
|||||||
fn point_add(p1: Point, p2: Point) -> Point {
|
fn point_add(p1: Point, p2: Point) -> Point {
|
||||||
Point::new(p1.x + p2.x, p1.y + p2.y)
|
Point::new(p1.x + p2.x, p1.y + p2.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将弧度规范到 [0, 2π)
|
||||||
|
fn normalize_radian(radian: f32) -> f32 {
|
||||||
|
let mut r = radian;
|
||||||
|
while r < 0.0 {
|
||||||
|
r += std::f32::consts::TAU;
|
||||||
|
}
|
||||||
|
while r >= std::f32::consts::TAU {
|
||||||
|
r -= std::f32::consts::TAU;
|
||||||
|
}
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|||||||
49
src/paint.rs
49
src/paint.rs
@@ -177,6 +177,7 @@ pub enum ConfigOption {
|
|||||||
LineWidth,
|
LineWidth,
|
||||||
AirbrushRadius,
|
AirbrushRadius,
|
||||||
AirbrushDensity,
|
AirbrushDensity,
|
||||||
|
RoundedRadius,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -185,6 +186,7 @@ struct Config {
|
|||||||
line_width: i32,
|
line_width: i32,
|
||||||
airbrush_radius: i32,
|
airbrush_radius: i32,
|
||||||
airbrush_density: i32,
|
airbrush_density: i32,
|
||||||
|
rounded_radius: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -199,6 +201,7 @@ impl Config {
|
|||||||
eraser_width: 4,
|
eraser_width: 4,
|
||||||
airbrush_radius: 4,
|
airbrush_radius: 4,
|
||||||
airbrush_density: 16,
|
airbrush_density: 16,
|
||||||
|
rounded_radius: 3,
|
||||||
};
|
};
|
||||||
fn incr(&mut self, option: ConfigOption, step: i32) {
|
fn incr(&mut self, option: ConfigOption, step: i32) {
|
||||||
match option {
|
match option {
|
||||||
@@ -226,6 +229,12 @@ impl Config {
|
|||||||
self.airbrush_density = Self::DEFAULT.airbrush_density;
|
self.airbrush_density = Self::DEFAULT.airbrush_density;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ConfigOption::RoundedRadius => {
|
||||||
|
self.rounded_radius += step;
|
||||||
|
if self.rounded_radius < Self::DEFAULT.rounded_radius {
|
||||||
|
self.rounded_radius = Self::DEFAULT.rounded_radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,6 +414,15 @@ impl PaintApp {
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
.padding(padding::right(5)),
|
.padding(padding::right(5)),
|
||||||
|
column![
|
||||||
|
text("Rounded Radius"),
|
||||||
|
row![
|
||||||
|
button("+").on_press(Message::Increment(ConfigOption::RoundedRadius)),
|
||||||
|
text(self.config.rounded_radius).size(20).center(),
|
||||||
|
button("-").on_press(Message::Decrement(ConfigOption::RoundedRadius)),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
.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));
|
||||||
|
|
||||||
@@ -450,6 +468,9 @@ impl PaintApp {
|
|||||||
Tool::Ellipse => {
|
Tool::Ellipse => {
|
||||||
self.update_with_ellipse(message);
|
self.update_with_ellipse(message);
|
||||||
}
|
}
|
||||||
|
Tool::RoundedRectangle => {
|
||||||
|
self.update_with_rounded_rectangle(message);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,6 +840,32 @@ impl PaintApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_with_rounded_rectangle(&mut self, message: Message) {
|
||||||
|
match message {
|
||||||
|
Message::MousePressed(pos) => {
|
||||||
|
self.is_drawing = true;
|
||||||
|
self.canvas.save_pixels();
|
||||||
|
self.begin_point = pos;
|
||||||
|
}
|
||||||
|
Message::MouseReleased(pos) => {
|
||||||
|
self.is_drawing = false;
|
||||||
|
self.begin_point = pos;
|
||||||
|
}
|
||||||
|
Message::MouseMoved(pos) => {
|
||||||
|
if self.is_drawing {
|
||||||
|
self.canvas.restore_pixels();
|
||||||
|
self.canvas.round_rect1(
|
||||||
|
self.begin_point,
|
||||||
|
pos,
|
||||||
|
self.config.rounded_radius as f32,
|
||||||
|
);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
pub fn update_tool_states(&mut self, tool: Tool) {
|
pub fn update_tool_states(&mut self, tool: Tool) {
|
||||||
@@ -917,7 +964,7 @@ fn save_pixels_async(pixels: Vec<u8>, width: u32, height: u32, scale: u32, path:
|
|||||||
// 用 Arc 共享数据(或直接 move 进去)
|
// 用 Arc 共享数据(或直接 move 进去)
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let pixels = scale_pixels(pixels, width, height, scale);
|
let pixels = scale_pixels(pixels, width, height, scale);
|
||||||
match save_rgba_to_png(pixels, width, height, &path) {
|
match save_rgba_to_png(pixels, width * scale, height * scale, &path) {
|
||||||
Ok(()) => println!("✅ Image saved to {}", path),
|
Ok(()) => println!("✅ Image saved to {}", path),
|
||||||
Err(e) => eprintln!("❌ Failed to save image: {}", e),
|
Err(e) => eprintln!("❌ Failed to save image: {}", e),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user