paper.js/src/basic/Matrix.js

703 lines
19 KiB
JavaScript
Raw Normal View History

2011-03-06 19:50:44 -05:00
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
2011-03-07 20:41:50 -05:00
* http://paperjs.org/
*
* Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
2011-03-06 19:50:44 -05:00
* http://lehni.org/ & http://jonathanpuckey.com/
*
2011-07-01 06:17:45 -04:00
* Distributed under the MIT license. See LICENSE file for details.
*
2011-03-07 20:41:50 -05:00
* All rights reserved.
2011-03-06 19:50:44 -05:00
*/
// 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:
* <pre>
* [ 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 ]
* </pre>
*
* 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 = this.Matrix = Base.extend(/** @lends Matrix# */{
_type: 'matrix',
/**
2011-05-22 18:54:27 -04:00
* Creates a 2D affine transform.
*
* @param {Number} a The scaleX coordinate of the transform
* @param {Number} c The shearY coordinate of the transform
* @param {Number} b The shearX coordinate of the transform
* @param {Number} d The scaleY coordinate of the transform
* @param {Number} tx The translateX coordinate of the transform
* @param {Number} ty The translateY coordinate of the transform
*/
2011-07-25 04:15:35 -04:00
initialize: function(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);
2011-07-25 04:15:35 -04:00
} else if (Array.isArray(arg)) {
this.set.apply(this, arg);
} else {
ok = false;
}
2011-07-25 04:15:35 -04:00
} else if (count == 0) {
2011-12-20 17:14:30 -05:00
this.setIdentity();
2011-07-25 04:15:35 -04:00
} else {
ok = false;
}
if (!ok)
2011-03-03 12:29:40 -05:00
throw new Error('Unsupported matrix parameters');
},
_serialize: function() {
return this.getValues();
},
/**
* @return {Matrix} A copy of this transform.
*/
clone: function() {
return Matrix.create(this._a, this._c, this._b, this._d,
this._tx, this._ty);
},
/**
* Sets this transform to the matrix specified by the 6 values.
*
* @param {Number} a The scaleX coordinate of the transform
* @param {Number} c The shearY coordinate of the transform
* @param {Number} b The shearX coordinate of the transform
* @param {Number} d The scaleY coordinate of the transform
* @param {Number} tx The translateX coordinate of the transform
* @param {Number} ty The translateY coordinate of the transform
* @return {Matrix} This affine transform
*/
set: function(a, c, b, d, tx, ty) {
this._a = a;
this._c = c;
this._b = b;
this._d = d;
this._tx = tx;
this._ty = ty;
return this;
},
2011-12-20 17:14:30 -05:00
setIdentity: function() {
this._a = this._d = 1;
this._c = this._b = this._tx = this._ty = 0;
2011-12-20 17:32:53 -05:00
return this;
2011-12-20 17:14:30 -05:00
},
/**
* Concatentates 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
*/
/**
* Concatentates 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) {
// Do not modify scale, center, since that would arguments of which
// we're reading from!
var _scale = Point.read(arguments),
_center = Point.read(arguments);
// TODO: Isn't center always set this way??
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());
return this;
},
2011-05-31 10:08:25 -04:00
/**
* Concatentates this transform with a translate transformation.
*
2011-05-31 10:08:25 -04:00
* @name Matrix#translate
* @function
* @param {Point} point The vector to translate by
* @return {Matrix} This affine transform
2011-05-31 10:08:25 -04:00
*/
/**
* Concatentates 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) {
point = Point.read(arguments);
var x = point.x,
y = point.y;
this._tx += x * this._a + y * this._b;
this._ty += x * this._c + y * this._d;
return this;
},
2011-05-31 10:08:25 -04:00
/**
* Concatentates this transform with a rotation transformation around an
* anchor point.
*
2011-05-31 10:08:25 -04:00
* @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
2011-05-31 10:08:25 -04:00
*/
/**
* Concatentates 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) {
return this.concatenate(
Matrix.getRotateInstance.apply(Matrix, arguments));
},
2011-05-31 10:08:25 -04:00
/**
* Concatentates this transform with a shear transformation.
*
2011-05-31 10:08:25 -04:00
* @name Matrix#shear
* @function
* @param {Point} point The shear factor in x and y direction
* @param {Point} [center] The center for the shear transformation
* @return {Matrix} This affine transform
2011-05-31 10:08:25 -04:00
*/
/**
* Concatentates 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(point, center) {
// Do not modify point, center, since that would arguments of which
// we're reading from!
var _point = Point.read(arguments),
_center = Point.read(arguments);
if (_center)
this.translate(_center);
var a = this._a,
c = this._c;
this._a += _point.y * this._b;
this._c += _point.y * this._d;
this._b += _point.x * a;
this._d += _point.x * c;
if (_center)
this.translate(_center.negate());
return this;
},
/**
* @return {String} A string representation of this transform.
*/
toString: function() {
var format = Base.formatFloat;
return '[[' + [format(this._a), format(this._b),
format(this._tx)].join(', ') + '], ['
+ [format(this._c), format(this._d),
format(this._ty)].join(', ') + ']]';
},
/**
* The scaling factor in the x-direction ({@code a}).
2011-07-25 04:15:18 -04:00
*
* @name Matrix#scaleX
* @type Number
*/
/**
* The scaling factor in the y-direction ({@code d}).
2011-07-25 04:15:18 -04:00
*
* @name Matrix#scaleY
* @type Number
*/
/**
* @return {Number} The shear factor in the x-direction ({@code b}).
2011-07-25 04:15:18 -04:00
*
* @name Matrix#shearX
2011-07-25 04:15:18 -04:00
* @type Number
*/
/**
* @return {Number} The shear factor in the y-direction ({@code c}).
2011-07-25 04:15:18 -04:00
*
* @name Matrix#shearY
2011-07-25 04:15:18 -04:00
* @type Number
*/
/**
* The translation in the x-direction ({@code tx}).
2011-07-25 04:15:18 -04:00
*
* @name Matrix#translateX
2011-07-25 04:15:18 -04:00
* @type Number
*/
/**
* The translation in the y-direction ({@code ty}).
2011-07-25 04:15:18 -04:00
*
* @name Matrix#translateY
2011-07-25 04:15:18 -04:00
* @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)}.
*
* @type Number[]
* @bean
*/
getValues: function() {
return [ this._a, this._c, this._b, this._d, this._tx, this._ty ];
},
/**
* Concatenates an affine transform to this transform.
*
* @param {Matrix} mx The transform to concatenate
* @return {Matrix} This affine transform
*/
concatenate: function(mx) {
var a = this._a,
b = this._b,
c = this._c,
d = this._d;
this._a = mx._a * a + mx._c * b;
this._b = mx._b * a + mx._d * b;
this._c = mx._a * c + mx._c * d;
this._d = mx._b * c + mx._d * d;
2011-12-20 05:39:10 -05:00
this._tx += mx._tx * a + mx._ty * b;
this._ty += mx._tx * c + mx._ty * d;
return this;
},
/**
* Pre-concatenates an affine transform to this transform.
*
* @param {Matrix} mx The transform to preconcatenate
* @return {Matrix} This affine transform
*/
preConcatenate: function(mx) {
var a = this._a,
b = this._b,
c = this._c,
d = this._d,
tx = this._tx,
ty = this._ty;
this._a = mx._a * a + mx._b * c;
this._b = mx._a * b + mx._b * d;
2011-12-20 05:39:10 -05:00
this._c = mx._c * a + mx._d * c;
this._d = mx._c * b + mx._d * d;
this._tx = mx._a * tx + mx._b * ty + mx._tx;
this._ty = mx._c * tx + mx._d * ty + mx._ty;
return this;
},
/**
* 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} srcOff The offset to the first point to be transformed
* @param {Number[]} dst The array into which to store the transformed
* point pairs
* @param {Number} dstOff The offset of the location of the first
* transformed point in the destination array
* @param {Number} numPts The number of points to tranform
* @return {Number[]} The dst array, containing the transformed coordinates.
*/
transform: function(/* point | */ src, srcOff, dst, dstOff, numPts) {
return arguments.length < 5
// TODO: Check for rectangle and use _tranformBounds?
? this._transformPoint(Point.read(arguments))
: this._transformCoordinates(src, srcOff, dst, dstOff, numPts);
},
/**
* 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 = Base.create(Point);
return dest.set(
x * this._a + y * this._b + this._tx,
x * this._c + y * this._d + this._ty,
dontNotify
);
},
_transformCoordinates: function(src, srcOff, dst, dstOff, numPts) {
var i = srcOff, j = dstOff,
srcEnd = srcOff + 2 * numPts;
while (i < srcEnd) {
2013-02-08 21:17:51 -05:00
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, 0, coords, 0, 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 reactangle!
*/
_transformBounds: function(bounds, dest, dontNotify) {
var coords = this._transformCorners(bounds),
min = coords.slice(0, 2),
max = coords.slice(0);
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 = Base.create(Rectangle);
return dest.set(min[0], min[1], max[0] - min[0], max[1] - min[1],
dontNotify);
},
2011-09-23 05:19:03 -04:00
/**
* 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));
},
/**
* Returns the determinant of this transform, but only if the matrix is
* reversible, null otherwise.
*/
_getDeterminant: function() {
var det = this._a * this._d - this._b * this._c;
return isFinite(det) && !Numerical.isZero(det)
&& isFinite(this._tx) && isFinite(this._ty)
? det : null;
},
2011-09-23 05:19:03 -04:00
_inverseTransform: function(point, dest, dontNotify) {
var det = this._getDeterminant();
if (!det)
return null;
var x = point.x - this._tx,
y = point.y - this._ty;
if (!dest)
dest = Base.create(Point);
2011-09-23 05:19:03 -04:00
return dest.set(
(x * this._d - y * this._b) / det,
(y * this._a - x * this._c) / det,
dontNotify
);
},
getTranslation: function() {
2011-12-20 17:32:53 -05:00
return Point.create(this._tx, this._ty);
},
getScaling: function() {
// http://math.stackexchange.com/questions/13150/
// http://stackoverflow.com/questions/4361242/
var hor = Math.sqrt(this._a * this._a + this._b * this._b),
ver = Math.sqrt(this._c * this._c + this._d * this._d);
return Point.create(
this._a < 0 ? -hor : hor,
this._d < 0 ? -ver : ver);
},
/**
2011-08-16 07:53:36 -04:00
* The rotation angle of the matrix. If a non-uniform rotation is applied as
* a result of a shear() or scale() command, undefined is returned, as the
* resulting transformation cannot be expressed in one rotation angle.
*
* @type Number
* @bean
*/
getRotation: function() {
var angle1 = -Math.atan2(this._b, this._a),
angle2 = Math.atan2(this._c, this._d);
return Math.abs(angle1 - angle2) < /*#=*/ Numerical.EPSILON
? angle1 * 180 / Math.PI : undefined;
},
2011-11-26 06:04:44 -05:00
/**
* 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) {
2012-12-27 07:04:21 -05:00
return mx && this._a == mx._a && this._b == mx._b && this._c == mx._c
2011-11-26 06:04:44 -05:00
&& this._d == mx._d && this._tx == mx._tx && this._ty == mx._ty;
},
/**
* @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;
},
/**
2011-02-15 17:51:02 -05:00
* 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() {
return !!this._getDeterminant();
},
/**
* Checks whether the matrix is singular or not. Singular matrices cannot be
* inverted.
*
* @return {Boolean} Whether the matrix is singular
*/
isSingular: function() {
return !this._getDeterminant();
},
/**
* Inverts the transformation of the matrix. If the matrix is not invertible
* (in which case {@link #isSingular()} returns true), {@code null } is
* returned.
*
* @return {Matrix} The inverted matrix, or {@code null }, if the matrix is
* singular
*/
createInverse: function() {
var det = this._getDeterminant();
return det && Matrix.create(
this._d / det,
-this._c / det,
-this._b / det,
this._a / det,
(this._b * this._ty - this._d * this._tx) / det,
(this._c * this._tx - this._a * this._ty) / det);
},
createShiftless: function() {
return Matrix.create(this._a, this._c, this._b, this._d, 0, 0);
},
/**
* Sets this transform to a scaling transformation.
*
* @param {Number} hor The horizontal scaling factor
* @param {Number} ver The vertical scaling factor
* @return {Matrix} This affine transform
*/
setToScale: function(hor, ver) {
return this.set(hor, 0, 0, ver, 0, 0);
},
/**
* Sets this transform to a translation transformation.
*
* @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
*/
setToTranslation: function(delta) {
delta = Point.read(arguments);
return this.set(1, 0, 0, 1, delta.x, delta.y);
},
/**
* Sets this transform to a shearing transformation.
*
* @param {Number} hor The horizontal shear factor
* @param {Number} ver The vertical shear factor
* @return {Matrix} This affine transform
*/
setToShear: function(hor, ver) {
return this.set(1, ver, hor, 1, 0, 0);
},
/**
* Sets this transform to a rotation transformation.
*
* @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
*/
setToRotation: function(angle, center) {
center = Point.read(arguments, 1);
2011-05-31 10:19:43 -04:00
angle = angle * Math.PI / 180;
var x = center.x,
y = center.y,
cos = Math.cos(angle),
sin = Math.sin(angle);
return this.set(cos, sin, -sin, cos,
x - x * cos + y * sin,
y - x * sin - y * cos);
},
2011-02-21 13:03:57 -05:00
/**
* Applies this matrix to the specified Canvas Context.
*
* @param {CanvasRenderingContext2D} ctx
* @param {Boolean} [reset=false]
2011-02-21 13:03:57 -05:00
*/
applyToContext: function(ctx, reset) {
ctx[reset ? 'setTransform' : 'transform'](
this._a, this._c, this._b, this._d, this._tx, this._ty);
return this;
2011-02-21 13:03:57 -05:00
},
2011-06-22 18:58:50 -04:00
statics: /** @lends Matrix */{
// See Point.create()
create: function(a, c, b, d, tx, ty) {
return Base.create(Matrix).set(a, c, b, d, tx, ty);
},
/**
* Creates a transform representing a scaling transformation.
*
* @param {Number} hor The horizontal scaling factor
* @param {Number} ver The vertical scaling factor
* @return {Matrix} A transform representing a scaling
* transformation
*/
getScaleInstance: function(hor, ver) {
var mx = new Matrix();
return mx.setToScale.apply(mx, arguments);
},
/**
* Creates a transform representing a translation transformation.
*
* @param {Number} dx The distance to translate in the x direction
* @param {Number} dy The distance to translate in the y direction
2011-02-15 17:51:02 -05:00
* @return {Matrix} A transform representing a translation
* transformation
*/
getTranslateInstance: function(delta) {
var mx = new Matrix();
return mx.setToTranslation.apply(mx, arguments);
},
/**
* Creates a transform representing a shearing transformation.
*
* @param {Number} hor The horizontal shear factor
* @param {Number} ver The vertical shear factor
* @return {Matrix} A transform representing a shearing transformation
*/
getShearInstance: function(hor, ver, center) {
var mx = new Matrix();
return mx.setToShear.apply(mx, arguments);
},
/**
* Creates a transform representing a rotation transformation.
*
* @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} A transform representing a rotation transformation
*/
getRotateInstance: function(angle, center) {
var mx = new Matrix();
return mx.setToRotation.apply(mx, arguments);
}
}
}, new function() {
return Base.each({
scaleX: '_a',
scaleY: '_d',
translateX: '_tx',
translateY: '_ty',
shearX: '_b',
shearY: '_c'
}, function(prop, name) {
name = Base.capitalize(name);
this['get' + name] = function() {
return this[prop];
};
this['set' + name] = function(value) {
this[prop] = value;
};
}, {});
});