nfroidure/svgicons2svgfont

View on GitHub
src/Matrix.js

Summary

Maintainability
F
4 days
Test Coverage
/* eslint-disable no-undef */
/*!
  2D Transformation Matrix v2.7.5
  (c) Epistemex.com 2014-2018
  License: MIT
*/

/**
 * 2D transformation matrix object initialized with identity matrix.
 *
 * The matrix can synchronize a canvas 2D context by supplying the context
 * as an argument, or later apply current absolute transform to an
 * existing context.
 *
 * To synchronize a DOM element you can use [`toCSS()`]{@link Matrix#toCSS} or [`toCSS3D()`]{@link Matrix#toCSS3D}.
 * together with for example the `style.transform` property.
 *
 * @param {CanvasRenderingContext2D} [context] - Optional context to sync with Matrix
 * @param {HTMLElement} [element=null] - DOM Element to synchronize
 * @prop {number} a - scale x
 * @prop {number} b - shear y
 * @prop {number} c - shear x
 * @prop {number} d - scale y
 * @prop {number} e - translate x
 * @prop {number} f - translate y
 * @prop {CanvasRenderingContext2D} [context] - set or get current synchronized 2D context
 * @prop {HTMLElement} [element] - get current synchronized DOM element
 * @prop {boolean} [useCSS3D=false] - is a DOM element is defined for sync., choose whether to use 2D (false) or 3D (true) matrix to sync it.
 * @constructor
 * @license MIT license
 * @copyright Epistemex.com 2014-2018
 */
function Matrix(context, element) {
  var me = this,
    _el;
  me._t = me.transform;

  me.a = me.d = 1;
  me.b = me.c = me.e = me.f = 0;

  // sync context
  if (context) (me.context = context).setTransform(1, 0, 0, 1, 0, 0);

  // sync DOM element
  Object.defineProperty(me, 'element', {
    get: function () {
      return _el;
    },
    set: function (el) {
      if (!_el) {
        me._px = me._getPX();
        me.useCSS3D = false;
      }
      _el = el;
      (me._st = _el.style)[me._px] = me.toCSS();
    },
  });

  if (element) me.element = element;
}

/**
 * Returns a new matrix that transforms a triangle `t1` into another triangle
 * `t2`, or throws an exception if it is impossible.
 *
 * Note: the method can take both arrays as well as literal objects.
 * Just make sure that both arguments (`t1`, `t2`) are of the same type.
 *
 * @param {{px: number, py: number, qx: number, qy: number, rx: number, ry: number}|Array} t1 - Object or array containing the three points for the triangle.
 * For object use obj.px, obj.py, obj.qx, obj.qy, obj.rx and obj.ry. For arrays provide the points in the order [px, py, qx, qy, rx, ry], or as point array [{x:,y:}, {x:,y:}, {x:,y:}]
 * @param {{px: number, py: number, qx: number, qy: number, rx: number, ry: number}|Array} t2 - See description for t1.
 * @param {CanvasRenderingContext2D} [context] - optional canvas 2D context to use for the matrix
 * @returns {Matrix}
 * @throws Exception is matrix becomes not invertible
 * @static
 */
Matrix.fromTriangles = function (t1, t2, context) {
  var m1 = new Matrix(),
    m2 = new Matrix(context),
    r1,
    r2,
    rx1,
    ry1,
    rx2,
    ry2;

  if (Array.isArray(t1)) {
    if (typeof t1[0] === 'number') {
      rx1 = t1[4];
      ry1 = t1[5];
      rx2 = t2[4];
      ry2 = t2[5];
      r1 = [t1[0] - rx1, t1[1] - ry1, t1[2] - rx1, t1[3] - ry1, rx1, ry1];
      r2 = [t2[0] - rx2, t2[1] - ry2, t2[2] - rx2, t2[3] - ry2, rx2, ry2];
    } else {
      rx1 = t1[2].x;
      ry1 = t1[2].y;
      rx2 = t2[2].x;
      ry2 = t2[2].y;
      r1 = [
        t1[0].x - rx1,
        t1[0].y - ry1,
        t1[1].x - rx1,
        t1[1].y - ry1,
        rx1,
        ry1,
      ];
      r2 = [
        t2[0].x - rx2,
        t2[0].y - ry2,
        t2[1].x - rx2,
        t2[1].y - ry2,
        rx2,
        ry2,
      ];
    }
  } else {
    r1 = [
      t1.px - t1.rx,
      t1.py - t1.ry,
      t1.qx - t1.rx,
      t1.qy - t1.ry,
      t1.rx,
      t1.ry,
    ];
    r2 = [
      t2.px - t2.rx,
      t2.py - t2.ry,
      t2.qx - t2.rx,
      t2.qy - t2.ry,
      t2.rx,
      t2.ry,
    ];
  }

  m1.setTransform.apply(m1, r1);
  m2.setTransform.apply(m2, r2);

  return m2.multiply(m1.inverse());
};

