motion-dom
Version:
170 lines (167 loc) • 7.24 kB
JavaScript
import { positionalKeys } from '../../render/utils/keys-position.mjs';
import { findDimensionValueType } from '../../value/types/dimensions.mjs';
import { getVariableValue } from '../utils/css-variables-conversion.mjs';
import { containsCSSVariable, isCSSVariableToken } from '../utils/is-css-variable.mjs';
import { KeyframeResolver } from './KeyframesResolver.mjs';
import { isNone } from './utils/is-none.mjs';
import { makeNoneKeyframesAnimatable } from './utils/make-none-animatable.mjs';
import { positionalValues, isNumOrPxType } from './utils/unit-conversion.mjs';
class DOMKeyframesResolver extends KeyframeResolver {
/**
* Passes isAsync to the base constructor rather than setting it as a
* class field, which would compile to a constructor that spreads
* `arguments` into super(). That spread is slow in the interpreter,
* and animate() constructs one resolver per value.
*/
constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
}
/**
* Keyframes that don't need reading from the DOM, or measuring,
* resolve straight away rather than in the frame loop's batched read.
*/
scheduleResolve() {
if (this.canReadNow()) {
this.readKeyframes();
if (!this.needsMeasurement)
return this.complete();
}
super.scheduleResolve();
}
canReadNow() {
const { unresolvedKeyframes, element, motionValue } = this;
const origin = unresolvedKeyframes[0] ?? motionValue?.get();
if (!element?.current || origin === undefined)
return false;
for (let i = 0; i < unresolvedKeyframes.length; i++) {
if (containsCSSVariable(i ? unresolvedKeyframes[i] : origin)) {
return false;
}
}
return true;
}
readKeyframes() {
const { unresolvedKeyframes, element, name } = this;
if (!element?.current)
return;
super.readKeyframes();
/**
* If any keyframe is a CSS variable, we need to find its value by sampling the element
*/
for (let i = 0; i < unresolvedKeyframes.length; i++) {
let keyframe = unresolvedKeyframes[i];
if (typeof keyframe === "string") {
keyframe = keyframe.trim();
if (isCSSVariableToken(keyframe)) {
const resolved = getVariableValue(keyframe, element.current);
if (resolved !== undefined) {
unresolvedKeyframes[i] = resolved;
}
if (i === unresolvedKeyframes.length - 1) {
this.finalKeyframe = keyframe;
}
}
}
}
/**
* Resolve "none" values. We do this potentially twice - once before and once after measuring keyframes.
* This could be seen as inefficient but it's a trade-off to avoid measurements in more situations, which
* have a far bigger performance impact.
*/
this.resolveNoneKeyframes();
/**
* Check to see if unit type has changed. If so schedule jobs that will
* temporarily set styles to the destination keyframes.
* Skip if we have more than two keyframes or this isn't a positional value.
* TODO: We can throw if there are multiple keyframes and the value type changes.
*/
if (!positionalKeys.has(name) || unresolvedKeyframes.length !== 2) {
return;
}
const [origin, target] = unresolvedKeyframes;
// Two numbers share a unit, so skip the value type search.
if (typeof origin === "number" && typeof target === "number")
return;
const originType = findDimensionValueType(origin);
const targetType = findDimensionValueType(target);
/**
* If one keyframe contains embedded CSS variables (e.g. in calc()) and the other
* doesn't, we need to measure to convert to pixels. This handles GitHub issue #3410.
*/
const originHasVar = containsCSSVariable(origin);
const targetHasVar = containsCSSVariable(target);
if (originHasVar !== targetHasVar && positionalValues[name]) {
this.needsMeasurement = true;
return;
}
/**
* Either we don't recognise these value types or we can animate between them.
*/
if (originType === targetType)
return;
/**
* If both values are numbers or pixels, we can animate between them by
* converting them to numbers.
*/
if (isNumOrPxType(originType) && isNumOrPxType(targetType)) {
for (let i = 0; i < unresolvedKeyframes.length; i++) {
const value = unresolvedKeyframes[i];
if (typeof value === "string") {
unresolvedKeyframes[i] = parseFloat(value);
}
}
}
else if (positionalValues[name]) {
/**
* Else, the only way to resolve this is by measuring the element.
*/
this.needsMeasurement = true;
}
}
resolveNoneKeyframes() {
const { unresolvedKeyframes, name } = this;
const noneKeyframeIndexes = [];
for (let i = 0; i < unresolvedKeyframes.length; i++) {
if (unresolvedKeyframes[i] === null ||
isNone(unresolvedKeyframes[i])) {
noneKeyframeIndexes.push(i);
}
}
if (noneKeyframeIndexes.length) {
makeNoneKeyframesAnimatable(unresolvedKeyframes, noneKeyframeIndexes, name);
}
}
measure() {
const { element, name } = this;
return positionalValues[name](window.getComputedStyle(element.current), () => element.measureViewportBox());
}
measureInitialState() {
const { element, unresolvedKeyframes, name } = this;
if (!element?.current)
return;
if (name === "height")
this.suspendedScrollY = window.pageYOffset;
this.measuredOrigin = this.measure();
unresolvedKeyframes[0] = this.measuredOrigin;
// Set final key frame to measure after next render
const measureKeyframe = unresolvedKeyframes[unresolvedKeyframes.length - 1];
if (measureKeyframe !== undefined) {
this.motionValue?.jump(measureKeyframe, false);
}
}
measureEndState() {
const { element, unresolvedKeyframes } = this;
if (!element?.current)
return;
this.motionValue?.jump(this.measuredOrigin, false);
const finalKeyframeIndex = unresolvedKeyframes.length - 1;
const finalKeyframe = unresolvedKeyframes[finalKeyframeIndex];
unresolvedKeyframes[finalKeyframeIndex] = this.measure();
if (finalKeyframe !== null && this.finalKeyframe === undefined) {
this.finalKeyframe = finalKeyframe;
}
this.resolveNoneKeyframes();
}
}
export { DOMKeyframesResolver };
//# sourceMappingURL=DOMKeyframesResolver.mjs.map