diff --git a/components/canvas/backend.rs b/components/canvas/backend.rs index 5da67feaf3f..78c48efc8c7 100644 --- a/components/canvas/backend.rs +++ b/components/canvas/backend.rs @@ -98,6 +98,7 @@ pub(crate) trait GenericDrawTarget { fn get_transform(&self) -> Transform2D; fn pop_clip(&mut self); fn push_clip(&mut self, path: &B::Path); + fn push_clip_rect(&mut self, rect: &Rect); fn set_transform(&mut self, matrix: &Transform2D); fn stroke( &mut self, @@ -309,11 +310,13 @@ pub(crate) trait GenericPath> { } } } + fn bounding_box(&self) -> Rect; } pub(crate) trait PatternHelpers { fn is_zero_size_gradient(&self) -> bool; - fn draw_rect(&self, rect: &Rect) -> Rect; + fn x_bound(&self) -> Option; + fn y_bound(&self) -> Option; } pub(crate) trait StrokeOptionsHelpers { @@ -327,4 +330,5 @@ pub(crate) trait StrokeOptionsHelpers { pub(crate) trait DrawOptionsHelpers { fn set_alpha(&mut self, val: f32); + fn is_clear(&self) -> bool; } diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index 87122aa4fc6..77755265c2c 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -492,11 +492,17 @@ impl<'a, B: Backend> CanvasData<'a, B> { // > from left to right (if any), adding to the array, for each glyph, the shape of the glyph // > as it is in the inline box, positioned on a coordinate space using CSS pixels with its // > origin is at the anchor point. - self.drawtarget.fill_text( - shaped_runs, - start, - &self.state.fill_style, - &self.state.draw_options, + self.maybe_bound_shape_with_pattern( + self.state.fill_style.clone(), + &Rect::from_size(Size2D::new(total_advance, size)), + |self_| { + self_.drawtarget.fill_text( + shaped_runs, + start, + &self_.state.fill_style, + &self_.state.draw_options, + ); + }, ); } @@ -676,19 +682,22 @@ impl<'a, B: Backend> CanvasData<'a, B> { return; // Paint nothing if gradient size is zero. } - let draw_rect = self.state.fill_style.draw_rect(rect); - if self.need_to_draw_shadow() { - self.draw_with_shadow(&draw_rect, |new_draw_target: &mut B::DrawTarget| { - new_draw_target.fill_rect( - &draw_rect, - &self.state.fill_style, - &self.state.draw_options, - ); + self.draw_with_shadow(rect, |new_draw_target: &mut B::DrawTarget| { + new_draw_target.fill_rect(rect, &self.state.fill_style, &self.state.draw_options); }); } else { - self.drawtarget - .fill_rect(&draw_rect, &self.state.fill_style, &self.state.draw_options); + self.maybe_bound_shape_with_pattern( + self.state.fill_style.clone(), + &rect.cast(), + |self_| { + self_.drawtarget.fill_rect( + rect, + &self_.state.fill_style, + &self_.state.draw_options, + ); + }, + ); } } @@ -711,12 +720,18 @@ impl<'a, B: Backend> CanvasData<'a, B> { ); }); } else { - self.drawtarget.stroke_rect( - rect, - &self.state.stroke_style, - &self.state.stroke_opts, - &self.state.draw_options, - ); + self.maybe_bound_shape_with_pattern( + self.state.stroke_style.clone(), + &rect.cast(), + |self_| { + self_.drawtarget.stroke_rect( + rect, + &self_.state.stroke_style, + &self_.state.stroke_opts, + &self_.state.draw_options, + ); + }, + ) } } @@ -781,11 +796,17 @@ impl<'a, B: Backend> CanvasData<'a, B> { return; // Path is uninvertible. }; - self.drawtarget.fill( - &path, - &self.state.fill_style, - &self.state.draw_options.clone(), - ); + self.maybe_bound_shape_with_pattern( + self.state.fill_style.clone(), + &path.bounding_box(), + |self_| { + self_.drawtarget.fill( + &path, + &self_.state.fill_style, + &self_.state.draw_options.clone(), + ); + }, + ) } pub(crate) fn fill_path(&mut self, path_segments: &[PathSegment]) { @@ -809,12 +830,18 @@ impl<'a, B: Backend> CanvasData<'a, B> { return; // Path is uninvertible. }; - self.drawtarget.stroke( - &path, - &self.state.stroke_style, - &self.state.stroke_opts, - &self.state.draw_options, - ); + self.maybe_bound_shape_with_pattern( + self.state.stroke_style.clone(), + &path.bounding_box(), + |self_| { + self_.drawtarget.stroke( + &path, + &self_.state.stroke_style, + &self_.state.stroke_opts, + &self_.state.draw_options, + ); + }, + ) } pub(crate) fn stroke_path(&mut self, path_segments: &[PathSegment]) { @@ -825,12 +852,18 @@ impl<'a, B: Backend> CanvasData<'a, B> { let mut path = B::Path::new(); path.add_segments(path_segments); - self.drawtarget.stroke( - &path, - &self.state.stroke_style, - &self.state.stroke_opts, - &self.state.draw_options, - ); + self.maybe_bound_shape_with_pattern( + self.state.stroke_style.clone(), + &path.bounding_box(), + |self_| { + self_.drawtarget.stroke( + &path, + &self_.state.stroke_style, + &self_.state.stroke_opts, + &self_.state.draw_options, + ); + }, + ) } pub(crate) fn clip(&mut self) { @@ -1183,6 +1216,34 @@ impl<'a, B: Backend> CanvasData<'a, B> { ); } + /// Push a clip to the draw target to respect the non-repeating bound (either x, y, or both) + /// of the given pattern. + fn maybe_bound_shape_with_pattern( + &mut self, + pattern: B::Pattern<'_>, + path_bound_box: &Rect, + draw_shape: F, + ) where + F: FnOnce(&mut Self), + { + let x_bound = pattern.x_bound(); + let y_bound = pattern.y_bound(); + // Clear operations are also unbounded. + if self.state.draw_options.is_clear() || (x_bound.is_none() && y_bound.is_none()) { + draw_shape(self); + return; + } + let rect = Rect::from_size(Size2D::new( + x_bound.unwrap_or(path_bound_box.size.width.ceil() as u32), + y_bound.unwrap_or(path_bound_box.size.height.ceil() as u32), + )) + .cast(); + let rect = self.get_transform().outer_transformed_rect(&rect); + self.drawtarget.push_clip_rect(&rect.cast()); + draw_shape(self); + self.drawtarget.pop_clip(); + } + /// It reads image data from the canvas /// canvas_size: The size of the canvas we're reading from /// read_rect: The area of the canvas we want to read from diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index 20c57408fff..94a0fc6db9f 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use canvas_traits::canvas::*; use compositing_traits::SerializableImageData; use cssparser::color::clamp_unit_f32; -use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; +use euclid::default::{Box2D, Point2D, Rect, Size2D, Transform2D, Vector2D}; use font_kit::font::Font; use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods}; use ipc_channel::ipc::IpcSharedMemory; @@ -197,15 +197,13 @@ impl SurfacePattern { transform, } } - pub fn size(&self) -> Size2D { - self.image.size().cast() - } + pub fn repetition(&self) -> &Repetition { &self.repeat } } -#[derive(Clone)] +#[derive(Clone, Copy, Debug)] pub enum Repetition { Repeat, RepeatX, @@ -284,33 +282,23 @@ impl PatternHelpers for Pattern { } } - fn draw_rect(&self, rect: &Rect) -> Rect { + fn x_bound(&self) -> Option { match self { - Pattern::Surface(pattern) => { - let pattern_rect = Rect::new(Point2D::origin(), pattern.size()); - let mut draw_rect = rect.intersection(&pattern_rect).unwrap_or(Rect::zero()); - - match pattern.repetition() { - Repetition::NoRepeat => { - draw_rect.size.width = draw_rect.size.width.min(pattern_rect.size.width); - draw_rect.size.height = draw_rect.size.height.min(pattern_rect.size.height); - }, - Repetition::RepeatX => { - draw_rect.size.width = rect.size.width; - draw_rect.size.height = draw_rect.size.height.min(pattern_rect.size.height); - }, - Repetition::RepeatY => { - draw_rect.size.height = rect.size.height; - draw_rect.size.width = draw_rect.size.width.min(pattern_rect.size.width); - }, - Repetition::Repeat => { - draw_rect = *rect; - }, - } - - draw_rect + Pattern::Surface(pattern) => match pattern.repetition() { + Repetition::RepeatX | Repetition::Repeat => None, // x is not bounded + Repetition::RepeatY | Repetition::NoRepeat => Some(pattern.image.size().width), }, - Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => *rect, + Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => None, + } + } + + fn y_bound(&self) -> Option { + match self { + Pattern::Surface(pattern) => match pattern.repetition() { + Repetition::RepeatY | Repetition::Repeat => None, // y is not bounded + Repetition::RepeatX | Repetition::NoRepeat => Some(pattern.image.size().height), + }, + Pattern::Color(..) | Pattern::LinearGradient(..) | Pattern::RadialGradient(..) => None, } } } @@ -340,6 +328,10 @@ impl DrawOptionsHelpers for raqote::DrawOptions { fn set_alpha(&mut self, val: f32) { self.alpha = val; } + + fn is_clear(&self) -> bool { + matches!(self.blend_mode, raqote::BlendMode::Clear) + } } fn create_gradient_stops(gradient_stops: Vec) -> Vec { @@ -576,6 +568,9 @@ impl GenericDrawTarget for raqote::DrawTarget { fn push_clip(&mut self, path: &::Path) { self.push_clip(&path.into()); } + fn push_clip_rect(&mut self, rect: &Rect) { + self.push_clip_rect(rect.to_box2d()); + } fn set_transform(&mut self, matrix: &Transform2D) { self.set_transform(matrix); } @@ -730,6 +725,28 @@ impl GenericPath for Path { 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> { diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index 38bbb5c06bc..547dfe75822 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -478477,7 +478477,7 @@ [] ], "fill-and-stroke-styles.yaml": [ - "f2888e648f42bd052fdc3f38c317866c7f153824", + "32402680bf14ee87815aafee0b33af3703c71c3b", [] ], "filters.yaml": [ @@ -703792,6 +703792,13 @@ {} ] ], + "2d.pattern.paint.norepeat.outside_arc.html": [ + "73159d1bbc6e40a1c361e393bd1b0ea33e30478c", + [ + null, + {} + ] + ], "2d.pattern.paint.orientation.canvas.html": [ "b68e04272cd62933a1f09da984adda4746a48e1a", [ @@ -714584,6 +714591,20 @@ {} ] ], + "2d.pattern.paint.norepeat.outside_arc.html": [ + "9052f14e5b501a98c82538504911013e08913bef", + [ + null, + {} + ] + ], + "2d.pattern.paint.norepeat.outside_arc.worker.js": [ + "96e27da06b0714ae27d1414908a5b320396fc84a", + [ + "html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.worker.html", + {} + ] + ], "2d.pattern.paint.orientation.canvas.html": [ "3fa8a96a4ad8a17559c613c761eb30a34b71445e", [ diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.html new file mode 100644 index 00000000000..73159d1bbc6 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.html @@ -0,0 +1,44 @@ + + + +Canvas test: 2d.pattern.paint.norepeat.outside_arc + + + + + + +

2d.pattern.paint.norepeat.outside_arc

+

+ + +

Actual output:

+

FAIL (fallback content)

+

Expected output:

+

    + + + diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.html b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.html new file mode 100644 index 00000000000..9052f14e5b5 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.html @@ -0,0 +1,40 @@ + + + +OffscreenCanvas test: 2d.pattern.paint.norepeat.outside_arc + + + + +

    2d.pattern.paint.norepeat.outside_arc

    +

    + + + diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.worker.js b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.worker.js new file mode 100644 index 00000000000..96e27da06b0 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.paint.norepeat.outside_arc.worker.js @@ -0,0 +1,33 @@ +// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. +// OffscreenCanvas test in a worker:2d.pattern.paint.norepeat.outside_arc +// Description: +// Note: + +importScripts("/resources/testharness.js"); +importScripts("/html/canvas/resources/canvas-tests.js"); + +promise_test(async t => { + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + var response = await fetch('/images/red-16x16.png') + var blob = await response.blob(); + var img = await createImageBitmap(blob); + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = pattern; + ctx.beginPath(); + ctx.arc(0, 0, 50, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 16, 16); + _assertPixel(canvas, 1,1, 0,255,0,255); + _assertPixel(canvas, 20,1, 0,255,0,255); + _assertPixel(canvas, 1,20, 0,255,0,255); + _assertPixel(canvas, 48,48, 0,255,0,255); +}, ""); +done(); diff --git a/tests/wpt/tests/html/canvas/tools/yaml/fill-and-stroke-styles.yaml b/tests/wpt/tests/html/canvas/tools/yaml/fill-and-stroke-styles.yaml index f2888e648f4..32402680bf1 100644 --- a/tests/wpt/tests/html/canvas/tools/yaml/fill-and-stroke-styles.yaml +++ b/tests/wpt/tests/html/canvas/tools/yaml/fill-and-stroke-styles.yaml @@ -1867,6 +1867,30 @@ expected: green variants: *load-image-variant-definition +- name: 2d.pattern.paint.norepeat.outside_arc + images: + - red-16x16.png + code: | + ctx.fillStyle = '#f00'; + ctx.fillRect(0, 0, 100, 50); + {{ load_image }} + var pattern = ctx.createPattern(img, 'no-repeat'); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 100, 50); + ctx.fillStyle = pattern; + ctx.beginPath(); + ctx.arc(0, 0, 50, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = '#0f0'; + ctx.fillRect(0, 0, 16, 16); + @assert pixel 1,1 == 0,255,0,255; + @assert pixel 20,1 == 0,255,0,255; + @assert pixel 1,20 == 0,255,0,255; + @assert pixel 48,48 == 0,255,0,255; + expected: green + variants: *load-image-variant-definition + - name: 2d.pattern.paint.norepeat.coord1 images: - green.png