react-native/Libraries/Utilities/buildStyleInterpolator.js
Sebastian Markbage 373537b281 Deprecate transformMatrix and decomposedMatrix
Summary:
transformMatrix only worked on iOS and there is an equivalent API that (mostly)
works cross platform.

decomposedMatrix could technically be passed on Android but it wasn't document and explicitly flagged as not working.

My goal is to deprecate both uses and then the only supported API is the `transform: [{ matrix: ... }]` form.

The only difference is that on Android the matrix gets decomposed.

Currently there is some special cased magic that renames transform -> transformMatrix or decomposedMatrix depending on platform.

https://github.com/facebook/react/blob/master/src/renderers/native/ReactNative/ReactNativeAttributePayload.js#L50

Therefore I'm adding an alias for both native platforms called just "transform".

Next I'll swap over the JS to always target the name "transform". The only difference is how the value is marshalled over the bridge in processTransform.

To do this, I have to clean up a few callers. Mostly that's just swapping to the new API.

For buildInterpolator this is a bit trickier but this fixes it for all our use cases (which is only the Navigator in AdsManager).

Reviewed By: vjeux

Differential Revision: D3239960

fb-gh-sync-id: 838edb6644c6cdd0716834f712042f226ff3136f
fbshipit-source-id: 838edb6644c6cdd0716834f712042f226ff3136f
2016-04-29 14:19:25 -07:00

569 lines
17 KiB
JavaScript

