react-native/Libraries/Utilities/buildStyleInterpolator.js

561 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('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 whos 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 transformMatrix = result.transformMatrix !== undefined ? ' +
'result.transformMatrix : (result.transformMatrix = []);'
];
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) {
return Function(createFunctionString(anims))();
};
module.exports = buildStyleInterpolator;