canvas: Add CanvasTransform 'setTransform(transform)' method (#37692)

Follow the HTML canvas specification and add missing
'setTransform(transform)' method to CanvasTransform interface.

https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix

The third-party WebIDL doesn't support different extended attributes
on different overloads of methods, so 'Throws' attribute was added
to another 'setTransform(...)' method.
https://bugzilla.mozilla.org/show_bug.cgi?id=1020975

Testing: Improvements in the tests
- css/geometry/DOMMatrix*
-
html/canvas/element/transformations/2d.transformation.setTransform.multiple.html
-
html/canvas/offscreen/transformations/2d.transformation.setTransform.multiple

New failing tests due to disabled 'paint worklet' feature
- css/css-paint-api/setTransform-00*.https.html

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-06-25 19:11:54 +03:00 committed by GitHub
parent 64259de1f7
commit 59b99de90a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 226 additions and 362 deletions

View file

@ -42,6 +42,7 @@ use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
CanvasDirection, CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin,
CanvasTextAlign, CanvasTextBaseline, ImageDataMethods,
};
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::DOMMatrix2DInit;
use crate::dom::bindings::codegen::UnionTypes::StringOrCanvasGradientOrCanvasPattern;
use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
use crate::dom::bindings::inheritance::Castable;
@ -51,6 +52,7 @@ use crate::dom::bindings::str::DOMString;
use crate::dom::canvasgradient::{CanvasGradient, CanvasGradientStyle, ToFillOrStrokeStyle};
use crate::dom::canvaspattern::CanvasPattern;
use crate::dom::dommatrix::DOMMatrix;
use crate::dom::dommatrixreadonly::dommatrix2dinit_to_matrix;
use crate::dom::element::{Element, cors_setting_for_element};
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlcanvaselement::HTMLCanvasElement;
@ -1901,23 +1903,57 @@ impl CanvasState {
DOMMatrix::new(global, true, transform.cast::<f64>().to_3d(), can_gc)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform>
pub(crate) fn set_transform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
if !(a.is_finite() &&
b.is_finite() &&
c.is_finite() &&
d.is_finite() &&
e.is_finite() &&
f.is_finite())
// Step 1. If any of the arguments are infinite or NaN, then return.
if !a.is_finite() ||
!b.is_finite() ||
!c.is_finite() ||
!d.is_finite() ||
!e.is_finite() ||
!f.is_finite()
{
return;
}
// Step 2. Reset the current transformation matrix to the matrix described by:
self.state.borrow_mut().transform =
Transform2D::new(a as f32, b as f32, c as f32, d as f32, e as f32, f as f32);
self.update_transform()
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix>
pub(crate) fn set_transform_(&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 current transformation matrix to matrix.
self.state.borrow_mut().transform = 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,
);
self.update_transform();
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-resettransform
pub(crate) fn reset_transform(&self) {
self.state.borrow_mut().transform = Transform2D::identity();

View file

@ -18,6 +18,7 @@ use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
CanvasDirection, CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin,
CanvasRenderingContext2DMethods, CanvasTextAlign, CanvasTextBaseline,
};
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::DOMMatrix2DInit;
use crate::dom::bindings::codegen::UnionTypes::{
HTMLCanvasElementOrOffscreenCanvas, StringOrCanvasGradientOrCanvasPattern,
};
@ -235,9 +236,15 @@ impl CanvasRenderingContext2DMethods<crate::DomTypeHolder> for CanvasRenderingCo
self.canvas_state.get_transform(&self.global(), can_gc)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform
fn SetTransform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
self.canvas_state.set_transform(a, b, c, d, e, f)
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform>
fn SetTransform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> ErrorResult {
self.canvas_state.set_transform(a, b, c, d, e, f);
Ok(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix>
fn SetTransform_(&self, transform: &DOMMatrix2DInit) -> ErrorResult {
self.canvas_state.set_transform_(transform)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-resettransform

View file

@ -18,7 +18,9 @@ use url::Url;
use crate::dom::bindings::buffer_source::create_buffer_source;
use crate::dom::bindings::cell::{DomRefCell, Ref};
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::{DOMMatrixInit, DOMMatrixMethods};
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::{
DOMMatrix2DInit, DOMMatrixInit, DOMMatrixMethods,
};
use crate::dom::bindings::codegen::Bindings::DOMMatrixReadOnlyBinding::DOMMatrixReadOnlyMethods;
use crate::dom::bindings::codegen::Bindings::DOMPointBinding::DOMPointInit;
use crate::dom::bindings::codegen::UnionTypes::StringOrUnrestrictedDoubleSequence;
@ -966,80 +968,147 @@ pub(crate) fn entries_to_matrix(entries: &[f64]) -> Fallible<(bool, Transform3D<
}
}
// https://drafts.fxtf.org/geometry-1/#validate-and-fixup
pub(crate) fn dommatrixinit_to_matrix(dict: &DOMMatrixInit) -> Fallible<(bool, Transform3D<f64>)> {
// Step 1.
if dict.parent.a.is_some() &&
dict.parent.m11.is_some() &&
dict.parent.a.unwrap() != dict.parent.m11.unwrap() ||
dict.parent.b.is_some() &&
dict.parent.m12.is_some() &&
dict.parent.b.unwrap() != dict.parent.m12.unwrap() ||
dict.parent.c.is_some() &&
dict.parent.m21.is_some() &&
dict.parent.c.unwrap() != dict.parent.m21.unwrap() ||
dict.parent.d.is_some() &&
dict.parent.m22.is_some() &&
dict.parent.d.unwrap() != dict.parent.m22.unwrap() ||
dict.parent.e.is_some() &&
dict.parent.m41.is_some() &&
dict.parent.e.unwrap() != dict.parent.m41.unwrap() ||
dict.parent.f.is_some() &&
dict.parent.m42.is_some() &&
dict.parent.f.unwrap() != dict.parent.m42.unwrap() ||
dict.is2D.is_some() &&
dict.is2D.unwrap() &&
(dict.m31 != 0.0 ||
dict.m32 != 0.0 ||
dict.m13 != 0.0 ||
dict.m23 != 0.0 ||
dict.m43 != 0.0 ||
dict.m14 != 0.0 ||
dict.m24 != 0.0 ||
dict.m34 != 0.0 ||
dict.m33 != 1.0 ||
dict.m44 != 1.0)
/// <https://drafts.fxtf.org/geometry-1/#matrix-validate-and-fixup-2d>
fn validate_and_fixup_2d(dict: &DOMMatrix2DInit) -> Fallible<[f64; 6]> {
// <https://tc39.es/ecma262/#sec-numeric-types-number-sameValueZero>
let same_value_zero = |x: f64, y: f64| -> bool { x.is_nan() && y.is_nan() || x == y };
// Step 1. If if at least one of the following conditions are true for dict,
// then throw a TypeError exception and abort these steps.
if dict.a.is_some() &&
dict.m11.is_some() &&
!same_value_zero(dict.a.unwrap(), dict.m11.unwrap()) ||
dict.b.is_some() &&
dict.m12.is_some() &&
!same_value_zero(dict.b.unwrap(), dict.m12.unwrap()) ||
dict.c.is_some() &&
dict.m21.is_some() &&
!same_value_zero(dict.c.unwrap(), dict.m21.unwrap()) ||
dict.d.is_some() &&
dict.m22.is_some() &&
!same_value_zero(dict.d.unwrap(), dict.m22.unwrap()) ||
dict.e.is_some() &&
dict.m41.is_some() &&
!same_value_zero(dict.e.unwrap(), dict.m41.unwrap()) ||
dict.f.is_some() &&
dict.m42.is_some() &&
!same_value_zero(dict.f.unwrap(), dict.m42.unwrap())
{
Err(error::Error::Type("Invalid matrix initializer.".to_owned()))
} else {
let mut is_2d = dict.is2D;
// Step 2.
let m11 = dict.parent.m11.unwrap_or(dict.parent.a.unwrap_or(1.0));
// Step 3.
let m12 = dict.parent.m12.unwrap_or(dict.parent.b.unwrap_or(0.0));
// Step 4.
let m21 = dict.parent.m21.unwrap_or(dict.parent.c.unwrap_or(0.0));
// Step 5.
let m22 = dict.parent.m22.unwrap_or(dict.parent.d.unwrap_or(1.0));
// Step 6.
let m41 = dict.parent.m41.unwrap_or(dict.parent.e.unwrap_or(0.0));
// Step 7.
let m42 = dict.parent.m42.unwrap_or(dict.parent.f.unwrap_or(0.0));
// Step 8.
if is_2d.is_none() &&
(dict.m31 != 0.0 ||
dict.m32 != 0.0 ||
dict.m13 != 0.0 ||
dict.m23 != 0.0 ||
dict.m43 != 0.0 ||
dict.m14 != 0.0 ||
dict.m24 != 0.0 ||
dict.m34 != 0.0 ||
dict.m33 != 1.0 ||
dict.m44 != 1.0)
{
is_2d = Some(false);
}
// Step 9.
if is_2d.is_none() {
is_2d = Some(true);
}
let matrix = Transform3D::new(
m11, m12, dict.m13, dict.m14, m21, m22, dict.m23, dict.m24, dict.m31, dict.m32,
dict.m33, dict.m34, m41, m42, dict.m43, dict.m44,
);
Ok((is_2d.unwrap(), matrix))
return Err(error::Error::Type(
"Property mismatch on matrix initialization.".to_owned(),
));
}
// Step 2. If m11 is not present then set it to the value of member a,
// or value 1 if a is also not present.
let m11 = dict.m11.unwrap_or(dict.a.unwrap_or(1.0));
// Step 3. If m12 is not present then set it to the value of member b,
// or value 0 if b is also not present.
let m12 = dict.m12.unwrap_or(dict.b.unwrap_or(0.0));
// Step 4. If m21 is not present then set it to the value of member c,
// or value 0 if c is also not present.
let m21 = dict.m21.unwrap_or(dict.c.unwrap_or(0.0));
// Step 5. If m22 is not present then set it to the value of member d,
// or value 1 if d is also not present.
let m22 = dict.m22.unwrap_or(dict.d.unwrap_or(1.0));
// Step 6. If m41 is not present then set it to the value of member e,
// or value 0 if e is also not present.
let m41 = dict.m41.unwrap_or(dict.e.unwrap_or(0.0));
// Step 7. If m42 is not present then set it to the value of member f,
// or value 0 if f is also not present.
let m42 = dict.m42.unwrap_or(dict.f.unwrap_or(0.0));
Ok([m11, m12, m21, m22, m41, m42])
}
/// <https://drafts.fxtf.org/geometry-1/#matrix-validate-and-fixup>
fn validate_and_fixup(dict: &DOMMatrixInit) -> Fallible<(bool, [f64; 16])> {
// Step 1. Validate and fixup (2D) dict.
let entries = validate_and_fixup_2d(&dict.parent)?;
// Step 2. If is2D is true and: at least one of m13, m14, m23, m24, m31,
// m32, m34, m43 are present with a value other than 0 or -0, or at least
// one of m33, m44 are present with a value other than 1, then throw
// a TypeError exception and abort these steps.
if dict.is2D == Some(true) &&
(dict.m13 != 0.0 ||
dict.m14 != 0.0 ||
dict.m23 != 0.0 ||
dict.m24 != 0.0 ||
dict.m31 != 0.0 ||
dict.m32 != 0.0 ||
dict.m34 != 0.0 ||
dict.m43 != 0.0 ||
dict.m33 != 1.0 ||
dict.m44 != 1.0)
{
return Err(error::Error::Type(
"The is2D member is set to true but the input matrix is a 3d matrix.".to_owned(),
));
}
let mut is_2d = dict.is2D;
// Step 3. If is2D is not present and at least one of m13, m14, m23, m24,
// m31, m32, m34, m43 are present with a value other than 0 or -0, or at
// least one of m33, m44 are present with a value other than 1, set is2D
// to false.
if is_2d.is_none() &&
(dict.m13 != 0.0 ||
dict.m14 != 0.0 ||
dict.m23 != 0.0 ||
dict.m24 != 0.0 ||
dict.m31 != 0.0 ||
dict.m32 != 0.0 ||
dict.m34 != 0.0 ||
dict.m43 != 0.0 ||
dict.m33 != 1.0 ||
dict.m44 != 1.0)
{
is_2d = Some(false);
}
// Step 4. If is2D is still not present, set it to true.
if is_2d.is_none() {
is_2d = Some(true);
}
Ok((
is_2d.unwrap(),
[
entries[0], entries[1], dict.m13, dict.m14, entries[2], entries[3], dict.m23, dict.m24,
dict.m31, dict.m32, dict.m33, dict.m34, entries[4], entries[5], dict.m43, dict.m44,
],
))
}
/// <https://drafts.fxtf.org/geometry-1/#create-a-dommatrixreadonly-from-the-2d-dictionary>
pub(crate) fn dommatrix2dinit_to_matrix(dict: &DOMMatrix2DInit) -> Fallible<Transform3D<f64>> {
// Step 1. Validate and fixup (2D) other.
let entries = validate_and_fixup_2d(dict)?;
// Step 2. Return the result of invoking create a 2d matrix of type
// DOMMatrixReadOnly or DOMMatrix as appropriate, with a sequence of
// numbers, the values being the 6 elements m11, m12, m21, m22, m41 and m42
// of other in the given order.
Ok(create_2d_matrix(&entries))
}
/// <https://drafts.fxtf.org/geometry-1/#create-a-dommatrix-from-the-dictionary>
pub(crate) fn dommatrixinit_to_matrix(dict: &DOMMatrixInit) -> Fallible<(bool, Transform3D<f64>)> {
// Step 1. Validate and fixup other.
let (is_2d, entries) = validate_and_fixup(dict)?;
// Step 2. Return the result of invoking create a 3d matrix of type
// DOMMatrixReadOnly or DOMMatrix as appropriate, with a sequence of
// numbers, the values being the 16 elements m11, m12, m13, ..., m44
// of other in the given order.
Ok((is_2d, create_3d_matrix(&entries)))
}
#[inline]

View file

@ -13,6 +13,7 @@ use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
CanvasDirection, CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin,
CanvasTextAlign, CanvasTextBaseline,
};
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::DOMMatrix2DInit;
use crate::dom::bindings::codegen::Bindings::OffscreenCanvasRenderingContext2DBinding::OffscreenCanvasRenderingContext2DMethods;
use crate::dom::bindings::codegen::UnionTypes::StringOrCanvasGradientOrCanvasPattern;
use crate::dom::bindings::error::{ErrorResult, Fallible};
@ -529,11 +530,16 @@ impl OffscreenCanvasRenderingContext2DMethods<crate::DomTypeHolder>
self.context.GetTransform(can_gc)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform
fn SetTransform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform>
fn SetTransform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> ErrorResult {
self.context.SetTransform(a, b, c, d, e, f)
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix>
fn SetTransform_(&self, transform: &DOMMatrix2DInit) -> ErrorResult {
self.context.SetTransform_(transform)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-resettransform
fn ResetTransform(&self) {
self.context.ResetTransform()

View file

@ -17,6 +17,7 @@ use crate::canvas_state::CanvasState;
use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin,
};
use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::DOMMatrix2DInit;
use crate::dom::bindings::codegen::Bindings::PaintRenderingContext2DBinding::PaintRenderingContext2DMethods;
use crate::dom::bindings::codegen::UnionTypes::StringOrCanvasGradientOrCanvasPattern;
use crate::dom::bindings::error::{ErrorResult, Fallible};
@ -131,10 +132,18 @@ impl PaintRenderingContext2DMethods<crate::DomTypeHolder> for PaintRenderingCont
self.canvas_state.get_transform(&self.global(), can_gc)
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform
fn SetTransform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform>
fn SetTransform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> ErrorResult {
self.canvas_state.set_transform(a, b, c, d, e, f);
self.scale_by_device_pixel_ratio();
Ok(())
}
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix>
fn SetTransform_(&self, transform: &DOMMatrix2DInit) -> ErrorResult {
self.canvas_state.set_transform_(transform)?;
self.scale_by_device_pixel_ratio();
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-context-2d-resettransform