/**
 * Create a matrix from a transform list from an SVG shape. The list
 * can be for example baseVal (i.e. `shape.transform.baseVal`).
 *
 * The resulting matrix has all transformations from that list applied
 * in the same order as the list.
 *
 * @param {SVGTransformList} tList - transform list from an SVG shape.
 * @param {CanvasRenderingContext2D} [context] - optional canvas 2D context to use for the matrix
 * @param {HTMLElement} [dom] - optional DOM element to use for the matrix
 * @returns {Matrix}
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGTransformList|MDN / SVGTransformList}
 */
Matrix.fromSVGTransformList = function (tList, context, dom) {
  var m = new Matrix(context, dom),
    i = 0;

  while (i < tList.length) m.multiply(tList[i++].matrix);

  return m;
};

/**
 * Create and transform a new matrix based on given matrix values, or
 * provide SVGMatrix or a (2D) DOMMatrix, WebKitCSSMatrix or another
 * instance of a generic Matrix.
 *
 * @example
 *
 * var m = Matrix.from(1, 0.2, 0, 2, 120, 97);
 * var m = Matrix.from(domMatrix, ctx);
 * var m = Matrix.from(svgMatrix);
 * var m = Matrix.from(cssMatrix);
 * var m = Matrix.from(matrix);
 * var m = Matrix.from(vector [,pre-x] [,pre-y] [,doScale]);
 *
 * @param {*} a - number representing a in [a-f], or a Matrix object containing properties a-f. Vector is given as an object with properties x and y.
 * @param {*} [b] - b property if a is not a matrix object, or optional canvas 2D context.
 * If vector is input this will be pre-translate for x.
 * @param {number} [c] - If vector is input this will be pre-translate for y.
 * @param {number} [d] - If vector is input, set this to true to use scale and translate of 1,
 * false to use hypotenuse as translate distance instead and no scale.
 * @param {number} [e]
 * @param {number} [f]
 * @param {CanvasRenderingContext2D} [context] - optional canvas context to synchronize
 * @param {HTMLElement} [dom] - optional DOM element to use for the matrix
 * @returns {Matrix}
 * @static
 */
Matrix.from = function (a, b, c, d, e, f, context, dom) {
  var m = new Matrix(context, dom),
    scale,
    dist,
    q;

  if (typeof a === 'number') m.setTransform(a, b, c, d, e, f);
  else if (typeof a.x === 'number') {
    // vector

    q = Math.sqrt(a.x * a.x + a.y * a.y);
    scale = dist = 1;

    if (d) scale = q;
    else dist = q;

    m.translate(b || 0, c || 0)
      .rotateFromVector(a)
      .scaleU(scale)
      .translate(dist, 0);
  } else {
    if (typeof a.is2D === 'boolean' && !a.is2D)
      throw 'Cannot use 3D DOMMatrix.';
    if (b) m.context = b;
    if (c) m.element = c;
    m.multiply(a);
  }

  return m;
};