/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule buildStyleInterpolator
*/
/**
* Cannot "use strict" because we must use eval in this file.
*/
/* eslint-disable global-strict */
var keyOf = require('fbjs/lib/keyOf');
var X_DIM = keyOf({x: null});
var Y_DIM = keyOf({y: null});
var Z_DIM = keyOf({z: null});
var W_DIM = keyOf({w: null});
var TRANSFORM_ROTATE_NAME = keyOf({transformRotateRadians: null});
var ShouldAllocateReusableOperationVars = {
transformRotateRadians: true,
transformScale: true,
transformTranslate: true,
};
var InitialOperationField = {
transformRotateRadians: [0, 0, 0, 1],
transformTranslate: [0, 0, 0],
transformScale: [1, 1, 1],
};
/**
* Creates a highly specialized animation function that may be evaluated every
* frame. For example:
*
* var ToTheLeft = {
* opacity: {
* from: 1,
* to: 0.7,
* min: 0,
* max: 1,
* type: 'linear',
* extrapolate: false,
* round: 100,
* },
* left: {
* from: 0,
* to: -SCREEN_WIDTH * 0.3,
* min: 0,
* max: 1,
* type: 'linear',
* extrapolate: true,
* round: PixelRatio.get(),
* },
* };
*
* var toTheLeft = buildStyleInterpolator(ToTheLeft);
*
* Would returns a specialized function of the form:
*
* function(result, value) {
* var didChange = false;
* var nextScalarVal;
* var ratio;
* ratio = (value - 0) / 1;
* ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);
* nextScalarVal = Math.round(100 * (1 * (1 - ratio) + 0.7 * ratio)) / 100;
* if (!didChange) {
* var prevVal = result.opacity;
* result.opacity = nextScalarVal;
* didChange = didChange || (nextScalarVal !== prevVal);
* } else {
* result.opacity = nextScalarVal;
* }
* ratio = (value - 0) / 1;
* nextScalarVal = Math.round(2 * (0 * (1 - ratio) + -30 * ratio)) / 2;
* if (!didChange) {
* var prevVal = result.left;
* result.left = nextScalarVal;
* didChange = didChange || (nextScalarVal !== prevVal);
* } else {
* result.left = nextScalarVal;
* }
* return didChange;
* }
*/
var ARGUMENT_NAMES_RE = /([^\s,]+)/g;
/**
* This is obviously a huge hack. Proper tooling would allow actual inlining.
* This only works in a few limited cases (where there is no function return
* value, and the function operates mutatively on parameters).
*
* Example:
*
*
* var inlineMe(a, b) {
* a = b + b;
* };
*
* inline(inlineMe, ['hi', 'bye']); // "hi = bye + bye;"
*
* @param {function} func Any simple function who's arguments can be replaced via a regex.
* @param {array<string>} replaceWithArgs Corresponding names of variables
* within an environment, to replace `func` args with.
* @return {string} Resulting function body string.
*/
var inline = function(func, replaceWithArgs) {
var fnStr = func.toString();
var parameterNames = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')'))
.match(ARGUMENT_NAMES_RE) ||
[];
var replaceRegexStr = parameterNames.map(function(paramName) {
return '\\b' + paramName + '\\b';
}).join('|');
var replaceRegex = new RegExp(replaceRegexStr, 'g');
var fnBody = fnStr.substring(fnStr.indexOf('{') + 1, fnStr.lastIndexOf('}'));
var newFnBody = fnBody.replace(replaceRegex, function(parameterName) {
var indexInParameterNames = parameterNames.indexOf(parameterName);
var replacementName = replaceWithArgs[indexInParameterNames];
return replacementName;
});
return newFnBody.split('\n');
};
/**
* Simply a convenient way to inline functions using the function's toString
* method.
*/
var MatrixOps = {
unroll: function(matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) {
m0 = matVar[0];
m1 = matVar[1];
m2 = matVar[2];
m3 = matVar[3];
m4 = matVar[4];
m5 = matVar[5];
m6 = matVar[6];
m7 = matVar[7];
m8 = matVar[8];
m9 = matVar[9];
m10 = matVar[10];
m11 = matVar[11];
m12 = matVar[12];
m13 = matVar[13];
m14 = matVar[14];
m15 = matVar[15];
},
matrixDiffers: function(retVar, matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) {
retVar = retVar ||
m0 !== matVar[0] ||
m1 !== matVar[1] ||
m2 !== matVar[2] ||
m3 !== matVar[3] ||
m4 !== matVar[4] ||
m5 !== matVar[5] ||
m6 !== matVar[6] ||
m7 !== matVar[7] ||
m8 !== matVar[8] ||
m9 !== matVar[9] ||
m10 !== matVar[10] ||
m11 !== matVar[11] ||
m12 !== matVar[12] ||
m13 !== matVar[13] ||
m14 !== matVar[14] ||
m15 !== matVar[15];
},
transformScale: function(matVar, opVar) {
// Scaling matVar by opVar
var x = opVar[0];
var y = opVar[1];
var z = opVar[2];
matVar[0] = matVar[0] * x;
matVar[1] = matVar[1] * x;
matVar[2] = matVar[2] * x;
matVar[3] = matVar[3] * x;
matVar[4] = matVar[4] * y;
matVar[5] = matVar[5] * y;
matVar[6] = matVar[6] * y;
matVar[7] = matVar[7] * y;
matVar[8] = matVar[8] * z;
matVar[9] = matVar[9] * z;
matVar[10] = matVar[10] * z;
matVar[11] = matVar[11] * z;
matVar[12] = matVar[12];
matVar[13] = matVar[13];
matVar[14] = matVar[14];
matVar[15] = matVar[15];
},
/**
* All of these matrix transforms are not general purpose utilities, and are
* only suitable for being inlined for the use of building up interpolators.
*/
transformTranslate: function(matVar, opVar) {
// Translating matVar by opVar
var x = opVar[0];
var y = opVar[1];
var z = opVar[2];
matVar[12] = matVar[0] * x + matVar[4] * y + matVar[8] * z + matVar[12];
matVar[13] = matVar[1] * x + matVar[5] * y + matVar[9] * z + matVar[13];
matVar[14] = matVar[2] * x + matVar[6] * y + matVar[10] * z + matVar[14];
matVar[15] = matVar[3] * x + matVar[7] * y + matVar[11] * z + matVar[15];
},
/**
* @param {array} matVar Both the input, and the output matrix.
* @param {quaternion specification} q Four element array describing rotation.
*/
transformRotateRadians: function(matVar, q) {
// Rotating matVar by q
var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3];
var x2Quat = xQuat + xQuat;
var y2Quat = yQuat + yQuat;
var z2Quat = zQuat + zQuat;
var xxQuat = xQuat * x2Quat;
var xyQuat = xQuat * y2Quat;
var xzQuat = xQuat * z2Quat;
var yyQuat = yQuat * y2Quat;
var yzQuat = yQuat * z2Quat;
var zzQuat = zQuat * z2Quat;
var wxQuat = wQuat * x2Quat;
var wyQuat = wQuat * y2Quat;
var wzQuat = wQuat * z2Quat;
// Step 1: Inlines the construction of a quaternion matrix (`quatMat`)
var quatMat0 = 1 - (yyQuat + zzQuat);
var quatMat1 = xyQuat + wzQuat;
var quatMat2 = xzQuat - wyQuat;
var quatMat4 = xyQuat - wzQuat;
var quatMat5 = 1 - (xxQuat + zzQuat);
var quatMat6 = yzQuat + wxQuat;
var quatMat8 = xzQuat + wyQuat;
var quatMat9 = yzQuat - wxQuat;
var quatMat10 = 1 - (xxQuat + yyQuat);
// quatMat3/7/11/12/13/14 = 0, quatMat15 = 1
// Step 2: Inlines multiplication, takes advantage of constant quatMat cells
var a00 = matVar[0];
var a01 = matVar[1];
var a02 = matVar[2];
var a03 = matVar[3];
var a10 = matVar[4];
var a11 = matVar[5];
var a12 = matVar[6];
var a13 = matVar[7];
var a20 = matVar[8];
var a21 = matVar[9];
var a22 = matVar[10];
var a23 = matVar[11];
var b0 = quatMat0, b1 = quatMat1, b2 = quatMat2;
matVar[0] = b0 * a00 + b1 * a10 + b2 * a20;
matVar[1] = b0 * a01 + b1 * a11 + b2 * a21;
matVar[2] = b0 * a02 + b1 * a12 + b2 * a22;
matVar[3] = b0 * a03 + b1 * a13 + b2 * a23;
b0 = quatMat4; b1 = quatMat5; b2 = quatMat6;
matVar[4] = b0 * a00 + b1 * a10 + b2 * a20;
matVar[5] = b0 * a01 + b1 * a11 + b2 * a21;
matVar[6] = b0 * a02 + b1 * a12 + b2 * a22;
matVar[7] = b0 * a03 + b1 * a13 + b2 * a23;
b0 = quatMat8; b1 = quatMat9; b2 = quatMat10;
matVar[8] = b0 * a00 + b1 * a10 + b2 * a20;
matVar[9] = b0 * a01 + b1 * a11 + b2 * a21;
matVar[10] = b0 * a02 + b1 * a12 + b2 * a22;
matVar[11] = b0 * a03 + b1 * a13 + b2 * a23;
}
};
// Optimized version of general operation applications that can be used when
// the target matrix is known to be the identity matrix.
var MatrixOpsInitial = {
transformScale: function(matVar, opVar) {
// Scaling matVar known to be identity by opVar
matVar[0] = opVar[0];
matVar[1] = 0;
matVar[2] = 0;
matVar[3] = 0;
matVar[4] = 0;
matVar[5] = opVar[1];
matVar[6] = 0;
matVar[7] = 0;
matVar[8] = 0;
matVar[9] = 0;
matVar[10] = opVar[2];
matVar[11] = 0;
matVar[12] = 0;
matVar[13] = 0;
matVar[14] = 0;
matVar[15] = 1;
},
transformTranslate: function(matVar, opVar) {
// Translating matVar known to be identity by opVar';
matVar[0] = 1;
matVar[1] = 0;
matVar[2] = 0;
matVar[3] = 0;
matVar[4] = 0;
matVar[5] = 1;
matVar[6] = 0;
matVar[7] = 0;
matVar[8] = 0;
matVar[9] = 0;
matVar[10] = 1;
matVar[11] = 0;
matVar[12] = opVar[0];
matVar[13] = opVar[1];
matVar[14] = opVar[2];
matVar[15] = 1;
},
/**
* @param {array} matVar Both the input, and the output matrix - assumed to be
* identity.
* @param {quaternion specification} q Four element array describing rotation.
*/
transformRotateRadians: function(matVar, q) {
// Rotating matVar which is known to be identity by q
var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3];
var x2Quat = xQuat + xQuat;
var y2Quat = yQuat + yQuat;
var z2Quat = zQuat + zQuat;
var xxQuat = xQuat * x2Quat;
var xyQuat = xQuat * y2Quat;
var xzQuat = xQuat * z2Quat;
var yyQuat = yQuat * y2Quat;
var yzQuat = yQuat * z2Quat;
var zzQuat = zQuat * z2Quat;
var wxQuat = wQuat * x2Quat;
var wyQuat = wQuat * y2Quat;
var wzQuat = wQuat * z2Quat;
// Step 1: Inlines the construction of a quaternion matrix (`quatMat`)
var quatMat0 = 1 - (yyQuat + zzQuat);
var quatMat1 = xyQuat + wzQuat;
var quatMat2 = xzQuat - wyQuat;
var quatMat4 = xyQuat - wzQuat;
var quatMat5 = 1 - (xxQuat + zzQuat);
var quatMat6 = yzQuat + wxQuat;
var quatMat8 = xzQuat + wyQuat;
var quatMat9 = yzQuat - wxQuat;
var quatMat10 = 1 - (xxQuat + yyQuat);
// quatMat3/7/11/12/13/14 = 0, quatMat15 = 1
// Step 2: Inlines the multiplication with identity matrix.
var b0 = quatMat0, b1 = quatMat1, b2 = quatMat2;
matVar[0] = b0;
matVar[1] = b1;
matVar[2] = b2;
matVar[3] = 0;
b0 = quatMat4; b1 = quatMat5; b2 = quatMat6;
matVar[4] = b0;
matVar[5] = b1;
matVar[6] = b2;
matVar[7] = 0;
b0 = quatMat8; b1 = quatMat9; b2 = quatMat10;
matVar[8] = b0;
matVar[9] = b1;
matVar[10] = b2;
matVar[11] = 0;
matVar[12] = 0;
matVar[13] = 0;
matVar[14] = 0;
matVar[15] = 1;
}
};
var setNextValAndDetectChange = function(name, tmpVarName) {
return (
' if (!didChange) {\n' +
' var prevVal = result.' + name + ';\n' +
' result.' + name + ' = ' + tmpVarName + ';\n' +
' didChange = didChange || (' + tmpVarName + ' !== prevVal);\n' +
' } else {\n' +
' result.' + name + ' = ' + tmpVarName + ';\n' +
' }\n'
);
};
var computeNextValLinear = function(anim, from, to, tmpVarName) {
var hasRoundRatio = 'round' in anim;
var roundRatio = anim.round;
var fn = ' ratio = (value - ' + anim.min + ') / ' + (anim.max - anim.min) + ';\n';
if (!anim.extrapolate) {
fn += ' ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);\n';
}
var roundOpen = (hasRoundRatio ? 'Math.round(' + roundRatio + ' * ' : '' );
var roundClose = (hasRoundRatio ? ') / ' + roundRatio : '' );
fn +=
' ' + tmpVarName + ' = ' +
roundOpen +
'(' + from + ' * (1 - ratio) + ' + to + ' * ratio)' +
roundClose + ';\n';
return fn;
};
var computeNextValLinearScalar = function(anim) {
return computeNextValLinear(anim, anim.from, anim.to, 'nextScalarVal');
};
var computeNextValConstant = function(anim) {
var constantExpression = JSON.stringify(anim.value);
return ' nextScalarVal = ' + constantExpression + ';\n';
};
var computeNextValStep = function(anim) {
return (
' nextScalarVal = value >= ' +
(anim.threshold + ' ? ' + anim.to + ' : ' + anim.from) + ';\n'
);
};
var computeNextValIdentity = function(anim) {
return ' nextScalarVal = value;\n';
};
var operationVar = function(name) {
return name + 'ReuseOp';
};
var createReusableOperationVars = function(anims) {
var ret = '';
for (var name in anims) {
if (ShouldAllocateReusableOperationVars[name]) {
ret += 'var ' + operationVar(name) + ' = [];\n';
}
}
return ret;
};
var newlines = function(statements) {
return '\n' + statements.join('\n') + '\n';
};
/**
* @param {Animation} anim Configuration entry.
* @param {key} dimension Key to examine in `from`/`to`.
* @param {number} index Field in operationVar to set.
* @return {string} Code that sets the operation variable's field.
*/
var computeNextMatrixOperationField = function(anim, name, dimension, index) {
var fieldAccess = operationVar(name) + '[' + index + ']';
if (anim.from[dimension] !== undefined && anim.to[dimension] !== undefined) {
return ' ' + anim.from[dimension] !== anim.to[dimension] ?
computeNextValLinear(anim, anim.from[dimension], anim.to[dimension], fieldAccess) :
fieldAccess + ' = ' + anim.from[dimension] + ';';
} else {
return ' ' + fieldAccess + ' = ' + InitialOperationField[name][index] + ';';
}
};
var unrolledVars = [];
for (var varIndex = 0; varIndex < 16; varIndex++) {
unrolledVars.push('m' + varIndex);
}
var setNextMatrixAndDetectChange = function(orderedMatrixOperations) {
var fn = [
' var transform = result.transform !== undefined ? ' +
'result.transform : (result.transform = [{ matrix: [] }]);' +
' var transformMatrix = transform[0].matrix;'
];
fn.push.apply(
fn,
inline(MatrixOps.unroll, ['transformMatrix'].concat(unrolledVars))
);
for (var i = 0; i < orderedMatrixOperations.length; i++) {
var opName = orderedMatrixOperations[i];
if (i === 0) {
fn.push.apply(
fn,
inline(MatrixOpsInitial[opName], ['transformMatrix', operationVar(opName)])
);
} else {
fn.push.apply(
fn,
inline(MatrixOps[opName], ['transformMatrix', operationVar(opName)])
);
}
}
fn.push.apply(
fn,
inline(MatrixOps.matrixDiffers, ['didChange', 'transformMatrix'].concat(unrolledVars))
);
return fn;
};
var InterpolateMatrix = {
transformTranslate: true,
transformRotateRadians: true,
transformScale: true,
};
var createFunctionString = function(anims) {
// We must track the order they appear in so transforms are applied in the
// correct order.
var orderedMatrixOperations = [];
// Wrapping function allows the final function to contain state (for
// caching).
var fn = 'return (function() {\n';
fn += createReusableOperationVars(anims);
fn += 'return function(result, value) {\n';
fn += ' var didChange = false;\n';
fn += ' var nextScalarVal;\n';
fn += ' var ratio;\n';
for (var name in anims) {
var anim = anims[name];
if (anim.type === 'linear') {
if (InterpolateMatrix[name]) {
orderedMatrixOperations.push(name);
var setOperations = [
computeNextMatrixOperationField(anim, name, X_DIM, 0),
computeNextMatrixOperationField(anim, name, Y_DIM, 1),
computeNextMatrixOperationField(anim, name, Z_DIM, 2)
];
if (name === TRANSFORM_ROTATE_NAME) {
setOperations.push(computeNextMatrixOperationField(anim, name, W_DIM, 3));
}
fn += newlines(setOperations);
} else {
fn += computeNextValLinearScalar(anim, 'nextScalarVal');
fn += setNextValAndDetectChange(name, 'nextScalarVal');
}
} else if (anim.type === 'constant') {
fn += computeNextValConstant(anim);
fn += setNextValAndDetectChange(name, 'nextScalarVal');
} else if (anim.type === 'step') {
fn += computeNextValStep(anim);
fn += setNextValAndDetectChange(name, 'nextScalarVal');
} else if (anim.type === 'identity') {
fn += computeNextValIdentity(anim);
fn += setNextValAndDetectChange(name, 'nextScalarVal');
}
}
if (orderedMatrixOperations.length) {
fn += newlines(setNextMatrixAndDetectChange(orderedMatrixOperations));
}
fn += ' return didChange;\n';
fn += '};\n';
fn += '})()';
return fn;
};
/**
* @param {object} anims Animation configuration by style property name.
* @return {function} Function accepting style object, that mutates that style
* object and returns a boolean describing if any update was actually applied.
*/
var buildStyleInterpolator = function(anims) {
// Defer compiling this method until we really need it.
var interpolator = null;
function lazyStyleInterpolator(result, value) {
if (interpolator === null) {
interpolator = Function(createFunctionString(anims))();
}
return interpolator(result, value);
}
return lazyStyleInterpolator;
};
module.exports = buildStyleInterpolator;