diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 4fdfd4afb..c1410d023 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -189,6 +189,10 @@ const APIExamples = [ key: 'ToastAndroidExample', module: require('./ToastAndroidExample'), }, + { + key: 'TransformExample', + module: require('./TransformExample'), + }, { key: 'VibrationExample', module: require('./VibrationExample'), diff --git a/Libraries/StyleSheet/processTransform.js b/Libraries/StyleSheet/processTransform.js index 6978efdcf..fc44b5f8c 100644 --- a/Libraries/StyleSheet/processTransform.js +++ b/Libraries/StyleSheet/processTransform.js @@ -81,13 +81,6 @@ function processTransform(transform: Object): Object { } }); - // Android does not support the direct application of a transform matrix to - // a view, so we need to decompose the result matrix into transforms that can - // get applied in the specific order of (1) translate (2) scale (3) rotate. - // Once we can directly apply a matrix, we can remove this decomposition. - if (Platform.OS === 'android') { - return MatrixMath.decomposeMatrix(result); - } return result; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 0bcefba58..c74651755 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -6,6 +6,7 @@ import android.graphics.Color; import android.os.Build; import android.view.View; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.annotations.ReactProp; @@ -17,14 +18,6 @@ public abstract class BaseViewManager { private static final String PROP_BACKGROUND_COLOR = ViewProps.BACKGROUND_COLOR; - private static final String PROP_DECOMPOSED_MATRIX = "decomposedMatrix"; - private static final String PROP_DECOMPOSED_MATRIX_ROTATE = "rotate"; - private static final String PROP_DECOMPOSED_MATRIX_ROTATE_X = "rotateX"; - private static final String PROP_DECOMPOSED_MATRIX_ROTATE_Y = "rotateY"; - private static final String PROP_DECOMPOSED_MATRIX_SCALE_X = "scaleX"; - private static final String PROP_DECOMPOSED_MATRIX_SCALE_Y = "scaleY"; - private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_X = "translateX"; - private static final String PROP_DECOMPOSED_MATRIX_TRANSLATE_Y = "translateY"; private static final String PROP_TRANSFORM = "transform"; private static final String PROP_OPACITY = "opacity"; private static final String PROP_ELEVATION = "elevation"; @@ -46,28 +39,21 @@ public abstract class BaseViewManager row[1][2]) { + quaternion[0] = -quaternion[0]; + } + if (row[0][2] > row[2][0]) { + quaternion[1] = -quaternion[1]; + } + if (row[1][0] > row[0][1]) { + quaternion[2] = -quaternion[2]; + } + + // correct for occasional, weird Euler synonyms for 2d rotation + + if (quaternion[0] < 0.001 && quaternion[0] >= 0 && + quaternion[1] < 0.001 && quaternion[1] >= 0) { + // this is a 2d rotation on the z-axis + rotationDegrees[0] = rotationDegrees[1] = 0d; + rotationDegrees[2] = roundTo3Places(Math.atan2(row[0][1], row[0][0]) * 180 / Math.PI); + } else { + quaternionToDegreesXYZ(quaternion, rotationDegrees); + } + } + + public static double determinant(double[] matrix) { + double m00 = matrix[0], m01 = matrix[1], m02 = matrix[2], m03 = matrix[3], m10 = matrix[4], + m11 = matrix[5], m12 = matrix[6], m13 = matrix[7], m20 = matrix[8], m21 = matrix[9], + m22 = matrix[10], m23 = matrix[11], m30 = matrix[12], m31 = matrix[13], m32 = matrix[14], + m33 = matrix[15]; + return ( + m03 * m12 * m21 * m30 - m02 * m13 * m21 * m30 - + m03 * m11 * m22 * m30 + m01 * m13 * m22 * m30 + + m02 * m11 * m23 * m30 - m01 * m12 * m23 * m30 - + m03 * m12 * m20 * m31 + m02 * m13 * m20 * m31 + + m03 * m10 * m22 * m31 - m00 * m13 * m22 * m31 - + m02 * m10 * m23 * m31 + m00 * m12 * m23 * m31 + + m03 * m11 * m20 * m32 - m01 * m13 * m20 * m32 - + m03 * m10 * m21 * m32 + m00 * m13 * m21 * m32 + + m01 * m10 * m23 * m32 - m00 * m11 * m23 * m32 - + m02 * m11 * m20 * m33 + m01 * m12 * m20 * m33 + + m02 * m10 * m21 * m33 - m00 * m12 * m21 * m33 - + m01 * m10 * m22 * m33 + m00 * m11 * m22 * m33 + ); + } + + /** + * Inverse of a matrix. Multiplying by the inverse is used in matrix math + * instead of division. + * + * Formula from: + * http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm + */ + public static double[] inverse(double[] matrix) { + double det = determinant(matrix); + if (isZero(det)) { + return matrix; + } + double m00 = matrix[0], m01 = matrix[1], m02 = matrix[2], m03 = matrix[3], m10 = matrix[4], + m11 = matrix[5], m12 = matrix[6], m13 = matrix[7], m20 = matrix[8], m21 = matrix[9], + m22 = matrix[10], m23 = matrix[11], m30 = matrix[12], m31 = matrix[13], m32 = matrix[14], + m33 = matrix[15]; + return new double[] { + (m12 * m23 * m31 - m13 * m22 * m31 + m13 * m21 * m32 - m11 * m23 * m32 - m12 * m21 * m33 + m11 * m22 * m33) / det, + (m03 * m22 * m31 - m02 * m23 * m31 - m03 * m21 * m32 + m01 * m23 * m32 + m02 * m21 * m33 - m01 * m22 * m33) / det, + (m02 * m13 * m31 - m03 * m12 * m31 + m03 * m11 * m32 - m01 * m13 * m32 - m02 * m11 * m33 + m01 * m12 * m33) / det, + (m03 * m12 * m21 - m02 * m13 * m21 - m03 * m11 * m22 + m01 * m13 * m22 + m02 * m11 * m23 - m01 * m12 * m23) / det, + (m13 * m22 * m30 - m12 * m23 * m30 - m13 * m20 * m32 + m10 * m23 * m32 + m12 * m20 * m33 - m10 * m22 * m33) / det, + (m02 * m23 * m30 - m03 * m22 * m30 + m03 * m20 * m32 - m00 * m23 * m32 - m02 * m20 * m33 + m00 * m22 * m33) / det, + (m03 * m12 * m30 - m02 * m13 * m30 - m03 * m10 * m32 + m00 * m13 * m32 + m02 * m10 * m33 - m00 * m12 * m33) / det, + (m02 * m13 * m20 - m03 * m12 * m20 + m03 * m10 * m22 - m00 * m13 * m22 - m02 * m10 * m23 + m00 * m12 * m23) / det, + (m11 * m23 * m30 - m13 * m21 * m30 + m13 * m20 * m31 - m10 * m23 * m31 - m11 * m20 * m33 + m10 * m21 * m33) / det, + (m03 * m21 * m30 - m01 * m23 * m30 - m03 * m20 * m31 + m00 * m23 * m31 + m01 * m20 * m33 - m00 * m21 * m33) / det, + (m01 * m13 * m30 - m03 * m11 * m30 + m03 * m10 * m31 - m00 * m13 * m31 - m01 * m10 * m33 + m00 * m11 * m33) / det, + (m03 * m11 * m20 - m01 * m13 * m20 - m03 * m10 * m21 + m00 * m13 * m21 + m01 * m10 * m23 - m00 * m11 * m23) / det, + (m12 * m21 * m30 - m11 * m22 * m30 - m12 * m20 * m31 + m10 * m22 * m31 + m11 * m20 * m32 - m10 * m21 * m32) / det, + (m01 * m22 * m30 - m02 * m21 * m30 + m02 * m20 * m31 - m00 * m22 * m31 - m01 * m20 * m32 + m00 * m21 * m32) / det, + (m02 * m11 * m30 - m01 * m12 * m30 - m02 * m10 * m31 + m00 * m12 * m31 + m01 * m10 * m32 - m00 * m11 * m32) / det, + (m01 * m12 * m20 - m02 * m11 * m20 + m02 * m10 * m21 - m00 * m12 * m21 - m01 * m10 * m22 + m00 * m11 * m22) / det + }; + } + + /** + * Turns columns into rows and rows into columns. + */ + public static double[] transpose(double[] m) { + return new double[] { + m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15] + }; + } + + /** + * Based on: http://tog.acm.org/resources/GraphicsGems/gemsii/unmatrix.c + */ + public static void multiplyVectorByMatrix(double[] v, double[] m, double[] result) { + double vx = v[0], vy = v[1], vz = v[2], vw = v[3]; + result[0] = vx * m[0] + vy * m[4] + vz * m[8] + vw * m[12]; + result[1] = vx * m[1] + vy * m[5] + vz * m[9] + vw * m[13]; + result[2] = vx * m[2] + vy * m[6] + vz * m[10] + vw * m[14]; + result[3] = vx * m[3] + vy * m[7] + vz * m[11] + vw * m[15]; + } + + /** + * From: https://code.google.com/p/webgl-mjs/source/browse/mjs.js + */ + public static double v3Length(double[] a) { + return Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]); + } + + /** + * Based on: https://code.google.com/p/webgl-mjs/source/browse/mjs.js + */ + public static double[] v3Normalize(double[] vector, double norm) { + double im = 1 / (isZero(norm) ? v3Length(vector) : norm); + return new double[] { + vector[0] * im, + vector[1] * im, + vector[2] * im + }; + } + + /** + * The dot product of a and b, two 3-element vectors. + * From: https://code.google.com/p/webgl-mjs/source/browse/mjs.js + */ + public static double v3Dot(double[] a, double[] b) { + return a[0] * b[0] + + a[1] * b[1] + + a[2] * b[2]; + } + + /** + * From: + * http://www.opensource.apple.com/source/WebCore/WebCore-514/platform/graphics/transforms/TransformationMatrix.cpp + */ + public static double[] v3Combine(double[] a, double[] b, double aScale, double bScale) { + return new double[]{ + aScale * a[0] + bScale * b[0], + aScale * a[1] + bScale * b[1], + aScale * a[2] + bScale * b[2] + }; + } + + /** + * From: + * http://www.opensource.apple.com/source/WebCore/WebCore-514/platform/graphics/transforms/TransformationMatrix.cpp + */ + public static double[] v3Cross(double[] a, double[] b) { + return new double[]{ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + }; + } + + /** + * Based on: + * http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/ + * and: + * http://quat.zachbennett.com/ + * + * Note that this rounds degrees to the thousandth of a degree, due to + * floating point errors in the creation of the quaternion. + * + * Also note that this expects the qw value to be last, not first. + * + * Also, when researching this, remember that: + * yaw === heading === z-axis + * pitch === elevation/attitude === y-axis + * roll === bank === x-axis + */ + public static void quaternionToDegreesXYZ(double[] q, double[] result) { + double qx = q[0], qy = q[1], qz = q[2], qw = q[3]; + double qw2 = qw * qw; + double qx2 = qx * qx; + double qy2 = qy * qy; + double qz2 = qz * qz; + double test = qx * qy + qz * qw; + double unit = qw2 + qx2 + qy2 + qz2; + double conv = 180 / Math.PI; + + if (test > 0.49999 * unit) { + result[0] = 0; + result[1] = 2 * Math.atan2(qx, qw) * conv; + result[2] = 90; + return; + } + if (test < -0.49999 * unit) { + result[0] = 0; + result[1] = -2 * Math.atan2(qx, qw) * conv; + result[2] = -90; + return; + } + + result[0] = roundTo3Places(Math.atan2(2 * qx * qw - 2 * qy * qz, 1 - 2 * qx2 - 2 * qz2) * conv); + result[1] = roundTo3Places(Math.atan2(2 * qy * qw - 2 * qx * qz, 1 - 2 * qy2 - 2 * qz2) * conv); + result[2] = roundTo3Places(Math.asin(2 * qx * qy + 2 * qz * qw) * conv); + } + + public static double roundTo3Places(double n) { + return Math.round(n * 1000d) * 0.001; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java new file mode 100644 index 000000000..66e9a2144 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/MatrixMathHelperTest.java @@ -0,0 +1,156 @@ +package com.facebook.react.uimanager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * Test for {@link MatrixMathHelper} + */ +@RunWith(RobolectricTestRunner.class) +public class MatrixMathHelperTest { + + private void verifyZRotatedMatrix(double degrees, double rotX, double rotY, double rotZ) { + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + double[] matrix = createRotateZ(degreesToRadians(degrees)); + MatrixMathHelper.decomposeMatrix(matrix, ctx); + assertThat(ctx.rotationDegrees).containsSequence(rotX, rotY, rotZ); + } + + private void verifyYRotatedMatrix(double degrees, double rotX, double rotY, double rotZ) { + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + double[] matrix = createRotateY(degreesToRadians(degrees)); + MatrixMathHelper.decomposeMatrix(matrix, ctx); + assertThat(ctx.rotationDegrees).containsSequence(rotX, rotY, rotZ); + } + + private void verifyXRotatedMatrix(double degrees, double rotX, double rotY, double rotZ) { + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + double[] matrix = createRotateX(degreesToRadians(degrees)); + MatrixMathHelper.decomposeMatrix(matrix, ctx); + assertThat(ctx.rotationDegrees).containsSequence(rotX, rotY, rotZ); + } + + @Test + public void testDecomposing4x4MatrixToProduceAccurateZaxisAngles() { + + MatrixMathHelper.MatrixDecompositionContext ctx = + new MatrixMathHelper.MatrixDecompositionContext(); + + MatrixMathHelper.decomposeMatrix( + new double[]{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}, + ctx); + + assertThat(ctx.rotationDegrees).containsSequence(0d, 0d, 0d); + + double[] angles = new double[]{30, 45, 60, 75, 90, 100, 115, 120, 133, 167}; + for (double angle : angles) { + verifyZRotatedMatrix(angle, 0d, 0d, angle); + verifyZRotatedMatrix(-angle, 0d, 0d, -angle); + } + + verifyZRotatedMatrix(180d, 0d, 0d, 180d); + + // all values are between 0 and 180; + // change of sign and direction in the third and fourth quadrant + verifyZRotatedMatrix(222, 0d, 0d, -138d); + + verifyZRotatedMatrix(270, 0d, 0d, -90d); + + // 360 is expressed as 0 + verifyZRotatedMatrix(360, 0d, 0d, 0d); + + verifyZRotatedMatrix(33.33333333, 0d, 0d, 33.333d); + + verifyZRotatedMatrix(86.75309, 0d, 0d, 86.753d); + + verifyZRotatedMatrix(42.00000000001, 0d, 0d, 42d); + + verifyZRotatedMatrix(42.99999999999, 0d, 0d, 43d); + + verifyZRotatedMatrix(42.99999999999, 0d, 0d, 43d); + + verifyZRotatedMatrix(42.49999999999, 0d, 0d, 42.5d); + + verifyZRotatedMatrix(42.55555555555, 0d, 0d, 42.556d); + } + + @Test + public void testDecomposing4x4MatrixToProduceAccurateYaxisAngles() { + double[] angles = new double[]{30, 45, 60, 75, 90, 100, 110, 120, 133, 167}; + for (double angle : angles) { + verifyYRotatedMatrix(angle, 0d, angle, 0d); + verifyYRotatedMatrix(-angle, 0d, -angle, 0d); + } + + // all values are between 0 and 180; + // change of sign and direction in the third and fourth quadrant + verifyYRotatedMatrix(222, 0d, -138d, 0d); + + verifyYRotatedMatrix(270, 0d, -90d, 0d); + + verifyYRotatedMatrix(360, 0d, 0d, 0d); + } + + @Test + public void testDecomposing4x4MatrixToProduceAccurateXaxisAngles() { + double[] angles = new double[]{30, 45, 60, 75, 90, 100, 110, 120, 133, 167}; + for (double angle : angles) { + verifyXRotatedMatrix(angle, angle, 0d, 0d); + verifyXRotatedMatrix(-angle, -angle, 0d, 0d); + } + + // all values are between 0 and 180; + // change of sign and direction in the third and fourth quadrant + verifyXRotatedMatrix(222, -138d, 0d, 0d); + + verifyXRotatedMatrix(270, -90d, 0d, 0d); + + verifyXRotatedMatrix(360, 0d, 0d, 0d); + } + + private static double[] createIdentityMatrix() { + return new double[] { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + }; + } + + private static double degreesToRadians(double degrees) { + return degrees * Math.PI / 180; + } + + private static double[] createRotateZ(double radians) { + double[] mat = createIdentityMatrix(); + mat[0] = Math.cos(radians); + mat[1] = Math.sin(radians); + mat[4] = -Math.sin(radians); + mat[5] = Math.cos(radians); + return mat; + } + + private static double[] createRotateY(double radians) { + double[] mat = createIdentityMatrix(); + mat[0] = Math.cos(radians); + mat[2] = -Math.sin(radians); + mat[8] = Math.sin(radians); + mat[10] = Math.cos(radians); + return mat; + } + + private static double[] createRotateX(double radians) { + double[] mat = createIdentityMatrix(); + mat[5] = Math.cos(radians); + mat[6] = Math.sin(radians); + mat[9] = -Math.sin(radians); + mat[10] = Math.cos(radians); + return mat; + } +}