canvas: Respect FillRule (#38294)

We just need to pass user provided FillRule via IPC to canvas paint
thread, then pass it all down to backend, which will handle it.

Testing: Added WPT tests.

---------

Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
sagudev 2025-07-26 19:20:04 +02:00 committed by GitHub
parent 4188852963
commit bc71fb8c0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 693 additions and 32 deletions

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use canvas_traits::canvas::{
CompositionOptions, FillOrStrokeStyle, LineOptions, Path, ShadowOptions,
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions,
};
use compositing_traits::SerializableImageData;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
@ -47,6 +47,7 @@ pub(crate) trait GenericDrawTarget {
fn fill(
&mut self,
path: &Path,
fill_rule: FillRule,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
@ -68,7 +69,7 @@ pub(crate) trait GenericDrawTarget {
);
fn get_size(&self) -> Size2D<i32>;
fn pop_clip(&mut self);
fn push_clip(&mut self, path: &Path, transform: Transform2D<f32>);
fn push_clip(&mut self, path: &Path, fill_rule: FillRule, transform: Transform2D<f32>);
fn push_clip_rect(&mut self, rect: &Rect<i32>);
fn stroke(
&mut self,

View file

@ -567,6 +567,7 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
pub(crate) fn fill_path(
&mut self,
path: &Path,
fill_rule: FillRule,
style: FillOrStrokeStyle,
_shadow_options: ShadowOptions,
composition_options: CompositionOptions,
@ -584,7 +585,7 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
|self_, style| {
self_
.drawtarget
.fill(path, style, composition_options, transform)
.fill(path, fill_rule, style, composition_options, transform)
},
)
}
@ -615,8 +616,13 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
)
}
pub(crate) fn clip_path(&mut self, path: &Path, transform: Transform2D<f32>) {
self.drawtarget.push_clip(path, transform);
pub(crate) fn clip_path(
&mut self,
path: &Path,
fill_rule: FillRule,
transform: Transform2D<f32>,
) {
self.drawtarget.push_clip(path, fill_rule, transform);
}
/// <https://html.spec.whatwg.org/multipage/#reset-the-rendering-context-to-its-default-state>

View file

@ -173,9 +173,17 @@ impl CanvasPaintThread {
Canvas2dMsg::ClearRect(ref rect, transform) => {
self.canvas(canvas_id).clear_rect(rect, transform)
},
Canvas2dMsg::FillPath(style, path, shadow_options, composition_options, transform) => {
Canvas2dMsg::FillPath(
style,
path,
fill_rule,
shadow_options,
composition_options,
transform,
) => {
self.canvas(canvas_id).fill_path(
&path,
fill_rule,
style,
shadow_options,
composition_options,
@ -199,8 +207,9 @@ impl CanvasPaintThread {
transform,
);
},
Canvas2dMsg::ClipPath(path, transform) => {
self.canvas(canvas_id).clip_path(&path, transform);
Canvas2dMsg::ClipPath(path, fill_rule, transform) => {
self.canvas(canvas_id)
.clip_path(&path, fill_rule, transform);
},
Canvas2dMsg::DrawImage(
snapshot,
@ -412,19 +421,30 @@ impl Canvas {
fn fill_path(
&mut self,
path: &Path,
fill_rule: FillRule,
style: FillOrStrokeStyle,
shadow_options: ShadowOptions,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
match self {
Canvas::Raqote(canvas_data) => {
canvas_data.fill_path(path, style, shadow_options, composition_options, transform)
},
Canvas::Raqote(canvas_data) => canvas_data.fill_path(
path,
fill_rule,
style,
shadow_options,
composition_options,
transform,
),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => {
canvas_data.fill_path(path, style, shadow_options, composition_options, transform)
},
Canvas::Vello(canvas_data) => canvas_data.fill_path(
path,
fill_rule,
style,
shadow_options,
composition_options,
transform,
),
}
}
@ -515,11 +535,11 @@ impl Canvas {
}
}
fn clip_path(&mut self, path: &Path, transform: Transform2D<f32>) {
fn clip_path(&mut self, path: &Path, fill_rule: FillRule, transform: Transform2D<f32>) {
match self {
Canvas::Raqote(canvas_data) => canvas_data.clip_path(path, transform),
Canvas::Raqote(canvas_data) => canvas_data.clip_path(path, fill_rule, transform),
#[cfg(feature = "vello")]
Canvas::Vello(canvas_data) => canvas_data.clip_path(path, transform),
Canvas::Vello(canvas_data) => canvas_data.clip_path(path, fill_rule, transform),
}
}

View file

@ -207,3 +207,12 @@ impl Convert<peniko::ImageQuality> for Filter {
}
}
}
impl Convert<peniko::Fill> for FillRule {
fn convert(self) -> peniko::Fill {
match self {
FillRule::Nonzero => peniko::Fill::NonZero,
FillRule::Evenodd => peniko::Fill::EvenOdd,
}
}
}

View file

@ -277,6 +277,7 @@ impl GenericDrawTarget for raqote::DrawTarget {
fn fill(
&mut self,
path: &canvas_traits::canvas::Path,
fill_rule: FillRule,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
@ -284,7 +285,11 @@ impl GenericDrawTarget for raqote::DrawTarget {
self.set_transform(&transform);
let draw_options = draw_options(composition_options);
let pattern = style.to_raqote_pattern();
let path = to_path(path);
let mut path = to_path(path);
path.winding = match fill_rule {
FillRule::Nonzero => raqote::Winding::NonZero,
FillRule::Evenodd => raqote::Winding::EvenOdd,
};
fill_draw_target(self, draw_options, &source(&pattern), path);
}
@ -368,7 +373,14 @@ impl GenericDrawTarget for raqote::DrawTarget {
rect.size.height,
);
<Self as GenericDrawTarget>::fill(self, &pb, style, composition_options, transform);
<Self as GenericDrawTarget>::fill(
self,
&pb,
FillRule::Nonzero,
style,
composition_options,
transform,
);
}
fn get_size(&self) -> Size2D<i32> {
Size2D::new(self.width(), self.height())
@ -376,9 +388,19 @@ impl GenericDrawTarget for raqote::DrawTarget {
fn pop_clip(&mut self) {
self.pop_clip();
}
fn push_clip(&mut self, path: &canvas_traits::canvas::Path, transform: Transform2D<f32>) {
fn push_clip(
&mut self,
path: &canvas_traits::canvas::Path,
fill_rule: FillRule,
transform: Transform2D<f32>,
) {
self.set_transform(&transform);
self.push_clip(&to_path(path));
let mut path = to_path(path);
path.winding = match fill_rule {
FillRule::Nonzero => raqote::Winding::NonZero,
FillRule::Evenodd => raqote::Winding::EvenOdd,
};
self.push_clip(&path);
}
fn push_clip_rect(&mut self, rect: &Rect<i32>) {
self.push_clip_rect(rect.to_box2d());

View file

@ -17,7 +17,7 @@ use std::num::NonZeroUsize;
use std::rc::Rc;
use canvas_traits::canvas::{
CompositionOptions, FillOrStrokeStyle, LineOptions, Path, ShadowOptions,
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions,
};
use compositing_traits::SerializableImageData;
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
@ -311,13 +311,14 @@ impl GenericDrawTarget for VelloDrawTarget {
fn fill(
&mut self,
path: &Path,
fill_rule: FillRule,
style: FillOrStrokeStyle,
composition_options: CompositionOptions,
transform: Transform2D<f32>,
) {
self.with_draw_options(&composition_options, |self_| {
self_.scene.fill(
peniko::Fill::NonZero,
fill_rule.convert(),
transform.cast().into(),
&style
.convert()
@ -418,7 +419,7 @@ impl GenericDrawTarget for VelloDrawTarget {
self.scene.pop_layer();
}
fn push_clip(&mut self, path: &Path, transform: Transform2D<f32>) {
fn push_clip(&mut self, path: &Path, _fill_rule: FillRule, transform: Transform2D<f32>) {
self.scene
.push_layer(peniko::Mix::Clip, 1.0, transform.cast().into(), &path.0);
}
@ -432,7 +433,7 @@ impl GenericDrawTarget for VelloDrawTarget {
rect.size.width,
rect.size.height,
);
self.push_clip(&path, Transform2D::identity());
self.push_clip(&path, FillRule::Nonzero, Transform2D::identity());
}
fn stroke(

View file

@ -38,6 +38,7 @@ use url::Url;
use webrender_api::ImageKey;
use crate::canvas_context::{CanvasContext, OffscreenRenderingContext, RenderingContext};
use crate::conversions::Convert;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
CanvasDirection, CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin,
@ -1927,12 +1928,12 @@ impl CanvasState {
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
pub(crate) fn fill_(&self, path: Path, _fill_rule: CanvasFillRule) {
// TODO: Process fill rule
pub(crate) fn fill_(&self, path: Path, fill_rule: CanvasFillRule) {
let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
self.send_canvas_2d_msg(Canvas2dMsg::FillPath(
style,
path,
fill_rule.convert(),
self.state.borrow().shadow_options(),
self.state.borrow().composition_options(),
self.state.borrow().transform,
@ -1964,9 +1965,12 @@ impl CanvasState {
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
pub(crate) fn clip_(&self, path: Path, _fill_rule: CanvasFillRule) {
// TODO: Process fill rule
self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(path, self.state.borrow().transform));
pub(crate) fn clip_(&self, path: Path, fill_rule: CanvasFillRule) {
self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(
path,
fill_rule.convert(),
self.state.borrow().transform,
));
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
@ -2332,3 +2336,12 @@ fn adjust_canvas_size(size: Size2D<u64>) -> Size2D<u64> {
Size2D::zero()
}
}
impl Convert<FillRule> for CanvasFillRule {
fn convert(self) -> FillRule {
match self {
CanvasFillRule::Nonzero => FillRule::Nonzero,
CanvasFillRule::Evenodd => FillRule::Evenodd,
}
}
}

View file

@ -482,11 +482,12 @@ pub enum Canvas2dMsg {
Transform2D<f32>,
),
ClearRect(Rect<f32>, Transform2D<f32>),
ClipPath(Path, Transform2D<f32>),
ClipPath(Path, FillRule, Transform2D<f32>),
PopClip,
FillPath(
FillOrStrokeStyle,
Path,
FillRule,
ShadowOptions,
CompositionOptions,
Transform2D<f32>,