diff --git a/src/mscanvas.rs b/src/mscanvas.rs index e2a9751..6c92108 100644 --- a/src/mscanvas.rs +++ b/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 +} diff --git a/src/paint.rs b/src/paint.rs index 9dd4cab..dc857ec 100644 --- a/src/paint.rs +++ b/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, 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), }