diff --git a/src/basic/Matrix.js b/src/basic/Matrix.js new file mode 100644 index 00000000..c7d5defd --- /dev/null +++ b/src/basic/Matrix.js @@ -0,0 +1,493 @@ +// 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"); + +var Matrix = Base.extend({ + /** + * Creates a 2D affine transform. 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'] [ m00 m01 m02 ] [ x ] [ m00x + m01y + m02 ] + * [ y'] = [ m10 m11 m12 ] [ y ] = [ m10x + m11y + m12 ] + * [ 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). + * + * @param {number} m00 The m00 coordinate of the transform. + * @param {number} m10 The m10 coordinate of the transform. + * @param {number} m01 The m01 coordinate of the transform. + * @param {number} m11 The m11 coordinate of the transform. + * @param {number} m02 The m02 coordinate of the transform. + * @param {number} m12 The m12 coordinate of the transform. + * @constructor + */ + initialize: function(m00, m10, m01, m11, m02, m12) { + if (arguments.length == 6) { + this.set(m00, m10, m01, m11, m02, m12); + } else if (arguments == 1) { + var mx = arguments[0]; + // TODO: Check for array! + this.set(mx._m00, mx._m10, mx._m01, mx._m11, mx._m02, mx._m12); + } else if (arguments.length) { + throw Error('Insufficient matrix parameters'); + } else { + this._m00 = this._m11 = 1; + this._m10 = this._m01 = this._m02 = this._m12 = 0; + } + }, + + /** + * @return {Matrix} A copy of this transform. + */ + clone: function() { + return new Matrix(this._m00, this._m10, this._m01, + this._m11, this._m02, this._m12); + }, + + /** + * Sets this transform to the matrix specified by the 6 values. + * + * @param {number} m00 The m00 coordinate of the transform. + * @param {number} m10 The m10 coordinate of the transform. + * @param {number} m01 The m01 coordinate of the transform. + * @param {number} m11 The m11 coordinate of the transform. + * @param {number} m02 The m02 coordinate of the transform. + * @param {number} m12 The m12 coordinate of the transform. + * @return {Matrix} This affine transform. + */ + set: function(m00, m10, m01, m11, m02, m12) { + this._m00 = m00; + this._m10 = m10; + this._m01 = m01; + this._m11 = m11; + this._m02 = m02; + this._m12 = m12; + return this; + }, + + /** + * Concatentates this transform with a scaling transformation. + * + * @param {number} sx The x-axis scaling factor. + * @param {number} sy The y-axis scaling factor. + * @param {Point} center The optional center for the scaling transformation. + * @return {Matrix} This affine transform. + */ + scale: function(sx, sy /* | scale */, center) { + // TODO: Make single scale parameter work with center points! + // Check arguments.length and typeof arguments[1], if object, assume + // scale + center = Point.read(arguments, 2); + // TODO: Optimise calls to translate to not rely on point conversion + // use private translate function instead. + if (center) + this.translate(center.x, center.y); + this._m00 *= sx; + this._m10 *= sx; + this._m01 *= sy; + this._m11 *= sy; + if (center) + this.translate(-center.x, -center.y); + return this; + }, + + /** + * Concatentates this transform with a translate 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. + */ + translate: function(point) { + point = Point.read(arguments); + if (point) { + var x = point.x, y = point.y; + this._m02 += x * this._m00 + y * this._m01; + this._m12 += x * this._m10 + y * this._m11; + } + return this; + }, + + /** + * Concatentates this transform with a rotation transformation around an + * anchor point. + * + * @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)); + }, + + /** + * Concatentates this transform with a shear transformation. + * + * @param {number} shx The x shear factor. + * @param {number} shy The y shear factor. + * @param {Point} center The optional center for the shear transformation. + * @return {Matrix} This affine transform. + */ + shear: function(shx, shy, center) { + center = Point.read(arguments, 2); + // TODO: Optimise calls to translate to not rely on point conversion + // use private translate function instead. + if (center) + this.translate(center.x, center.y); + var m00 = this._m00; + var m10 = this._m10; + this._m00 += shy * this._m01; + this._m10 += shy * this._m11; + this._m01 += shx * m00; + this._m11 += shx * m10; + if (center) + this.translate(-center.x, -center.y); + return this; + }, + + /** + * @return {string} A string representation of this transform. The format of + * of the string is compatible with SVG matrix notation, i.e. + * "matrix(a,b,c,d,e,f)". + */ + toString: function() { + // TODO: Make behave the same as in Scriptographer + return 'matrix(' + [this._m00, this._m10, this._m01, this._m11, + this._m02, this._m12].join(',') + ')'; + }, + + /** + * @return {number} The scaling factor in the x-direction (m00). + */ + getScaleX: function() { + return this._m00; + }, + + setScaleX: function(scaleX) { + this._m00 = scaleX; + }, + + /** + * @return {number} The scaling factor in the y-direction (m11). + */ + getScaleY: function() { + return this._m11; + }, + + setScaleY: function(scaleY) { + this._m11 = scaleY; + }, + + /** + * @return {number} The translation in the x-direction (m02). + */ + getTranslateX: function() { + return this._m02; + }, + + setTranslateX: function(translateX) { + this._m02 = translateX; + }, + + /** + * @return {number} The translation in the y-direction (m12). + */ + getTranslateY: function() { + return this._m12; + }, + + setTranslateY: function(translateY) { + this._m12 = translateY; + }, + + /** + * @return {number} The shear factor in the x-direction (m01). + */ + getShearX: function() { + return this._m01; + }, + + setShearX: function(shearX) { + this._m01 = shearX; + }, + + /** + * @return {number} The shear factor in the y-direction (m10). + */ + getShearY: function() { + return this._m10; + }, + + setShearY: function(shearY) { + this._m10 = shearY; + }, + + /** + * Concatenates an affine transform to this transform. + * + * @param {Matrix} mx The transform to concatenate. + * @return {Matrix} This affine transform. + */ + concatenate: function(mx) { + var m0 = this._m00; + var m1 = this._m01; + this._m00 = mx._m00 * m0 + mx._m10 * m1; + this._m01 = mx._m01 * m0 + mx._m11 * m1; + this._m02 += mx._m02 * m0 + mx._m12 * m1; + + m0 = this._m10; + m1 = this._m11; + this._m10 = mx._m00 * m0 + mx._m10 * m1; + this._m11 = mx._m01 * m0 + mx._m11 * m1; + this._m12 += mx._m02 * m0 + mx._m12 * m1; + 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 m0 = this._m00; + var m1 = this._m10; + this._m00 = mx._m00 * m0 + mx._m01 * m1; + this._m10 = mx._m10 * m0 + mx._m11 * m1; + + m0 = this._m01; + m1 = this._m11; + this._m01 = mx._m00 * m0 + mx._m01 * m1; + this._m11 = mx._m10 * m0 + mx._m11 * m1; + + m0 = this._m02; + m1 = this._m12; + this._m02 = mx._m00 * m0 + mx._m01 * m1 + mx._m02; + this._m12 = mx._m10 * m0 + mx._m11 * m1 + mx._m12; + return this; + }, + + /** + * Transforms a point or an array of coordinates by this matrix and returns + * the result. If an array is transformed, the the result is stored into a + * destination array. + * + * @param {Point} point The point to be transformed. + * + * @param {Array} 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 {Array} 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. + */ + transform: function(/* point | */ src, srcOff, dst, dstOff, numPts) { + if (arguments.length == 5) { + var i = srcOff; + var j = dstOff; + var srcEnd = srcOff + 2 * numPts; + while (i < srcEnd) { + var x = src[i++]; + var y = src[i++]; + dst[j++] = x * this._m00 + y * this._m01 + this._m02; + dst[j++] = x * this._m10 + y * this._m11 + this._m12; + } + return dst; + } else if (arguments.length > 0) { + var point = Point.read(arguments); + if (point) { + var x = point.x, y = point.y; + return new Point( + x * this._m00 + y * this._m01 + this._m02, + x * this._m10 + y * this._m11 + this._m12 + ); + } + } + return null; + }, + + /** + * @return {number} The determinant of this transform. + */ + getDeterminant: function() { + return this._m00 * this._m11 - this._m01 * this._m10; + }, + + /** + * @return {boolean} Whether this transform is the identity transform. + */ + isIdentity: function() { + return this._m00 == 1 && this._m10 == 0 && this._m01 == 0 && + this._m11 == 1 && this._m02 == 0 && this._m12 == 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.getDeterminant(); + return isFinite(det) && det != 0 && isFinite(this._m02) + && isFinite(this._m12); + }, + + /** + * 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(); + }, + + /** + * @return {Matrix} An Matrix object representing the inverse transformation. + */ + createInverse: function() { + var det = this.getDeterminant(); + if (isFinite(det) && det != 0 && isFinite(this._m02) + && isFinite(this._m12)) { + return new Matrix( + this._m11 / det, + -this._m10 / det, + -this._m01 / det, + this._m00 / det, + (this._m01 * this._m12 - this._m11 * this._m02) / det, + (this._m10 * this._m02 - this._m00 * this._m12) / det); + } + return null; + }, + + /** + * Sets this transform to a scaling transformation. + * + * @param {number} sx The x-axis scaling factor. + * @param {number} sy The y-axis scaling factor. + * @return {Matrix} This affine transform. + */ + setToScale: function(sx, sy) { + return this.set(sx, 0, 0, sy, 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); + if (delta) { + return this.set(1, 0, 0, 1, delta.x, delta.y); + } + return this; + }, + + /** + * Sets this transform to a shearing transformation. + * + * @param {number} shx The x-axis shear factor. + * @param {number} shy The y-axis shear factor. + * @return {Matrix} This affine transform. + */ + setToShear: function(shx, shy) { + return this.set(1, shy, shx, 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); + if (center) { + angle = angle * Math.PI / 180.0; + var x = center.x, y = center.y; + var cos = Math.cos(angle); + var sin = Math.sin(angle); + return this.set(cos, sin, -sin, cos, + x - x * cos + y * sin, + y - x * sin - y * cos); + } + return this; + }, + + statics: { + /** + * Creates a transform representing a scaling transformation. + * + * @param {number} sx The x-axis scaling factor. + * @param {number} sy The y-axis scaling factor. + * @return {Matrix} A transform representing a scaling + * transformation. + */ + getScaleInstance: function(sx, sy) { + 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. + * @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} shx The x-axis shear factor. + * @param {number} shy The y-axis shear factor. + * @return {Matrix} A transform representing a shearing + * transformation. + */ + getShearInstance: function(shx, shy, 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); + } + } +}); + diff --git a/src/item/Item.js b/src/item/Item.js index 7d9c12b4..45708d3a 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -384,5 +384,152 @@ Item = Base.extend({ parent = parent.parent; } return false; + }, + + getBounds: function() { + // TODO: Implement + return new Rectangle(); + }, + + setBounds: function(rect) { + var bounds = this.bounds; + rect = Rectangle.read(arguments); + var matrix = new Matrix(); + // Read this from bottom to top: + // Translate to new center: + var center = rect.center; + matrix.translate(center); + // Scale to new Size, if size changes and avoid divisions by 0: + if (rect.width != bounds.width || rect.height != bounds.height) { + matrix.scale( + bounds.width != 0 ? rect.width / bounds.width : 1, + bounds.height != 0 ? rect.height / bounds.height : 1); + } + // Translate to center: + center = bounds.center; + matrix.translate(-center.x, -center.y); + // Now execute the transformation: + transform(matrix); + }, + + /** + * The item's position within the art board. This is the + * {@link Rectangle#getCenter()} of the {@link Item#getBounds()} rectangle. + * + * Sample code: + *
+ * // Create a circle at position { x: 10, y: 10 }
+ * var circle = new Path.Circle(new Point(10, 10), 10);
+ *
+ * // Move the circle to { x: 20, y: 20 }
+ * circle.position = new Point(20, 20);
+ *
+ * // Move the circle 10 points to the right
+ * circle.position += new Point(10, 0);
+ * print(circle.position); // { x: 30, y: 20 }
+ *
+ */
+ getPosition: function() {
+ return this.bounds.center;
+ },
+
+ setPosition: function(point) {
+ translate(point.subtract(this.position));
+ },
+
+ /**
+ * @param flags: Array of any of the following: 'objects', 'children',
+ * 'fill-gradients', 'fill-patterns', 'stroke-patterns', 'lines'.
+ * Default: ['objects', 'children']
+ */
+ transform: function(matrix, flags) {
+ // TODO: Walk DOM and call transform on chidren, depending on flags
+ // TODO: Handle flags, add TransformFlag class and convert to bit mask
+ // for quicker checking
+ if (this.transformContent)
+ this.transformContent(matrix, flags);
+ if (this.children) {
+ for (var i = 0, l = this.children.length; i < l; i++) {
+ var child = this.children[i];
+ child.transform(matrix, flags);
+ }
+ }
+ },
+
+/*
+ transformContent: function(matrix, flags) {
+ // The code that performs the actual transformation of content,
+ // if defined. Item itself does not define this.
+ },
+*/
+ /**
+ * Translates (moves) the item by the given offset point.
+ *
+ * Sample code:
+ *
+ * // Create a circle at position { x: 10, y: 10 }
+ * var circle = new Path.Circle(new Point(10, 10), 10);
+ * circle.translate(new Point(5, 10));
+ * print(circle.position); // {x: 15, y: 20}
+ *
+ *
+ * Alternatively you can also add to the {@link #getPosition()} of the item:
+ *
+ * // Create a circle at position { x: 10, y: 10 }
+ * var circle = new Path.Circle(new Point(10, 10), 10);
+ * circle.position += new Point(5, 10);
+ * print(circle.position); // {x: 15, y: 20}
+ *
+ *
+ * @param delta
+ */
+ translate: function(delta) {
+ var mx = new Matrix();
+ mx.translate.apply(mx, arguments);
+ this.transform(mx);
+ },
+
+ /**
+ * {@grouptitle Transform Functions}
+ *
+ * Scales the item by the given values from its center point, or optionally
+ * by a supplied point.
+ *
+ * @param sx
+ * @param sy
+ * @param center {@default the center point of the item}
+ *
+ * @see Matrix#scale(double, double, Point center)
+ */
+ scale: function(sx, sy /* | scale */, center) {
+ // TODO: Make single scale parameter work, and still pass center
+ // or position
+ this.transform(new Matrix().scale(sx, sy, center || this.position));
+ },
+
+ /**
+ * Rotates the item by a given angle around the given point.
+ *
+ * Angles are oriented clockwise and measured in degrees by default. Read
+ * more about angle units and orientation in the description of the
+ * {@link com.scriptographer.ai.Point#getAngle()} property.
+ *
+ * @param angle the rotation angle
+ * @see Matrix#rotate(double, Point)
+ */
+ rotate: function(angle, center) {
+ this.transform(new Matrix().rotate(angle, center || this.position));
+ },
+
+ /**
+ * Shears the item with a given amount around its center point.
+ *
+ * @param shx
+ * @param shy
+ * @see Matrix#shear(double, double)
+ */
+ shear: function(shx, shy, center) {
+ // TODO: Add support for center ack to Scriptographer too!
+ this.transform(new Matrix().shear(shx, shy, center || this.position));
}
});
diff --git a/src/path/PathItem.js b/src/path/PathItem.js
index 3fb1ee90..05e60be0 100644
--- a/src/path/PathItem.js
+++ b/src/path/PathItem.js
@@ -145,8 +145,17 @@ PathItem = Item.extend(new function() {
return new Rectangle(min.x, min.y, max.x - min.x , max.y - min.y);
},
- setBounds: function(bounds) {
- // TODO:
+ transformContent: function(matrix, flags) {
+ for (var i = 0, l = this._segments.length; i < l; i++) {
+ var segment = this._segments[i];
+ var point = segment.point;
+ var handleIn = segment.handleIn.add(point);
+ var handleOut = segment.handleOut.add(point);
+ point = matrix.transform(point);
+ segment.point = point;
+ segment.handleIn = matrix.transform(handleIn).subtract(point);
+ segment.handleOut = matrix.transform(handleOut).subtract(point);
+ }
},
addSegment: function(segment) {