diff --git a/Cargo.lock b/Cargo.lock index 16a21955977..46079445c71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1087,6 +1087,7 @@ dependencies = [ "font-kit", "fonts", "ipc-channel", + "kurbo", "log", "lyon_geom", "net_traits", @@ -1108,6 +1109,7 @@ dependencies = [ "euclid", "glow", "ipc-channel", + "kurbo", "malloc_size_of_derive", "pixels", "serde", @@ -4681,6 +4683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" dependencies = [ "arrayvec", + "serde", "smallvec", ] @@ -7089,6 +7092,7 @@ dependencies = [ "itertools 0.14.0", "jstraceable_derive", "keyboard-types", + "kurbo", "layout_api", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index d09c35cf96d..4cf809c447d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ ipc-channel = "0.20" itertools = "0.14" js = { package = "mozjs", git = "https://github.com/servo/mozjs" } keyboard-types = "0.7" +kurbo = "0.11" libc = "0.2" log = "0.4" mach2 = "0.4" diff --git a/components/canvas/Cargo.toml b/components/canvas/Cargo.toml index 9284c2f89bc..5d0a9f144c3 100644 --- a/components/canvas/Cargo.toml +++ b/components/canvas/Cargo.toml @@ -21,6 +21,7 @@ euclid = { workspace = true } font-kit = "0.14" fonts = { path = "../fonts" } ipc-channel = { workspace = true } +kurbo = { workspace = true } log = { workspace = true } lyon_geom = "1.0.4" net_traits = { workspace = true } diff --git a/components/canvas/backend.rs b/components/canvas/backend.rs index 78c48efc8c7..195d7d99799 100644 --- a/components/canvas/backend.rs +++ b/components/canvas/backend.rs @@ -3,17 +3,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use canvas_traits::canvas::{ - CompositionOrBlending, FillOrStrokeStyle, LineCapStyle, LineJoinStyle, PathSegment, + CompositionOrBlending, FillOrStrokeStyle, LineCapStyle, LineJoinStyle, Path, }; use compositing_traits::SerializableImageData; -use euclid::Angle; use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; -use lyon_geom::Arc; use pixels::Snapshot; use style::color::AbsoluteColor; use webrender_api::ImageDescriptor; -use crate::canvas_data::{CanvasPaintState, Filter, PathBuilderRef, TextRun}; +use crate::canvas_data::{CanvasPaintState, Filter, TextRun}; pub(crate) trait Backend: Clone + Sized { type Pattern<'a>: PatternHelpers + Clone; @@ -22,7 +20,6 @@ pub(crate) trait Backend: Clone + Sized { type DrawOptions: DrawOptionsHelpers + Clone; type CompositionOp; type DrawTarget: GenericDrawTarget; - type Path: GenericPath + Clone; type SourceSurface; type GradientStop; type GradientStops; @@ -80,7 +77,7 @@ pub(crate) trait GenericDrawTarget { sigma: f32, operator: B::CompositionOp, ); - fn fill(&mut self, path: &B::Path, pattern: &B::Pattern<'_>, draw_options: &B::DrawOptions); + fn fill(&mut self, path: &Path, pattern: &B::Pattern<'_>, draw_options: &B::DrawOptions); fn fill_text( &mut self, text_runs: Vec, @@ -97,12 +94,12 @@ pub(crate) trait GenericDrawTarget { fn get_size(&self) -> Size2D; fn get_transform(&self) -> Transform2D; fn pop_clip(&mut self); - fn push_clip(&mut self, path: &B::Path); + fn push_clip(&mut self, path: &Path); fn push_clip_rect(&mut self, rect: &Rect); fn set_transform(&mut self, matrix: &Transform2D); fn stroke( &mut self, - path: &B::Path, + path: &Path, pattern: &B::Pattern<'_>, stroke_options: &B::StrokeOptions, draw_options: &B::DrawOptions, @@ -119,200 +116,6 @@ pub(crate) trait GenericDrawTarget { fn snapshot(&self) -> Snapshot; } -/// A generic Path that abstracts the interface for raqote's PathBuilder/Path. -pub(crate) trait GenericPath> { - fn new() -> Self; - fn transform(&mut self, transform: &Transform2D); - fn arc( - &mut self, - origin: Point2D, - radius: f32, - start_angle: f32, - end_angle: f32, - anticlockwise: bool, - ) { - Self::ellipse( - self, - origin, - radius, - radius, - 0., - start_angle, - end_angle, - anticlockwise, - ); - } - fn bezier_curve_to( - &mut self, - control_point1: &Point2D, - control_point2: &Point2D, - control_point3: &Point2D, - ); - fn close(&mut self); - #[allow(clippy::too_many_arguments)] - fn ellipse( - &mut self, - origin: Point2D, - radius_x: f32, - radius_y: f32, - rotation_angle: f32, - start_angle: f32, - end_angle: f32, - anticlockwise: bool, - ) { - let mut start = Angle::radians(start_angle); - let mut end = Angle::radians(end_angle); - - // Wrap angles mod 2 * PI if necessary - if !anticlockwise && start > end + Angle::two_pi() || - anticlockwise && end > start + Angle::two_pi() - { - start = start.positive(); - end = end.positive(); - } - - // Calculate the total arc we're going to sweep. - let sweep = match anticlockwise { - true => { - if end - start == Angle::two_pi() { - -Angle::two_pi() - } else if end > start { - -(Angle::two_pi() - (end - start)) - } else { - -(start - end) - } - }, - false => { - if start - end == Angle::two_pi() { - Angle::two_pi() - } else if start > end { - Angle::two_pi() - (start - end) - } else { - end - start - } - }, - }; - - let arc: Arc = Arc { - center: origin, - radii: Vector2D::new(radius_x, radius_y), - start_angle: start, - sweep_angle: sweep, - x_rotation: Angle::radians(rotation_angle), - }; - - self.line_to(arc.from()); - - if sweep.radians.abs() < 1e-3 { - return; - } - - arc.for_each_quadratic_bezier(&mut |q| { - self.quadratic_curve_to(&q.ctrl, &q.to); - }); - } - fn get_current_point(&mut self) -> Option>; - fn line_to(&mut self, point: Point2D); - fn move_to(&mut self, point: Point2D); - fn quadratic_curve_to(&mut self, control_point: &Point2D, end_point: &Point2D); - fn svg_arc( - &mut self, - radius_x: f32, - radius_y: f32, - rotation_angle: f32, - large_arc: bool, - sweep: bool, - end_point: Point2D, - ) { - let Some(start) = self.get_current_point() else { - return; - }; - - let arc = lyon_geom::SvgArc { - from: start, - to: end_point, - radii: lyon_geom::vector(radius_x, radius_y), - x_rotation: lyon_geom::Angle::degrees(rotation_angle), - flags: lyon_geom::ArcFlags { large_arc, sweep }, - }; - - arc.for_each_quadratic_bezier(&mut |q| { - self.quadratic_curve_to(&q.ctrl, &q.to); - }); - } - fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D) -> bool; - fn add_segments(&mut self, path: &[PathSegment]) { - let mut build_ref = PathBuilderRef:: { - builder: self, - transform: Transform2D::identity(), - }; - for &seg in path { - match seg { - PathSegment::ClosePath => build_ref.close(), - PathSegment::MoveTo { x, y } => build_ref.move_to(&Point2D::new(x, y)), - PathSegment::LineTo { x, y } => build_ref.line_to(&Point2D::new(x, y)), - PathSegment::Quadratic { cpx, cpy, x, y } => { - build_ref.quadratic_curve_to(&Point2D::new(cpx, cpy), &Point2D::new(x, y)) - }, - PathSegment::Bezier { - cp1x, - cp1y, - cp2x, - cp2y, - x, - y, - } => build_ref.bezier_curve_to( - &Point2D::new(cp1x, cp1y), - &Point2D::new(cp2x, cp2y), - &Point2D::new(x, y), - ), - PathSegment::ArcTo { - cp1x, - cp1y, - cp2x, - cp2y, - radius, - } => build_ref.arc_to(&Point2D::new(cp1x, cp1y), &Point2D::new(cp2x, cp2y), radius), - PathSegment::Ellipse { - x, - y, - radius_x, - radius_y, - rotation, - start_angle, - end_angle, - anticlockwise, - } => build_ref.ellipse( - &Point2D::new(x, y), - radius_x, - radius_y, - rotation, - start_angle, - end_angle, - anticlockwise, - ), - PathSegment::SvgArc { - radius_x, - radius_y, - rotation, - large_arc, - sweep, - x, - y, - } => build_ref.svg_arc( - radius_x, - radius_y, - rotation, - large_arc, - sweep, - &Point2D::new(x, y), - ), - } - } - } - fn bounding_box(&self) -> Rect; -} - pub(crate) trait PatternHelpers { fn is_zero_size_gradient(&self) -> bool; fn x_bound(&self) -> Option; diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index 77755265c2c..b61e4d1e73a 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -26,7 +26,7 @@ use unicode_script::Script; use webrender_api::ImageKey; use crate::backend::{ - Backend, DrawOptionsHelpers as _, GenericDrawTarget as _, GenericPath, PatternHelpers, + Backend, DrawOptionsHelpers as _, GenericDrawTarget as _, PatternHelpers, StrokeOptionsHelpers as _, }; @@ -43,42 +43,32 @@ const MIN_WR_IMAGE_SIZE: Size2D = Size2D::new(1, 1); /// draw the path, we convert it back to userspace and draw it /// with the correct transform applied. /// TODO: De-abstract now that Azure is removed? -enum PathState { +enum PathState { /// Path in user-space. If a transform has been applied but /// but no further path operations have occurred, it is stored /// in the optional field. - UserSpacePath(B::Path, Option>), + UserSpacePath(Path, Option>), /// Path in device-space. - DeviceSpacePath(B::Path), + DeviceSpacePath(Path), } /// A wrapper around a stored PathBuilder and an optional transformation that should be /// applied to any points to ensure they are in the matching device space. -pub(crate) struct PathBuilderRef<'a, B: Backend> { - pub(crate) builder: &'a mut B::Path, +pub(crate) struct PathBuilderRef<'a> { + pub(crate) builder: &'a mut Path, pub(crate) transform: Transform2D, } -impl PathBuilderRef<'_, B> { - /// - pub(crate) fn ensure_there_is_a_subpath(&mut self, point: &Point2D) { - if self.builder.get_current_point().is_none() { - self.builder.move_to(*point); - } - } - +impl PathBuilderRef<'_> { /// pub(crate) fn line_to(&mut self, pt: &Point2D) { - // 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y). - self.ensure_there_is_a_subpath(pt); - - let pt = self.transform.transform_point(*pt); - self.builder.line_to(pt); + let pt = self.transform.transform_point(*pt).cast(); + self.builder.line_to(pt.x, pt.y); } pub(crate) fn move_to(&mut self, pt: &Point2D) { - let pt = self.transform.transform_point(*pt); - self.builder.move_to(pt); + let pt = self.transform.transform_point(*pt).cast(); + self.builder.move_to(pt.x, pt.y); } pub(crate) fn rect(&mut self, rect: &Rect) { @@ -101,13 +91,10 @@ impl PathBuilderRef<'_, B> { /// pub(crate) fn quadratic_curve_to(&mut self, cp: &Point2D, endpoint: &Point2D) { - // 2. Ensure there is a subpath for (cpx, cpy). - self.ensure_there_is_a_subpath(cp); - - self.builder.quadratic_curve_to( - &self.transform.transform_point(*cp), - &self.transform.transform_point(*endpoint), - ) + let cp = self.transform.transform_point(*cp).cast(); + let endpoint = self.transform.transform_point(*endpoint).cast(); + self.builder + .quadratic_curve_to(cp.x, cp.y, endpoint.x, endpoint.y) } /// @@ -117,14 +104,11 @@ impl PathBuilderRef<'_, B> { cp2: &Point2D, endpoint: &Point2D, ) { - // 2. Ensure there is a subpath for (cp1x, cp1y). - self.ensure_there_is_a_subpath(cp1); - - self.builder.bezier_curve_to( - &self.transform.transform_point(*cp1), - &self.transform.transform_point(*cp2), - &self.transform.transform_point(*endpoint), - ) + let cp1 = self.transform.transform_point(*cp1).cast(); + let cp2 = self.transform.transform_point(*cp2).cast(); + let endpoint = self.transform.transform_point(*endpoint).cast(); + self.builder + .bezier_curve_to(cp1.x, cp1.y, cp2.x, cp2.y, endpoint.x, endpoint.y) } pub(crate) fn arc( @@ -135,17 +119,23 @@ impl PathBuilderRef<'_, B> { end_angle: f32, ccw: bool, ) { - let center = self.transform.transform_point(*center); - self.builder - .arc(center, radius, start_angle, end_angle, ccw); + let center = self.transform.transform_point(*center).cast(); + let _ = self.builder.arc( + center.x, + center.y, + radius as f64, + start_angle as f64, + end_angle as f64, + ccw, + ); } /// pub(crate) fn arc_to(&mut self, cp1: &Point2D, cp2: &Point2D, radius: f32) { let cp0 = if let (Some(inverse), Some(point)) = - (self.transform.inverse(), self.builder.get_current_point()) + (self.transform.inverse(), self.builder.last_point()) { - inverse.transform_point(Point2D::new(point.x, point.y)) + inverse.transform_point(Point2D::new(point.x, point.y).cast()) } else { *cp1 }; @@ -218,40 +208,21 @@ impl PathBuilderRef<'_, B> { end_angle: f32, ccw: bool, ) { - let center = self.transform.transform_point(*center); - self.builder.ellipse( - center, - radius_x, - radius_y, - rotation_angle, - start_angle, - end_angle, + let center = self.transform.transform_point(*center).cast(); + let _ = self.builder.ellipse( + center.x, + center.y, + radius_x as f64, + radius_y as f64, + rotation_angle as f64, + start_angle as f64, + end_angle as f64, ccw, ); } - pub(crate) fn svg_arc( - &mut self, - radius_x: f32, - radius_y: f32, - rotation_angle: f32, - large_arc: bool, - sweep: bool, - end_point: &Point2D, - ) { - let end_point = self.transform.transform_point(*end_point); - self.builder.svg_arc( - radius_x, - radius_y, - rotation_angle, - large_arc, - sweep, - end_point, - ); - } - pub(crate) fn close(&mut self) { - self.builder.close(); + self.builder.close_path(); } } @@ -334,7 +305,7 @@ pub(crate) enum Filter { pub(crate) struct CanvasData<'a, B: Backend> { backend: B, drawtarget: B::DrawTarget, - path_state: Option>, + path_state: Option, state: CanvasPaintState<'a, B>, saved_states: Vec>, compositor_api: CrossProcessCompositorApi, @@ -746,10 +717,10 @@ impl<'a, B: Backend> CanvasData<'a, B> { /// Turn the [`Self::path_state`] into a user-space path, returning `None` if the /// path transformation matrix is uninvertible. - fn ensure_path(&mut self) -> Option<&B::Path> { + fn ensure_path(&mut self) -> Option<&Path> { // If there's no record of any path yet, create a new path in user-space. if self.path_state.is_none() { - self.path_state = Some(PathState::UserSpacePath(B::Path::new(), None)); + self.path_state = Some(PathState::UserSpacePath(Path::new(), None)); } // If a user-space path exists, create a device-space path based on it if @@ -757,7 +728,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { let new_state = match *self.path_state.as_ref().unwrap() { PathState::UserSpacePath(ref path, Some(ref transform)) => { let mut path = path.clone(); - path.transform(transform); + path.transform(transform.cast()); Some(path) }, PathState::UserSpacePath(..) | PathState::DeviceSpacePath(..) => None, @@ -772,7 +743,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { PathState::DeviceSpacePath(ref mut builder) => { let inverse = self.drawtarget.get_transform().inverse()?; let mut path = builder.clone(); - path.transform(&inverse); + path.transform(inverse.cast()); Some(path) }, PathState::UserSpacePath(..) => None, @@ -809,16 +780,13 @@ impl<'a, B: Backend> CanvasData<'a, B> { ) } - pub(crate) fn fill_path(&mut self, path_segments: &[PathSegment]) { + pub(crate) fn fill_path(&mut self, path: &Path) { if self.state.fill_style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } - let mut path = B::Path::new(); - path.add_segments(path_segments); - self.drawtarget - .fill(&path, &self.state.fill_style, &self.state.draw_options); + .fill(path, &self.state.fill_style, &self.state.draw_options); } pub(crate) fn stroke(&mut self) { @@ -844,20 +812,17 @@ impl<'a, B: Backend> CanvasData<'a, B> { ) } - pub(crate) fn stroke_path(&mut self, path_segments: &[PathSegment]) { + pub(crate) fn stroke_path(&mut self, path: &Path) { if self.state.stroke_style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } - let mut path = B::Path::new(); - path.add_segments(path_segments); - self.maybe_bound_shape_with_pattern( self.state.stroke_style.clone(), &path.bounding_box(), |self_| { self_.drawtarget.stroke( - &path, + path, &self_.state.stroke_style, &self_.state.stroke_opts, &self_.state.draw_options, @@ -874,17 +839,15 @@ impl<'a, B: Backend> CanvasData<'a, B> { self.drawtarget.push_clip(&path); } - pub(crate) fn clip_path(&mut self, path_segments: &[PathSegment]) { - let mut path = B::Path::new(); - path.add_segments(path_segments); - self.drawtarget.push_clip(&path); + pub(crate) fn clip_path(&mut self, path: &Path) { + self.drawtarget.push_clip(path); } pub(crate) fn is_point_in_path( &mut self, x: f64, y: f64, - _fill_rule: FillRule, + fill_rule: FillRule, chan: IpcSender, ) { self.ensure_path(); @@ -892,31 +855,15 @@ impl<'a, B: Backend> CanvasData<'a, B> { Some(PathState::UserSpacePath(path, transform)) => { let target_transform = self.drawtarget.get_transform(); let path_transform = transform.as_ref().unwrap_or(&target_transform); - path.contains_point(x, y, path_transform) + let mut path = path.clone(); + path.transform(path_transform.cast()); + path.is_point_in_path(x, y, fill_rule) }, Some(_) | None => false, }; chan.send(result).unwrap(); } - pub(crate) fn is_point_in_path_( - &mut self, - path_segments: &[PathSegment], - x: f64, - y: f64, - _fill_rule: FillRule, - chan: IpcSender, - ) { - let path_transform = match self.path_state.as_ref() { - Some(PathState::UserSpacePath(_, Some(transform))) => transform, - Some(_) | None => &self.drawtarget.get_transform(), - }; - let mut path = B::Path::new(); - path.add_segments(path_segments); - let result = path.contains_point(x, y, path_transform); - chan.send(result).unwrap(); - } - pub(crate) fn move_to(&mut self, point: &Point2D) { self.path_builder().move_to(point); } @@ -925,9 +872,9 @@ impl<'a, B: Backend> CanvasData<'a, B> { self.path_builder().line_to(point); } - fn path_builder(&mut self) -> PathBuilderRef { + fn path_builder(&mut self) -> PathBuilderRef { if self.path_state.is_none() { - self.path_state = Some(PathState::UserSpacePath(B::Path::new(), None)); + self.path_state = Some(PathState::UserSpacePath(Path::new(), None)); } // Rust is not pleased by returning a reference to a builder in some branches @@ -938,7 +885,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { PathState::DeviceSpacePath(_) => None, PathState::UserSpacePath(ref path, Some(ref transform)) => { let mut path = path.clone(); - path.transform(transform); + path.transform(transform.cast()); Some(PathState::DeviceSpacePath(path)) }, PathState::UserSpacePath(ref path, None) => { diff --git a/components/canvas/canvas_paint_thread.rs b/components/canvas/canvas_paint_thread.rs index eb28fe97037..553171d0233 100644 --- a/components/canvas/canvas_paint_thread.rs +++ b/components/canvas/canvas_paint_thread.rs @@ -156,7 +156,7 @@ impl<'a> CanvasPaintThread<'a> { }, Canvas2dMsg::FillPath(style, path) => { self.canvas(canvas_id).set_fill_style(style); - self.canvas(canvas_id).fill_path(&path[..]); + self.canvas(canvas_id).fill_path(&path); }, Canvas2dMsg::Stroke(style) => { self.canvas(canvas_id).set_stroke_style(style); @@ -164,16 +164,13 @@ impl<'a> CanvasPaintThread<'a> { }, Canvas2dMsg::StrokePath(style, path) => { self.canvas(canvas_id).set_stroke_style(style); - self.canvas(canvas_id).stroke_path(&path[..]); + self.canvas(canvas_id).stroke_path(&path); }, Canvas2dMsg::Clip => self.canvas(canvas_id).clip(), - Canvas2dMsg::ClipPath(path) => self.canvas(canvas_id).clip_path(&path[..]), + Canvas2dMsg::ClipPath(path) => self.canvas(canvas_id).clip_path(&path), Canvas2dMsg::IsPointInCurrentPath(x, y, fill_rule, chan) => self .canvas(canvas_id) .is_point_in_path(x, y, fill_rule, chan), - Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, chan) => self - .canvas(canvas_id) - .is_point_in_path_(&path[..], x, y, fill_rule, chan), Canvas2dMsg::DrawImage(snapshot, dest_rect, source_rect, smoothing_enabled) => { self.canvas(canvas_id).draw_image( snapshot.to_owned(), @@ -333,7 +330,7 @@ impl Canvas<'_> { } } - fn fill_path(&mut self, path: &[PathSegment]) { + fn fill_path(&mut self, path: &Path) { match self { Canvas::Raqote(canvas_data) => canvas_data.fill_path(path), } @@ -345,7 +342,7 @@ impl Canvas<'_> { } } - fn stroke_path(&mut self, path: &[PathSegment]) { + fn stroke_path(&mut self, path: &Path) { match self { Canvas::Raqote(canvas_data) => canvas_data.stroke_path(path), } @@ -363,21 +360,6 @@ impl Canvas<'_> { } } - fn is_point_in_path_( - &mut self, - path: &[PathSegment], - x: f64, - y: f64, - fill_rule: FillRule, - chan: IpcSender, - ) { - match self { - Canvas::Raqote(canvas_data) => { - canvas_data.is_point_in_path_(path, x, y, fill_rule, chan) - }, - } - } - fn clear_rect(&mut self, rect: &Rect) { match self { Canvas::Raqote(canvas_data) => canvas_data.clear_rect(rect), @@ -582,7 +564,7 @@ impl Canvas<'_> { } } - fn clip_path(&mut self, path: &[PathSegment]) { + fn clip_path(&mut self, path: &Path) { match self { Canvas::Raqote(canvas_data) => canvas_data.clip_path(path), } diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index 94a0fc6db9f..f6d72183b3d 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -8,20 +8,19 @@ use std::collections::HashMap; use canvas_traits::canvas::*; use compositing_traits::SerializableImageData; use cssparser::color::clamp_unit_f32; -use euclid::default::{Box2D, Point2D, Rect, Size2D, Transform2D, Vector2D}; +use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; use font_kit::font::Font; use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods}; use ipc_channel::ipc::IpcSharedMemory; use log::warn; use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat}; use range::Range; -use raqote::PathOp; +use raqote::PathBuilder; use style::color::AbsoluteColor; use webrender_api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat}; use crate::backend::{ - Backend, DrawOptionsHelpers, GenericDrawTarget, GenericPath, PatternHelpers, - StrokeOptionsHelpers, + Backend, DrawOptionsHelpers, GenericDrawTarget, PatternHelpers, StrokeOptionsHelpers, }; use crate::canvas_data::{CanvasPaintState, Filter, TextRun}; @@ -44,7 +43,6 @@ impl Backend for RaqoteBackend { type CompositionOp = raqote::BlendMode; type DrawTarget = raqote::DrawTarget; type SourceSurface = Vec; // TODO: See if we can avoid the alloc (probably?) - type Path = Path; type GradientStop = raqote::GradientStop; type GradientStops = Vec; @@ -346,7 +344,8 @@ fn create_gradient_stops(gradient_stops: Vec) -> Vec for raqote::DrawTarget { fn clear_rect(&mut self, rect: &Rect) { - let mut pb = raqote::PathBuilder::new(); + let rect = rect.cast(); + let mut pb = canvas_traits::canvas::Path::new(); pb.rect( rect.origin.x, rect.origin.y, @@ -356,7 +355,7 @@ impl GenericDrawTarget for raqote::DrawTarget { let mut options = raqote::DrawOptions::new(); options.blend_mode = raqote::BlendMode::Clear; let pattern = Pattern::Color(0, 0, 0, 0); - >::fill(self, &pb.into(), &pattern, &options); + >::fill(self, &pb, &pattern, &options); } #[allow(unsafe_code)] fn copy_surface( @@ -424,15 +423,15 @@ impl GenericDrawTarget for raqote::DrawTarget { transform, )); - let mut pb = raqote::PathBuilder::new(); + let mut pb = canvas_traits::canvas::Path::new(); pb.rect( - dest.origin.x as f32, - dest.origin.y as f32, - dest.size.width as f32, - dest.size.height as f32, + dest.origin.x, + dest.origin.y, + dest.size.width, + dest.size.height, ); - >::fill(self, &pb.into(), &pattern, draw_options); + >::fill(self, &pb, &pattern, draw_options); } fn draw_surface_with_shadow( &self, @@ -447,11 +446,11 @@ impl GenericDrawTarget for raqote::DrawTarget { } fn fill( &mut self, - path: &::Path, + path: &canvas_traits::canvas::Path, pattern: &Pattern, draw_options: &::DrawOptions, ) { - let path = path.into(); + let path = to_path(path); match draw_options.blend_mode { raqote::BlendMode::Src => { self.clear(raqote::SolidSource::from_unpremultiplied_argb(0, 0, 0, 0)); @@ -546,7 +545,8 @@ impl GenericDrawTarget for raqote::DrawTarget { pattern: &::Pattern<'_>, draw_options: &::DrawOptions, ) { - let mut pb = raqote::PathBuilder::new(); + let rect = rect.cast(); + let mut pb = canvas_traits::canvas::Path::new(); pb.rect( rect.origin.x, rect.origin.y, @@ -554,7 +554,7 @@ impl GenericDrawTarget for raqote::DrawTarget { rect.size.height, ); - >::fill(self, &pb.into(), pattern, draw_options); + >::fill(self, &pb, pattern, draw_options); } fn get_size(&self) -> Size2D { Size2D::new(self.width(), self.height()) @@ -565,8 +565,8 @@ impl GenericDrawTarget for raqote::DrawTarget { fn pop_clip(&mut self) { self.pop_clip(); } - fn push_clip(&mut self, path: &::Path) { - self.push_clip(&path.into()); + fn push_clip(&mut self, path: &canvas_traits::canvas::Path) { + self.push_clip(&to_path(path)); } fn push_clip_rect(&mut self, rect: &Rect) { self.push_clip_rect(rect.to_box2d()); @@ -579,12 +579,17 @@ impl GenericDrawTarget for raqote::DrawTarget { } fn stroke( &mut self, - path: &::Path, + path: &canvas_traits::canvas::Path, pattern: &Pattern, stroke_options: &::StrokeOptions, draw_options: &::DrawOptions, ) { - self.stroke(&path.into(), &source(pattern), stroke_options, draw_options); + self.stroke( + &to_path(path), + &source(pattern), + stroke_options, + draw_options, + ); } fn stroke_rect( &mut self, @@ -665,99 +670,6 @@ impl Clone for Path { } } -impl GenericPath for Path { - fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D) -> bool { - let path: raqote::Path = self.into(); - path.transform(path_transform) - .contains_point(0.01, x as f32, y as f32) - } - - fn new() -> Self { - Self(Cell::new(raqote::PathBuilder::new())) - } - - fn bezier_curve_to( - &mut self, - control_point1: &Point2D, - control_point2: &Point2D, - control_point3: &Point2D, - ) { - self.0.get_mut().cubic_to( - control_point1.x, - control_point1.y, - control_point2.x, - control_point2.y, - control_point3.x, - control_point3.y, - ); - } - - fn close(&mut self) { - self.0.get_mut().close(); - } - - fn get_current_point(&mut self) -> Option> { - let path: raqote::Path = (&*self).into(); - - path.ops.iter().last().and_then(|op| match op { - PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)), - PathOp::CubicTo(_, _, point) => Some(Point2D::new(point.x, point.y)), - PathOp::QuadTo(_, point) => Some(Point2D::new(point.x, point.y)), - PathOp::Close => path.ops.first().and_then(get_first_point), - }) - } - - fn line_to(&mut self, point: Point2D) { - self.0.get_mut().line_to(point.x, point.y); - } - - fn move_to(&mut self, point: Point2D) { - self.0.get_mut().move_to(point.x, point.y); - } - - fn quadratic_curve_to(&mut self, control_point: &Point2D, end_point: &Point2D) { - self.0 - .get_mut() - .quad_to(control_point.x, control_point.y, end_point.x, end_point.y); - } - - fn transform(&mut self, transform: &Transform2D) { - let path: raqote::Path = (&*self).into(); - self.0.set(path.transform(transform).into()); - } - - fn bounding_box(&self) -> Rect { - let path: raqote::Path = self.into(); - let mut points = vec![]; - for op in path.ops { - match op { - PathOp::MoveTo(p) => points.push(p), - PathOp::LineTo(p) => points.push(p), - PathOp::QuadTo(p1, p2) => { - points.push(p1); - points.push(p2); - }, - PathOp::CubicTo(p1, p2, p3) => { - points.push(p1); - points.push(p2); - points.push(p3); - }, - PathOp::Close => {}, - } - } - Box2D::from_points(points).to_rect().cast() - } -} - -fn get_first_point(op: &PathOp) -> Option> { - match op { - PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)), - PathOp::CubicTo(point, _, _) => Some(Point2D::new(point.x, point.y)), - PathOp::QuadTo(point, _) => Some(Point2D::new(point.x, point.y)), - PathOp::Close => None, - } -} - pub trait ToRaqoteStyle { type Target; @@ -934,3 +846,26 @@ impl ToRaqoteStyle for CompositionStyle { } } } + +fn to_path(path: &canvas_traits::canvas::Path) -> raqote::Path { + let mut pb = PathBuilder::new(); + for cmd in &path.0 { + match cmd { + kurbo::PathEl::MoveTo(kurbo::Point { x, y }) => pb.move_to(x as f32, y as f32), + kurbo::PathEl::LineTo(kurbo::Point { x, y }) => pb.line_to(x as f32, y as f32), + kurbo::PathEl::QuadTo(cp, p) => { + pb.quad_to(cp.x as f32, cp.y as f32, p.x as f32, p.y as f32) + }, + kurbo::PathEl::CurveTo(cp1, cp2, p) => pb.cubic_to( + cp1.x as f32, + cp1.y as f32, + cp2.x as f32, + cp2.y as f32, + p.x as f32, + p.y as f32, + ), + kurbo::PathEl::ClosePath => pb.close(), + } + } + pb.finish() +} diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 553ecd0e7bb..767bf2460a0 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -81,6 +81,7 @@ itertools = { workspace = true } js = { workspace = true } jstraceable_derive = { path = "../jstraceable_derive" } keyboard-types = { workspace = true } +kurbo = { workspace = true } layout_api = { workspace = true } libc = { workspace = true } log = { workspace = true } diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index e77b1ddc092..49767a77dd4 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use canvas_traits::canvas::{ Canvas2dMsg, CanvasId, CanvasMsg, CompositionOrBlending, Direction, FillOrStrokeStyle, - FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, PathSegment, RadialGradientStyle, + FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, Path, RadialGradientStyle, RepetitionStyle, TextAlign, TextBaseline, TextMetrics as CanvasTextMetrics, }; use constellation_traits::ScriptToConstellationMessage; @@ -1799,7 +1799,7 @@ impl CanvasState { } // https://html.spec.whatwg.org/multipage/#dom-context-2d-fill - pub(crate) fn fill_(&self, path: Vec, _fill_rule: CanvasFillRule) { + pub(crate) fn fill_(&self, path: Path, _fill_rule: CanvasFillRule) { // TODO: Process fill rule let style = self.state.borrow().fill_style.to_fill_or_stroke_style(); self.send_canvas_2d_msg(Canvas2dMsg::FillPath(style, path)); @@ -1811,7 +1811,7 @@ impl CanvasState { self.send_canvas_2d_msg(Canvas2dMsg::Stroke(style)); } - pub(crate) fn stroke_(&self, path: Vec) { + pub(crate) fn stroke_(&self, path: Path) { let style = self.state.borrow().stroke_style.to_fill_or_stroke_style(); self.send_canvas_2d_msg(Canvas2dMsg::StrokePath(style, path)); } @@ -1823,7 +1823,7 @@ impl CanvasState { } // https://html.spec.whatwg.org/multipage/#dom-context-2d-clip - pub(crate) fn clip_(&self, path: Vec, _fill_rule: CanvasFillRule) { + pub(crate) fn clip_(&self, path: Path, _fill_rule: CanvasFillRule) { // TODO: Process fill rule self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(path)); } @@ -1853,24 +1853,17 @@ impl CanvasState { // https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath pub(crate) fn is_point_in_path_( &self, - global: &GlobalScope, - path: Vec, + _global: &GlobalScope, + path: Path, x: f64, y: f64, fill_rule: CanvasFillRule, ) -> bool { - if !(x.is_finite() && y.is_finite()) { - return false; - } - let fill_rule = match fill_rule { CanvasFillRule::Nonzero => FillRule::Nonzero, CanvasFillRule::Evenodd => FillRule::Evenodd, }; - let (sender, receiver) = - profiled_ipc::channel::(global.time_profiler_chan().clone()).unwrap(); - self.send_canvas_2d_msg(Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, sender)); - receiver.recv().unwrap() + path.is_point_in_path(x, y, fill_rule) } // https://html.spec.whatwg.org/multipage/#dom-context-2d-scale diff --git a/components/script/dom/path2d.rs b/components/script/dom/path2d.rs index 476889cfbbc..982f4b35883 100644 --- a/components/script/dom/path2d.rs +++ b/components/script/dom/path2d.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; -use canvas_traits::canvas::PathSegment; +use canvas_traits::canvas::Path; use dom_struct::dom_struct; use js::rust::HandleObject; use script_bindings::str::DOMString; @@ -15,20 +15,20 @@ use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::DomRoot; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::CanGc; -use crate::svgpath::PathParser; #[dom_struct] pub(crate) struct Path2D { reflector_: Reflector, + #[ignore_malloc_size_of = "Defined in kurbo."] #[no_trace] - path: RefCell>, + path: RefCell, } impl Path2D { pub(crate) fn new() -> Path2D { Self { reflector_: Reflector::new(), - path: RefCell::new(vec![]), + path: RefCell::new(Path::new()), } } pub(crate) fn new_with_path(other: &Path2D) -> Path2D { @@ -37,26 +37,15 @@ impl Path2D { path: other.path.clone(), } } + pub(crate) fn new_with_str(path: &str) -> Path2D { - let mut path_segments = Vec::new(); - - for segment in PathParser::new(path) { - if let Ok(segment) = segment { - path_segments.push(segment); - } else { - break; - } - } - Self { reflector_: Reflector::new(), - path: RefCell::new(path_segments), + path: RefCell::new(Path::from_svg(path)), } } - pub(crate) fn push(&self, seg: PathSegment) { - self.path.borrow_mut().push(seg); - } - pub(crate) fn segments(&self) -> Vec { + + pub(crate) fn segments(&self) -> Path { self.path.borrow().clone() } } @@ -64,143 +53,49 @@ impl Path2D { impl Path2DMethods for Path2D { /// fn AddPath(&self, other: &Path2D) { + let other = other.segments(); // Step 7. Add all the subpaths in c to a. - if std::ptr::eq(&self.path, &other.path) { - // Note: this is not part of the spec, but it is a workaround to - // avoids borrow conflict when path is same as other.path - self.path.borrow_mut().extend_from_within(..); - } else { - let mut dest = self.path.borrow_mut(); - dest.extend(other.path.borrow().iter().copied()); - } + self.path.borrow_mut().0.extend(other.0); } /// fn ClosePath(&self) { - self.push(PathSegment::ClosePath); + self.path.borrow_mut().close_path(); } /// fn MoveTo(&self, x: f64, y: f64) { - // Step 1. If either of the arguments are infinite or NaN, then return. - if !(x.is_finite() && y.is_finite()) { - return; - } - - // Step 2. Create a new subpath with the specified point as its first (and only) point. - self.push(PathSegment::MoveTo { - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().move_to(x, y); } /// fn LineTo(&self, x: f64, y: f64) { - // Step 1. If either of the arguments are infinite or NaN, then return. - if !(x.is_finite() && y.is_finite()) { - return; - } - - self.push(PathSegment::LineTo { - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().line_to(x, y); } /// fn QuadraticCurveTo(&self, cpx: f64, cpy: f64, x: f64, y: f64) { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) { - return; - } - - self.push(PathSegment::Quadratic { - cpx: cpx as f32, - cpy: cpy as f32, - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().quadratic_curve_to(cpx, cpy, x, y); } /// fn BezierCurveTo(&self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(cp1x.is_finite() && - cp1y.is_finite() && - cp2x.is_finite() && - cp2y.is_finite() && - x.is_finite() && - y.is_finite()) - { - return; - } - - self.push(PathSegment::Bezier { - cp1x: cp1x as f32, - cp1y: cp1y as f32, - cp2x: cp2x as f32, - cp2y: cp2y as f32, - x: x as f32, - y: y as f32, - }); + self.path + .borrow_mut() + .bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y); } /// - fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, r: f64) -> Fallible<()> { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x1.is_finite() && y1.is_finite() && x2.is_finite() && y2.is_finite() && r.is_finite()) - { - return Ok(()); - } - - // Step 3. If radius is negative, then throw an "IndexSizeError" DOMException. - if r < 0.0 { - return Err(Error::IndexSize); - } - - self.push(PathSegment::ArcTo { - cp1x: x1 as f32, - cp1y: y1 as f32, - cp2x: x2 as f32, - cp2y: y2 as f32, - radius: r as f32, - }); - Ok(()) + fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) -> Fallible<()> { + self.path + .borrow_mut() + .arc_to(x1, y1, x2, y2, radius) + .map_err(|_| Error::IndexSize) } /// fn Rect(&self, x: f64, y: f64, w: f64, h: f64) { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) { - return; - } - // Step 2. Create a new subpath containing just the four points - // (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order, - // with those four points connected by straight lines. - self.push(PathSegment::MoveTo { - x: x as f32, - y: y as f32, - }); - self.push(PathSegment::LineTo { - x: (x + w) as f32, - y: y as f32, - }); - self.push(PathSegment::LineTo { - x: (x + w) as f32, - y: (y + h) as f32, - }); - self.push(PathSegment::LineTo { - x: x as f32, - y: (y + h) as f32, - }); - // Step 3. Mark the subpath as closed. - self.push(PathSegment::ClosePath); - - // Step 4. Create a new subpath with the point (x, y) as the only point in the subpath. - self.push(PathSegment::MoveTo { - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().rect(x, y, w, h); } /// @@ -208,37 +103,15 @@ impl Path2DMethods for Path2D { &self, x: f64, y: f64, - r: f64, - start: f64, - end: f64, - anticlockwise: bool, + radius: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, ) -> Fallible<()> { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x.is_finite() && - y.is_finite() && - r.is_finite() && - start.is_finite() && - end.is_finite()) - { - return Ok(()); - } - - // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException. - if r < 0.0 { - return Err(Error::IndexSize); - } - - self.push(PathSegment::Ellipse { - x: x as f32, - y: y as f32, - radius_x: r as f32, - radius_y: r as f32, - rotation: 0., - start_angle: start as f32, - end_angle: end as f32, - anticlockwise, - }); - Ok(()) + self.path + .borrow_mut() + .arc(x, y, radius, start_angle, end_angle, counterclockwise) + .map_err(|_| Error::IndexSize) } /// @@ -246,41 +119,26 @@ impl Path2DMethods for Path2D { &self, x: f64, y: f64, - rx: f64, - ry: f64, - rotation: f64, - start: f64, - end: f64, - anticlockwise: bool, + radius_x: f64, + radius_y: f64, + rotation_angle: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, ) -> Fallible<()> { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x.is_finite() && - y.is_finite() && - rx.is_finite() && - ry.is_finite() && - rotation.is_finite() && - start.is_finite() && - end.is_finite()) - { - return Ok(()); - } - - // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException. - if rx < 0.0 || ry < 0.0 { - return Err(Error::IndexSize); - } - - self.push(PathSegment::Ellipse { - x: x as f32, - y: y as f32, - radius_x: rx as f32, - radius_y: ry as f32, - rotation: rotation as f32, - start_angle: start as f32, - end_angle: end as f32, - anticlockwise, - }); - Ok(()) + self.path + .borrow_mut() + .ellipse( + x, + y, + radius_x, + radius_y, + rotation_angle, + start_angle, + end_angle, + counterclockwise, + ) + .map_err(|_| Error::IndexSize) } /// diff --git a/components/script/lib.rs b/components/script/lib.rs index 742f952d9d6..73564d85c38 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -74,8 +74,6 @@ mod drag_data_store; mod links; mod xpath; -mod svgpath; - pub use init::init; pub(crate) use script_bindings::DomTypes; pub use script_runtime::JSEngineSetup; diff --git a/components/script/svgpath/mod.rs b/components/script/svgpath/mod.rs deleted file mode 100644 index f3093e3d282..00000000000 --- a/components/script/svgpath/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -mod number; -mod path; -mod stream; - -#[derive(Debug, Default, Eq, PartialEq)] -pub struct Error; - -pub(crate) use path::PathParser; -pub(crate) use stream::Stream; diff --git a/components/script/svgpath/number.rs b/components/script/svgpath/number.rs deleted file mode 100644 index b199b357868..00000000000 --- a/components/script/svgpath/number.rs +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2018 the SVG Types Authors -// Copyright 2025 the Servo Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use std::str::FromStr; - -use crate::svgpath::{Error, Stream}; - -/// An [SVG number](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber). -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Number(pub f32); - -impl std::str::FromStr for Number { - type Err = Error; - - fn from_str(text: &str) -> Result { - let mut s = Stream::from(text); - let n = s.parse_number()?; - s.skip_spaces(); - if !s.at_end() { - return Err(Error); - } - - Ok(Self(n)) - } -} - -impl Stream<'_> { - /// Parses number from the stream. - /// - /// This method will detect a number length and then - /// will pass a substring to the `f64::from_str` method. - /// - /// - pub fn parse_number(&mut self) -> Result { - // Strip off leading whitespaces. - self.skip_spaces(); - - if self.at_end() { - return Err(Error); - } - - self.parse_number_impl().map_err(|_| Error) - } - - fn parse_number_impl(&mut self) -> Result { - let start = self.pos(); - - let mut c = self.curr_byte()?; - - // Consume sign. - if matches!(c, b'+' | b'-') { - self.advance(1); - c = self.curr_byte()?; - } - - // Consume integer. - match c { - b'0'..=b'9' => self.skip_digits(), - b'.' => {}, - _ => return Err(Error), - } - - // Consume fraction. - if let Ok(b'.') = self.curr_byte() { - self.advance(1); - self.skip_digits(); - } - - if let Ok(c) = self.curr_byte() { - if matches!(c, b'e' | b'E') { - let c2 = self.next_byte()?; - // Check for `em`/`ex`. - if c2 != b'm' && c2 != b'x' { - self.advance(1); - - match self.curr_byte()? { - b'+' | b'-' => { - self.advance(1); - self.skip_digits(); - }, - b'0'..=b'9' => self.skip_digits(), - _ => { - return Err(Error); - }, - } - } - } - } - - let s = self.slice_back(start); - - // Use the default f32 parser now. - if let Ok(n) = f32::from_str(s) { - // inf, nan, etc. are an error. - if n.is_finite() { - return Ok(n); - } - } - - Err(Error) - } - - /// Parses number from a list of numbers. - pub fn parse_list_number(&mut self) -> Result { - if self.at_end() { - return Err(Error); - } - - let n = self.parse_number()?; - self.skip_spaces(); - self.parse_list_separator(); - Ok(n) - } -} - -/// A pull-based [``] parser. -/// -/// # Examples -/// -/// ``` -/// use svgtypes::NumberListParser; -/// -/// let mut p = NumberListParser::from("10, 20 -50"); -/// assert_eq!(p.next().unwrap().unwrap(), 10.0); -/// assert_eq!(p.next().unwrap().unwrap(), 20.0); -/// assert_eq!(p.next().unwrap().unwrap(), -50.0); -/// assert_eq!(p.next().is_none(), true); -/// ``` -/// -/// [``]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct NumberListParser<'a>(Stream<'a>); - -impl<'a> From<&'a str> for NumberListParser<'a> { - #[inline] - fn from(v: &'a str) -> Self { - NumberListParser(Stream::from(v)) - } -} - -impl Iterator for NumberListParser<'_> { - type Item = Result; - - fn next(&mut self) -> Option { - if self.0.at_end() { - None - } else { - let v = self.0.parse_list_number(); - if v.is_err() { - self.0.jump_to_end(); - } - - Some(v) - } - } -} - -#[rustfmt::skip] -#[cfg(test)] -mod tests { - use crate::svgpath::Stream; - - macro_rules! test_p { - ($name:ident, $text:expr, $result:expr) => ( - #[test] - fn $name() { - let mut s = Stream::from($text); - assert_eq!(s.parse_number().unwrap(), $result); - } - ) - } - - test_p!(parse_1, "0", 0.0); - test_p!(parse_2, "1", 1.0); - test_p!(parse_3, "-1", -1.0); - test_p!(parse_4, " -1 ", -1.0); - test_p!(parse_5, " 1 ", 1.0); - test_p!(parse_6, ".4", 0.4); - test_p!(parse_7, "-.4", -0.4); - test_p!(parse_8, "-.4text", -0.4); - test_p!(parse_9, "-.01 text", -0.01); - test_p!(parse_10, "-.01 4", -0.01); - test_p!(parse_11, ".0000000000008", 0.0000000000008); - test_p!(parse_12, "1000000000000", 1000000000000.0); - test_p!(parse_13, "123456.123456", 123456.123456); - test_p!(parse_14, "+10", 10.0); - test_p!(parse_15, "1e2", 100.0); - test_p!(parse_16, "1e+2", 100.0); - test_p!(parse_17, "1E2", 100.0); - test_p!(parse_18, "1e-2", 0.01); - test_p!(parse_19, "1ex", 1.0); - test_p!(parse_20, "1em", 1.0); - test_p!(parse_21, "12345678901234567890", 12345678901234567000.0); - test_p!(parse_22, "0.", 0.0); - test_p!(parse_23, "1.3e-2", 0.013); - // test_number!(parse_24, "1e", 1.0); // TODO: this -} diff --git a/components/script/svgpath/path.rs b/components/script/svgpath/path.rs deleted file mode 100644 index 7d97df22cd2..00000000000 --- a/components/script/svgpath/path.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2021 the SVG Types Authors -// Copyright 2025 the Servo Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use canvas_traits::canvas::PathSegment; - -use crate::svgpath::{Error, Stream}; - -pub struct PathParser<'a> { - stream: Stream<'a>, - state: State, - last_cmd: u8, -} - -impl<'a> PathParser<'a> { - pub fn new(string: &'a str) -> Self { - Self { - stream: Stream::from(string), - state: State::default(), - last_cmd: b' ', - } - } -} - -impl Iterator for PathParser<'_> { - type Item = Result; - - fn next(&mut self) -> Option { - self.stream.skip_spaces(); - - let Ok(curr_byte) = self.stream.curr_byte() else { - return None; - }; - - let cmd = if self.last_cmd == b' ' { - if let move_to @ (b'm' | b'M') = curr_byte { - self.stream.advance(1); - move_to - } else { - return Some(Err(Error)); - } - } else if curr_byte.is_ascii_alphabetic() { - self.stream.advance(1); - curr_byte - } else { - match self.last_cmd { - b'm' => b'l', - b'M' => b'L', - b'z' | b'Z' => return Some(Err(Error)), - cmd => cmd, - } - }; - - self.last_cmd = cmd; - Some(to_point(&mut self.stream, cmd, &mut self.state)) - } -} - -#[derive(Default)] -pub struct State { - start: (f32, f32), - pos: (f32, f32), - quad: (f32, f32), - cubic: (f32, f32), -} - -pub fn to_point(s: &mut Stream<'_>, cmd: u8, state: &mut State) -> Result { - let abs = cmd.is_ascii_uppercase(); - let cmd = cmd.to_ascii_lowercase(); - let (dx, dy) = if abs { (0., 0.) } else { state.pos }; - let seg = match cmd { - b'm' => PathSegment::MoveTo { - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'l' => PathSegment::LineTo { - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'h' => PathSegment::LineTo { - x: s.parse_list_number()? + dx, - y: state.pos.1, - }, - b'v' => PathSegment::LineTo { - x: state.pos.0, - y: s.parse_list_number()? + dy, - }, - b'c' => PathSegment::Bezier { - cp1x: s.parse_list_number()? + dx, - cp1y: s.parse_list_number()? + dy, - cp2x: s.parse_list_number()? + dx, - cp2y: s.parse_list_number()? + dy, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b's' => PathSegment::Bezier { - cp1x: state.cubic.0, - cp1y: state.cubic.1, - cp2x: s.parse_list_number()? + dx, - cp2y: s.parse_list_number()? + dy, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'q' => PathSegment::Quadratic { - cpx: s.parse_list_number()? + dx, - cpy: s.parse_list_number()? + dy, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b't' => PathSegment::Quadratic { - cpx: state.quad.0, - cpy: state.quad.1, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'a' => PathSegment::SvgArc { - radius_x: s.parse_list_number()?, - radius_y: s.parse_list_number()?, - rotation: s.parse_list_number()?, - large_arc: s.parse_flag()?, - sweep: s.parse_flag()?, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'z' => PathSegment::ClosePath, - _ => return Err(crate::svgpath::Error), - }; - - match seg { - PathSegment::MoveTo { x, y } => { - state.start = (x, y); - state.pos = (x, y); - state.quad = (x, y); - state.cubic = (x, y); - }, - PathSegment::LineTo { x, y } | PathSegment::SvgArc { x, y, .. } => { - state.pos = (x, y); - state.quad = (x, y); - state.cubic = (x, y); - }, - PathSegment::Bezier { - cp2x, cp2y, x, y, .. - } => { - state.pos = (x, y); - state.quad = (x, y); - state.cubic = (x * 2.0 - cp2x, y * 2.0 - cp2y); - }, - PathSegment::Quadratic { cpx, cpy, x, y, .. } => { - state.pos = (x, y); - state.quad = (x * 2.0 - cpx, y * 2.0 - cpy); - state.cubic = (x, y); - }, - PathSegment::ClosePath => { - state.pos = state.start; - state.quad = state.start; - state.cubic = state.start; - }, - _ => {}, - } - - Ok(seg) -} - -#[rustfmt::skip] -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! test { - ($name:ident, $text:expr, $( $seg:expr ),*) => ( - #[test] - fn $name() { - let mut s = PathParser::new($text); - $( - assert_eq!(s.next().unwrap().unwrap(), $seg); - )* - - if let Some(res) = s.next() { - assert!(res.is_err()); - } - } - ) - } - - test!(null, "", ); - test!(not_a_path, "q", ); - test!(not_a_move_to, "L 20 30", ); - test!(stop_on_err_1, "M 10 20 L 30 40 L 50", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 } - ); - - test!(move_to_1, "M 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 }); - test!(move_to_2, "m 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 }); - test!(move_to_3, "M 10 20 30 40 50 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 60.0 } - ); - test!(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 60.0 }, - PathSegment::MoveTo { x: 70.0, y: 80.0 }, - PathSegment::LineTo { x: 90.0, y: 100.0 }, - PathSegment::LineTo { x: 110.0, y: 120.0 } - ); - - test!(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::SvgArc { - radius_x: 5.0, radius_y: 5.0, - rotation: 30.0, - large_arc: true, sweep: true, - x: 20.0, y: 20.0 - } - ); - - test!(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::SvgArc { - radius_x: 5.0, radius_y: 5.0, - rotation: 30.0, - large_arc: false, sweep: false, - x: 30.0, y: 40.0 - } - ); - - test!(arc_to_10, "M10-20A5.5.3-4 010-.1", - PathSegment::MoveTo { x: 10.0, y: -20.0 }, - PathSegment::SvgArc { - radius_x: 5.5, radius_y: 0.3, - rotation: -4.0, - large_arc: false, sweep: true, - x: 0.0, y: -0.1 - } - ); - - test!(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_5, "M10 20V30H40V50H60Z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 10.0, y: 30.0 }, - PathSegment::LineTo { x: 40.0, y: 30.0 }, - PathSegment::LineTo { x: 40.0, y: 50.0 }, - PathSegment::LineTo { x: 60.0, y: 50.0 }, - PathSegment::ClosePath - ); - - test!(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160 - Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 60.0 }, - PathSegment::Bezier { - cp1x: 70.0, cp1y: 80.0, - cp2x: 90.0, cp2y: 100.0, - x: 110.0, y: 120.0, - }, - PathSegment::Bezier { - cp1x: 130.0, cp1y: 140.0, - cp2x: 130.0, cp2y: 140.0, - x: 150.0, y: 160.0, - }, - PathSegment::Quadratic { - cpx: 170.0, cpy: 180.0, - x: 190.0, y: 200.0, - }, - PathSegment::Quadratic { - cpx: 210.0, cpy: 220.0, - x: 210.0, y: 220.0, - }, - PathSegment::SvgArc { - radius_x: 50.0, radius_y: 50.0, - rotation: 30.0, - large_arc: true, sweep: true, - x: 230.0, y: 240.0 - }, - PathSegment::ClosePath - ); - - test!(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160 - q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 40.0, y: 60.0 }, - PathSegment::LineTo { x: 90.0, y: 60.0 }, - PathSegment::LineTo { x: 90.0, y: 120.0 }, - PathSegment::Bezier { - cp1x: 160.0, cp1y: 200.0, - cp2x: 180.0, cp2y: 220.0, - x: 200.0, y: 240.0, - }, - PathSegment::Bezier { - cp1x: 220.0, cp1y: 260.0, //? - cp2x: 330.0, cp2y: 380.0, - x: 350.0, y: 400.0, - }, - PathSegment::Quadratic { - cpx: 520.0, cpy: 580.0, - x: 540.0, y: 600.0, - }, - PathSegment::Quadratic { - cpx: 560.0, cpy: 620.0, //? - x: 750.0, y: 820.0 - }, - PathSegment::SvgArc { - radius_x: 50.0, radius_y: 50.0, - rotation: 30.0, - large_arc: true, sweep: true, - x: 980.0, y: 1060.0 - }, - PathSegment::ClosePath - ); - - test!(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::ClosePath, - PathSegment::MoveTo { x: 100.0, y: 200.0 }, - PathSegment::LineTo { x: 300.0, y: 400.0 } - ); - - test!(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::ClosePath, - PathSegment::MoveTo { x: 100.0, y: 200.0 }, - PathSegment::LineTo { x: 300.0, y: 400.0 } - ); - - test!(close_path_3, "M10 20 L 30 40 Z Z Z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::ClosePath, - PathSegment::ClosePath, - PathSegment::ClosePath - ); - - // first token should be EndOfStream - test!(invalid_1, "M\t.", ); - - // ClosePath can't be followed by a number - test!(invalid_2, "M 0 0 Z 2", - PathSegment::MoveTo { x: 0.0, y: 0.0 }, - PathSegment::ClosePath - ); - - // ClosePath can be followed by any command - test!(invalid_3, "M 0 0 Z H 10", - PathSegment::MoveTo { x: 0.0, y: 0.0 }, - PathSegment::ClosePath, - PathSegment::LineTo { x: 10.0, y: 0.0 } - ); -} diff --git a/components/script/svgpath/stream.rs b/components/script/svgpath/stream.rs deleted file mode 100644 index 2ed044ba3b6..00000000000 --- a/components/script/svgpath/stream.rs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2018 the SVG Types Authors -// Copyright 2025 the Servo Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::svgpath::Error; - -/// A streaming text parsing interface. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct Stream<'a> { - text: &'a str, - pos: usize, -} - -impl<'a> From<&'a str> for Stream<'a> { - #[inline] - fn from(text: &'a str) -> Self { - Stream { text, pos: 0 } - } -} - -impl<'a> Stream<'a> { - /// Returns the current position in bytes. - #[inline] - pub fn pos(&self) -> usize { - self.pos - } - - /// Sets current position equal to the end. - /// - /// Used to indicate end of parsing on error. - #[inline] - pub fn jump_to_end(&mut self) { - self.pos = self.text.len(); - } - - /// Checks if the stream is reached the end. - /// - /// Any [`pos()`] value larger than original text length indicates stream end. - /// - /// Accessing stream after reaching end via safe methods will produce - /// an error. - /// - /// Accessing stream after reaching end via *_unchecked methods will produce - /// a Rust's bound checking error. - /// - /// [`pos()`]: #method.pos - #[inline] - pub fn at_end(&self) -> bool { - self.pos >= self.text.len() - } - - /// Returns a byte from a current stream position. - #[inline] - pub fn curr_byte(&self) -> Result { - if self.at_end() { - return Err(Error); - } - - Ok(self.curr_byte_unchecked()) - } - - /// Returns a byte from a current stream position. - /// - /// # Panics - /// - /// - if the current position is after the end of the data - #[inline] - pub fn curr_byte_unchecked(&self) -> u8 { - self.text.as_bytes()[self.pos] - } - - /// Checks that current byte is equal to provided. - /// - /// Returns `false` if no bytes left. - #[inline] - pub fn is_curr_byte_eq(&self, c: u8) -> bool { - if !self.at_end() { - self.curr_byte_unchecked() == c - } else { - false - } - } - - /// Returns a next byte from a current stream position. - #[inline] - pub fn next_byte(&self) -> Result { - if self.pos + 1 >= self.text.len() { - return Err(Error); - } - - Ok(self.text.as_bytes()[self.pos + 1]) - } - - /// Advances by `n` bytes. - #[inline] - pub fn advance(&mut self, n: usize) { - debug_assert!(self.pos + n <= self.text.len()); - self.pos += n; - } - - /// Skips whitespaces. - /// - /// Accepted values: `' ' \n \r \t`. - pub fn skip_spaces(&mut self) { - while !self.at_end() && matches!(self.curr_byte_unchecked(), b' ' | b'\t' | b'\n' | b'\r') { - self.advance(1); - } - } - - /// Consumes bytes by the predicate. - pub fn skip_bytes(&mut self, f: F) - where - F: Fn(&Stream<'_>, u8) -> bool, - { - while !self.at_end() { - let c = self.curr_byte_unchecked(); - if f(self, c) { - self.advance(1); - } else { - break; - } - } - } - - /// Slices data from `pos` to the current position. - #[inline] - pub fn slice_back(&self, pos: usize) -> &'a str { - &self.text[pos..self.pos] - } - - /// Skips digits. - pub fn skip_digits(&mut self) { - self.skip_bytes(|_, c| c.is_ascii_digit()); - } - - #[inline] - pub(crate) fn parse_list_separator(&mut self) { - if self.is_curr_byte_eq(b',') { - self.advance(1); - } - } - - // By the SVG spec 'large-arc' and 'sweep' must contain only one char - // and can be written without any separators, e.g.: 10 20 30 01 10 20. - pub(crate) fn parse_flag(&mut self) -> Result { - self.skip_spaces(); - - let c = self.curr_byte()?; - match c { - b'0' | b'1' => { - self.advance(1); - if self.is_curr_byte_eq(b',') { - self.advance(1); - } - self.skip_spaces(); - - Ok(c == b'1') - }, - _ => Err(Error), - } - } -} diff --git a/components/shared/canvas/Cargo.toml b/components/shared/canvas/Cargo.toml index 463d52b8c87..80ea22fc398 100644 --- a/components/shared/canvas/Cargo.toml +++ b/components/shared/canvas/Cargo.toml @@ -20,6 +20,7 @@ crossbeam-channel = { workspace = true } euclid = { workspace = true } glow = { workspace = true } ipc-channel = { workspace = true } +kurbo = { workspace = true, features = ["serde"] } malloc_size_of = { workspace = true } malloc_size_of_derive = { workspace = true } pixels = { path = "../../pixels" } diff --git a/components/shared/canvas/canvas.rs b/components/shared/canvas/canvas.rs index cc46be5cde2..f043ad26b9c 100644 --- a/components/shared/canvas/canvas.rs +++ b/components/shared/canvas/canvas.rs @@ -5,8 +5,11 @@ use std::default::Default; use std::str::FromStr; +use euclid::Angle; +use euclid::approxeq::ApproxEq; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; use ipc_channel::ipc::IpcSender; +use kurbo::{Affine, BezPath, ParamCurveNearest as _, PathEl, Point, Shape, Triangle}; use malloc_size_of_derive::MallocSizeOf; use pixels::IpcSnapshot; use serde::{Deserialize, Serialize}; @@ -14,57 +17,387 @@ use strum::{Display, EnumString}; use style::color::AbsoluteColor; use style::properties::style_structs::Font as FontStyleStruct; -#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] -pub enum PathSegment { - ClosePath, - MoveTo { - x: f32, - y: f32, - }, - LineTo { - x: f32, - y: f32, - }, - Quadratic { - cpx: f32, - cpy: f32, - x: f32, - y: f32, - }, - Bezier { - cp1x: f32, - cp1y: f32, - cp2x: f32, - cp2y: f32, - x: f32, - y: f32, - }, - ArcTo { - cp1x: f32, - cp1y: f32, - cp2x: f32, - cp2y: f32, - radius: f32, - }, - Ellipse { - x: f32, - y: f32, - radius_x: f32, - radius_y: f32, - rotation: f32, - start_angle: f32, - end_angle: f32, - anticlockwise: bool, - }, - SvgArc { - radius_x: f32, - radius_y: f32, - rotation: f32, - large_arc: bool, - sweep: bool, - x: f32, - y: f32, - }, +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Path(pub BezPath); + +pub struct IndexSizeError; + +impl Path { + pub fn new() -> Self { + Self(BezPath::new()) + } + + pub fn from_svg(s: &str) -> Self { + Self(BezPath::from_svg(s).unwrap_or_default()) + } + + pub fn transform(&mut self, transform: Transform2D) { + self.0.apply_affine(Affine::new(transform.to_array())); + } + + /// + pub fn ensure_there_is_a_subpath(&mut self, x: f64, y: f64) { + // The user agent must check to see if the path has its need new subpath flag set. + if self.0.elements().is_empty() { + // If it does, then the user agent must create a new subpath with the point (x, y) + // as its first (and only) point, + // as if the moveTo() method had been called, + // and must then unset the path's need new subpath flag. + self.0.move_to((x, y)); + } + } + + /// + pub fn close_path(&mut self) { + // must do nothing if the object's path has no subpaths + if matches!(self.0.elements().last(), None | Some(PathEl::ClosePath)) { + return; + } + // Otherwise, it must mark the last subpath as closed, + // create a new subpath whose first point is the same as the previous subpath's first point, + // and finally add this new subpath to the path. + self.0.close_path(); + } + + /// + pub fn move_to(&mut self, x: f64, y: f64) { + // Step 1. If either of the arguments are infinite or NaN, then return. + if !(x.is_finite() && y.is_finite()) { + return; + } + + // Step 2. Create a new subpath with the specified point as its first (and only) point. + self.0.move_to((x, y)); + } + + /// + pub fn line_to(&mut self, x: f64, y: f64) { + // Step 1. If either of the arguments are infinite or NaN, then return. + if !(x.is_finite() && y.is_finite()) { + return; + } + + // Step 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y). + self.ensure_there_is_a_subpath(x, y); + + // Step 3. Otherwise, connect the last point in the subpath to the given point (x, y) using a straight line, + // and then add the given point (x, y) to the subpath. + self.0.line_to((x, y)); + } + + /// + pub fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) { + return; + } + + // Step 2. Ensure there is a subpath for (cpx, cpy). + self.ensure_there_is_a_subpath(cpx, cpy); + + // 3. Connect the last point in the subpath to the given point (x, y) + // using a quadratic Bézier curve with control point (cpx, cpy). [BEZIER] + // 4. Add the given point (x, y) to the subpath. + self.0.quad_to((cpx, cpy), (x, y)); + } + + /// + pub fn bezier_curve_to(&mut self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(cp1x.is_finite() && + cp1y.is_finite() && + cp2x.is_finite() && + cp2y.is_finite() && + x.is_finite() && + y.is_finite()) + { + return; + } + + // Step 2. Ensure there is a subpath for (cp1x, cp1y). + self.ensure_there_is_a_subpath(cp1x, cp1y); + + // Step 3. Connect the last point in the subpath to the given point (x, y) + // using a cubic Bézier curve with control points (cp1x, cp1y) and (cp2x, cp2y). [BEZIER] + // Step 4. Add the point (x, y) to the subpath. + self.0.curve_to((cp1x, cp1y), (cp2x, cp2y), (x, y)); + } + + /// + pub fn arc_to( + &mut self, + x1: f64, + y1: f64, + x2: f64, + y2: f64, + radius: f64, + ) -> Result<(), IndexSizeError> { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(x1.is_finite() && + y1.is_finite() && + x2.is_finite() && + y2.is_finite() && + radius.is_finite()) + { + return Ok(()); + } + + // Step 2. Ensure there is a subpath for (x1, y1). + self.ensure_there_is_a_subpath(x1, y1); + + // Step 3. If either radius is negative, then throw an "IndexSizeError" DOMException. + if radius.is_sign_negative() { + return Err(IndexSizeError); + } + + // Step 4. Let the point (x0, y0) be the last point in the subpath. + let Point { x: x0, y: y0 } = self.last_point().unwrap(); + + // Step 5. If the point (x0, y0) is equal to the point (x1, y1), + // or if the point (x1, y1) is equal to the point (x2, y2), + // or if radius is zero, then add the point (x1, y1) to the subpath, + // and connect that point to the previous point (x0, y0) by a straight line. + if ((x0, y0) == (x1, y1)) || ((x1, y1) == (x2, y2)) || radius.approx_eq(&0.0) { + self.0.line_to((x1, y1)); + return Ok(()); + } + + // Step 6. Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) + // all lie on a single straight line, then add the point (x1, y1) to the subpath, + // and connect that point to the previous point (x0, y0) by a straight line. + let direction = Triangle::from_coords((x0, y0), (x1, y1), (x2, y2)).area(); + if direction == 0.0 { + self.0.line_to((x1, y1)); + return Ok(()); + } + + // Step 7. Otherwise, let The Arc be the shortest arc given by circumference of the circle + // that has radius radius, and that has one point tangent to the half-infinite line + // that crosses the point (x0, y0) and ends at the point (x1, y1), + // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1) + // and crosses the point (x2, y2). + // The points at which this circle touches these two lines are called the start + // and end tangent points respectively. + // Connect the point (x0, y0) to the start tangent point by a straight line, + // adding the start tangent point to the subpath, + // and then connect the start tangent point to the end tangent point by The Arc, + // adding the end tangent point to the subpath. + + let a2 = (x0 - x1).powi(2) + (y0 - y1).powi(2); + let b2 = (x1 - x2).powi(2) + (y1 - y2).powi(2); + let d = { + let c2 = (x0 - x2).powi(2) + (y0 - y2).powi(2); + let cosx = (a2 + b2 - c2) / (2.0 * (a2 * b2).sqrt()); + let sinx = (1.0 - cosx.powi(2)).sqrt(); + radius / ((1.0 - cosx) / sinx) + }; + + // first tangent point + let anx = (x1 - x0) / a2.sqrt(); + let any = (y1 - y0) / a2.sqrt(); + let tp1 = Point2D::new(x1 - anx * d, y1 - any * d); + + // second tangent point + let bnx = (x1 - x2) / b2.sqrt(); + let bny = (y1 - y2) / b2.sqrt(); + let tp2 = Point2D::new(x1 - bnx * d, y1 - bny * d); + + // arc center and angles + let anticlockwise = direction < 0.0; + let cx = tp1.x + any * radius * if anticlockwise { 1.0 } else { -1.0 }; + let cy = tp1.y - anx * radius * if anticlockwise { 1.0 } else { -1.0 }; + let angle_start = (tp1.y - cy).atan2(tp1.x - cx); + let angle_end = (tp2.y - cy).atan2(tp2.x - cx); + + self.0.line_to((tp1.x, tp2.x)); + + self.arc(cx, cy, radius, angle_start, angle_end, anticlockwise) + } + + pub fn last_point(&mut self) -> Option { + // https://github.com/linebender/kurbo/pull/462 + match self.0.elements().last()? { + PathEl::ClosePath => self + .0 + .elements() + .iter() + .rev() + .skip(1) + .take_while(|el| !matches!(el, PathEl::ClosePath)) + .last() + .and_then(|el| el.end_point()), + other => other.end_point(), + } + } + + #[allow(clippy::too_many_arguments)] + /// + pub fn arc( + &mut self, + x: f64, + y: f64, + radius: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, + ) -> Result<(), IndexSizeError> { + // ellipse() with both radii are equal and rotation is 0. + self.ellipse( + x, + y, + radius, + radius, + 0., + start_angle, + end_angle, + counterclockwise, + ) + } + + #[allow(clippy::too_many_arguments)] + /// + pub fn ellipse( + &mut self, + x: f64, + y: f64, + radius_x: f64, + radius_y: f64, + rotation_angle: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, + ) -> Result<(), IndexSizeError> { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(x.is_finite() && + y.is_finite() && + radius_x.is_finite() && + radius_y.is_finite() && + rotation_angle.is_finite() && + start_angle.is_finite() && + end_angle.is_finite()) + { + return Ok(()); + } + + // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException. + if radius_x.is_sign_negative() || radius_y.is_sign_negative() { + return Err(IndexSizeError); + } + + let mut start = Angle::radians(start_angle); + let mut end = Angle::radians(end_angle); + + // Wrap angles mod 2 * PI if necessary + if !counterclockwise && start > end + Angle::two_pi() || + counterclockwise && end > start + Angle::two_pi() + { + start = start.positive(); + end = end.positive(); + } + + // Calculate the total arc we're going to sweep. + let sweep = match counterclockwise { + true => { + if end - start == Angle::two_pi() { + -Angle::two_pi() + } else if end > start { + -(Angle::two_pi() - (end - start)) + } else { + -(start - end) + } + }, + false => { + if start - end == Angle::two_pi() { + Angle::two_pi() + } else if start > end { + Angle::two_pi() - (start - end) + } else { + end - start + } + }, + }; + + let arc = kurbo::Arc::new( + (x, y), + (radius_x, radius_y), + start.radians, + sweep.radians, + rotation_angle, + ); + + let mut iter = arc.path_elements(0.01); + let kurbo::PathEl::MoveTo(start_point) = iter.next().unwrap() else { + unreachable!() + }; + + self.line_to(start_point.x, start_point.y); + + if sweep.radians.abs() > 1e-3 { + self.0.extend(iter); + } + + Ok(()) + } + + /// + pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) { + return; + } + + // Step 2. Create a new subpath containing just the four points + // (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order, + // with those four points connected by straight lines. + self.0.move_to((x, y)); + self.0.line_to((x + w, y)); + self.0.line_to((x + w, y + h)); + self.0.line_to((x, y + h)); + + // Step 3. Mark the subpath as closed. + self.0.close_path(); + + // Step 4. Create a new subpath with the point (x, y) as the only point in the subpath. + self.0.move_to((x, y)); + } + + /// + pub fn is_point_in_path(&self, x: f64, y: f64, fill_rule: FillRule) -> bool { + let p = Point::new(x, y); + // Step 1. If x or y are infinite or NaN, then return false. + if !p.is_finite() { + return false; + } + + // Step 2. If the point given by the x and y coordinates, + // when treated as coordinates in the canvas coordinate space unaffected by the current transformation, + // is inside the intended path for path as determined by the fill rule indicated by fillRule, + // then return true. + // Open subpaths must be implicitly closed when computing the area inside the path, + // without affecting the actual subpaths. + let mut path = self.clone(); + path.close_path(); + let winding = path.0.winding(p); + let is_inside = match fill_rule { + FillRule::Nonzero => winding != 0, + FillRule::Evenodd => (winding % 2) != 0, + }; + if is_inside { + return true; + } + // Points on the path itself must be considered to be inside the path. + path.0 + .segments() + .any(|seg| seg.nearest(p, 0.00001).distance_sq < 0.00001) + } + + pub fn bounding_box(&self) -> Rect { + let rect = self.0.control_box(); + Rect::new( + Point2D::new(rect.origin().x, rect.origin().y), + Size2D::new(rect.width(), rect.height()), + ) + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -95,17 +428,16 @@ pub enum Canvas2dMsg { BezierCurveTo(Point2D, Point2D, Point2D), ClearRect(Rect), Clip, - ClipPath(Vec), + ClipPath(Path), ClosePath, Ellipse(Point2D, f32, f32, f32, f32, f32, bool), Fill(FillOrStrokeStyle), - FillPath(FillOrStrokeStyle, Vec), + FillPath(FillOrStrokeStyle, Path), FillText(String, f64, f64, Option, FillOrStrokeStyle, bool), FillRect(Rect, FillOrStrokeStyle), GetImageData(Rect, Size2D, IpcSender), GetTransform(IpcSender>), IsPointInCurrentPath(f64, f64, FillRule, IpcSender), - IsPointInPath(Vec, f64, f64, FillRule, IpcSender), LineTo(Point2D), MoveTo(Point2D), MeasureText(String, IpcSender), @@ -116,7 +448,7 @@ pub enum Canvas2dMsg { SaveContext, StrokeRect(Rect, FillOrStrokeStyle), Stroke(FillOrStrokeStyle), - StrokePath(FillOrStrokeStyle, Vec), + StrokePath(FillOrStrokeStyle, Path), SetLineWidth(f32), SetLineCap(LineCapStyle), SetLineJoin(LineJoinStyle), diff --git a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini deleted file mode 100644 index f5b107a7d84..00000000000 --- a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[2d.path.arc.shape.1.html] - [arc() from 0 to pi does not draw anything in the wrong half] - expected: FAIL diff --git a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini deleted file mode 100644 index 7be04136c27..00000000000 --- a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[2d.path.isPointInpath.multi.path.html] - [Verify the winding rule in isPointInPath works for path object.] - expected: FAIL - diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini deleted file mode 100644 index bd75f8ab39b..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[2d.path.arc.shape.1.html] - [arc() from 0 to pi does not draw anything in the wrong half] - expected: FAIL - diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini deleted file mode 100644 index c7698ce2f86..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini +++ /dev/null @@ -1,4 +0,0 @@ -[2d.path.arc.shape.1.worker.html] - [arc() from 0 to pi does not draw anything in the wrong half] - expected: FAIL - diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini deleted file mode 100644 index 6267ee28a52..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[2d.path.isPointInpath.multi.path.html] - [Verify the winding rule in isPointInPath works for path object.] - expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini deleted file mode 100644 index 43b5909c290..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini +++ /dev/null @@ -1,3 +0,0 @@ -[2d.path.isPointInpath.multi.path.worker.html] - [Verify the winding rule in isPointInPath works for path object.] - expected: FAIL