Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[geometry-1] Improve algorithm steps for identity transforms #579

Open
trusktr opened this issue Dec 27, 2024 · 2 comments
Open

[geometry-1] Improve algorithm steps for identity transforms #579

trusktr opened this issue Dec 27, 2024 · 2 comments

Comments

@trusktr
Copy link

trusktr commented Dec 27, 2024

There's a discrepancy between browsers. This code:

new DOMMatrix().rotateAxisAngleSelf(1, 0, 0, 0).toString()

Outputs different values across browsers:

  • Firefox: matrix(1, 0, 0, 1, 0, 0)
  • Chrome, Safari, and Edge: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)

Technically all browsers except Firefox are "correct" because they do exactly what the spec says:

  1. Post-multiply a rotation transformation on the current matrix around the specified vector x, y, z by the specified rotation angle in degrees. The 3D rotation matrix is described in CSS Transforms with alpha = angle in degrees. [CSS3-TRANSFORMS]
  2. If x or y are not 0 or -0, set is 2D of the current matrix to false.
  3. Return the current matrix.

However, Firefox's behavior is better: if the rotation is zero, then nothing needs to be done, the rotateAxisAngleSelf method can return early to avoid unnecessary work, and in this case is2D should remain true because basically "nothing happened" due to the rotation being zero.

I believe it would be best if the spec did the following, and maybe we can change the spec because browsers are not currently aligned, however it might be easier for a single browser (Firefox) to adopt the less desirable current spec behavior.

Here's what I would change the algorithm steps to:

  1. If angle is 0 or -0 return the current matrix.
  2. Post-multiply a rotation transformation on the current matrix around the specified vector x, y, z by the specified rotation angle in degrees. The 3D rotation matrix is described in CSS Transforms with alpha = angle in degrees. [CSS3-TRANSFORMS]
  3. If x or y are not 0 or -0, set is 2D of the current matrix to false.
  4. Return the current matrix.

If "nothing happened", then there should be no effects, arbitrarily switching from 2D to 3D due to a zero-angle rotation is unnecessary.

We can also improve other algorithm steps, for example translateSelf can return early if x, y, and z translation values are all zero.

This,

new DOMMatrix().translateSelf(0, 0, 0).toString()

returns matrix(1, 0, 0, 1, 0, 0) in all browsers, which makes sense because "nothing happened" (and early returns would prevent unecessary work). Similar can be done with rotateSelf, scaleSelf, etc.

@trusktr
Copy link
Author

trusktr commented Dec 27, 2024

The spec (and behavior in all browsers but Firefox) is also at odds with rotateSelf. Here are the steps for rotateSelf:

  1. If rotY and rotZ are both missing, set rotZ to the value of rotX and set rotX and rotY to 0.
  2. If rotY is still missing, set rotY to 0.
  3. If rotZ is still missing, set rotZ to 0.
  4. If rotX or rotY are not 0 or -0, set is 2D of the current matrix to false.
  5. Post-multiply a rotation transformation on the current matrix around the vector 0, 0, 1 by the specified rotation rotZ in degrees. The 3D rotation matrix is described in CSS Transforms with alpha = rotZ in degrees. [CSS3-TRANSFORMS]
  6. Post-multiply a rotation transformation on the current matrix around the vector 0, 1, 0 by the specified rotation rotY in degrees. The 3D rotation matrix is described in CSS Transforms with alpha = rotY in degrees. [CSS3-TRANSFORMS]
  7. Post-multiply a rotation transformation on the current matrix around the vector 1, 0, 0 by the specified rotation rotX in degrees. The 3D rotation matrix is described in CSS Transforms with alpha = rotX in degrees. [CSS3-TRANSFORMS]
  8. Return the current matrix.

Note that, if the angle of rotation around X or Y are zero, then is2D shall not be set to false! However, with rotateAxisAngleSelf, if the rotation (angle) around X or Y are zero, is2D does get set to false!

It would be good for consistency if Firefox's behavior was adopted into the spec for rotateAxisAngleSelf (basically early return if angle is zero).

With this inconsistency fixed in the spec, then the implementation of rotateAxisAngleSelf can be easily re-used for rotateSelf while the expected behavior (of Firefox) will apply to both cases. Here's an example in TypeScript:

	rotateAxisAngleSelf(x = 0, y = 0, z = 0, angle = 0) {
		if (angle === 0) return this // <--------------- Firefox behavior.
		toAxisRotation(axisRotationMatrix, x, y, z, angle)
		this.multiplySelf(axisRotationMatrix) // "post multiply"
		if (x !== 0 || y !== 0) this[is2D] = false // <---- without early return, all other browsers do this even if "nothing happened"
		return this
	}

	rotateSelf(rotX = 0, rotY?: number, rotZ?: number) {
		if (rotY == null && rotZ == null) {
			rotZ = rotX
			rotX = 0
			rotY = 0
		}

		rotY ??= 0
		rotZ ??= 0

		this.rotateAxisAngleSelf(0, 0, 1, rotZ)
		this.rotateAxisAngleSelf(0, 1, 0, rotY)
		this.rotateAxisAngleSelf(1, 0, 0, rotX)

		return this
	}

If we remove the early return from rotateAxisAngleSelf then we get the behavior in all browsers other than Firefox, but in that case, we need to implement rotateSelf like this:

	rotateAxisAngleSelf(x = 0, y = 0, z = 0, angle = 0) {
		// if (angle === 0) return this // <--------------- Firefox behavior (commented out)
		toAxisRotation(axisRotationMatrix, x, y, z, angle)
		this.multiplySelf(axisRotationMatrix) // "post multiply"
		if (x !== 0 || y !== 0) this[is2D] = false // <---- without early return, all other browsers do this even if "nothing happened"
		return this
	}

	rotateSelf(rotX = 0, rotY?: number, rotZ?: number) {
		if (rotY == null && rotZ == null) {
			rotZ = rotX
			rotX = 0
			rotY = 0
		}

		rotY ??= 0
		rotZ ??= 0

		// the conditional checks prevent is2D from unnecessarily becoming false. 
		if (rotZ) this.rotateAxisAngleSelf(0, 0, 1, rotZ)
		if (rotY) this.rotateAxisAngleSelf(0, 1, 0, rotY)
		if (rotX) this.rotateAxisAngleSelf(1, 0, 0, rotX)

		return this
	}

As you can see, to achieve the desired behavior of the spec'd rotateSelf method, we have to add the "early outs" for rotateAxisAngleSelf calls in rotateSelf. Instead, the "early outs" should be early returns in rotateAxisAngleSelf and then they'll both be consistent.

@trusktr trusktr changed the title Improve algorithm steps for identity transforms [geometry-interfaces] Improve algorithm steps for identity transforms Dec 27, 2024
@trusktr trusktr changed the title [geometry-interfaces] Improve algorithm steps for identity transforms [geometry] Improve algorithm steps for identity transforms Dec 27, 2024
@trusktr trusktr changed the title [geometry] Improve algorithm steps for identity transforms [geometry-1] Improve algorithm steps for identity transforms Dec 27, 2024
@trusktr
Copy link
Author

trusktr commented Dec 27, 2024

I'm not sure how labeling works, but I labeled this as "geometry-1" because it is referring to that spec (and then if it gets modified, it would be "geometry-2" the version that has a change). Should this instead be labeled "geometry-2" because such a change would be for the next version?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant