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 };