Skip to content
Advertisement

Transformation matrix rotation not preserving local axis scaling?

I have a simple transform class to apply translations, scales and rotations on a div in any arbitrary order:

class TransformDiv{

  constructor(div)
  {
    this.div = div;
    this.translateX = 0;
    this.translateY = 0;
    this.scaleX = 1;
    this.scaleY = 1;
    this.shearX = 0;
    this.shearY = 0;
  }
  
  translate(x, y)
  {
    this.translateX += x;
    this.translateY += y;
    this.setTransform();
  }
  
  scale(x, y, anchorX = 0, anchorY = 0)
  {
    this.scaleX *= x;
    this.shearX *= x;
    this.scaleY *= y;
    this.shearY *= y;
    this.translateX -= (this.translateX - anchorX) * (1 - x);
    this.translateY -= (this.translateY - anchorY) * (1 - y);
    this.setTransform();
  }
  
  rotate(rad, anchorX = 0, anchorY = 0)
  {
    let cos = Math.cos(rad);
    let sin = Math.sin(rad);
    
    // the composition of two successive rotations are additive
    let newScaleX = this.scaleX * cos + this.shearX * sin;
    let newShearX = this.scaleX * (-sin) + this.shearX * cos;
    let newShearY = this.shearY * cos + this.scaleY * sin;
    let newScaleY = this.shearY * (-sin) + this.scaleY * cos;
    this.scaleX = newScaleX;
    this.shearX = newShearX;
    this.shearY = newShearY;
    this.scaleY = newScaleY;
    //rotation about an arbitrary point
    let originX = (this.translateX - anchorX);
    let originY = (this.translateY - anchorY);
    this.translateX -= (originY * sin - originX * (cos - 1));
    this.translateY -= (-originY * (cos - 1) - originX * sin);
    
    this.setTransform();
  }
  
  setTransform()
  {
    this.div.style.transform = `matrix(${this.scaleX}, ${this.shearY}, ${this.shearX}, ${this.scaleY}, ${this.translateX}, ${this.translateY})`;
  }
}

A problem arises when I wish to rotate after a non-uniform scale has been made.

Edit – Newer interactive example: https://codepen.io/manstie/pen/RwGGOmB

Here is the example I made: https://jsfiddle.net/ft61q230/1/

In the example here:

div2.translate(100, 100);
div2.scale(2, 1, 100, 100);
div2.rotate(Math.PI / 2, 100, 100);

The expected result is for Test 1 Text and Test 2 Text to be the same length, as if you were rotating from the top left of the div clockwise 90 degrees; but as you can see the result is such that the rotation logic I am performing retains the scale on the world-space axis, so now Test 2 Text is twice as tall rather than twice as long.

Current outcome:

Current outcome

Desired outcome:

Desired outcome

The current rotation logic is based on multiplying the existing transformation matrix that makes up rotation by another transformation matrix containing an angle to rotate by, but I realize it is not as simple as that and I am missing something to retain local-axial scale.

Thank you for your assistance.

Edit:

Was recommended DOMMatrix which does all this math for me, but it has the same problem, although there is some skew which I don’t think is accurate:

https://jsfiddle.net/heqo7vrt/1/

The skew is caused by the scale function scaling it’s local X axis while it is rotated, and then rotating after not keeping that local X axis scaling. Also, DOMMatrix translate function has the translations apply on its local axis which is not desired in my situation but if its rotate function worked as expected I would be able to use it.

Advertisement

Answer

I managed to fix it here:

Regular: https://jsfiddle.net/sbca61k5/

let newScaleX = cos * this.scaleX + sin * this.shearY;
let newShearX = cos * this.shearX + sin * this.scaleY;
let newShearY = -sin * this.scaleX + cos * this.shearY;
let newScaleY = -sin * this.shearX + cos * this.scaleY;

DOMMatrix version: https://jsfiddle.net/b36kqrsg/

this.matrix = new DOMMatrix([cos, sin, -sin, cos, 0, 0]).multiply(this.matrix);
// or
this.matrix = new DOMMatrix().rotate(deg).multiply(this.matrix);

The difference is to have the rotation matrix multiplied by the rest of the matrix to “add” it on, not the other way round:

[a c e]   [cos -sin 0]   [scx shy tx]
[b d f] = [sin  cos 0] . [shx scy ty]
[0 0 1]   [0    0   1]   [0   0   1 ]

I’m unsure about the details of the anchor mathematics but the DOMMatrix version’s anchor is relative to its own top left whereas the other is relative to the top left of the document.

From my interactive example the anchor maths does not work as after a multitude of rotations the objects get further away from the anchor origin. https://codepen.io/manstie/pen/PoGXMed

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement