Skip to content

Fix pointer position when canvas has CSS transforms (e.g. rotate)#7278

Open
roy7 wants to merge 1 commit intophaserjs:masterfrom
roy7:fix/css-transform-pointer-coords
Open

Fix pointer position when canvas has CSS transforms (e.g. rotate)#7278
roy7 wants to merge 1 commit intophaserjs:masterfrom
roy7:fix/css-transform-pointer-coords

Conversation

@roy7
Copy link
Copy Markdown

@roy7 roy7 commented Apr 12, 2026

Summary

  • Fixes incorrect pointer/input coordinates when the canvas or a parent element has a CSS transform (e.g. rotate(90deg) for forcing landscape in portrait-locked webviews)
  • Adds transformXY method to ScaleManager that accounts for CSS rotation/scale/skew via DOMMatrix
  • Updates InputManager.transformPointer to use the combined XY transform instead of separate transformX/transformY calls
  • Zero overhead when no CSS transforms are present — falls back to the existing math

Detail

Closes #7175.

The existing transformX and transformY methods convert pointer coordinates independently:

transformX: function (pageX) {
    return (pageX - this.canvasBounds.left) * this.displayScale.x;
}

This breaks when the canvas has a CSS rotation because the X and Y axes become coupled — a screen-space X offset maps to both game X and Y. The simple subtract-and-scale approach can't account for this.

The fix adds getInverseCSSTransform() which walks the DOM tree from the canvas to the document root, composing any CSS transform values found along the way. It extracts the rotation/scale/skew portion (translation is already handled by getBoundingClientRect) and caches its inverse. The new transformXY method uses this inverse to convert screen-space pointer offsets back to the canvas's local coordinate system before scaling to game coordinates.

The inverse matrix is only recomputed in updateBounds() (on resize/orientation change), not on every pointer event.

Test plan

  • Existing test suite passes (23,304 tests pass, 0 new failures)
  • Verified pointer accuracy with transform: rotate(90deg) on canvas parent (reproduction from Pointer position incorrect of canvas is rotated #7175)
  • Verified no regression for non-transformed canvas (standard setup)

🤖 Generated with Claude Code


Note

Medium Risk
Touches core pointer coordinate transformation logic in ScaleManager/InputManager, which could impact all mouse/touch input; mitigation is the cached inverse matrix and explicit fallback to the prior math when no CSS transforms are present.

Overview
Fixes incorrect pointer coordinates when the canvas (or an ancestor) has CSS transforms like rotation/skew by introducing a coupled ScaleManager.transformXY path.

ScaleManager now caches an inverse accumulated CSS transform matrix (via DOMMatrix) during updateBounds() and uses it to convert (pageX,pageY) into game-space coordinates; InputManager.transformPointer switches from separate transformX/transformY calls to this new combined transform, falling back to the existing offset+scale behavior when no CSS transforms are detected.

Reviewed by Cursor Bugbot for commit 0625d14. Bugbot is set up for automated code reviews on this repo. Configure here.

The existing transformX/transformY methods convert pointer coordinates
independently, which breaks when the canvas or a parent element has a
CSS rotation — the X and Y axes become coupled. This adds transformXY
which uses the DOMMatrix API to apply the inverse of accumulated CSS
transforms, correctly mapping screen-space pointer positions back to
canvas-local coordinates. Zero overhead when no CSS transforms exist.

Closes phaserjs#7175

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@roy7
Copy link
Copy Markdown
Author

roy7 commented Apr 12, 2026

I was thinking about a PR for a different issue I was having locally, but it seems it might be working as intended so decided to just use my local workaround and not report it as a bug. (Phaser's useHandCursor changes the cursor for all interactive objects under the pointer regardless of mask visibility or depth ordering, so masked/hidden objects still show a hand cursor. But the docs are clear masking is purely for graphics and not used for interactions. So I worked around it by manually checking for cursor changes instead of relying on useHandCursor.)

However when exploring known issues I saw this one and figured I'd let Claude Code take a crack at it. I did test locally that this fixes the bug in the example HTML provided in the original issue, and it passed the test suite, so I'm sending it in for your review in case you think it is worthwhile.

Thanks for reviewing!

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

Successfully merging this pull request may close these issues.

Pointer position incorrect of canvas is rotated

1 participant