Matrix.prototype = {
  _getPX: function () {
    var lst = ['t', 'oT', 'msT', 'mozT', 'webkitT', 'khtmlT'],
      i = 0,
      p,
      style = document.createElement('div').style;

    while ((p = lst[i++]))
      if (typeof style[p + 'ransform'] !== 'undefined') return p + 'ransform';
  },

  /**
   * Concatenates transforms of this matrix onto the given child matrix and
   * returns a new matrix. This instance is used on left side.
   *
   * @param {Matrix|SVGMatrix} cm - child matrix to apply concatenation to
   * @returns {Matrix} - new Matrix instance
   */
  concat: function (cm) {
    return this.clone().multiply(cm);
  },

  /**
   * Flips the horizontal values.
   * @returns {Matrix}
   */
  flipX: function () {
    return this._t(-1, 0, 0, 1, 0, 0);
  },

  /**
   * Flips the vertical values.
   * @returns {Matrix}
   */
  flipY: function () {
    return this._t(1, 0, 0, -1, 0, 0);
  },

  /**
   * Reflects incoming (velocity) vector on the normal which will be the
   * current transformed x axis. Call when a trigger condition is met.
   *
   * @param {number} x - vector end point for x (start = 0)
   * @param {number} y - vector end point for y (start = 0)
   * @returns {{x: number, y: number}}
   */
  reflectVector: function (x, y) {
    var v = this.applyToPoint(0, 1),
      d = (v.x * x + v.y * y) * 2;

    x -= d * v.x;
    y -= d * v.y;

    return { x: x, y: y };
  },

  /**
   * Short-hand to reset current matrix to an identity matrix.
   * @returns {Matrix}
   */
  reset: function () {
    return this.setTransform(1, 0, 0, 1, 0, 0);
  },

  /**
   * Rotates current matrix by angle (accumulative).
   * @param {number} angle - angle in radians
   * @returns {Matrix}
   */
  rotate: function (angle) {
    var cos = Math.cos(angle),
      sin = Math.sin(angle);
    return this._t(cos, sin, -sin, cos, 0, 0);
  },

  /**
   * Converts a vector given as `x` and `y` to angle, and
   * rotates (accumulative). x can instead contain an object with
   * properties x and y and if so, y parameter will be ignored.
   * @param {number|*} x
   * @param {number} [y]
   * @returns {Matrix}
   */
  rotateFromVector: function (x, y) {
    return this.rotate(
      typeof x === 'number' ? Math.atan2(y, x) : Math.atan2(x.y, x.x)
    );
  },

  /**
   * Helper method to make a rotation based on an angle in degrees.
   * @param {number} angle - angle in degrees
   * @returns {Matrix}
   */
  rotateDeg: function (angle) {
    return this.rotate((angle * Math.PI) / 180);
  },

  /**
   * Scales current matrix uniformly and accumulative.
   * @param {number} f - scale factor for both x and y (1 does nothing)
   * @returns {Matrix}
   */
  scaleU: function (f) {
    return this._t(f, 0, 0, f, 0, 0);
  },

  /**
   * Scales current matrix accumulative.
   * @param {number} sx - scale factor x (1 does nothing)
   * @param {number} sy - scale factor y (1 does nothing)
   * @returns {Matrix}
   */
  scale: function (sx, sy) {
    return this._t(sx, 0, 0, sy, 0, 0);
  },

  /**
   * Scales current matrix on x axis accumulative.
   * @param {number} sx - scale factor x (1 does nothing)
   * @returns {Matrix}
   */
  scaleX: function (sx) {
    return this._t(sx, 0, 0, 1, 0, 0);
  },

  /**
   * Scales current matrix on y axis accumulative.
   * @param {number} sy - scale factor y (1 does nothing)
   * @returns {Matrix}
   */
  scaleY: function (sy) {
    return this._t(1, 0, 0, sy, 0, 0);
  },

  /**
   * Converts a vector given as `x` and `y` to normalized scale.
   * @param x
   * @param y
   * @returns {Matrix}
   */
  scaleFromVector: function (x, y) {
    return this.scaleU(Math.sqrt(x * x + y * y));
  },

  /**
   * Apply shear to the current matrix accumulative.
   * @param {number} sx - amount of shear for x
   * @param {number} sy - amount of shear for y
   * @returns {Matrix}
   */
  shear: function (sx, sy) {
    return this._t(1, sy, sx, 1, 0, 0);
  },

  /**
   * Apply shear for x to the current matrix accumulative.
   * @param {number} sx - amount of shear for x
   * @returns {Matrix}
   */
  shearX: function (sx) {
    return this._t(1, 0, sx, 1, 0, 0);
  },

  /**
   * Apply shear for y to the current matrix accumulative.
   * @param {number} sy - amount of shear for y
   * @returns {Matrix}
   */
  shearY: function (sy) {
    return this._t(1, sy, 0, 1, 0, 0);
  },

  /**
   * Apply skew to the current matrix accumulative. Angles in radians.
   * Also see [`skewDeg()`]{@link Matrix#skewDeg}.
   * @param {number} ax - angle of skew for x
   * @param {number} ay - angle of skew for y
   * @returns {Matrix}
   */
  skew: function (ax, ay) {
    return this.shear(Math.tan(ax), Math.tan(ay));
  },

  /**
   * Apply skew to the current matrix accumulative. Angles in degrees.
   * Also see [`skew()`]{@link Matrix#skew}.
   * @param {number} ax - angle of skew for x
   * @param {number} ay - angle of skew for y
   * @returns {Matrix}
   */
  skewDeg: function (ax, ay) {
    return this.shear(
      Math.tan((ax / 180) * Math.PI),
      Math.tan((ay / 180) * Math.PI)
    );
  },

  /**
   * Apply skew for x to the current matrix accumulative. Angles in radians.
   * Also see [`skewDeg()`]{@link Matrix#skewDeg}.
   * @param {number} ax - angle of skew for x
   * @returns {Matrix}
   */
  skewX: function (ax) {
    return this.shearX(Math.tan(ax));
  },

  /**
   * Apply skew for y to the current matrix accumulative. Angles in radians.
   * Also see [`skewDeg()`]{@link Matrix#skewDeg}.
   * @param {number} ay - angle of skew for y
   * @returns {Matrix}
   */
  skewY: function (ay) {
    return this.shearY(Math.tan(ay));
  },

  /**
   * Set current matrix to new absolute matrix.
   * @param {number} a - scale x
   * @param {number} b - shear y
   * @param {number} c - shear x
   * @param {number} d - scale y
   * @param {number} e - translate x
   * @param {number} f - translate y
   * @returns {Matrix}
   */
  setTransform: function (a, b, c, d, e, f) {
    var me = this;
    me.a = a;
    me.b = b;
    me.c = c;
    me.d = d;
    me.e = e;
    me.f = f;
    return me._x();
  },

  /**
   * Translate current matrix accumulative.
   * @param {number} tx - translation for x
   * @param {number} ty - translation for y
   * @returns {Matrix}
   */
  translate: function (tx, ty) {
    return this._t(1, 0, 0, 1, tx, ty);
  },

  /**
   * Translate current matrix on x axis accumulative.
   * @param {number} tx - translation for x
   * @returns {Matrix}
   */
  translateX: function (tx) {
    return this._t(1, 0, 0, 1, tx, 0);
  },

  /**
   * Translate current matrix on y axis accumulative.
   * @param {number} ty - translation for y
   * @returns {Matrix}
   */
  translateY: function (ty) {
    return this._t(1, 0, 0, 1, 0, ty);
  },

  /**
   * Multiplies current matrix with new matrix values. Also see [`multiply()`]{@link Matrix#multiply}.
   *
   * @param {number} a2 - scale x
   * @param {number} b2 - skew y
   * @param {number} c2 - skew x
   * @param {number} d2 - scale y
   * @param {number} e2 - translate x
   * @param {number} f2 - translate y
   * @returns {Matrix}
   */
  transform: function (a2, b2, c2, d2, e2, f2) {
    var me = this,
      a1 = me.a,
      b1 = me.b,
      c1 = me.c,
      d1 = me.d,
      e1 = me.e,
      f1 = me.f;

    /* matrix column order is:
     *   a c e
     *   b d f
     *   0 0 1
     */
    me.a = a1 * a2 + c1 * b2;
    me.b = b1 * a2 + d1 * b2;
    me.c = a1 * c2 + c1 * d2;
    me.d = b1 * c2 + d1 * d2;
    me.e = a1 * e2 + c1 * f2 + e1;
    me.f = b1 * e2 + d1 * f2 + f1;

    return me._x();
  },

  /**
   * Multiplies current matrix with source matrix.
   * @param {Matrix|DOMMatrix|SVGMatrix} m - source matrix to multiply with.
   * @returns {Matrix}
   */
  multiply: function (m) {
    return this._t(m.a, m.b, m.c, m.d, m.e, m.f);
  },

  /**
   * Divide this matrix on input matrix which must be invertible.
   * @param {Matrix} m - matrix to divide on (divisor)
   * @throws Exception if input matrix is not invertible
   * @returns {Matrix}
   */
  divide: function (m) {
    return this.multiply(m.inverse());
  },

  /**
   * Divide current matrix on scalar value != 0.
   * @param {number} d - divisor
   * @throws Exception if divisor is zero
   * @returns {Matrix}
   */
  divideScalar: function (d) {
    var me = this;

    if (!d) throw 'Division on zero';

    me.a /= d;
    me.b /= d;
    me.c /= d;
    me.d /= d;
    me.e /= d;
    me.f /= d;

    return me._x();
  },

  /**
   * Get an inverse matrix of current matrix. The method returns a new
   * matrix with values you need to use to get to an identity matrix.
   * Context from parent matrix is not applied to the returned matrix.
   *
   * @param {boolean} [cloneContext=false] - clone current context to resulting matrix
   * @param {boolean} [cloneDOM=false] - clone current DOM element to resulting matrix
   * @throws Exception is input matrix is not invertible
   * @returns {Matrix} - new Matrix instance
   */
  inverse: function (cloneContext, cloneDOM) {
    var me = this,
      m = new Matrix(
        cloneContext ? me.context : null,
        cloneDOM ? me.element : null
      ),
      dt = me.determinant();

    if (!dt) throw 'Matrix not invertible.';

    m.a = me.d / dt;
    m.b = -me.b / dt;
    m.c = -me.c / dt;
    m.d = me.a / dt;
    m.e = (me.c * me.f - me.d * me.e) / dt;
    m.f = -(me.a * me.f - me.b * me.e) / dt;

    return m;
  },

  /**
   * Interpolate this matrix with another and produce a new matrix.
   * `t` is a value in the range [0.0, 1.0] where 0 is this instance and
   * 1 is equal to the second matrix. The `t` value is not clamped.
   *
   * Context from parent matrix is not applied to the returned matrix.
   *
   * Note: this interpolation is naive. For animation containing rotation,
   * shear or skew use the [`interpolateAnim()`]{@link Matrix#interpolateAnim} method instead
   * to avoid unintended flipping.
   *
   * @param {Matrix|SVGMatrix} m2 - the matrix to interpolate with.
   * @param {number} t - interpolation [0.0, 1.0]
   * @param {CanvasRenderingContext2D} [context] - optional context to affect
   * @param {HTMLElement} [dom] - optional DOM element to use for the matrix
   * @returns {Matrix} - new Matrix instance with the interpolated result
   */
  interpolate: function (m2, t, context, dom) {
    var me = this,
      m = new Matrix(context, dom);

    m.a = me.a + (m2.a - me.a) * t;
    m.b = me.b + (m2.b - me.b) * t;
    m.c = me.c + (m2.c - me.c) * t;
    m.d = me.d + (m2.d - me.d) * t;
    m.e = me.e + (m2.e - me.e) * t;
    m.f = me.f + (m2.f - me.f) * t;

    return m._x();
  },

  /**
   * Interpolate this matrix with another and produce a new matrix.
   * `t` is a value in the range [0.0, 1.0] where 0 is this instance and
   * 1 is equal to the second matrix. The `t` value is not constrained.
   *
   * Context from parent matrix is not applied to the returned matrix.
   *
   * To obtain easing `t` can be preprocessed using easing-functions
   * before being passed to this method.
   *
   * Note: this interpolation method uses decomposition which makes
   * it suitable for animations (in particular where rotation takes
   * places).
   *
   * @param {Matrix} m2 - the matrix to interpolate with.
   * @param {number} t - interpolation [0.0, 1.0]
   * @param {CanvasRenderingContext2D} [context] - optional context to affect
   * @param {HTMLElement} [dom] - optional DOM element to use for the matrix
   * @returns {Matrix} - new Matrix instance with the interpolated result
   */
  interpolateAnim: function (m2, t, context, dom) {
    var m = new Matrix(context, dom),
      d1 = this.decompose(),
      d2 = m2.decompose(),
      t1 = d1.translate,
      t2 = d2.translate,
      s1 = d1.scale;

    // QR order (t-r-s-sk)
    m.translate(t1.x + (t2.x - t1.x) * t, t1.y + (t2.y - t1.y) * t);
    m.rotate(d1.rotation + (d2.rotation - d1.rotation) * t);
    m.scale(s1.x + (d2.scale.x - s1.x) * t, s1.y + (d2.scale.y - s1.y) * t);
    //todo test skew scenarios

    return m._x();
  },

  /**
   * Decompose the current matrix into simple transforms using either
   * QR (default) or LU decomposition.
   *
   * @param {boolean} [useLU=false] - set to true to use LU rather than QR decomposition
   * @returns {*} - an object containing current decomposed values (translate, rotation, scale, skew)
   * @see {@link https://en.wikipedia.org/wiki/QR_decomposition|More on QR decomposition}
   * @see {@link https://en.wikipedia.org/wiki/LU_decomposition|More on LU decomposition}
   */
  decompose: function (useLU) {
    var me = this,
      a = me.a,
      b = me.b,
      c = me.c,
      d = me.d,
      acos = Math.acos,
      atan = Math.atan,
      sqrt = Math.sqrt,
      pi = Math.PI,
      translate = { x: me.e, y: me.f },
      rotation = 0,
      scale = { x: 1, y: 1 },
      skew = { x: 0, y: 0 },
      determ = a * d - b * c, // determinant(), skip DRY here...
      r,
      s;

    if (useLU) {
      if (a) {
        skew = { x: atan(c / a), y: atan(b / a) };
        scale = { x: a, y: determ / a };
      } else if (b) {
        rotation = pi * 0.5;
        scale = { x: b, y: determ / b };
        skew.x = atan(d / b);
      } else {
        // a = b = 0
        scale = { x: c, y: d };
        skew.x = pi * 0.25;
      }
    } else {
      // Apply the QR-like decomposition.
      if (a || b) {
        r = sqrt(a * a + b * b);
        rotation = b > 0 ? acos(a / r) : -acos(a / r);
        scale = { x: r, y: determ / r };
        skew.x = atan((a * c + b * d) / (r * r));
      } else if (c || d) {
        s = sqrt(c * c + d * d);
        rotation = pi * 0.5 - (d > 0 ? acos(-c / s) : -acos(c / s));
        scale = { x: determ / s, y: s };
        skew.y = atan((a * c + b * d) / (s * s));
      } else {
        // a = b = c = d = 0
        scale = { x: 0, y: 0 };
      }
    }

    return {
      translate: translate,
      rotation: rotation,
      scale: scale,
      skew: skew,
    };
  },

  /**
   * Returns the determinant of the current matrix.
   * @returns {number}
   */
  determinant: function () {
    return this.a * this.d - this.b * this.c;
  },

  /**
   * Apply current matrix to `x` and `y` of a point.
   * Returns a point object.
   *
   * @param {number} x - value for x
   * @param {number} y - value for y
   * @returns {{x: number, y: number}} A new transformed point object
   */
  applyToPoint: function (x, y) {
    var me = this;
    return {
      x: x * me.a + y * me.c + me.e,
      y: x * me.b + y * me.d + me.f,
    };
  },

  /**
   * Apply current matrix to array with point objects or point pairs.
   * Returns a new array with points in the same format as the input array.
   *
   * A point object is an object literal:
   *
   *     {x: x, y: y}
   *
   * so an array would contain either:
   *
   *     [{x: x1, y: y1}, {x: x2, y: y2}, ... {x: xn, y: yn}]
   *
   * or
   *
   *     [x1, y1, x2, y2, ... xn, yn]
   *
   * @param {Array} points - array with point objects or pairs
   * @returns {Array} A new array with transformed points
   */
  applyToArray: function (points) {
    var i = 0,
      p,
      l,
      mxPoints = [];

    if (typeof points[0] === 'number') {
      l = points.length;

      while (i < l) {
        p = this.applyToPoint(points[i++], points[i++]);
        mxPoints.push(p.x, p.y);
      }
    } else {
      while ((p = points[i++])) {
        mxPoints.push(this.applyToPoint(p.x, p.y));
      }
    }

    return mxPoints;
  },

  /**
   * Apply current matrix to a typed array with point pairs. Although
   * the input array may be an ordinary array, this method is intended
   * for more performant use where typed arrays are used. The returned
   * array is regardless always returned as a `Float32Array`.
   *
   * @param {*} points - (typed) array with point pairs [x1, y1, ..., xn, yn]
   * @param {boolean} [use64=false] - use Float64Array instead of Float32Array
   * @returns {*} A new typed array with transformed points
   */
  applyToTypedArray: function (points, use64) {
    var i = 0,
      p,
      l = points.length,
      mxPoints = use64 ? new Float64Array(l) : new Float32Array(l);

    while (i < l) {
      p = this.applyToPoint(points[i], points[i + 1]);
      mxPoints[i++] = p.x;
      mxPoints[i++] = p.y;
    }

    return mxPoints;
  },

  /**
   * Apply to any canvas 2D context object. This does not affect the
   * context that optionally was referenced in constructor unless it is
   * the same context.
   *
   * @param {CanvasRenderingContext2D} context - target context
   * @returns {Matrix}
   */
  applyToContext: function (context) {
    var me = this;
    context.setTransform(me.a, me.b, me.c, me.d, me.e, me.f);
    return me;
  },

  /**
   * Apply to any DOM element. This does not affect the DOM element
   * that optionally was referenced in constructor unless it is
   * the same element.
   *
   * The method will auto-detect the correct browser prefix if any.
   *
   * @param {HTMLElement} element - target DOM element
   * @param {boolean} [use3D=false] - use 3D transformation matrix instead of 2D
   * @returns {Matrix}
   */
  applyToElement: function (element, use3D) {
    var me = this;
    if (!me._px) me._px = me._getPX();
    element.style[me._px] = use3D ? me.toCSS3D() : me.toCSS();
    return me;
  },

  /**
   * Instead of creating a new instance of a Matrix, DOMMatrix or SVGMatrix
   * the current settings of this instance can be applied to an external
   * object of a different (or same) type. You can also pass in an
   * empty literal object.
   *
   * Note that the properties a-f will be set regardless of if they
   * already exist or not.
   *
   * @param {*} obj - target object.
   * @returns {Matrix}
   */
  applyToObject: function (obj) {
    var me = this;
    obj.a = me.a;
    obj.b = me.b;
    obj.c = me.c;
    obj.d = me.d;
    obj.e = me.e;
    obj.f = me.f;
    return me;
  },

  /**
   * Returns true if matrix is an identity matrix (no transforms applied).
   * @returns {boolean}
   */
  isIdentity: function () {
    var me = this;
    return me.a === 1 && !me.b && !me.c && me.d === 1 && !me.e && !me.f;
  },

  /**
   * Returns true if matrix is invertible
   * @returns {boolean}
   */
  isInvertible: function () {
    return !this._q(this.determinant(), 0);
  },

  /**
   * The method is intended for situations where scale is accumulated
   * via multiplications, to detect situations where scale becomes
   * "trapped" with a value of zero. And in which case scale must be
   * set explicitly to a non-zero value.
   *
   * @returns {boolean}
   */
  isValid: function () {
    return !(this.a * this.d);
  },

  /**
   * Compares current matrix with another matrix. Returns true if equal
   * (within epsilon tolerance).
   * @param {Matrix|SVGMatrix} m - matrix to compare this matrix with
   * @returns {boolean}
   */
  isEqual: function (m) {
    var me = this,
      q = me._q;

    return (
      q(me.a, m.a) &&
      q(me.b, m.b) &&
      q(me.c, m.c) &&
      q(me.d, m.d) &&
      q(me.e, m.e) &&
      q(me.f, m.f)
    );
  },

  /**
   * Clones current instance and returning a new matrix.
   * @param {boolean} [noContext=false] don't clone context reference if true
   * @returns {Matrix} - a new Matrix instance with identical transformations as this instance
   */
  clone: function (noContext) {
    return new Matrix(noContext ? null : this.context).multiply(this);
  },

  /**
   * Returns an array with current matrix values.
   * @returns {Array}
   */
  toArray: function () {
    var me = this;
    return [me.a, me.b, me.c, me.d, me.e, me.f];
  },

  /**
   * Returns a binary 32-bit floating point typed array.
   * @returns {*}
   */
  toTypedArray: function () {
    var me = this;
    return new Float32Array([me.a, me.b, me.c, me.d, me.e, me.f]);
  },

  /**
   * Generates a string that can be used with CSS `transform`.
   * @example
   *     element.style.transform = m.toCSS();
   * @returns {string}
   */
  toCSS: function () {
    return 'matrix(' + this.toArray() + ')';
  },

  /**
   * Generates a `matrix3d()` string that can be used with CSS `transform`.
   * Although the matrix is for 2D use you may see performance benefits
   * on some devices using a 3D CSS transform instead of a 2D.
   * @example
   *     element.style.transform = m.toCSS3D();
   * @returns {string}
   */
  toCSS3D: function () {
    var me = this,
      n2 = ',0,0,';
    return (
      'matrix3d(' +
      me.a +
      ',' +
      me.b +
      n2 +
      me.c +
      ',' +
      me.d +
      n2 +
      n2 +
      ',1,0,' +
      me.e +
      ',' +
      me.f +
      ',0,1)'
    );
  },

  /**
   * Returns a JSON compatible string of current matrix.
   * @returns {string}
   */
  toJSON: function () {
    var me = this;
    return (
      '{"a":' +
      me.a +
      ',"b":' +
      me.b +
      ',"c":' +
      me.c +
      ',"d":' +
      me.d +
      ',"e":' +
      me.e +
      ',"f":' +
      me.f +
      '}'
    );
  },

  /**
   * Returns a string with current matrix as comma-separated list.
   * @param {number} [fixLen=4] - truncate decimal values to number of digits
   * @returns {string}
   */
  toString: function (fixLen) {
    var me = this;
    fixLen = fixLen || 4;
    return (
      'a=' +
      me.a.toFixed(fixLen) +
      ' b=' +
      me.b.toFixed(fixLen) +
      ' c=' +
      me.c.toFixed(fixLen) +
      ' d=' +
      me.d.toFixed(fixLen) +
      ' e=' +
      me.e.toFixed(fixLen) +
      ' f=' +
      me.f.toFixed(fixLen)
    );
  },

  /**
   * Returns a string with current matrix as comma-separated values
   * string with line-end (CR+LF).
   * @returns {string}
   */
  toCSV: function () {
    return this.toArray().join() + '\r\n';
  },

  /**
   * Convert current matrix into a `DOMMatrix`. If `DOMMatrix` is not
   * supported, a `null` is returned.
   *
   * @returns {DOMMatrix}
   * @see {@link https://drafts.fxtf.org/geometry/#dommatrix|MDN / SVGMatrix}
   */
  toDOMMatrix: function () {
    var m = null;
    if ('DOMMatrix' in window) {
      m = new DOMMatrix();
      m.a = this.a;
      m.b = this.b;
      m.c = this.c;
      m.d = this.d;
      m.e = this.e;
      m.f = this.f;
    }
    return m;
  },

  /**
   * Convert current matrix into a `SVGMatrix`. If `SVGMatrix` is not
   * supported, a `null` is returned.
   *
   * @returns {SVGMatrix}
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix|MDN / SVGMatrix}
   */
  toSVGMatrix: function () {
    var me = this,
      svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
      svgMatrix = null;

    if (svg) {
      svgMatrix = svg.createSVGMatrix();
      svgMatrix.a = me.a;
      svgMatrix.b = me.b;
      svgMatrix.c = me.c;
      svgMatrix.d = me.d;
      svgMatrix.e = me.e;
      svgMatrix.f = me.f;
    }

    return svgMatrix;
  },

  /**
   * Compares floating point values with some tolerance (epsilon)
   * @param {number} f1 - float 1
   * @param {number} f2 - float 2
   * @returns {boolean}
   * @private
   */
  _q: function (f1, f2) {
    return Math.abs(f1 - f2) < 1e-14;
  },

  /**
   * Apply current absolute matrix to context if defined, to sync it.
   * Apply current absolute matrix to element if defined, to sync it.
   * @returns {Matrix}
   * @private
   */
  _x: function () {
    var me = this;

    if (me.context) me.context.setTransform(me.a, me.b, me.c, me.d, me.e, me.f);

    if (me._st) me._st[me._px] = me.useCSS3D ? me.toCSS3D() : me.toCSS(); // can be optimized pre-storing func ref.

    return me;
  },
};

// Node support
if (typeof exports !== 'undefined') exports.Matrix = Matrix;