UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

175 lines (172 loc) • 7.41 kB
import { createGeneratorEasing, supportsLinearEasing, calcGeneratorDuration, maxGeneratorDuration, generateLinearEasing } from 'motion-dom'; import { millisecondsToSeconds, secondsToMilliseconds } from 'motion-utils'; import { clamp } from '../../../utils/clamp.mjs'; import { calcGeneratorVelocity } from '../utils/velocity.mjs'; import { springDefaults } from './defaults.mjs'; import { findSpring, calcAngularFreq } from './find.mjs'; const durationKeys = ["duration", "bounce"]; const physicsKeys = ["stiffness", "damping", "mass"]; function isSpringType(options, keys) { return keys.some((key) => options[key] !== undefined); } function getSpringOptions(options) { let springOptions = { velocity: springDefaults.velocity, stiffness: springDefaults.stiffness, damping: springDefaults.damping, mass: springDefaults.mass, isResolvedFromDuration: false, ...options, }; // stiffness/damping/mass overrides duration/bounce if (!isSpringType(options, physicsKeys) && isSpringType(options, durationKeys)) { if (options.visualDuration) { const visualDuration = options.visualDuration; const root = (2 * Math.PI) / (visualDuration * 1.2); const stiffness = root * root; const damping = 2 * clamp(0.05, 1, 1 - (options.bounce || 0)) * Math.sqrt(stiffness); springOptions = { ...springOptions, mass: springDefaults.mass, stiffness, damping, }; } else { const derived = findSpring(options); springOptions = { ...springOptions, ...derived, mass: springDefaults.mass, }; springOptions.isResolvedFromDuration = true; } } return springOptions; } function spring(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) { const options = typeof optionsOrVisualDuration !== "object" ? { visualDuration: optionsOrVisualDuration, keyframes: [0, 1], bounce, } : optionsOrVisualDuration; let { restSpeed, restDelta } = options; const origin = options.keyframes[0]; const target = options.keyframes[options.keyframes.length - 1]; /** * This is the Iterator-spec return value. We ensure it's mutable rather than using a generator * to reduce GC during animation. */ const state = { done: false, value: origin }; const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({ ...options, velocity: -millisecondsToSeconds(options.velocity || 0), }); const initialVelocity = velocity || 0.0; const dampingRatio = damping / (2 * Math.sqrt(stiffness * mass)); const initialDelta = target - origin; const undampedAngularFreq = millisecondsToSeconds(Math.sqrt(stiffness / mass)); /** * If we're working on a granular scale, use smaller defaults for determining * when the spring is finished. * * These defaults have been selected emprically based on what strikes a good * ratio between feeling good and finishing as soon as changes are imperceptible. */ const isGranularScale = Math.abs(initialDelta) < 5; restSpeed || (restSpeed = isGranularScale ? springDefaults.restSpeed.granular : springDefaults.restSpeed.default); restDelta || (restDelta = isGranularScale ? springDefaults.restDelta.granular : springDefaults.restDelta.default); let resolveSpring; if (dampingRatio < 1) { const angularFreq = calcAngularFreq(undampedAngularFreq, dampingRatio); // Underdamped spring resolveSpring = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); return (target - envelope * (((initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) / angularFreq) * Math.sin(angularFreq * t) + initialDelta * Math.cos(angularFreq * t))); }; } else if (dampingRatio === 1) { // Critically damped spring resolveSpring = (t) => target - Math.exp(-undampedAngularFreq * t) * (initialDelta + (initialVelocity + undampedAngularFreq * initialDelta) * t); } else { // Overdamped spring const dampedAngularFreq = undampedAngularFreq * Math.sqrt(dampingRatio * dampingRatio - 1); resolveSpring = (t) => { const envelope = Math.exp(-dampingRatio * undampedAngularFreq * t); // When performing sinh or cosh values can hit Infinity so we cap them here const freqForT = Math.min(dampedAngularFreq * t, 300); return (target - (envelope * ((initialVelocity + dampingRatio * undampedAngularFreq * initialDelta) * Math.sinh(freqForT) + dampedAngularFreq * initialDelta * Math.cosh(freqForT))) / dampedAngularFreq); }; } const generator = { calculatedDuration: isResolvedFromDuration ? duration || null : null, next: (t) => { const current = resolveSpring(t); if (!isResolvedFromDuration) { let currentVelocity = 0.0; /** * We only need to calculate velocity for under-damped springs * as over- and critically-damped springs can't overshoot, so * checking only for displacement is enough. */ if (dampingRatio < 1) { currentVelocity = t === 0 ? secondsToMilliseconds(initialVelocity) : calcGeneratorVelocity(resolveSpring, t, current); } const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed; const isBelowDisplacementThreshold = Math.abs(target - current) <= restDelta; state.done = isBelowVelocityThreshold && isBelowDisplacementThreshold; } else { state.done = t >= duration; } state.value = state.done ? target : current; return state; }, toString: () => { const calculatedDuration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration); const easing = generateLinearEasing((progress) => generator.next(calculatedDuration * progress).value, calculatedDuration, 30); return calculatedDuration + "ms " + easing; }, toTransition: () => { }, }; return generator; } spring.applyToOptions = (options) => { const generatorOptions = createGeneratorEasing(options, 100, spring); options.ease = supportsLinearEasing() ? generatorOptions.ease : "easeOut"; options.duration = secondsToMilliseconds(generatorOptions.duration); options.type = "keyframes"; return options; }; export { spring };