canvas: Add CanvasPattern 'setTranform(transform)' method (#37731)

Follow the HTML canvas specification and add missing
'setTransform(transform)' method to CanvasPattern interface.
https://html.spec.whatwg.org/multipage/#dom-canvaspattern-settransform

Testing: Improvements in the tests
- html/canvas/element/fill-and-stroke-styles/2d.pattern.transform*
- html/canvas/offscreen/fill-and-stroke-styles/2d.pattern.transform*

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-06-26 17:38:30 +03:00 committed by GitHub
parent 5286869b96
commit 3c16db2642
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 65 additions and 51 deletions

View file

@ -121,17 +121,6 @@ pub enum Pattern<'a> {
Surface(SurfacePattern<'a>), Surface(SurfacePattern<'a>),
} }
impl Pattern<'_> {
fn set_transform(&mut self, transform: Transform2D<f32>) {
match self {
Pattern::Surface(pattern) => pattern.set_transform(transform),
Pattern::LinearGradient(..) | Pattern::RadialGradient(..) | Pattern::Color(..) => {
warn!("transform not supported")
},
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct LinearGradientPattern { pub struct LinearGradientPattern {
gradient: raqote::Gradient, gradient: raqote::Gradient,
@ -186,7 +175,12 @@ pub struct SurfacePattern<'a> {
} }
impl<'a> SurfacePattern<'a> { impl<'a> SurfacePattern<'a> {
fn new(image: raqote::Image<'a>, filter: raqote::FilterMode, repeat: Repetition) -> Self { fn new(
image: raqote::Image<'a>,
filter: raqote::FilterMode,
repeat: Repetition,
transform: Transform2D<f32>,
) -> Self {
let extend = match repeat { let extend = match repeat {
Repetition::NoRepeat => raqote::ExtendMode::Pad, Repetition::NoRepeat => raqote::ExtendMode::Pad,
Repetition::RepeatX | Repetition::RepeatY | Repetition::Repeat => { Repetition::RepeatX | Repetition::RepeatY | Repetition::Repeat => {
@ -198,12 +192,9 @@ impl<'a> SurfacePattern<'a> {
filter, filter,
extend, extend,
repeat, repeat,
transform: Transform2D::identity(), transform,
} }
} }
fn set_transform(&mut self, transform: Transform2D<f32>) {
self.transform = transform;
}
pub fn size(&self) -> Size2D<f32> { pub fn size(&self) -> Size2D<f32> {
Size2D::new(self.image.width as f32, self.image.height as f32) Size2D::new(self.image.width as f32, self.image.height as f32)
} }
@ -427,18 +418,19 @@ impl GenericDrawTarget<RaqoteBackend> for raqote::DrawTarget {
}, },
}; };
let mut pattern = Pattern::Surface(SurfacePattern::new(
image,
filter.to_raqote(),
Repetition::NoRepeat,
));
let transform = let transform =
raqote::Transform::translation(-dest.origin.x as f32, -dest.origin.y as f32) raqote::Transform::translation(-dest.origin.x as f32, -dest.origin.y as f32)
.then_scale( .then_scale(
image.width as f32 / dest.size.width as f32, image.width as f32 / dest.size.width as f32,
image.height as f32 / dest.size.height as f32, image.height as f32 / dest.size.height as f32,
); );
pattern.set_transform(transform);
let pattern = Pattern::Surface(SurfacePattern::new(
image,
filter.to_raqote(),
Repetition::NoRepeat,
transform,
));
let mut pb = raqote::PathBuilder::new(); let mut pb = raqote::PathBuilder::new();
pb.rect( pb.rect(
@ -809,6 +801,7 @@ impl ToRaqotePattern<'_> for FillOrStrokeStyle {
image, image,
raqote::FilterMode::Nearest, raqote::FilterMode::Nearest,
repeat, repeat,
style.transform,
))) )))
}, },
} }

View file

