diff --git a/elements/Shape.js b/elements/Shape.js index 8e8120c4..911f5a94 100644 --- a/elements/Shape.js +++ b/elements/Shape.js @@ -12,7 +12,7 @@ class Shape extends Component { this.state = this.touchableGetInitialState(); } - extractProps = (props, options = {stroke: true, fill: true, responder: true}) => { + extractProps = (props, options = {stroke: true, transform: true, fill: true, responder: true}) => { let extractedProps = extractProps(props, options); if (extractedProps.touchable && !extractedProps.disabled) { _.assign(extractedProps, { diff --git a/lib/Matrix2D.js b/lib/Matrix2D.js new file mode 100644 index 00000000..8695d225 --- /dev/null +++ b/lib/Matrix2D.js @@ -0,0 +1,285 @@ +/** + * based on + * https://github.com/CreateJS/EaselJS/blob/631cdffb85eff9413dab43b4676f059b4232d291/src/easeljs/geom/Matrix2D.js + */ + +const DEG_TO_RAD = Math.PI/180; + +/** + * Represents an affine transformation matrix, and provides tools for constructing and concatenating matrices. + * + * This matrix can be visualized as: + * + * [ a c tx + * b d ty + * 0 0 1 ] + * + * Note the locations of b and c. + * + * @class Matrix2D + * @param {Number} [a=1] Specifies the a property for the new matrix. + * @param {Number} [b=0] Specifies the b property for the new matrix. + * @param {Number} [c=0] Specifies the c property for the new matrix. + * @param {Number} [d=1] Specifies the d property for the new matrix. + * @param {Number} [tx=0] Specifies the tx property for the new matrix. + * @param {Number} [ty=0] Specifies the ty property for the new matrix. + * @constructor + **/ +class Matrix2D { + constructor(a, b, c, d, tx, ty) { + this.setTransform(a, b, c, d, tx, ty); + + // public properties: + // assigned in the setValues method. + /** + * Position (0, 0) in a 3x3 affine transformation matrix. + * @property a + * @type Number + **/ + + /** + * Position (0, 1) in a 3x3 affine transformation matrix. + * @property b + * @type Number + **/ + + /** + * Position (1, 0) in a 3x3 affine transformation matrix. + * @property c + * @type Number + **/ + + /** + * Position (1, 1) in a 3x3 affine transformation matrix. + * @property d + * @type Number + **/ + + /** + * Position (2, 0) in a 3x3 affine transformation matrix. + * @property tx + * @type Number + **/ + + /** + * Position (2, 1) in a 3x3 affine transformation matrix. + * @property ty + * @type Number + **/ + } + + /** + * Set current matrix to new absolute matrix. + * @method setTransform + * @param {Number} [a=1] Specifies the a property for the new matrix. + * @param {Number} [b=0] Specifies the b property for the new matrix. + * @param {Number} [c=0] Specifies the c property for the new matrix. + * @param {Number} [d=1] Specifies the d property for the new matrix. + * @param {Number} [tx=0] Specifies the tx property for the new matrix. + * @param {Number} [ty=0] Specifies the ty property for the new matrix. + * @return {Matrix2D} This instance. Useful for chaining method calls. + */ + setTransform = function(a, b, c, d, tx, ty) { + // don't forget to update docs in the constructor if these change: + this.a = (a == null) ? 1 : a; + this.b = b || 0; + this.c = c || 0; + this.d = (d == null) ? 1 : d; + this.tx = tx || 0; + this.ty = ty || 0; + return this; + }; + + /** + * Reset current matrix to an identity matrix. + * @method reset + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + reset = function() { + this.a = this.d = 1; + this.b = this.c = this.tx = this.ty = 0; + return this; + }; + + /** + * Returns an array with current matrix values. + * @method toArray + * @return {Array} an array with current matrix values. + **/ + toArray = function() { + return [this.a, this.b, this.c, this.d, this.tx, this.ty]; + }; + + /** + * Copies all properties from the specified matrix to this matrix. + * @method copy + * @param {Matrix2D} matrix The matrix to copy properties from. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + */ + copy = function(matrix) { + return this.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty); + }; + + /** + * Clones current instance and returning a new matrix. + * @method clone + * @return {Matrix2D} a clone of the Matrix2D instance. + **/ + clone = function() { + return new Matrix2D(this.a, this.b, this.c, this.d, this.tx, this.ty); + }; + + /** + * Prepends the specified matrix properties to this matrix. + * This is the equivalent of multiplying `(specified matrix) * (this matrix)`. + * All parameters are required. + * @method prepend + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} tx + * @param {Number} ty + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + prepend = function(a, b, c, d, tx, ty) { + var a1 = this.a; + var c1 = this.c; + var tx1 = this.tx; + + this.a = a*a1+c*this.b; + this.b = b*a1+d*this.b; + this.c = a*c1+c*this.d; + this.d = b*c1+d*this.d; + this.tx = a*tx1+c*this.ty+tx; + this.ty = b*tx1+d*this.ty+ty; + return this; + }; + + /** + * Appends the specified matrix properties to this matrix. All parameters are required. + * This is the equivalent of multiplying `(this matrix) * (specified matrix)`. + * @method append + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} tx + * @param {Number} ty + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + append = function(a, b, c, d, tx, ty) { + var a1 = this.a; + var b1 = this.b; + var c1 = this.c; + var d1 = this.d; + if (a != 1 || b != 0 || c != 0 || d != 1) { + this.a = a1*a+c1*b; + this.b = b1*a+d1*b; + this.c = a1*c+c1*d; + this.d = b1*c+d1*d; + } + this.tx = a1*tx+c1*ty+this.tx; + this.ty = b1*tx+d1*ty+this.ty; + return this; + }; + + /** + * Generates matrix properties from the specified display object transform properties, and appends them to this matrix. + * For example, you can use this to generate a matrix representing the transformations of a display object: + * + * var mtx = new createjs.Matrix2D(); + * mtx.appendTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation); + * @method appendTransform + * @param {Number} x + * @param {Number} y + * @param {Number} scaleX + * @param {Number} scaleY + * @param {Number} rotation + * @param {Number} skewX + * @param {Number} skewY + * @param {Number} regX Optional. + * @param {Number} regY Optional. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + appendTransform = function(x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { + if (rotation%360) { + var r = rotation*DEG_TO_RAD; + var cos = Math.cos(r); + var sin = Math.sin(r); + } else { + cos = 1; + sin = 0; + } + + if (skewX || skewY) { + // TODO: can this be combined into a single append operation? + skewX *= DEG_TO_RAD; + skewY *= DEG_TO_RAD; + this.append(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), x, y); + this.append(cos*scaleX, sin*scaleX, -sin*scaleY, cos*scaleY, 0, 0); + } else { + this.append(cos*scaleX, sin*scaleX, -sin*scaleY, cos*scaleY, x, y); + } + + if (regX || regY) { + // append the registration offset: + this.tx -= regX*this.a+regY*this.c; + this.ty -= regX*this.b+regY*this.d; + } + return this; + }; + + /** + * Generates matrix properties from the specified display object transform properties, and prepends them to this matrix. + * For example, you could calculate the combined transformation for a child object using: + * + * var o = myDisplayObject; + * var mtx = new createjs.Matrix2D(); + * do { + * // prepend each parent's transformation in turn: + * mtx.prependTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation, o.skewX, o.skewY, o.regX, o.regY); + * } while (o = o.parent); + * + * Note that the above example would not account for {{#crossLink "DisplayObject/transformMatrix:property"}}{{/crossLink}} + * values. See {{#crossLink "Matrix2D/prependMatrix"}}{{/crossLink}} for an example that does. + * @method prependTransform + * @param {Number} x + * @param {Number} y + * @param {Number} scaleX + * @param {Number} scaleY + * @param {Number} rotation + * @param {Number} skewX + * @param {Number} skewY + * @param {Number} regX Optional. + * @param {Number} regY Optional. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + prependTransform = function(x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { + if (rotation%360) { + var r = rotation*DEG_TO_RAD; + var cos = Math.cos(r); + var sin = Math.sin(r); + } else { + cos = 1; + sin = 0; + } + + if (regX || regY) { + // prepend the registration offset: + this.tx -= regX; this.ty -= regY; + } + if (skewX || skewY) { + // TODO: can this be combined into a single prepend operation? + skewX *= DEG_TO_RAD; + skewY *= DEG_TO_RAD; + this.prepend(cos*scaleX, sin*scaleX, -sin*scaleY, cos*scaleY, 0, 0); + this.prepend(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), x, y); + } else { + this.prepend(cos*scaleX, sin*scaleX, -sin*scaleY, cos*scaleY, x, y); + } + return this; + }; +} + +export default Matrix2D; diff --git a/lib/Transform.js b/lib/Transform.js deleted file mode 100644 index e2af2bbd..00000000 --- a/lib/Transform.js +++ /dev/null @@ -1,137 +0,0 @@ -import _ from 'lodash'; -export default class { - constructor(xx, yx, xy, yy, x, y){ - if (xx && typeof xx === 'object'){ - yx = xx.yx; yy = xx.yy; y = xx.y; - xy = xx.xy; x = xx.x; xx = xx.xx; - } - this.xx = _.isNil(xx) ? 1 : xx; - this.yx = yx || 0; - this.xy = xy || 0; - this.yy = _.isNil(yy) ? 1 : yy; - this.x = (_.isNil(x) ? this.x : x) || 0; - this.y = (_.isNil(y) ? this.y : y) || 0; - return this; - } - - xx = 1; - yx = 0; - x = 0; - xy = 0; - yy = 1; - y = 0; - - transform = (xx, yx, xy, yy, x, y) => { - if (xx && typeof xx === 'object'){ - yx = xx.yx; yy = xx.yy; y = xx.y; - xy = xx.xy; x = xx.x; xx = xx.xx; - } - - if (!x) { - x = 0; - } - - if (!y) { - y = 0; - } - return this.transformTo( - this.xx * xx + this.xy * yx, - this.yx * xx + this.yy * yx, - this.xx * xy + this.xy * yy, - this.yx * xy + this.yy * yy, - this.xx * x + this.xy * y + this.x, - this.yx * x + this.yy * y + this.y - ); - }; - - transformTo = this.constructor; - - translate = (x, y) => { - return this.transform(1, 0, 0, 1, x, y); - }; - - move = (x, y) => { - this.x += x || 0; - this.y += y || 0; - return this; - }; - - scale = (x, y) => { - return this.transform(x, 0, 0, _.isNil(y) ? x : y, 0, 0); - }; - - rotate = (deg, x, y) => { - if (_.isNil(x) || _.isNil(y)){ - x = (this.left || 0) + (this.width || 0) / 2; - y = (this.top || 0) + (this.height || 0) / 2; - } - - var rad = deg * Math.PI / 180, sin = Math.sin(rad), cos = Math.cos(rad); - - this.transform(1, 0, 0, 1, x, y); - - return this.transformTo( - cos * this.xx - sin * this.yx, - sin * this.xx + cos * this.yx, - cos * this.xy - sin * this.yy, - sin * this.xy + cos * this.yy, - this.x, - this.y - ).transform(1, 0, 0, 1, -x, -y); - }; - - moveTo = (x, y) => { - return this.transformTo(this.xx, this.yx, this.xy, this.yy, x, y); - }; - - rotateTo = (deg, x, y) => { - let flip = this.yx / this.xx > this.yy / this.xy ? -1 : 1; - if (this.xx < 0 ? this.xy >= 0 : this.xy < 0) { - flip = -flip; - } - return this.rotate(deg - Math.atan2(flip * this.yx, flip * this.xx) * 180 / Math.PI, x, y); - }; - - scaleTo = (x, y) => { - // Normalize - var h = Math.sqrt(this.xx * this.xx + this.yx * this.yx); - this.xx /= h; - this.yx /= h; - - h = Math.sqrt(this.yy * this.yy + this.xy * this.xy); - this.yy /= h; - this.xy /= h; - - return this.scale(x, y); - }; - - resizeTo = (width, height) => { - var w = this.width, h = this.height; - if (!w || !h) { - return this; - } - return this.scaleTo(width / w, height / h); - }; - - inversePoint = (x, y) => { - var a = this.xx, b = this.yx, - c = this.xy, d = this.yy, - e = this.x, f = this.y; - var det = b * c - a * d; - if (det === 0) { - return null; - } - return { - x: (d * (e - x) + c * (y - f)) / det, - y: (a * (f - y) + b * (x - e)) / det - }; - }; - - point = (x, y) => { - return { - x: this.xx * x + this.xy * y + this.x, - y: this.yx * x + this.yy * y + this.y - }; - }; -} - diff --git a/lib/extract/extractTransform.js b/lib/extract/extractTransform.js index 9bedf4b3..ecba2d06 100644 --- a/lib/extract/extractTransform.js +++ b/lib/extract/extractTransform.js @@ -1,45 +1,85 @@ -import Transform from '../Transform'; +import Matrix2D from '../Matrix2D'; import _ from 'lodash'; -let pooledTransform = new Transform(); +let pooledMatrix = new Matrix2D(); -function transformToMatrix(props) { - let scaleX = !_.isNil(props.scaleX) ? props.scaleX : - !_.isNil(props.scale) ? props.scale : 1; - let scaleY = !_.isNil(props.scaleY) ? props.scaleY : - !_.isNil(props.scale) ? props.scale : 1; +function transformToMatrix(props, transform) { + pooledMatrix.reset(); + appendTransform(props); - pooledTransform - .transformTo(1, 0, 0, 1, 0, 0) - .move(props.x || 0, props.y || 0) - .rotate(props.rotation || 0, props.originX, props.originY) - .scale(scaleX, scaleY, props.originX, props.originY); - - if (!_.isNil(props.transform)) { - pooledTransform.transform(props.transform); + if (transform) { + appendTransform(transform); } - return [ - pooledTransform.xx, pooledTransform.yx, - pooledTransform.xy, pooledTransform.yy, - pooledTransform.x, pooledTransform.y - ]; + return pooledMatrix.toArray(); } +function appendTransform(transform) { + pooledMatrix + .appendTransform( + transform.x + transform.originX, + transform.y + transform.originY, + transform.scaleX, transform.scaleY, + transform.rotation, + transform.skewX, + transform.skewY, + transform.originX, + transform.originY + ); +} +function universal2axis(universal, axisX, axisY, defaultValue) { + let coords = []; + let x; + let y; + if (_.isString(universal)) { + coords = universal.split(/\s*,\s*/); + if (coords.length === 2) { + x = +coords[0]; + y = +coords[1]; + } else if (coords.length === 1) { + x = y = +coords[0]; + } + } else if (_.isNumber(universal)) { + x = y = universal; + } + + axisX = +axisX; + if (!isNaN(axisX)) { + x = axisX; + } + + axisY = +axisY; + if (!isNaN(axisY)) { + y = axisY; + } + + return [x || defaultValue || 0, y || defaultValue || 0]; +} + +function props2transform(props) { + let [originX, originY] = universal2axis(props.origin, props.originX, props.originY); + let [scaleX, scaleY] = universal2axis(props.scale, props.scaleX, props.scaleY, 1); + let [skewX, skewY] = universal2axis(props.skew, props.skewX, props.skewY); + let [translateX, translateY] = universal2axis(props.translate, props.translateX, props.translateY); + + return { + rotation: +props.rotation || +props.rotate || 0, + scaleX: scaleX, + scaleY: scaleY, + originX: originX, + originY: originY, + skewX: skewX, + skewY: skewY, + x: translateX, + y: translateY + }; +} + +/** + * + * @param props + */ export default function (props) { - let coords = props.origin ? props.origin.split(/\s*,\s*/) : []; - - // TODO: support Percentage for originX,originY - let originX = coords.length === 2 ? coords[0] : props.originX; - let originY = coords.length === 2 ? coords[1] : props.originY; - return transformToMatrix({ - rotation: +props.rotation / 2 || +props.rotate / 2 || 0, - scale: props.scale, - scaleX: props.scaleX, - scaleY: props.scaleY, - originX: +originX || 0, - originY: +originY || 0, - x: +props.x || 0, - y: +props.y || 0 - }); + // TODO: support Percentage for transform + return transformToMatrix(props2transform(props), props.transform ? props2transform(props.transform) : null); } diff --git a/lib/props.js b/lib/props.js index 352e3fcb..bec7ca98 100644 --- a/lib/props.js +++ b/lib/props.js @@ -57,11 +57,19 @@ const transformProps = { scaleX: numberProp, scaleY: numberProp, rotate: numberProp, + rotation: numberProp, + translate: numberProp, + translateX: numberProp, + translateY: numberProp, x: numberProp, y: numberProp, + origin: numberProp, originX: numberProp, originY: numberProp, - transform: PropTypes.string + skew: numberProp, + skewX: numberProp, + skewY: numberProp, + transform: PropTypes.object }; const pathProps = {