Jump To …

matrix.coffee

Class that represents an affine transform. Inspired in the SVG class SVGMatrix.

Check the source at https://github.com/yizzreel/coffee-matrix

Matrix representation

A set of simple letters are used to represent each position in the 3x3 matrix:

[ a  c  e ]
[ b  d  f ]
[ 0  0  1 ]

Where each letter means

  •  a   Scale x
  •  b   Shear/Skew y
  •  c   Shear/Skew x
  •  d   Scale y
  •  e   Translate x
  •  f   Translate y

Workflow with the Matrix class

Matrix is a Value Object, and any instruction returns a new matrix without modifiying the original, thus all methods are chainable.

created by yizzreel (no license yet, use it as you please)

class exports.Matrix

Creates a new Matrix

new Matrix( a, b, c, d, e, f )

Passing no arguments produces an Identity Matrix

  constructor: (@a = 1, @b = 0, @c = 0, @d = 1, @e = 0, @f = 0) ->

Concatenates this matrix with a translate matrix

[ a  c  e ] [ 1  0  tx ]   [ a  c  e + tx*a + ty*c ]
[ b  d  f ] [ 0  1  ty ] = [ b  d  f + tx*b + ty*d ]
[ 0  0  1 ] [ 0  0  1  ]   [ 0  0  1               ]

It's the same as:

thisMatrix.concatenate new MatrixVO( 1, 0, 0, 1, tx, ty )
  translate: (tx,ty) ->
    new Matrix(
      @a, @b, @c, @d
      @e + tx * @a + ty * @c,
      @f + tx * @b + ty * @d
    )

Groups the current matrix with a translate matrix

  groupTranslate: (tx,ty) ->
    @group Matrix.translated(tx,ty)
  

Concatenates this matrix with a scale matrix

[ a  c  e ] [ sx  0   0 ]   [ a*sx  c*sy  e ]
[ b  d  f ] [ 0   sy  0 ] = [ b*sx  d*sy  f ]
[ 0  0  1 ] [ 0   0   1 ]   [ 0     0     1 ]

It's the same as:

thisMatrix.concatenate new Matrix( sx, 0, 0, sy )
  scale: (sx,sy) ->
    new Matrix(
      @a * sx,
      @b * sx,
      @c * sy,
      @d * sy,
      @e, @f
    )
    

Groups the current matrix with a scale matrix

  groupScale: (sx,sy,offset=x:0,y:0) ->
    @group Matrix.scaled(sx,sy,offset)
  

Concatenates this matrix with a rotation matrix. Expects the angle in radians

[ a  c  e ] [ cos(theta)  -sin(theta)  0 ]
[ b  d  f ] [ sin(theta)   cos(theta)  0 ]
[ 0  0  1 ] [ 0            0           1 ]

It's the same as:

thisMatrix.concatenate new Matrix( cos, sin, -sin, cos )
  rotate: (theta) ->
    sin = Math.sin theta
    cos = Math.cos theta

    new Matrix(
        cos * @a +   sin * @c,
        cos * @b +   sin * @d,
      - sin * @a +   cos * @c,
      - sin * @b +   cos * @d,
        @e, @f
    )
  

Groups the current matrix with a rotation matrix

  groupRotate: (theta,offset=x:0,y:0) ->
    @group Matrix.rotated(theta,offset)
  

Concatenates this matrix with a shear matrix.

[ a  c  e ] [  1  shx  e ]   [ a + c*shy  a*shx + c  e ]
[ b  d  f ] [ shy  1   f ] = [ b + d*shy  b*shx + d  f ]
[ 0  0  1 ] [  0   0   1 ]   [ 0          0          1 ]

It's the same as:

thisMatrix.concatenate new MatrixVO( 1, shy, shx )
  shear: (shx,shy) ->
    new Matrix(
      @a + shy * @c
      @b + shy * @d
      @c + shx * @a
      @d + shx * @b
      
      @e, @f
    )
  

Groups the current matrix with a scale matrix

  groupShear: (shx,shy,offset=x:0,y:0) ->
    @group Matrix.sheared(shx,shy,offset)
  