@ -4,15 +4,20 @@
use canvas_traits::canvas::{FillOrStrokeStyle, RepetitionStyle, SurfaceStyle}; use canvas_traits::canvas::{FillOrStrokeStyle, RepetitionStyle, SurfaceStyle};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use euclid::default::Size2D; use euclid::default::{Size2D, Transform2D};
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::CanvasPatternMethods;
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::DOMMatrix2DInit;
use crate::dom::bindings::error::ErrorResult;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::DomRoot;
use crate::dom::canvasgradient::ToFillOrStrokeStyle; use crate::dom::canvasgradient::ToFillOrStrokeStyle;
use crate::dom::dommatrixreadonly::dommatrix2dinit_to_matrix;
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
// https://html.spec.whatwg.org/multipage/#canvaspattern /// <https://html.spec.whatwg.org/multipage/#canvaspattern>
#[dom_struct] #[dom_struct]
pub(crate) struct CanvasPattern { pub(crate) struct CanvasPattern {
reflector_: Reflector, reflector_: Reflector,
@ -21,6 +26,8 @@ pub(crate) struct CanvasPattern {
surface_size: Size2D<u32>, surface_size: Size2D<u32>,
repeat_x: bool, repeat_x: bool,
repeat_y: bool, repeat_y: bool,
#[no_trace]
transform: DomRefCell<Transform2D<f32>>,
origin_clean: bool, origin_clean: bool,
} }
@ -44,6 +51,7 @@ impl CanvasPattern {
surface_size, surface_size,
repeat_x: x, repeat_x: x,
repeat_y: y, repeat_y: y,
transform: DomRefCell::new(Transform2D::identity()),
origin_clean, origin_clean,
} }
} }
@ -71,6 +79,40 @@ impl CanvasPattern {
} }
} }
impl CanvasPatternMethods<crate::DomTypeHolder> for CanvasPattern {
/// <https://html.spec.whatwg.org/multipage/#dom-canvaspattern-settransform>
fn SetTransform(&self, transform: &DOMMatrix2DInit) -> ErrorResult {
// Step 1. Let matrix be the result of creating a DOMMatrix from the 2D
// dictionary transform.
let matrix = dommatrix2dinit_to_matrix(transform)?;
// Step 2. If one or more of matrix's m11 element, m12 element, m21
// element, m22 element, m41 element, or m42 element are infinite or
// NaN, then return.
if !matrix.m11.is_finite() ||
!matrix.m12.is_finite() ||
!matrix.m21.is_finite() ||
!matrix.m22.is_finite() ||
!matrix.m41.is_finite() ||
!matrix.m42.is_finite()
{
return Ok(());
}
// Step 3. Reset the pattern's transformation matrix to matrix.
*self.transform.borrow_mut() = Transform2D::new(
matrix.m11 as f32,
matrix.m12 as f32,
matrix.m21 as f32,
matrix.m22 as f32,
matrix.m41 as f32,
matrix.m42 as f32,
);
Ok(())
}
}
impl ToFillOrStrokeStyle for &CanvasPattern { impl ToFillOrStrokeStyle for &CanvasPattern {
fn to_fill_or_stroke_style(self) -> FillOrStrokeStyle { fn to_fill_or_stroke_style(self) -> FillOrStrokeStyle {
FillOrStrokeStyle::Surface(SurfaceStyle::new( FillOrStrokeStyle::Surface(SurfaceStyle::new(
@ -78,6 +120,7 @@ impl ToFillOrStrokeStyle for &CanvasPattern {
self.surface_size, self.surface_size,
self.repeat_x, self.repeat_x,
self.repeat_y, self.repeat_y,
*self.transform.borrow(),
)) ))
} }
} }

View file

@ -254,7 +254,8 @@ interface CanvasGradient {
[Exposed=(Window, PaintWorklet, Worker)] [Exposed=(Window, PaintWorklet, Worker)]
interface CanvasPattern { interface CanvasPattern {
// opaque object // opaque object
//undefined setTransform(optional DOMMatrix2DInit transform = {}); [Throws]
undefined setTransform(optional DOMMatrix2DInit transform = {});
}; };
// TODO: Float16Array // TODO: Float16Array

View file

@ -214,6 +214,7 @@ pub struct SurfaceStyle {
pub surface_size: Size2D<u32>, pub surface_size: Size2D<u32>,
pub repeat_x: bool, pub repeat_x: bool,
pub repeat_y: bool, pub repeat_y: bool,
pub transform: Transform2D<f32>,
} }
impl SurfaceStyle { impl SurfaceStyle {
@ -222,12 +223,14 @@ impl SurfaceStyle {
surface_size: Size2D<u32>, surface_size: Size2D<u32>,
repeat_x: bool, repeat_x: bool,
repeat_y: bool, repeat_y: bool,
transform: Transform2D<f32>,
) -> Self { ) -> Self {
Self { Self {
surface_data: ByteBuf::from(surface_data), surface_data: ByteBuf::from(surface_data),
surface_size, surface_size,
repeat_x, repeat_x,
repeat_y, repeat_y,
transform,
} }
} }
} }

View file

@ -1,4 +0,0 @@
[2d.pattern.transform.identity.html]
[Canvas test: 2d.pattern.transform.identity]
expected: FAIL

View file

@ -1,4 +0,0 @@
[2d.pattern.transform.infinity.html]
[Canvas test: 2d.pattern.transform.infinity]
expected: FAIL

View file

@ -1,3 +0,0 @@
[2d.pattern.transform.identity.html]
[OffscreenCanvas test: 2d.pattern.transform.identity]
expected: FAIL

View file

@ -1,3 +0,0 @@
[2d.pattern.transform.identity.worker.html]
[2d]
expected: FAIL

View file

@ -1,3 +0,0 @@
[2d.pattern.transform.infinity.html]
[OffscreenCanvas test: 2d.pattern.transform.infinity]
expected: FAIL

View file

@ -1,3 +0,0 @@
[2d.pattern.transform.infinity.worker.html]
[2d]
expected: FAIL

View file

@ -1,7 +1,4 @@
[idlharness.any.worker.html] [idlharness.any.worker.html]
[CanvasPattern interface: operation setTransform(optional DOMMatrix2DInit)]
expected: FAIL
[Path2D interface: operation roundRect(unrestricted double, unrestricted double, unrestricted double, unrestricted double, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>))] [Path2D interface: operation roundRect(unrestricted double, unrestricted double, unrestricted double, unrestricted double, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>))]
expected: FAIL expected: FAIL

View file

@ -4343,9 +4343,6 @@
[CanvasRenderingContext2D interface: calling roundRect(unrestricted double, unrestricted double, unrestricted double, unrestricted double, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>)) on document.createElement("canvas").getContext("2d") with too few arguments must throw TypeError] [CanvasRenderingContext2D interface: calling roundRect(unrestricted double, unrestricted double, unrestricted double, unrestricted double, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>)) on document.createElement("canvas").getContext("2d") with too few arguments must throw TypeError]
expected: FAIL expected: FAIL
[CanvasPattern interface: operation setTransform(optional DOMMatrix2DInit)]
expected: FAIL
[Path2D interface: operation roundRect(unrestricted double, unrestricted double, unrestricted double, unrestricted double, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>))] [Path2D interface: operation roundRect(unrestricted double, unrestricted double, unrestricted double, unrestricted double, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>))]
expected: FAIL expected: FAIL