From a604b899c9a08f3fba2b5943f05df9e7949760fc Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:09:41 -0700 Subject: [PATCH 1/3] Modify transform code to allow fabric usage of transform --- .../Views/FrameworkElementViewManager.cpp | 110 +++++-- vnext/overrides.json | 7 + .../StyleSheet/processTransform.windows.js | 272 ++++++++++++++++++ 3 files changed, 371 insertions(+), 18 deletions(-) create mode 100644 vnext/src/Libraries/StyleSheet/processTransform.windows.js diff --git a/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp b/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp index e0fd8b7e5ed..f4cad155eec 100644 --- a/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp @@ -99,6 +99,23 @@ static void GetAccessibilityValueProps(const winrt::Microsoft::ReactNative::IJSV writer.WriteObjectEnd(); } + +inline float ToRadians(const winrt::Microsoft::ReactNative::JSValue &value) { + if ((value.Type() == winrt::Microsoft::ReactNative::JSValueType::Double)) { + return value.AsSingle(); + } + assert(value.Type() == winrt::Microsoft::ReactNative::JSValueType::String); + + auto stringValue = value.AsString(); + char *suffixStart; + double num = strtod( + stringValue.c_str(), &suffixStart); + if (0 == strncmp(suffixStart, "deg", 3)) { + return static_cast(num * M_PI / 180.0f); + } + return static_cast(num); // assume suffix is "rad" +} + void FrameworkElementViewManager::GetNativeProps(const winrt::Microsoft::ReactNative::IJSValueWriter &writer) const { Super::GetNativeProps(writer); @@ -139,24 +156,81 @@ bool FrameworkElementViewManager::UpdateProperty( if (element.try_as()) // Works on 19H1+ { if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Array) { - assert(propertyValue.AsArray().size() == 16); - winrt::Windows::Foundation::Numerics::float4x4 transformMatrix; - transformMatrix.m11 = static_cast(propertyValue[0].AsDouble()); - transformMatrix.m12 = static_cast(propertyValue[1].AsDouble()); - transformMatrix.m13 = static_cast(propertyValue[2].AsDouble()); - transformMatrix.m14 = static_cast(propertyValue[3].AsDouble()); - transformMatrix.m21 = static_cast(propertyValue[4].AsDouble()); - transformMatrix.m22 = static_cast(propertyValue[5].AsDouble()); - transformMatrix.m23 = static_cast(propertyValue[6].AsDouble()); - transformMatrix.m24 = static_cast(propertyValue[7].AsDouble()); - transformMatrix.m31 = static_cast(propertyValue[8].AsDouble()); - transformMatrix.m32 = static_cast(propertyValue[9].AsDouble()); - transformMatrix.m33 = static_cast(propertyValue[10].AsDouble()); - transformMatrix.m34 = static_cast(propertyValue[11].AsDouble()); - transformMatrix.m41 = static_cast(propertyValue[12].AsDouble()); - transformMatrix.m42 = static_cast(propertyValue[13].AsDouble()); - transformMatrix.m43 = static_cast(propertyValue[14].AsDouble()); - transformMatrix.m44 = static_cast(propertyValue[15].AsDouble()); + winrt::Windows::Foundation::Numerics::float4x4 transformMatrix{ + winrt::Windows::Foundation::Numerics::float4x4::identity()}; + for (const auto &transform : propertyValue.AsArray()) { + for (const auto &operation : transform.AsObject()) { + const std::string &transformType = operation.first; + const auto &innerValue = operation.second; + + if (transformType == "matrix") { + assert(innerValue.AsArray().size() == 16); + winrt::Windows::Foundation::Numerics::float4x4 innerMatrix; + innerMatrix.m11 = static_cast(innerValue[0].AsDouble()); + innerMatrix.m12 = static_cast(innerValue[1].AsDouble()); + innerMatrix.m13 = static_cast(innerValue[2].AsDouble()); + innerMatrix.m14 = static_cast(innerValue[3].AsDouble()); + innerMatrix.m21 = static_cast(innerValue[4].AsDouble()); + innerMatrix.m22 = static_cast(innerValue[5].AsDouble()); + innerMatrix.m23 = static_cast(innerValue[6].AsDouble()); + innerMatrix.m24 = static_cast(innerValue[7].AsDouble()); + innerMatrix.m31 = static_cast(innerValue[8].AsDouble()); + innerMatrix.m32 = static_cast(innerValue[9].AsDouble()); + innerMatrix.m33 = static_cast(innerValue[10].AsDouble()); + innerMatrix.m34 = static_cast(innerValue[11].AsDouble()); + innerMatrix.m41 = static_cast(innerValue[12].AsDouble()); + innerMatrix.m42 = static_cast(innerValue[13].AsDouble()); + innerMatrix.m43 = static_cast(innerValue[14].AsDouble()); + innerMatrix.m44 = static_cast(innerValue[15].AsDouble()); + transformMatrix = transformMatrix * innerMatrix; + } else if (transformType == "perspective") { + auto innerMatrix = winrt::Windows::Foundation::Numerics::float4x4::identity(); + innerMatrix.m34 = -1 / innerValue.AsSingle(); + transformMatrix = transformMatrix * innerMatrix; + } else if (transformType == "rotateX") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_rotation_x(ToRadians(innerValue)); + } else if (transformType == "rotateY") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_rotation_y(ToRadians(innerValue)); + } else if (transformType == "rotate" || transformType == "rotateZ") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_rotation_z(ToRadians(innerValue)); + } else if (transformType == "scale") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_scale( + innerValue.AsSingle(), innerValue.AsSingle(), 1); + } else if (transformType == "scaleX") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_scale(innerValue.AsSingle(), 1, 1); + } else if (transformType == "scaleY") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_scale(1, innerValue.AsSingle(), 1); + } else if (transformType == "translate") { + auto ¶ms = innerValue.AsArray(); + transformMatrix = + transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_translation( + params[0].AsSingle(), params[1].AsSingle(), params.size() > 2 ? params[2].AsSingle() : 0.f); + } else if (transformType == "translateX") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_translation(innerValue.AsSingle(), 0.f, 0.f); + } else if (transformType == "translateY") { + transformMatrix = transformMatrix * + winrt::Windows::Foundation::Numerics::make_float4x4_translation(0.f, innerValue.AsSingle(), 0.f); + } else if (transformType == "skewX") { + transformMatrix = + transformMatrix * + winrt::Windows::Foundation::Numerics::float4x4( + winrt::Windows::Foundation::Numerics::make_float3x2_skew(innerValue.AsSingle(), 0.f)); + } else if (transformType == "skewY") { + transformMatrix = + transformMatrix * + winrt::Windows::Foundation::Numerics::float4x4( + winrt::Windows::Foundation::Numerics::make_float3x2_skew(0.f, innerValue.AsSingle())); + } + } + } if (!element.IsLoaded()) { element.Loaded([=](auto sender, auto &&) -> auto { diff --git a/vnext/overrides.json b/vnext/overrides.json index 05b411bac30..90344804ba6 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -462,6 +462,13 @@ "type": "platform", "file": "src/Libraries/StyleSheet/PlatformColorValueTypes.windows.js" }, + { + "type": "patch", + "file": "src/Libraries/StyleSheet/processTransform.windows.js", + "baseFile": "Libraries/StyleSheet/processTransform.js", + "baseHash": "cf8b6fd7e28565a689d0ae1252801b54bccf6c7f", + "issue": 9797 + }, { "type": "derived", "file": "src/Libraries/Text/Text.windows.js", diff --git a/vnext/src/Libraries/StyleSheet/processTransform.windows.js b/vnext/src/Libraries/StyleSheet/processTransform.windows.js new file mode 100644 index 00000000000..9e833df026f --- /dev/null +++ b/vnext/src/Libraries/StyleSheet/processTransform.windows.js @@ -0,0 +1,272 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const MatrixMath = require('../Utilities/MatrixMath'); +const Platform = require('../Utilities/Platform'); + +const invariant = require('invariant'); +const stringifySafe = require('../Utilities/stringifySafe').default; + +/** + * 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, where transforms may + * be applied in an arbitrary order, and yet have a universal, singular + * interface to native code. + */ +function processTransform( + transform: Array, +): Array | Array { + if (__DEV__) { + _validateTransforms(transform); + } + + // Android & iOS implementations of transform property accept the list of + // transform properties as opposed to a transform Matrix. This is necessary + // to control transform property updates completely on the native thread. + if ( + Platform.OS === 'android' || + Platform.OS === 'ios' || + Platform.OS === 'windows' + ) { + return transform; + } + + const result = MatrixMath.createIdentityMatrix(); + + transform.forEach((transformation) => { + const key = Object.keys(transformation)[0]; + const value = transformation[key]; + + switch (key) { + case 'matrix': + MatrixMath.multiplyInto(result, result, value); + break; + case 'perspective': + _multiplyTransform(result, MatrixMath.reusePerspectiveCommand, [value]); + break; + case 'rotateX': + _multiplyTransform(result, MatrixMath.reuseRotateXCommand, [ + _convertToRadians(value), + ]); + break; + case 'rotateY': + _multiplyTransform(result, MatrixMath.reuseRotateYCommand, [ + _convertToRadians(value), + ]); + break; + case 'rotate': + case 'rotateZ': + _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; + case 'skewX': + _multiplyTransform(result, MatrixMath.reuseSkewXCommand, [ + _convertToRadians(value), + ]); + break; + case 'skewY': + _multiplyTransform(result, MatrixMath.reuseSkewYCommand, [ + _convertToRadians(value), + ]); + break; + default: + throw new Error('Invalid transform name: ' + key); + } + }); + + return result; +} + +/** + * Performs a destructive operation on a transform matrix. + */ +function _multiplyTransform( + result: Array, + matrixMathFunction: Function, + args: Array, +): void { + const matrixToApply = MatrixMath.createIdentityMatrix(); + const 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 { + const floatValue = parseFloat(value); + return value.indexOf('rad') > -1 ? floatValue : (floatValue * Math.PI) / 180; +} + +function _validateTransforms(transform: Array): void { + transform.forEach((transformation) => { + const keys = Object.keys(transformation); + invariant( + keys.length === 1, + 'You must specify exactly one property per transform object. Passed properties: %s', + stringifySafe(transformation), + ); + const key = keys[0]; + const value = transformation[key]; + _validateTransform(key, value, transformation); + }); +} + +function _validateTransform( + key: + | string + | $TEMPORARY$string<'matrix'> + | $TEMPORARY$string<'perspective'> + | $TEMPORARY$string<'rotate'> + | $TEMPORARY$string<'rotateX'> + | $TEMPORARY$string<'rotateY'> + | $TEMPORARY$string<'rotateZ'> + | $TEMPORARY$string<'scale'> + | $TEMPORARY$string<'scaleX'> + | $TEMPORARY$string<'scaleY'> + | $TEMPORARY$string<'skewX'> + | $TEMPORARY$string<'skewY'> + | $TEMPORARY$string<'translate'> + | $TEMPORARY$string<'translateX'> + | $TEMPORARY$string<'translateY'>, + value: any | number | string, + transformation: any, +) { + invariant( + !value.getValue, + 'You passed an Animated.Value to a normal component. ' + + 'You need to wrap that component in an Animated. For example, ' + + 'replace by .', + ); + + const 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, + stringifySafe(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', + /* $FlowFixMe[prop-missing] (>=0.84.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.84 was deployed. To + * see the error, delete this comment and run Flow. */ + value.length, + stringifySafe(transformation), + ); + break; + case 'translate': + invariant( + value.length === 2 || value.length === 3, + 'Transform with key translate must be an array of length 2 or 3, found %s: %s', + /* $FlowFixMe[prop-missing] (>=0.84.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.84 was deployed. To + * see the error, delete this comment and run Flow. */ + value.length, + stringifySafe(transformation), + ); + break; + case 'rotateX': + case 'rotateY': + case 'rotateZ': + case 'rotate': + case 'skewX': + case 'skewY': + invariant( + typeof value === 'string', + 'Transform with key of "%s" must be a string: %s', + key, + stringifySafe(transformation), + ); + invariant( + value.indexOf('deg') > -1 || value.indexOf('rad') > -1, + 'Rotate transform must be expressed in degrees (deg) or radians ' + + '(rad): %s', + stringifySafe(transformation), + ); + break; + case 'perspective': + invariant( + typeof value === 'number', + 'Transform with key of "%s" must be a number: %s', + key, + stringifySafe(transformation), + ); + invariant( + value !== 0, + 'Transform with key of "%s" cannot be zero: %s', + key, + stringifySafe(transformation), + ); + break; + case 'translateX': + case 'translateY': + case 'scale': + case 'scaleX': + case 'scaleY': + invariant( + typeof value === 'number', + 'Transform with key of "%s" must be a number: %s', + key, + stringifySafe(transformation), + ); + break; + default: + invariant( + false, + 'Invalid transform %s: %s', + key, + stringifySafe(transformation), + ); + } +} + +module.exports = processTransform; From b6528000b1b0ca809663bb41cff8392459024157 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:18:21 -0700 Subject: [PATCH 2/3] Change files --- ...ative-windows-7261b40e-9c20-4f47-a91d-4552206adf06.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-7261b40e-9c20-4f47-a91d-4552206adf06.json diff --git a/change/react-native-windows-7261b40e-9c20-4f47-a91d-4552206adf06.json b/change/react-native-windows-7261b40e-9c20-4f47-a91d-4552206adf06.json new file mode 100644 index 00000000000..6852e291e98 --- /dev/null +++ b/change/react-native-windows-7261b40e-9c20-4f47-a91d-4552206adf06.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Modify transform code to allow fabric usage of transform", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From b9913eb699722a7c5d82d4b5d3193cef703d5372 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:18:55 -0700 Subject: [PATCH 3/3] format --- .../Views/FrameworkElementViewManager.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp b/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp index f4cad155eec..627771ff377 100644 --- a/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp @@ -99,7 +99,6 @@ static void GetAccessibilityValueProps(const winrt::Microsoft::ReactNative::IJSV writer.WriteObjectEnd(); } - inline float ToRadians(const winrt::Microsoft::ReactNative::JSValue &value) { if ((value.Type() == winrt::Microsoft::ReactNative::JSValueType::Double)) { return value.AsSingle(); @@ -108,8 +107,7 @@ inline float ToRadians(const winrt::Microsoft::ReactNative::JSValue &value) { auto stringValue = value.AsString(); char *suffixStart; - double num = strtod( - stringValue.c_str(), &suffixStart); + double num = strtod(stringValue.c_str(), &suffixStart); if (0 == strncmp(suffixStart, "deg", 3)) { return static_cast(num * M_PI / 180.0f); }