Concatenates this matrix with a skewX matrix. Expects the angle in radians

[ 1  tan(theta)  0 ]
[ 0  1           0 ]
[ 0  0           1 ]
  skewX: (theta) ->
    @concatenate new Matrix( 1, 0, Math.tan(theta) )

Concatenates this matrix with a skewY matrix. Expects the angle in radians

[ 1           0  0 ]
[ tan(theta)  1  0 ]
[ 0           0  1 ]
  skewY: (theta) ->
    @concatenate new Matrix( 1, Math.tan(theta) )
  

Concatenates/multiplies this matrix with another matrix. To group transforamtions, use preConcatenate

[ a  c  e ] [ a'  c'  e' ]
[ b  d  f ] [ b'  d'  f' ]
[ 0  0  1 ] [ 0   0   1  ]

[ a*a' + c*b'  a*c' + c*d'  a*e' + c*f' + e ]
[ b*a' + d*b'  b*c' + d*d'  b*e' + d*f' + f ]
[ 0            0            1               ]
  concatenate: (other) ->
    new Matrix(
      @a * other.a +  @c * other.b,       # a
      @b * other.a +  @d * other.b,       # b
      @a * other.c +  @c * other.d,       # c
      @b * other.c +  @d * other.d,       # d
      @a * other.e +  @c * other.f + @e,  # e
      @b * other.e +  @d * other.f + @f   # f
    )
  

Pre concatenates/mutiplies this matrix with another matrix. Allows matrixes to group transformations

[ a'  c'  e' ] [ a  c  e ]
[ b'  d'  f' ] [ b  d  f ]
[ 0   0   1  ] [ 0  0  1 ]

[ a'*a + c'*b  a'*c + c'*d  a'*e + c'*f + e ]
[ b'*a + d'*b  b'*c + d'*d  b'*e + d'*f + f ]
[ 0            0            1               ]
  preConcatenate: (other) ->
    other.concatenate @
  

Alias of preConcatenate.

  group: (other) ->
    @preConcatenate other
  

Removes the given transform matrix from the current matrix. This actually inverts the given matrix and a groups it with this matrix, applying the inverse transform

It's a shortcut for:

thisMatrix.group otherMatrix.inverse()
  ungroup: (other) ->
    @group other.inverse()
    

Creates the inverse of the matrix.

        [ a  c  e ] -1            [ A  C  E ]
A^-1 =  [ b  d  f ]    = 1/det(A) [ B  D  F ]
        [ 0  0  1 ]               [ 0  0  1 ]

Where:

A = d * 1 - f * 0 =   d
B = f * 0 - b * 1 = - b
C = e * 0 - c * 1 = - c
D = a * 1 - e * 0 =   a
E                 =   c * f - d * e
F                 =   b * e - a * f

Matrix determinant:

          | a  c  e |
det(A)  = | b  d  f |
          | 0  0  1 |

a * d * 1 + c * f * 0 + e * b * 0 - e * d * 0 - c * b * 1 - a * f * 0

a * d - c * b

Result:

[   d / det  - c / det  ( c * f - d * e ) / det ]
[ - b / det    a / det  ( b * e - a * f ) / det ]
[   0          0        1                       ]
  inverse: ->
    det = @a * @d - @c * @b;

    new Matrix(
        @d / det,
      - @b / det,
      - @c / det,
        @a / det,
        ( @c * @f - @d * @e ) / det,
        ( @b * @e - @a * @f ) / det
    )
  

Transforms the given X,Y coordinates

[ a  c  e ] [ x ]   [ x*a + y*c + e ]
[ b  d  f ] [ y ] = [ x*b + y*d + f ]
[ 0  0  1 ] [ 1 ]   [ 1             ]

Use:

matrix = new Matrix(2, 0, 0, 2)
point = matrix.transform x: 50, y: 50 #=> {x: 100, y:100}
  transform: (points...) ->
    if( points.length is 1 and points[0] instanceof Array )
      @transform.apply(@,points[0])
    else
      result = for point in points
        do (point) =>
          x: point.x * @a + point.y * @c + @e
          y: point.x * @b + point.y * @d + @f
      
      if points.length is 1 then result[0] else result
  

Creates the identity Matix. When applied, it does not perform any change.

[ 1  0  0 ]
[ 0  1  0 ]
[ 0  0  1 ]

It's the same as creating a new matrix without arguments.

  @identity: ->
    new Matrix()

Creates a transform matrix to translate by tx and ty

[ 1  0  tx ]
[ 0  1  ty ]
[ 0  0  1  ]

It replicates these instructions:

Matrix.identity()
  .translate( tx, ty )
  @translated: (tx,ty) ->
    new Matrix( 1, 0, 0, 1, tx, ty )

Creates a transform matrix to scale by sx and sy. It keeps offset in the new coordinate system in the same place relatively to the old coordinate system. For instance, to scale a rectangle around its center, pass the rectangle center as the offset value.

[ sx  0   tx * ( 1 - sx ) ]
[ 0   sy  ty * ( 1 - sy ) ]
[ 0   0   1               ]

It replicates these instructions:

Matrix.identity()
  .translate( offset.x, offset.y )
  .scale( sx, sy )
  .translate( - offset.x, - offset.y )
  @scaled: (sx,sy,offset=x:0,y:0) ->
    new Matrix(
      sx, 0, 0, sy,

offset.x + ( - offset.x ) * sx + ( - offset.y ) * 0

      offset.x * ( 1 - sx ),

offset.y + ( - offset.x ) * 0 + ( - offset.y ) * sy

      offset.y * ( 1 - sy )
    )
  

Creates a transform matrix to rotate by theta. It keeps offset in the new coordinate system in the same place relatively to the old coordinate system. For instance, to rotate a rectangle around its center, pass the rectangle center as the offset value.

[ cos(theta)  -sin(theta)   tx * ( 1 - cos(theta) ) + ty * sin(theta)  ]
[ sin(theta)   cos(theta)   ty * ( 1 - cos(theta) ) - ty * sin(theta)  ]
[ 0            0            1                                          ]

It replicates these instructions:

Matrix.identity()
  .translate( offset.x , offset.y )
  .rotate( theta )
  .translate( - offset.x, - offset.y ) 
  @rotated: (theta,offset=x:0,y:0) ->
    sin = Math.sin theta
    cos = Math.cos theta
    
    new Matrix(
      cos, sin, - sin, cos,

offset.x + ( - offset.x ) * cos theta + ( - offset.y ) * ( - sin theta )

offset.x * ( 1 - cos theta ) + offset.y * sin theta

      offset.x * ( 1 - cos ) + offset.y * sin,

offset.y + ( - offset.x ) * sin theta + ( - offset.y ) * cos theta

offset.y * ( 1 - cos theta ) - offset.x * sin theta

      offset.y * ( 1 - cos ) - offset.x * sin
    )

Creates a transform matrix to shear by shx and shy. It keeps offset in the new coordinate system in the same place relatively to the old coordinate system. For instance, to shear a rectangle around its center, pass the rectangle center as the offset value.

[ 1    shx  shx * -ty ]
[ shy  1    shy * -tx ]
[ 0    0    1         ]

It replicates these instructions:

Matrix.identity()
  .translate( offset.x , offset.y )
  .shear( shx, shy )
  .translate( - offset.x, - offset.y ) 
  @sheared: (shx,shy,offset=x:0,y:0) ->
    new Matrix(
      1, shy, shx, 1
      
      - shx * offset.y
      - shy * offset.x
    )
  

Returns whether the matrix is an Identity Matrix

  isIdentity: ->
    @a is 1 and @b is 0 and @c is 0 and @d is 1 and @e is 0 and @f is 0

Converts the matrix into an array of 9 elements:

[ a, c, e, 
  b, d, f, 
  0, 0, 1 ]
  toArray: ->
    [ @a, @c, @e, @b, @d, @f, 0, 0, 1 ]

Converts the matrix into an object:

{ a, b, c, d, e, f }
  toObject: ->
    { @a, @b, @c, @d, @e, @f }
  

Converts the matrix into a string

  toString: ->
    "{ a: #{@a},b: #{@b},c: #{@c},d: #{@d},e: #{@e},f: #{@f} }"