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) {
|
||||
let Point { x, y } = point;
|
||||
let r = 5;
|
||||
let r = 10;
|
||||
for dy in -r..=r {
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -534,6 +530,118 @@ impl MSCanvas {
|
||||
(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) {
|
||||
for yi in y..(y + height) {
|
||||
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) {
|
||||
if width < 2 * self.line_width || height < 2 * self.line_width {
|
||||
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 {
|
||||
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,
|
||||
AirbrushRadius,
|
||||
AirbrushDensity,
|
||||
RoundedRadius,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -185,6 +186,7 @@ struct Config {
|
||||
line_width: i32,
|
||||
airbrush_radius: i32,
|
||||
airbrush_density: i32,
|
||||
rounded_radius: i32,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -199,6 +201,7 @@ impl Config {
|
||||
eraser_width: 4,
|
||||
airbrush_radius: 4,
|
||||
airbrush_density: 16,
|
||||
rounded_radius: 3,
|
||||
};
|
||||
fn incr(&mut self, option: ConfigOption, step: i32) {
|
||||
match option {
|
||||
@@ -226,6 +229,12 @@ impl Config {
|
||||
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)),
|
||||
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));
|
||||
|
||||
@@ -450,6 +468,9 @@ impl PaintApp {
|
||||
Tool::Ellipse => {
|
||||
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
|
||||
|
||||
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 进去)
|
||||
thread::spawn(move || {
|
||||
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),
|
||||
Err(e) => eprintln!("❌ Failed to save image: {}", e),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user