/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey * http://scratchdisk.com/ & http://jonathanpuckey.com/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ // Based on goog.graphics.AffineTransform, as part of the Closure Library. // Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); /** * @name Matrix * * @class An affine transform performs a linear mapping from 2D coordinates * to other 2D coordinates that preserves the "straightness" and * "parallelness" of lines. * * Such a coordinate transformation can be represented by a 3 row by 3 * column matrix with an implied last row of [ 0 0 1 ]. This matrix * transforms source coordinates (x,y) into destination coordinates (x',y') * by considering them to be a column vector and multiplying the coordinate * vector by the matrix according to the following process: * * [ x ] [ a b tx ] [ x ] [ a * x + b * y + tx ] * [ y ] = [ c d ty ] [ y ] = [ c * x + d * y + ty ] * [ 1 ] [ 0 0 1 ] [ 1 ] [ 1 ] * * This class is optimized for speed and minimizes calculations based on its * knowledge of the underlying matrix (as opposed to say simply performing * matrix multiplication). */ var Matrix = Base.extend(/** @lends Matrix# */{ _class: 'Matrix', /** * Creates a 2D affine transform. * * @param {Number} a the a property of the transform * @param {Number} c the c property of the transform * @param {Number} b the b property of the transform * @param {Number} d the d property of the transform * @param {Number} tx the tx property of the transform * @param {Number} ty the ty property of the transform */ initialize: function Matrix(arg) { var count = arguments.length, ok = true; if (count === 6) { this.set.apply(this, arguments); } else if (count === 1) { if (arg instanceof Matrix) { this.set(arg._a, arg._c, arg._b, arg._d, arg._tx, arg._ty); } else if (Array.isArray(arg)) { this.set.apply(this, arg); } else { ok = false; } } else if (count === 0) { this.reset(); } else { ok = false; } if (!ok) throw new Error('Unsupported matrix parameters'); }, /** * Sets this transform to the matrix specified by the 6 values. * * @param {Number} a the a property of the transform * @param {Number} c the c property of the transform * @param {Number} b the b property of the transform * @param {Number} d the d property of the transform * @param {Number} tx the tx property of the transform * @param {Number} ty the ty property of the transform * @return {Matrix} this affine transform */ set: function(a, c, b, d, tx, ty, _dontNotify) { this._a = a; this._c = c; this._b = b; this._d = d; this._tx = tx; this._ty = ty; if (!_dontNotify) this._changed(); return this; }, _serialize: function(options) { return Base.serialize(this.getValues(), options); }, _changed: function() { var owner = this._owner; if (owner) { // If owner has #applyMatrix set, directly bake the change in now. if (owner._applyMatrix) { owner.transform(null, true); } else { owner._changed(/*#=*/Change.GEOMETRY); } } }, /** * @return {Matrix} a copy of this transform */ clone: function() { return new Matrix(this._a, this._c, this._b, this._d, this._tx, this._ty); }, /** * Checks whether the two matrices describe the same transformation. * * @param {Matrix} matrix the matrix to compare this matrix to * @return {Boolean} {@true if the matrices are equal} */ equals: function(mx) { return mx === this || mx && this._a === mx._a && this._b === mx._b && this._c === mx._c && this._d === mx._d && this._tx === mx._tx && this._ty === mx._ty || false; }, /** * @return {String} a string representation of this transform */ toString: function() { var f = Formatter.instance; return '[[' + [f.number(this._a), f.number(this._b), f.number(this._tx)].join(', ') + '], [' + [f.number(this._c), f.number(this._d), f.number(this._ty)].join(', ') + ']]'; }, /** * Resets the matrix by setting its values to the ones of the identity * matrix that results in no transformation. */ reset: function(_dontNotify) { this._a = this._d = 1; this._c = this._b = this._tx = this._ty = 0; if (!_dontNotify) this._changed(); return this; }, /** * Attempts to apply the matrix to the content of item that it belongs to, * meaning its transformation is baked into the item's content or children. * * @param {Boolean} recursively controls whether to apply transformations * recursively on children * @return {Boolean} {@true if the matrix was applied} */ apply: function(recursively, _setApplyMatrix) { var owner = this._owner; if (owner) { owner.transform(null, true, Base.pick(recursively, true), _setApplyMatrix); // If the matrix was successfully applied, it will be reset now. return this.isIdentity(); } return false; }, /** * Concatenates this transform with a translate transformation. * * @name Matrix#translate * @function * @param {Point} point the vector to translate by * @return {Matrix} this affine transform */ /** * Concatenates this transform with a translate transformation. * * @name Matrix#translate * @function * @param {Number} dx the distance to translate in the x direction * @param {Number} dy the distance to translate in the y direction * @return {Matrix} this affine transform */ translate: function(/* point */) { var point = Point.read(arguments), x = point.x, y = point.y; this._tx += x * this._a + y * this._b; this._ty += x * this._c + y * this._d; this._changed(); return this; }, /** * Concatenates this transform with a scaling transformation. * * @name Matrix#scale * @function * @param {Number} scale the scaling factor * @param {Point} [center] the center for the scaling transformation * @return {Matrix} this affine transform */ /** * Concatenates this transform with a scaling transformation. * * @name Matrix#scale * @function * @param {Number} hor the horizontal scaling factor * @param {Number} ver the vertical scaling factor * @param {Point} [center] the center for the scaling transformation * @return {Matrix} this affine transform */ scale: function(/* scale, center */) { var scale = Point.read(arguments), center = Point.read(arguments, 0, { readNull: true }); if (center) this.translate(center); this._a *= scale.x; this._c *= scale.x; this._b *= scale.y; this._d *= scale.y; if (center) this.translate(center.negate()); this._changed(); return this; }, /** * Concatenates this transform with a rotation transformation around an * anchor point. * * @name Matrix#rotate * @function * @param {Number} angle the angle of rotation measured in degrees * @param {Point} center the anchor point to rotate around * @return {Matrix} this affine transform */ /** * Concatenates this transform with a rotation transformation around an * anchor point. * * @name Matrix#rotate * @function * @param {Number} angle the angle of rotation measured in degrees * @param {Number} x the x coordinate of the anchor point * @param {Number} y the y coordinate of the anchor point * @return {Matrix} this affine transform */ rotate: function(angle /*, center */) { angle *= Math.PI / 180; var center = Point.read(arguments, 1), // Concatenate rotation matrix into this one x = center.x, y = center.y, cos = Math.cos(angle), sin = Math.sin(angle), tx = x - x * cos + y * sin, ty = y - x * sin - y * cos, a = this._a, b = this._b, c = this._c, d = this._d; this._a = cos * a + sin * b; this._b = -sin * a + cos * b; this._c = cos * c + sin * d; this._d = -sin * c + cos * d; this._tx += tx * a + ty * b; this._ty += tx * c + ty * d; this._changed(); return this; }, /** * Concatenates this transform with a shear transformation. * * @name Matrix#shear * @function * @param {Point} shear the shear factor in x and y direction * @param {Point} [center] the center for the shear transformation * @return {Matrix} this affine transform */ /** * Concatenates this transform with a shear transformation. * * @name Matrix#shear * @function * @param {Number} hor the horizontal shear factor * @param {Number} ver the vertical shear factor * @param {Point} [center] the center for the shear transformation * @return {Matrix} this affine transform */ shear: function(/* shear, center */) { // Do not modify point, center, since that would arguments of which // we're reading from! var shear = Point.read(arguments), center = Point.read(arguments, 0, { readNull: true }); if (center) this.translate(center); var a = this._a, c = this._c; this._a += shear.y * this._b; this._c += shear.y * this._d; this._b += shear.x * a; this._d += shear.x * c; if (center) this.translate(center.negate()); this._changed(); return this; }, /** * Concatenates this transform with a skew transformation. * * @name Matrix#skew * @function * @param {Point} skew the skew angles in x and y direction in degrees * @param {Point} [center] the center for the skew transformation * @return {Matrix} this affine transform */ /** * Concatenates this transform with a skew transformation. * * @name Matrix#skew * @function * @param {Number} hor the horizontal skew angle in degrees * @param {Number} ver the vertical skew angle in degrees * @param {Point} [center] the center for the skew transformation * @return {Matrix} this affine transform */ skew: function(/* skew, center */) { var skew = Point.read(arguments), center = Point.read(arguments, 0, { readNull: true }), toRadians = Math.PI / 180, shear = new Point(Math.tan(skew.x * toRadians), Math.tan(skew.y * toRadians)); return this.shear(shear, center); }, /** * Appends the specified matrix to this matrix. This is the equivalent of * multiplying `(this matrix) * (specified matrix)`. * * @param {Matrix} matrix the matrix to append * @return {Matrix} this matrix, modified */ append: function(mx) { var a1 = this._a, b1 = this._b, c1 = this._c, d1 = this._d, a2 = mx._a, b2 = mx._b, c2 = mx._c, d2 = mx._d, tx2 = mx._tx, ty2 = mx._ty; this._a = a2 * a1 + c2 * b1; this._b = b2 * a1 + d2 * b1; this._c = a2 * c1 + c2 * d1; this._d = b2 * c1 + d2 * d1; this._tx += tx2 * a1 + ty2 * b1; this._ty += tx2 * c1 + ty2 * d1; this._changed(); return this; }, /** * Returns a new matrix as the result of appending the specified matrix to * this matrix. This is the equivalent of multiplying * `(this matrix) * (specified matrix)`. * * @param {Matrix} matrix the matrix to append * @return {Matrix} the newly created matrix */ appended: function(mx) { return this.clone().append(mx); }, /** * Prepends the specified matrix to this matrix. This is the equivalent of * multiplying `(specified matrix) * (this matrix)`. * * @param {Matrix} matrix the matrix to prepend * @return {Matrix} this matrix, modified */ prepend: function(mx) { var a1 = this._a, b1 = this._b, c1 = this._c, d1 = this._d, tx1 = this._tx, ty1 = this._ty, a2 = mx._a, b2 = mx._b, c2 = mx._c, d2 = mx._d, tx2 = mx._tx, ty2 = mx._ty; this._a = a2 * a1 + b2 * c1; this._b = a2 * b1 + b2 * d1; this._c = c2 * a1 + d2 * c1; this._d = c2 * b1 + d2 * d1; this._tx = a2 * tx1 + b2 * ty1 + tx2; this._ty = c2 * tx1 + d2 * ty1 + ty2; this._changed(); return this; }, /** * Returns a new matrix as the result of prepending the specified matrix * to this matrix. This is the equivalent of multiplying * `(specified matrix) s* (this matrix)`. * * @param {Matrix} matrix the matrix to prepend * @return {Matrix} the newly created matrix */ prepended: function(mx) { return this.clone().prepend(mx); }, /** * Inverts the matrix, causing it to perform the opposite transformation. * If the matrix is not invertible (in which case {@link #isSingular()} * returns true), `null` is returned. * * @return {Matrix} this matrix, or `null`, if the matrix is singular. */ invert: function() { var a = this._a, b = this._b, c = this._c, d = this._d, tx = this._tx, ty = this._ty, det = a * d - b * c, res = null; if (det && !isNaN(det) && isFinite(tx) && isFinite(ty)) { this._a = d / det; this._b = -b / det; this._c = -c / det; this._d = a / det; this._tx = (b * ty - d * tx) / det; this._ty = (c * tx - a * ty) / det; res = this; } return res; }, /** * Creates a new matrix that is the inversion of this matrix, causing it to * perform the opposite transformation. If the matrix is not invertible (in * which case {@link #isSingular()} returns true), `null` is returned. * * @return {Matrix} this matrix, or `null`, if the matrix is singular. */ inverted: function() { return this.clone().invert(); }, /** * @deprecated, use use {@link #append(matrix)} instead. */ concatenate: '#append', /** * @deprecated, use use {@link #prepend(matrix)} instead. */ preConcatenate: '#prepend', /** * @deprecated, use use {@link #appended(matrix)} instead. */ chain: '#appended', /** * A private helper function to create a clone of this matrix, without the * translation factored in. * * @return {Matrix} a clone of this matrix, with {@link #tx} and {@link #ty} * set to `0`. */ _shiftless: function() { return new Matrix(this._a, this._c, this._b, this._d, 0, 0); }, _orNullIfIdentity: function() { return this.isIdentity() ? null : this; }, /** * @return {Boolean} whether this transform is the identity transform */ isIdentity: function() { return this._a === 1 && this._c === 0 && this._b === 0 && this._d === 1 && this._tx === 0 && this._ty === 0; }, /** * Returns whether the transform is invertible. A transform is not * invertible if the determinant is 0 or any value is non-finite or NaN. * * @return {Boolean} whether the transform is invertible */ isInvertible: function() { var det = this._a * this._d - this._b * this._c; return det && !isNaN(det) && isFinite(this._tx) && isFinite(this._ty); }, /** * Checks whether the matrix is singular or not. Singular matrices cannot be * inverted. * * @return {Boolean} whether the matrix is singular */ isSingular: function() { return !this.isInvertible(); }, /** * Transforms a point and returns the result. * * @name Matrix#transform * @function * @param {Point} point the point to be transformed * @return {Point} the transformed point */ /** * Transforms an array of coordinates by this matrix and stores the results * into the destination array, which is also returned. * * @name Matrix#transform * @function * @param {Number[]} src the array containing the source points * as x, y value pairs * @param {Number[]} dst the array into which to store the transformed * point pairs * @param {Number} count the number of points to transform * @return {Number[]} the dst array, containing the transformed coordinates */ transform: function(/* point | */ src, dst, count) { return arguments.length < 3 // TODO: Check for rectangle and use _tranformBounds? ? this._transformPoint(Point.read(arguments)) : this._transformCoordinates(src, dst, count); }, /** * A faster version of transform that only takes one point and does not * attempt to convert it. */ _transformPoint: function(point, dest, _dontNotify) { var x = point.x, y = point.y; if (!dest) dest = new Point(); return dest.set( x * this._a + y * this._b + this._tx, x * this._c + y * this._d + this._ty, _dontNotify); }, _transformCoordinates: function(src, dst, count) { var i = 0, j = 0, max = 2 * count; while (i < max) { var x = src[i++], y = src[i++]; dst[j++] = x * this._a + y * this._b + this._tx; dst[j++] = x * this._c + y * this._d + this._ty; } return dst; }, _transformCorners: function(rect) { var x1 = rect.x, y1 = rect.y, x2 = x1 + rect.width, y2 = y1 + rect.height, coords = [ x1, y1, x2, y1, x2, y2, x1, y2 ]; return this._transformCoordinates(coords, coords, 4); }, /** * Returns the 'transformed' bounds rectangle by transforming each corner * point and finding the new bounding box to these points. This is not * really the transformed rectangle! */ _transformBounds: function(bounds, dest, _dontNotify) { var coords = this._transformCorners(bounds), min = coords.slice(0, 2), max = min.slice(); for (var i = 2; i < 8; i++) { var val = coords[i], j = i & 1; if (val < min[j]) { min[j] = val; } else if (val > max[j]) { max[j] = val; } } if (!dest) dest = new Rectangle(); return dest.set(min[0], min[1], max[0] - min[0], max[1] - min[1], _dontNotify); }, /** * Inverse transforms a point and returns the result. * * @param {Point} point the point to be transformed */ inverseTransform: function(/* point */) { return this._inverseTransform(Point.read(arguments)); }, _inverseTransform: function(point, dest, _dontNotify) { var a = this._a, b = this._b, c = this._c, d = this._d, tx = this._tx, ty = this._ty, det = a * d - b * c, res = null; if (det && !isNaN(det) && isFinite(tx) && isFinite(ty)) { var x = point.x - this._tx, y = point.y - this._ty; if (!dest) dest = new Point(); res = dest.set( (x * d - y * b) / det, (y * a - x * c) / det, _dontNotify); } return res; }, /** * Attempts to decompose the affine transformation described by this matrix * into `scaling`, `rotation` and `shearing`, and returns an object with * these properties if it succeeded, `null` otherwise. * * @return {Object} the decomposed matrix, or `null` if decomposition is not * possible */ decompose: function() { // http://dev.w3.org/csswg/css3-2d-transforms/#matrix-decomposition // http://stackoverflow.com/questions/4361242/ // https://github.com/wisec/DOMinator/blob/master/layout/style/nsStyleAnimation.cpp#L946 var a = this._a, b = this._b, c = this._c, d = this._d; if (Numerical.isZero(a * d - b * c)) return null; var scaleX = Math.sqrt(a * a + b * b); a /= scaleX; b /= scaleX; var shear = a * c + b * d; c -= a * shear; d -= b * shear; var scaleY = Math.sqrt(c * c + d * d); c /= scaleY; d /= scaleY; shear /= scaleY; // a * d - b * c should now be 1 or -1 if (a * d < b * c) { a = -a; b = -b; // We don't need c & d anymore, but if we did, we'd have to do this: // c = -c; // d = -d; shear = -shear; scaleX = -scaleX; } return { scaling: new Point(scaleX, scaleY), rotation: -Math.atan2(b, a) * 180 / Math.PI, shearing: shear }; }, /** * The value that affects the transformation along the x axis when scaling * or rotating, positioned at (0, 0) in the transformation matrix. * * @name Matrix#a * @type Number */ /** * The value that affects the transformation along the y axis when rotating * or skewing, positioned at (1, 0) in the transformation matrix. * * @name Matrix#c * @type Number */ /** * The value that affects the transformation along the x axis when rotating * or skewing, positioned at (0, 1) in the transformation matrix. * * @name Matrix#b * @type Number */ /** * The value that affects the transformation along the y axis when scaling * or rotating, positioned at (1, 1) in the transformation matrix. * * @name Matrix#d * @type Number */ /** * The distance by which to translate along the x axis, positioned at (2, 0) * in the transformation matrix. * * @name Matrix#tx * @type Number */ /** * The distance by which to translate along the y axis, positioned at (2, 1) * in the transformation matrix. * * @name Matrix#ty * @type Number */ /** * The transform values as an array, in the same sequence as they are passed * to {@link #initialize(a, c, b, d, tx, ty)}. * * @bean * @type Number[] */ getValues: function() { return [ this._a, this._c, this._b, this._d, this._tx, this._ty ]; }, /** * The translation of the matrix as a vector. * * @bean * @type Point */ getTranslation: function() { // No decomposition is required to extract translation. return new Point(this._tx, this._ty); }, /** * The scaling values of the matrix, if it can be decomposed. * * @bean * @type Point * @see #decompose() */ getScaling: function() { return (this.decompose() || {}).scaling; }, /** * The rotation angle of the matrix, if it can be decomposed. * * @bean * @type Number * @see #decompose() */ getRotation: function() { return (this.decompose() || {}).rotation; }, /** * Applies this matrix to the specified Canvas Context. * * @param {CanvasRenderingContext2D} ctx */ applyToContext: function(ctx) { if (!this.isIdentity()) { ctx.transform(this._a, this._c, this._b, this._d, this._tx, this._ty); } } }, Base.each(['a', 'c', 'b', 'd', 'tx', 'ty'], function(key) { // Create getters and setters for all internal attributes. var part = Base.capitalize(key), prop = '_' + key; this['get' + part] = function() { return this[prop]; }; this['set' + part] = function(value) { this[prop] = value; this._changed(); }; }, {}));