231 lines
9.5 KiB
JavaScript
231 lines
9.5 KiB
JavaScript
import { isKeyframesTarget } from '../../../animation/utils/is-keyframes-target.mjs';
|
|
import { invariant } from '../../../utils/errors.mjs';
|
|
import { transformPropOrder } from '../../html/utils/transform.mjs';
|
|
import { findDimensionValueType } from '../value-types/dimensions.mjs';
|
|
import { isBrowser } from '../../../utils/is-browser.mjs';
|
|
import { number } from '../../../value/types/numbers/index.mjs';
|
|
import { px } from '../../../value/types/numbers/units.mjs';
|
|
|
|
const positionalKeys = new Set([
|
|
"width",
|
|
"height",
|
|
"top",
|
|
"left",
|
|
"right",
|
|
"bottom",
|
|
"x",
|
|
"y",
|
|
"translateX",
|
|
"translateY",
|
|
]);
|
|
const isPositionalKey = (key) => positionalKeys.has(key);
|
|
const hasPositionalKey = (target) => {
|
|
return Object.keys(target).some(isPositionalKey);
|
|
};
|
|
const isNumOrPxType = (v) => v === number || v === px;
|
|
const getPosFromMatrix = (matrix, pos) => parseFloat(matrix.split(", ")[pos]);
|
|
const getTranslateFromMatrix = (pos2, pos3) => (_bbox, { transform }) => {
|
|
if (transform === "none" || !transform)
|
|
return 0;
|
|
const matrix3d = transform.match(/^matrix3d\((.+)\)$/);
|
|
if (matrix3d) {
|
|
return getPosFromMatrix(matrix3d[1], pos3);
|
|
}
|
|
else {
|
|
const matrix = transform.match(/^matrix\((.+)\)$/);
|
|
if (matrix) {
|
|
return getPosFromMatrix(matrix[1], pos2);
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
};
|
|
const transformKeys = new Set(["x", "y", "z"]);
|
|
const nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key));
|
|
function removeNonTranslationalTransform(visualElement) {
|
|
const removedTransforms = [];
|
|
nonTranslationalTransformKeys.forEach((key) => {
|
|
const value = visualElement.getValue(key);
|
|
if (value !== undefined) {
|
|
removedTransforms.push([key, value.get()]);
|
|
value.set(key.startsWith("scale") ? 1 : 0);
|
|
}
|
|
});
|
|
// Apply changes to element before measurement
|
|
if (removedTransforms.length)
|
|
visualElement.render();
|
|
return removedTransforms;
|
|
}
|
|
const positionalValues = {
|
|
// Dimensions
|
|
width: ({ x }, { paddingLeft = "0", paddingRight = "0" }) => x.max - x.min - parseFloat(paddingLeft) - parseFloat(paddingRight),
|
|
height: ({ y }, { paddingTop = "0", paddingBottom = "0" }) => y.max - y.min - parseFloat(paddingTop) - parseFloat(paddingBottom),
|
|
top: (_bbox, { top }) => parseFloat(top),
|
|
left: (_bbox, { left }) => parseFloat(left),
|
|
bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min),
|
|
right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min),
|
|
// Transform
|
|
x: getTranslateFromMatrix(4, 13),
|
|
y: getTranslateFromMatrix(5, 14),
|
|
};
|
|
// Alias translate longform names
|
|
positionalValues.translateX = positionalValues.x;
|
|
positionalValues.translateY = positionalValues.y;
|
|
const convertChangedValueTypes = (target, visualElement, changedKeys) => {
|
|
const originBbox = visualElement.measureViewportBox();
|
|
const element = visualElement.current;
|
|
const elementComputedStyle = getComputedStyle(element);
|
|
const { display } = elementComputedStyle;
|
|
const origin = {};
|
|
// If the element is currently set to display: "none", make it visible before
|
|
// measuring the target bounding box
|
|
if (display === "none") {
|
|
visualElement.setStaticValue("display", target.display || "block");
|
|
}
|
|
/**
|
|
* Record origins before we render and update styles
|
|
*/
|
|
changedKeys.forEach((key) => {
|
|
origin[key] = positionalValues[key](originBbox, elementComputedStyle);
|
|
});
|
|
// Apply the latest values (as set in checkAndConvertChangedValueTypes)
|
|
visualElement.render();
|
|
const targetBbox = visualElement.measureViewportBox();
|
|
changedKeys.forEach((key) => {
|
|
// Restore styles to their **calculated computed style**, not their actual
|
|
// originally set style. This allows us to animate between equivalent pixel units.
|
|
const value = visualElement.getValue(key);
|
|
value && value.jump(origin[key]);
|
|
target[key] = positionalValues[key](targetBbox, elementComputedStyle);
|
|
});
|
|
return target;
|
|
};
|
|
const checkAndConvertChangedValueTypes = (visualElement, target, origin = {}, transitionEnd = {}) => {
|
|
target = { ...target };
|
|
transitionEnd = { ...transitionEnd };
|
|
const targetPositionalKeys = Object.keys(target).filter(isPositionalKey);
|
|
// We want to remove any transform values that could affect the element's bounding box before
|
|
// it's measured. We'll reapply these later.
|
|
let removedTransformValues = [];
|
|
let hasAttemptedToRemoveTransformValues = false;
|
|
const changedValueTypeKeys = [];
|
|
targetPositionalKeys.forEach((key) => {
|
|
const value = visualElement.getValue(key);
|
|
if (!visualElement.hasValue(key))
|
|
return;
|
|
let from = origin[key];
|
|
let fromType = findDimensionValueType(from);
|
|
const to = target[key];
|
|
let toType;
|
|
// TODO: The current implementation of this basically throws an error
|
|
// if you try and do value conversion via keyframes. There's probably
|
|
// a way of doing this but the performance implications would need greater scrutiny,
|
|
// as it'd be doing multiple resize-remeasure operations.
|
|
if (isKeyframesTarget(to)) {
|
|
const numKeyframes = to.length;
|
|
const fromIndex = to[0] === null ? 1 : 0;
|
|
from = to[fromIndex];
|
|
fromType = findDimensionValueType(from);
|
|
for (let i = fromIndex; i < numKeyframes; i++) {
|
|
/**
|
|
* Don't allow wildcard keyframes to be used to detect
|
|
* a difference in value types.
|
|
*/
|
|
if (to[i] === null)
|
|
break;
|
|
if (!toType) {
|
|
toType = findDimensionValueType(to[i]);
|
|
invariant(toType === fromType ||
|
|
(isNumOrPxType(fromType) && isNumOrPxType(toType)), "Keyframes must be of the same dimension as the current value");
|
|
}
|
|
else {
|
|
invariant(findDimensionValueType(to[i]) === toType, "All keyframes must be of the same type");
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
toType = findDimensionValueType(to);
|
|
}
|
|
if (fromType !== toType) {
|
|
// If they're both just number or px, convert them both to numbers rather than
|
|
// relying on resize/remeasure to convert (which is wasteful in this situation)
|
|
if (isNumOrPxType(fromType) && isNumOrPxType(toType)) {
|
|
const current = value.get();
|
|
if (typeof current === "string") {
|
|
value.set(parseFloat(current));
|
|
}
|
|
if (typeof to === "string") {
|
|
target[key] = parseFloat(to);
|
|
}
|
|
else if (Array.isArray(to) && toType === px) {
|
|
target[key] = to.map(parseFloat);
|
|
}
|
|
}
|
|
else if ((fromType === null || fromType === void 0 ? void 0 : fromType.transform) &&
|
|
(toType === null || toType === void 0 ? void 0 : toType.transform) &&
|
|
(from === 0 || to === 0)) {
|
|
// If one or the other value is 0, it's safe to coerce it to the
|
|
// type of the other without measurement
|
|
if (from === 0) {
|
|
value.set(toType.transform(from));
|
|
}
|
|
else {
|
|
target[key] = fromType.transform(to);
|
|
}
|
|
}
|
|
else {
|
|
// If we're going to do value conversion via DOM measurements, we first
|
|
// need to remove non-positional transform values that could affect the bbox measurements.
|
|
if (!hasAttemptedToRemoveTransformValues) {
|
|
removedTransformValues =
|
|
removeNonTranslationalTransform(visualElement);
|
|
hasAttemptedToRemoveTransformValues = true;
|
|
}
|
|
changedValueTypeKeys.push(key);
|
|
transitionEnd[key] =
|
|
transitionEnd[key] !== undefined
|
|
? transitionEnd[key]
|
|
: target[key];
|
|
value.jump(to);
|
|
}
|
|
}
|
|
});
|
|
if (changedValueTypeKeys.length) {
|
|
const scrollY = changedValueTypeKeys.indexOf("height") >= 0
|
|
? window.pageYOffset
|
|
: null;
|
|
const convertedTarget = convertChangedValueTypes(target, visualElement, changedValueTypeKeys);
|
|
// If we removed transform values, reapply them before the next render
|
|
if (removedTransformValues.length) {
|
|
removedTransformValues.forEach(([key, value]) => {
|
|
visualElement.getValue(key).set(value);
|
|
});
|
|
}
|
|
// Reapply original values
|
|
visualElement.render();
|
|
// Restore scroll position
|
|
if (isBrowser && scrollY !== null) {
|
|
window.scrollTo({ top: scrollY });
|
|
}
|
|
return { target: convertedTarget, transitionEnd };
|
|
}
|
|
else {
|
|
return { target, transitionEnd };
|
|
}
|
|
};
|
|
/**
|
|
* Convert value types for x/y/width/height/top/left/bottom/right
|
|
*
|
|
* Allows animation between `'auto'` -> `'100%'` or `0` -> `'calc(50% - 10vw)'`
|
|
*
|
|
* @internal
|
|
*/
|
|
function unitConversion(visualElement, target, origin, transitionEnd) {
|
|
return hasPositionalKey(target)
|
|
? checkAndConvertChangedValueTypes(visualElement, target, origin, transitionEnd)
|
|
: { target, transitionEnd };
|
|
}
|
|
|
|
export { positionalValues, unitConversion };
|