diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js index bb22c6b2651..7bb795f1609 100644 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ b/Libraries/Components/View/ViewStylePropTypes.js @@ -34,7 +34,10 @@ var ViewStylePropTypes = { ), shadowOpacity: ReactPropTypes.number, shadowRadius: ReactPropTypes.number, + transform: ReactPropTypes.arrayOf(ReactPropTypes.object), transformMatrix: ReactPropTypes.arrayOf(ReactPropTypes.number), + + // DEPRECATED rotation: ReactPropTypes.number, scaleX: ReactPropTypes.number, scaleY: ReactPropTypes.number, diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index ec72a0b4f46..9d413e5c7ca 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -19,6 +19,7 @@ var TextInputState = require('TextInputState'); var flattenStyle = require('flattenStyle'); var invariant = require('invariant'); var mergeFast = require('mergeFast'); +var precomputeStyle = require('precomputeStyle'); type MeasureOnSuccessCallback = ( x: number, @@ -93,7 +94,7 @@ var NativeMethodsMixin = { break; } } - var style = flattenStyle(nativeProps.style); + var style = precomputeStyle(flattenStyle(nativeProps.style)); var props = null; if (hasOnlyStyle) { diff --git a/Libraries/ReactIOS/ReactIOSNativeComponent.js b/Libraries/ReactIOS/ReactIOSNativeComponent.js index b9abd5965c5..7f27ae0ea7d 100644 --- a/Libraries/ReactIOS/ReactIOSNativeComponent.js +++ b/Libraries/ReactIOS/ReactIOSNativeComponent.js @@ -23,6 +23,7 @@ var styleDiffer = require('styleDiffer'); var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); var diffRawProperties = require('diffRawProperties'); var flattenStyle = require('flattenStyle'); +var precomputeStyle = require('precomputeStyle'); var warning = require('warning'); var registrationNames = ReactIOSEventEmitter.registrationNames; @@ -160,7 +161,7 @@ ReactIOSNativeComponent.Mixin = { // before actually doing the expensive flattening operation in order to // compute the diff. if (styleDiffer(nextProps.style, prevProps.style)) { - var nextFlattenedStyle = flattenStyle(nextProps.style); + var nextFlattenedStyle = precomputeStyle(flattenStyle(nextProps.style)); updatePayload = diffRawProperties( updatePayload, this.previousFlattenedStyle, diff --git a/Libraries/StyleSheet/precomputeStyle.js b/Libraries/StyleSheet/precomputeStyle.js new file mode 100644 index 00000000000..ea2990b94e3 --- /dev/null +++ b/Libraries/StyleSheet/precomputeStyle.js @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule precomputeStyle + * @flow + */ +'use strict'; + +var MatrixMath = require('MatrixMath'); +var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); +var invariant = require('invariant'); + +/** + * This method provides a hook where flattened styles may be precomputed or + * otherwise prepared to become better input data for native code. + */ +function precomputeStyle(style: ?Object): ?Object { + if (!style || !style.transform) { + return style; + } + invariant( + !style.transformMatrix, + 'transformMatrix and transform styles cannot be used on the same component' + ); + var newStyle = _precomputeTransforms({...style}); + deepFreezeAndThrowOnMutationInDev(newStyle); + return newStyle; +} + +/** + * Generate a transform matrix based on the provided transforms, and use that + * within the style object instead. + * + * This allows us to provide an API that is similar to CSS and to have a + * universal, singular interface to native code. + */ +function _precomputeTransforms(style: Object): Object { + var {transform} = style; + var result = MatrixMath.createIdentityMatrix(); + + transform.forEach(transformation => { + var key = Object.keys(transformation)[0]; + var value = transformation[key]; + if (__DEV__) { + _validateTransform(key, value, transformation); + } + + switch (key) { + case 'matrix': + MatrixMath.multiplyInto(result, result, value); + break; + case 'rotate': + _multiplyTransform(result, MatrixMath.reuseRotateZCommand, [_convertToRadians(value)]); + break; + case 'scale': + _multiplyTransform(result, MatrixMath.reuseScaleCommand, [value]); + break; + case 'scaleX': + _multiplyTransform(result, MatrixMath.reuseScaleXCommand, [value]); + break; + case 'scaleY': + _multiplyTransform(result, MatrixMath.reuseScaleYCommand, [value]); + break; + case 'translate': + _multiplyTransform(result, MatrixMath.reuseTranslate3dCommand, [value[0], value[1], value[2] || 0]); + break; + case 'translateX': + _multiplyTransform(result, MatrixMath.reuseTranslate2dCommand, [value, 0]); + break; + case 'translateY': + _multiplyTransform(result, MatrixMath.reuseTranslate2dCommand, [0, value]); + break; + default: + throw new Error('Invalid transform name: ' + key); + } + }); + + return { + ...style, + transformMatrix: result, + }; +} + +/** + * Performs a destructive operation on a transform matrix. + */ +function _multiplyTransform( + result: Array, + matrixMathFunction: Function, + args: Array +): void { + var matrixToApply = MatrixMath.createIdentityMatrix(); + var argsWithIdentity = [matrixToApply].concat(args); + matrixMathFunction.apply(this, argsWithIdentity); + MatrixMath.multiplyInto(result, result, matrixToApply); +} + +/** + * Parses a string like '0.5rad' or '60deg' into radians expressed in a float. + * Note that validation on the string is done in `_validateTransform()`. + */ +function _convertToRadians(value: string): number { + var floatValue = parseFloat(value, 10); + return value.indexOf('rad') > -1 ? floatValue : floatValue * Math.PI / 180; +} + +function _validateTransform(key, value, transformation) { + var multivalueTransforms = [ + 'matrix', + 'translate', + ]; + if (multivalueTransforms.indexOf(key) !== -1) { + invariant( + Array.isArray(value), + 'Transform with key of %s must have an array as the value: %s', + key, + JSON.stringify(transformation) + ); + } + switch (key) { + case 'matrix': + invariant( + value.length === 9 || value.length === 16, + 'Matrix transform must have a length of 9 (2d) or 16 (3d). ' + + 'Provided matrix has a length of %s: %s', + value.length, + JSON.stringify(transformation) + ); + break; + case 'translate': + break; + case 'rotate': + invariant( + typeof value === 'string', + 'Transform with key of "%s" must be a string: %s', + key, + JSON.stringify(transformation) + ); + invariant( + value.indexOf('deg') > -1 || value.indexOf('rad') > -1, + 'Rotate transform must be expressed in degrees (deg) or radians ' + + '(rad): %s', + JSON.stringify(transformation) + ); + break; + default: + invariant( + typeof value === 'number', + 'Transform with key of "%s" must be a number: %s', + key, + JSON.stringify(transformation) + ); + } +} + +module.exports = precomputeStyle; diff --git a/Libraries/Utilities/MatrixMath.js b/Libraries/Utilities/MatrixMath.js new file mode 100755 index 00000000000..7f3d17c461b --- /dev/null +++ b/Libraries/Utilities/MatrixMath.js @@ -0,0 +1,131 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule MatrixMath + */ +'use strict'; + +/** + * Memory conservative (mutative) matrix math utilities. Uses "command" + * matrices, which are reusable. + */ +var MatrixMath = { + createIdentityMatrix: function() { + return [ + 1,0,0,0, + 0,1,0,0, + 0,0,1,0, + 0,0,0,1 + ]; + }, + + createCopy: function(m) { + return [ + m[0], m[1], m[2], m[3], + m[4], m[5], m[6], m[7], + m[8], m[9], m[10], m[11], + m[12], m[13], m[14], m[15], + ]; + }, + + createTranslate2d: function(x, y) { + var mat = MatrixMath.createIdentityMatrix(); + MatrixMath.reuseTranslate2dCommand(mat, x, y); + return mat; + }, + + reuseTranslate2dCommand: function(matrixCommand, x, y) { + matrixCommand[12] = x; + matrixCommand[13] = y; + }, + + reuseTranslate3dCommand: function(matrixCommand, x, y, z) { + matrixCommand[12] = x; + matrixCommand[13] = y; + matrixCommand[14] = z; + }, + + createScale: function(factor) { + var mat = MatrixMath.createIdentityMatrix(); + MatrixMath.reuseScaleCommand(mat, factor); + return mat; + }, + + reuseScaleCommand: function(matrixCommand, factor) { + matrixCommand[0] = factor; + matrixCommand[5] = factor; + }, + + reuseScale3dCommand: function(matrixCommand, x, y, z) { + matrixCommand[0] = x; + matrixCommand[5] = y; + matrixCommand[10] = z; + }, + + reuseScaleXCommand(matrixCommand, factor) { + matrixCommand[0] = factor; + }, + + reuseScaleYCommand(matrixCommand, factor) { + matrixCommand[5] = factor; + }, + + reuseScaleZCommand(matrixCommand, factor) { + matrixCommand[10] = factor; + }, + + reuseRotateYCommand: function(matrixCommand, amount) { + matrixCommand[0] = Math.cos(amount); + matrixCommand[2] = Math.sin(amount); + matrixCommand[8] = Math.sin(-amount); + matrixCommand[10] = Math.cos(amount); + }, + + createRotateZ: function(radians) { + var mat = MatrixMath.createIdentityMatrix(); + MatrixMath.reuseRotateZCommand(mat, radians); + return mat; + }, + + // http://www.w3.org/TR/css3-transforms/#recomposing-to-a-2d-matrix + reuseRotateZCommand: function(matrixCommand, radians) { + matrixCommand[0] = Math.cos(radians); + matrixCommand[1] = Math.sin(radians); + matrixCommand[4] = -Math.sin(radians); + matrixCommand[5] = Math.cos(radians); + }, + + multiplyInto: function(out, a, b) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; + out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; + out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; + out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; + out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + } + +}; + +module.exports = MatrixMath;