mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
LayoutAnimations: Use Quaternions to interpolate rotation transforms
Summary: Implement a real Transform interpolation. It uses quaternions/Slerp to interpolate rotations. This allows us to interpolate scale, rotation, and translation simultaneously. See caveats in code. Because of the way transform matrices work, there isn't much (anything?) we can do about skew, and certain values will look nonsensical. This seems to be true for any variant of this algorithm. This is a big step up from Classic RN which didn't support this in LayoutAnimations at all. Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D21615829 fbshipit-source-id: da6cb931ce857107d4b3d20108fb9bbecbf4f898
This commit is contained in:
committed by
Facebook GitHub Bot
parent
e781b31489
commit
3cbafcccba
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Portions Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <react/graphics/Float.h>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
// The following is a modified, stripped-down version of the Quaternion class
|
||||
// by Frank Astier. Copyright notice below.
|
||||
// The original has many, many more features, and has been stripped down
|
||||
// to support the exact data-structures and use-cases we need for React Native.
|
||||
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2015 Frank Astier
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
template <typename T = Float>
|
||||
class Quaternion {
|
||||
public:
|
||||
/**
|
||||
* Copy constructor.
|
||||
*/
|
||||
Quaternion(const Quaternion<T> &y) : a_(y.a_), b_(y.b_), c_(y.c_), d_(y.d_) {}
|
||||
|
||||
Quaternion(T a, T b, T c, T d) : a_(a), b_(b), c_(c), d_(d) {}
|
||||
|
||||
static Quaternion<T> fromRotationMatrix(std::array<T, 16> const &rm) {
|
||||
T t = rm[0 * 4 + 0] + rm[1 * 4 + 1] + rm[2 * 4 + 2];
|
||||
if (t > 0) {
|
||||
T s = (T)0.5 / std::sqrt(t + 1);
|
||||
return {(T)0.25 / s,
|
||||
(rm[2 * 4 + 1] - rm[1 * 4 + 2]) * s,
|
||||
(rm[0 * 4 + 2] - rm[2 * 4 + 0]) * s,
|
||||
(rm[1 * 4 + 0] - rm[0 * 4 + 1]) * s};
|
||||
} else if (rm[0 * 4 + 0] > rm[1 * 4 + 1] && rm[0 * 4 + 0] > rm[2 * 4 + 2]) {
|
||||
T s = (T)2.0 *
|
||||
std::sqrt(
|
||||
1.0 + rm[0 * 4 + 0] - rm[1 * 4 + 1] - rm[2 * 4 + 2]); // S=4*qx
|
||||
return {(rm[2 * 4 + 1] - rm[1 * 4 + 2]) / s,
|
||||
(T)0.25 * s,
|
||||
(rm[0 * 4 + 1] + rm[1 * 4 + 0]) / s,
|
||||
(rm[0 * 4 + 2] + rm[2 * 4 + 0]) / s};
|
||||
} else if (rm[1 * 4 + 1] > rm[2 * 4 + 2]) {
|
||||
T s = (T)2.0 *
|
||||
std::sqrt(
|
||||
1.0 + rm[1 * 4 + 1] - rm[0 * 4 + 0] - rm[2 * 4 + 2]); // S=4*qy
|
||||
return {(rm[0 * 4 + 2] - rm[2 * 4 + 0]) / s,
|
||||
(rm[0 * 4 + 1] + rm[1 * 4 + 0]) / s,
|
||||
(T)0.25 * s,
|
||||
(rm[1 * 4 + 2] + rm[2 * 4 + 1]) / s};
|
||||
} else {
|
||||
T s = (T)2.0 *
|
||||
std::sqrt(
|
||||
1.0 + rm[2 * 4 + 2] - rm[0 * 4 + 0] - rm[1 * 4 + 1]); // S=4*qz
|
||||
return {(rm[1 * 4 + 0] - rm[0 * 4 + 1]) / s,
|
||||
(rm[0 * 4 + 2] + rm[2 * 4 + 0]) / s,
|
||||
(rm[1 * 4 + 2] + rm[2 * 4 + 1]) / s,
|
||||
(T)0.25 * s};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 3D, 4x4 rotation matrix.
|
||||
* This is the "homogeneous" expression to convert to a rotation matrix,
|
||||
* which works even when the Quaternion is not a unit Quaternion.
|
||||
*/
|
||||
inline std::array<T, 16> toRotationMatrix4x4() {
|
||||
T a2 = a_ * a_, b2 = b_ * b_, c2 = c_ * c_, d2 = d_ * d_;
|
||||
T ab = a_ * b_, ac = a_ * c_, ad = a_ * d_;
|
||||
T bc = b_ * c_, bd = b_ * d_;
|
||||
T cd = c_ * d_;
|
||||
return {a2 + b2 - c2 - d2,
|
||||
2 * (bc - ad),
|
||||
2 * (bd + ac),
|
||||
0,
|
||||
2 * (bc + ad),
|
||||
a2 - b2 + c2 - d2,
|
||||
2 * (cd - ab),
|
||||
0,
|
||||
2 * (bd - ac),
|
||||
2 * (cd + ab),
|
||||
a2 - b2 - c2 + d2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1};
|
||||
}
|
||||
|
||||
inline Quaternion<T> normalize() const {
|
||||
assert(abs() > 0); // or this is not normalizable
|
||||
T factor = abs();
|
||||
return *this / (factor != 0 ? factor : 1);
|
||||
}
|
||||
|
||||
inline T dot(const Quaternion<T> &other) {
|
||||
return a_ * other.a_ + b_ * other.b_ + c_ * other.c_ + d_ * other.d_;
|
||||
}
|
||||
|
||||
/**
|
||||
* The square of the norm of the Quaternion.
|
||||
* (The square is sometimes useful, and it avoids paying for a sqrt).
|
||||
*/
|
||||
inline T norm_squared() const {
|
||||
return a_ * a_ + b_ * b_ + c_ * c_ + d_ * d_;
|
||||
}
|
||||
|
||||
/**
|
||||
* The norm of the Quaternion (the l2 norm).
|
||||
*/
|
||||
inline T abs() const {
|
||||
return std::sqrt(norm_squared());
|
||||
}
|
||||
|
||||
inline Quaternion<T> operator/=(T y) {
|
||||
a_ /= y;
|
||||
b_ /= y;
|
||||
c_ /= y;
|
||||
d_ /= y;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Quaternion<T> operator*=(T y) {
|
||||
a_ *= y;
|
||||
b_ *= y;
|
||||
c_ *= y;
|
||||
d_ *= y;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Quaternion<T> operator+=(Quaternion<T> const &other) {
|
||||
a_ += other.a_;
|
||||
b_ += other.b_;
|
||||
c_ += other.c_;
|
||||
d_ += other.d_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Quaternion<T> operator-=(Quaternion<T> const &other) {
|
||||
a_ -= other.a_;
|
||||
b_ -= other.b_;
|
||||
c_ -= other.c_;
|
||||
d_ -= other.d_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
T a_; // AKA w, qw
|
||||
T b_; // AKA x, qx
|
||||
T c_; // AKA y, qy
|
||||
T d_; // AKA z, qz
|
||||
};
|
||||
|
||||
template <typename T = Float>
|
||||
inline Quaternion<T> operator/(Quaternion<T> const &lhs, T rhs) {
|
||||
return Quaternion<T>(lhs) /= rhs;
|
||||
}
|
||||
|
||||
template <typename T = Float>
|
||||
inline Quaternion<T> operator*(Quaternion<T> const &lhs, T rhs) {
|
||||
return Quaternion<T>(lhs) *= rhs;
|
||||
}
|
||||
|
||||
template <typename T = Float>
|
||||
inline Quaternion<T> operator+(
|
||||
Quaternion<T> const &lhs,
|
||||
Quaternion<T> const &rhs) {
|
||||
return Quaternion<T>(lhs) += rhs;
|
||||
}
|
||||
|
||||
template <typename T = Float>
|
||||
inline Quaternion<T> operator-(
|
||||
Quaternion<T> const &lhs,
|
||||
Quaternion<T> const &rhs) {
|
||||
return Quaternion<T>(lhs) -= rhs;
|
||||
}
|
||||
|
||||
} // namespace react
|
||||
} // namespace facebook
|
||||
@@ -7,18 +7,34 @@
|
||||
|
||||
#include "Transform.h"
|
||||
|
||||
#include <react/graphics/Quaternion.h>
|
||||
#include <cmath>
|
||||
|
||||
#include <Glog/logging.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
#ifdef RN_DEBUG_STRING_CONVERTIBLE
|
||||
void Transform::print(Transform const &t, std::string prefix) {
|
||||
LOG(ERROR) << prefix << "[ " << t.matrix[0] << " " << t.matrix[1] << " "
|
||||
<< t.matrix[2] << " " << t.matrix[3] << " ]";
|
||||
LOG(ERROR) << prefix << "[ " << t.matrix[4] << " " << t.matrix[5] << " "
|
||||
<< t.matrix[6] << " " << t.matrix[7] << " ]";
|
||||
LOG(ERROR) << prefix << "[ " << t.matrix[8] << " " << t.matrix[9] << " "
|
||||
<< t.matrix[10] << " " << t.matrix[11] << " ]";
|
||||
LOG(ERROR) << prefix << "[ " << t.matrix[12] << " " << t.matrix[13] << " "
|
||||
<< t.matrix[14] << " " << t.matrix[15] << " ]";
|
||||
}
|
||||
#endif
|
||||
|
||||
Transform Transform::Identity() {
|
||||
return {};
|
||||
}
|
||||
|
||||
Transform Transform::Perspective(Float perspective) {
|
||||
auto transform = Transform{};
|
||||
transform.matrix[11] = -1.0 / perspective;
|
||||
transform.matrix[11] = -1 / perspective;
|
||||
return transform;
|
||||
}
|
||||
|
||||
@@ -86,16 +102,130 @@ Transform Transform::Rotate(Float x, Float y, Float z) {
|
||||
return transform;
|
||||
}
|
||||
|
||||
Transform::SRT Transform::ExtractSRT(Transform const &t) {
|
||||
// First we need to extract translation, rotation, and scale from both
|
||||
// matrices, in that order. Matrices must be in this form: [a b c d] [e f g h]
|
||||
// [i j k l]
|
||||
// [0 0 0 1]
|
||||
// We also assume that all scale factors are non-negative, because in
|
||||
assert(
|
||||
t.matrix[12] == 0 && t.matrix[13] == 0 && t.matrix[14] == 0 &&
|
||||
t.matrix[15] == 1 && "Last row of matrix must be [0,0,0,1]");
|
||||
|
||||
// lhs:
|
||||
// Translation: extract the values from the rightmost column
|
||||
Float translationX = t.matrix[3];
|
||||
Float translationY = t.matrix[7];
|
||||
Float translationZ = t.matrix[11];
|
||||
|
||||
// Scale: the length of the first three column vectors
|
||||
// TODO: do we need to do anything special for negative scale factors?
|
||||
// the last element is a uniform scale factor
|
||||
Float scaleX = t.matrix[15] *
|
||||
sqrt(pow(t.matrix[0], 2) + pow(t.matrix[4], 2) +
|
||||
pow(t.matrix[8], 2)); // sqrt(a^2 + e^2 + i^2)
|
||||
Float scaleY = t.matrix[15] *
|
||||
sqrt(pow(t.matrix[1], 2) + pow(t.matrix[5], 2) +
|
||||
pow(t.matrix[9], 2)); // sqrt(b^2 + f^2 + j^2)
|
||||
Float scaleZ = t.matrix[15] *
|
||||
sqrt(pow(t.matrix[2], 2) + pow(t.matrix[6], 2) +
|
||||
pow(t.matrix[10], 2)); // sqrt(c^2 + g^2 + k^2)
|
||||
|
||||
Float rScaleFactorX = scaleX == 0 ? 1 : scaleX;
|
||||
Float rScaleFactorY = scaleY == 0 ? 1 : scaleY;
|
||||
Float rScaleFactorZ = scaleZ == 0 ? 1 : scaleZ;
|
||||
|
||||
// Construct a rotation matrix and convert that to quaternions
|
||||
auto rotationMatrix = std::array<Float, 16>{t.matrix[0] / rScaleFactorX,
|
||||
t.matrix[1] / rScaleFactorY,
|
||||
t.matrix[2] / rScaleFactorZ,
|
||||
0,
|
||||
t.matrix[4] / rScaleFactorX,
|
||||
t.matrix[5] / rScaleFactorY,
|
||||
t.matrix[6] / rScaleFactorZ,
|
||||
0,
|
||||
t.matrix[8] / rScaleFactorX,
|
||||
t.matrix[9] / rScaleFactorY,
|
||||
t.matrix[10] / rScaleFactorZ,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1};
|
||||
|
||||
Quaternion<Float> q =
|
||||
Quaternion<Float>::fromRotationMatrix(rotationMatrix).normalize();
|
||||
|
||||
return Transform::SRT{
|
||||
translationX, translationY, translationZ, scaleX, scaleY, scaleZ, q};
|
||||
}
|
||||
|
||||
Transform Transform::Interpolate(
|
||||
float animationProgress,
|
||||
Transform const &lhs,
|
||||
Transform const &rhs) {
|
||||
auto result = Transform{};
|
||||
for (size_t i = 0; i < 16; i++) {
|
||||
result.matrix[i] =
|
||||
lhs.matrix[i] + (rhs.matrix[i] - lhs.matrix[i]) * animationProgress;
|
||||
// Extract SRT for both sides
|
||||
// This is extracted in the form: X,Y,Z coordinates for translations; X,Y,Z
|
||||
// coordinates for scale; and a quaternion for rotation.
|
||||
auto lhsSRT = ExtractSRT(lhs);
|
||||
auto rhsSRT = ExtractSRT(rhs);
|
||||
|
||||
// Interpolate translation and scale terms linearly (LERP)
|
||||
Float translateX =
|
||||
(lhsSRT.translationX +
|
||||
(rhsSRT.translationX - lhsSRT.translationX) * animationProgress);
|
||||
Float translateY =
|
||||
(lhsSRT.translationY +
|
||||
(rhsSRT.translationY - lhsSRT.translationY) * animationProgress);
|
||||
Float translateZ =
|
||||
(lhsSRT.translationZ +
|
||||
(rhsSRT.translationZ - lhsSRT.translationZ) * animationProgress);
|
||||
Float scaleX =
|
||||
(lhsSRT.scaleX + (rhsSRT.scaleX - lhsSRT.scaleX) * animationProgress);
|
||||
Float scaleY =
|
||||
(lhsSRT.scaleY + (rhsSRT.scaleY - lhsSRT.scaleY) * animationProgress);
|
||||
Float scaleZ =
|
||||
(lhsSRT.scaleZ + (rhsSRT.scaleZ - lhsSRT.scaleZ) * animationProgress);
|
||||
|
||||
// Use the quaternion vectors to produce an interpolated rotation via SLERP
|
||||
// dot: cos of the angle between the two quaternion vectors
|
||||
Quaternion<Float> q1 = lhsSRT.rotation;
|
||||
Quaternion<Float> q2 = rhsSRT.rotation;
|
||||
Float dot = q1.dot(q2);
|
||||
// Clamp dot between -1 and 1
|
||||
dot = (dot < -1 ? -1 : (dot > 1 ? 1 : dot));
|
||||
// There are two ways of performing an identical slerp: q1 and -q1.
|
||||
// If the dot-product is negative, we can multiply q1 by -1 and our animation
|
||||
// will take the "short way" around instead of the "long way".
|
||||
if (dot < 0) {
|
||||
q1 = q1 * (Float)-1;
|
||||
dot = dot * -1;
|
||||
}
|
||||
return result;
|
||||
// Interpolated angle
|
||||
Float theta = acosf(dot) * animationProgress;
|
||||
|
||||
Transform rotation = Transform::Identity();
|
||||
|
||||
// Compute orthonormal basis
|
||||
Quaternion<Float> orthonormalBasis = (q2 - q1 * dot);
|
||||
|
||||
if (orthonormalBasis.abs() > 0) {
|
||||
Quaternion<Float> orthonormalBasisNormalized = orthonormalBasis.normalize();
|
||||
|
||||
// Compute orthonormal basis
|
||||
// Final quaternion result - slerp!
|
||||
Quaternion<Float> resultingRotationVec =
|
||||
(q1 * (Float)cos(theta) +
|
||||
orthonormalBasisNormalized * (Float)sin(theta))
|
||||
.normalize();
|
||||
|
||||
// Convert quaternion to matrix
|
||||
rotation.matrix = resultingRotationVec.toRotationMatrix4x4();
|
||||
}
|
||||
|
||||
// Compose matrices and return
|
||||
return (Scale(scaleX, scaleY, scaleZ) * rotation) *
|
||||
Translate(translateX, translateY, translateZ);
|
||||
}
|
||||
|
||||
bool Transform::operator==(Transform const &rhs) const {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <folly/Hash.h>
|
||||
#include <react/graphics/Float.h>
|
||||
#include <react/graphics/Geometry.h>
|
||||
#include <react/graphics/Quaternion.h>
|
||||
|
||||
#ifdef ANDROID
|
||||
#include <folly/dynamic.h>
|
||||
@@ -20,13 +21,32 @@
|
||||
namespace facebook {
|
||||
namespace react {
|
||||
|
||||
struct ScaleRotationTranslation {
|
||||
Float translationX;
|
||||
Float translationY;
|
||||
Float translationZ;
|
||||
Float scaleX;
|
||||
Float scaleY;
|
||||
Float scaleZ;
|
||||
Quaternion<Float> rotation;
|
||||
};
|
||||
|
||||
/*
|
||||
* Defines transform matrix to apply affine transformations.
|
||||
*/
|
||||
struct Transform {
|
||||
using SRT = ScaleRotationTranslation;
|
||||
|
||||
std::array<Float, 16> matrix{
|
||||
{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}};
|
||||
|
||||
/**
|
||||
* For debugging only. Prints out the matrix.
|
||||
*/
|
||||
#ifdef RN_DEBUG_STRING_CONVERTIBLE
|
||||
static void print(Transform const &t, std::string prefix);
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Returns the identity transform (`[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]`).
|
||||
*/
|
||||
@@ -61,10 +81,29 @@ struct Transform {
|
||||
static Transform Rotate(Float angleX, Float angleY, Float angleZ);
|
||||
|
||||
/**
|
||||
* Perform a simple interpolation between lhs and rhs, given "progress"
|
||||
* between the two assuming that we are "moving" from lhs to rhs. This is a
|
||||
* simple linear interpolation between each matrix index and will only work
|
||||
* for simple scaling or translation; this will not work for rotation.
|
||||
* Extract SRT (scale, rotation, transformation) from a Transform matrix.
|
||||
*
|
||||
* CAVEATS:
|
||||
* 1. The input matrix must not have Skew applied.
|
||||
* 2. Scaling factors must be non-negative. Scaling by a negative factor is
|
||||
* equivalent to a rotation, and though it is possible to detect if 1 or
|
||||
* 3 of the scale signs are flipped (but not two), it is not possible
|
||||
* to detect WHICH of the scales are flipped. Thus, any animation
|
||||
* that involves a negative scale factor will not crash but will
|
||||
* interpolate over nonsensical values.
|
||||
* 3. Another caveat is that if the animation interpolates TO a 90º
|
||||
* rotation in the X, Y, or Z axis, the View will appear to suddenly
|
||||
* explode in size. Interpolating THROUGH 90º is fine as long as you don't end
|
||||
* up at 90º or close to it (89.99). The same is true for 0±90 and 360n+90,
|
||||
* etc.
|
||||
*/
|
||||
static SRT ExtractSRT(Transform const &transform);
|
||||
|
||||
/**
|
||||
* Perform an interpolation between lhs and rhs, given progress.
|
||||
* This first decomposes the matrices into translation, scale, and rotation,
|
||||
* performs slerp between the two rotations, and a linear interpolation
|
||||
* of scale and translation.
|
||||
*
|
||||
* @param progress
|
||||
* @param lhs
|
||||
|
||||
Reference in New Issue
Block a user