From ee8c30b518d0a5d794edeb99fccff3b23a105777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 14 Feb 2011 01:05:16 +0000 Subject: [PATCH] Implement Matrix, Item#position, Item#bounds (setter too), Item#transform(), Item#scale(), Item#rotate(), Item#transalte() and Item#shear(). Some of it work in progress. --- src/basic/Matrix.js | 493 +++++++++++++++++++++++++++++++++++++++++++ src/item/Item.js | 147 +++++++++++++ src/path/PathItem.js | 13 +- 3 files changed, 651 insertions(+), 2 deletions(-) create mode 100644 src/basic/Matrix.js 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) {