feat: 实现圆角矩形功能

This commit is contained in:
2026-03-01 17:38:11 +08:00
parent d7f4571217
commit 62be61c574
2 changed files with 259 additions and 6 deletions

View File

@@ -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
}

View File

@@ -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),
} }