From 08233257bb814fc82c4aa6a994f6200ea5a55819 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 5 Jan 2023 19:38:23 +0800 Subject: [PATCH] feat: roundRect (#601) --- __test__/draw.spec.ts | 36 ++++++++++++++++++++ __test__/snapshots/strokeRoundRect.png | Bin 0 -> 7966 bytes index.d.ts | 1 + skia-c/skia_c.cpp | 21 ++++++++++++ skia-c/skia_c.hpp | 8 +++++ src/ctx.rs | 45 +++++++++++++++++++++++++ src/path.rs | 40 ++++++++++++++++++++++ src/sk.rs | 38 +++++++++++++++++++++ 8 files changed, 189 insertions(+) create mode 100644 __test__/snapshots/strokeRoundRect.png diff --git a/__test__/draw.spec.ts b/__test__/draw.spec.ts index 4039dc62..e282421d 100644 --- a/__test__/draw.spec.ts +++ b/__test__/draw.spec.ts @@ -847,6 +847,42 @@ test('strokeRect', async (t) => { await snapshotImage(t) }) +test('strokeRoundRect', async (t) => { + const canvas = createCanvas(700, 300) + const ctx = canvas.getContext('2d') + // Rounded rectangle with zero radius (specified as a number) + ctx.strokeStyle = 'red' + ctx.beginPath() + ctx.roundRect(10, 20, 150, 100, 0) + ctx.stroke() + + // Rounded rectangle with 40px radius (single element list) + ctx.strokeStyle = 'blue' + ctx.beginPath() + ctx.roundRect(10, 20, 150, 100, [40]) + ctx.stroke() + + // Rounded rectangle with 2 different radii + ctx.strokeStyle = 'orange' + ctx.beginPath() + ctx.roundRect(10, 150, 150, 100, [10, 40]) + ctx.stroke() + + // Rounded rectangle with four different radii + ctx.strokeStyle = 'green' + ctx.beginPath() + ctx.roundRect(400, 20, 200, 100, [0, 30, 50, 60]) + ctx.stroke() + + // Same rectangle drawn backwards + ctx.strokeStyle = 'magenta' + ctx.beginPath() + ctx.roundRect(400, 150, -200, 100, [0, 30, 50, 60]) + ctx.stroke() + + await snapshotImage(t, { canvas, ctx }) +}) + test('strokeText', async (t) => { const { ctx, canvas } = t.context ctx.fillStyle = 'yellow' diff --git a/__test__/snapshots/strokeRoundRect.png b/__test__/snapshots/strokeRoundRect.png new file mode 100644 index 0000000000000000000000000000000000000000..81dc8689ef42917d3f4c3f172fb8e2d9c91111a4 GIT binary patch literal 7966 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 zgOEmYaddRoundRect(rect, radii_vec, ccw); + } + // PathEffect skiac_path_effect *skiac_path_effect_make_dash_path(const float *intervals, int count, float phase) diff --git a/skia-c/skia_c.hpp b/skia-c/skia_c.hpp index 41936f0e..38aac1fc 100644 --- a/skia-c/skia_c.hpp +++ b/skia-c/skia_c.hpp @@ -373,6 +373,14 @@ extern "C" bool skiac_path_is_empty(skiac_path *c_path); bool skiac_path_hit_test(skiac_path *c_path, float x, float y, int type); bool skiac_path_stroke_hit_test(skiac_path *c_path, float x, float y, float stroke_w); + void skiac_path_round_rect( + skiac_path *c_path, + SkScalar x, + SkScalar y, + SkScalar width, + SkScalar height, + SkScalar *radii, + bool clockwise); // PathEffect skiac_path_effect *skiac_path_effect_make_dash_path(const float *intervals, int count, float phase); diff --git a/src/ctx.rs b/src/ctx.rs index 0e0335e8..7983cce3 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -174,6 +174,10 @@ impl Context { self.path.add_rect(x, y, width, height); } + pub fn round_rect(&mut self, x: f32, y: f32, width: f32, height: f32, radii: [f32; 4]) { + self.path.round_rect(x, y, width, height, radii); + } + pub fn save(&mut self) { self.surface.canvas.save(); self.states.push(self.state.clone()); @@ -1294,6 +1298,47 @@ impl CanvasRenderingContext2D { .rect(x as f32, y as f32, width as f32, height as f32); } + #[napi] + pub fn round_rect( + &mut self, + x: f64, + y: f64, + width: f64, + height: f64, + radii: Either3, Undefined>, + ) { + // https://github.com/chromium/chromium/blob/111.0.5520.1/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc#L579 + let radii_array: [f32; 4] = match radii { + Either3::A(radii) => [radii as f32; 4], + Either3::B(radii_vec) => match radii_vec.len() { + 0 => [0f32; 4], + 1 => [radii_vec[0] as f32; 4], + 2 => [ + radii_vec[0] as f32, + radii_vec[1] as f32, + radii_vec[0] as f32, + radii_vec[1] as f32, + ], + 3 => [ + radii_vec[0] as f32, + radii_vec[1] as f32, + radii_vec[1] as f32, + radii_vec[2] as f32, + ], + _ => [ + radii_vec[0] as f32, + radii_vec[1] as f32, + radii_vec[2] as f32, + radii_vec[3] as f32, + ], + }, + Either3::C(_) => [0f32; 4], + }; + self + .context + .round_rect(x as f32, y as f32, width as f32, height as f32, radii_array); + } + #[napi] pub fn fill( &mut self, diff --git a/src/path.rs b/src/path.rs index 4141717a..62a1954e 100644 --- a/src/path.rs +++ b/src/path.rs @@ -257,6 +257,46 @@ impl Path { .add_rect(x as f32, y as f32, width as f32, height as f32); } + #[napi] + pub fn round_rect( + &mut self, + x: f64, + y: f64, + width: f64, + height: f64, + radii: Either3, Undefined>, + ) { + let radii_array: [f32; 4] = match radii { + Either3::A(radii) => [radii as f32; 4], + Either3::B(radii_vec) => match radii_vec.len() { + 0 => [0f32; 4], + 1 => [radii_vec[0] as f32; 4], + 2 => [ + radii_vec[0] as f32, + radii_vec[1] as f32, + radii_vec[0] as f32, + radii_vec[1] as f32, + ], + 3 => [ + radii_vec[0] as f32, + radii_vec[1] as f32, + radii_vec[1] as f32, + radii_vec[2] as f32, + ], + _ => [ + radii_vec[0] as f32, + radii_vec[1] as f32, + radii_vec[2] as f32, + radii_vec[3] as f32, + ], + }, + Either3::C(_) => [0f32; 4], + }; + self + .inner + .round_rect(x as f32, y as f32, width as f32, height as f32, radii_array); + } + #[napi] pub fn op(&mut self, other: &Path, op: PathOp) -> &Self { self.inner.op(&other.inner, op.into()); diff --git a/src/sk.rs b/src/sk.rs index b296ab66..6a010d5a 100644 --- a/src/sk.rs +++ b/src/sk.rs @@ -612,6 +612,16 @@ pub mod ffi { pub fn skiac_path_stroke_hit_test(path: *mut skiac_path, x: f32, y: f32, stroke_w: f32) -> bool; + pub fn skiac_path_round_rect( + path: *mut skiac_path, + x: f32, + y: f32, + width: f32, + height: f32, + radii: *const f32, + clockwise: bool, + ); + pub fn skiac_path_effect_make_dash_path( intervals: *const f32, count: i32, @@ -2680,6 +2690,34 @@ impl Path { unsafe { ffi::skiac_path_dash(self.0, on, off, phase) } } + pub fn round_rect( + &mut self, + mut x: f32, + mut y: f32, + mut width: f32, + mut height: f32, + mut radii: [f32; 4], + ) { + // https://github.com/chromium/chromium/blob/111.0.5520.1/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc#L601 + let mut clockwise = true; + if width < 0f32 { + clockwise = false; + x += width; + width = -width; + radii.swap(0, 1); + radii.swap(2, 3); + } + if height < 0f32 { + clockwise = !clockwise; + y += height; + height = -height; + radii.swap(0, 2); + radii.swap(1, 3); + } + unsafe { ffi::skiac_path_round_rect(self.0, x, y, width, height, radii.as_ptr(), clockwise) }; + unsafe { ffi::skiac_path_move_to(self.0, x, y) }; + } + fn ellipse_helper(&mut self, x: f32, y: f32, rx: f32, ry: f32, start_angle: f32, end_angle: f32) { let sweep_degrees = radians_to_degrees(end_angle - start_angle); let start_degrees = radians_to_degrees(start_angle);