till download files, everything works
This commit is contained in:
1 parent
ce893a4185
commit
ed1a580b09
6050 files changed
+768543
-177
No files matched your search
+78
@@ -0,0 +1,78 @@
|
||||
import { supportsScrollTimeline } from '../render/dom/scroll/supports.mjs';
|
||||
|
||||
class GroupPlaybackControls {
|
||||
constructor(animations) {
|
||||
// Bound to accomodate common `return animation.stop` pattern
|
||||
this.stop = () => this.runAll("stop");
|
||||
this.animations = animations.filter(Boolean);
|
||||
}
|
||||
then(onResolve, onReject) {
|
||||
return Promise.all(this.animations).then(onResolve).catch(onReject);
|
||||
}
|
||||
/**
|
||||
* TODO: Filter out cancelled or stopped animations before returning
|
||||
*/
|
||||
getAll(propName) {
|
||||
return this.animations[0][propName];
|
||||
}
|
||||
setAll(propName, newValue) {
|
||||
for (let i = 0; i < this.animations.length; i++) {
|
||||
this.animations[i][propName] = newValue;
|
||||
}
|
||||
}
|
||||
attachTimeline(timeline, fallback) {
|
||||
const subscriptions = this.animations.map((animation) => {
|
||||
if (supportsScrollTimeline() && animation.attachTimeline) {
|
||||
return animation.attachTimeline(timeline);
|
||||
}
|
||||
else {
|
||||
return fallback(animation);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
subscriptions.forEach((cancel, i) => {
|
||||
cancel && cancel();
|
||||
this.animations[i].stop();
|
||||
});
|
||||
};
|
||||
}
|
||||
get time() {
|
||||
return this.getAll("time");
|
||||
}
|
||||
set time(time) {
|
||||
this.setAll("time", time);
|
||||
}
|
||||
get speed() {
|
||||
return this.getAll("speed");
|
||||
}
|
||||
set speed(speed) {
|
||||
this.setAll("speed", speed);
|
||||
}
|
||||
get startTime() {
|
||||
return this.getAll("startTime");
|
||||
}
|
||||
get duration() {
|
||||
let max = 0;
|
||||
for (let i = 0; i < this.animations.length; i++) {
|
||||
max = Math.max(max, this.animations[i].duration);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
runAll(methodName) {
|
||||
this.animations.forEach((controls) => controls[methodName]());
|
||||
}
|
||||
play() {
|
||||
this.runAll("play");
|
||||
}
|
||||
pause() {
|
||||
this.runAll("pause");
|
||||
}
|
||||
cancel() {
|
||||
this.runAll("cancel");
|
||||
}
|
||||
complete() {
|
||||
this.runAll("complete");
|
||||
}
|
||||
}
|
||||
|
||||
export { GroupPlaybackControls };
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { GroupPlaybackControls } from '../GroupPlaybackControls.mjs';
|
||||
import { animateSequence } from './sequence.mjs';
|
||||
import { animateSubject } from './subject.mjs';
|
||||
|
||||
function isSequence(value) {
|
||||
return Array.isArray(value) && Array.isArray(value[0]);
|
||||
}
|
||||
/**
|
||||
* Creates an animation function that is optionally scoped
|
||||
* to a specific element.
|
||||
*/
|
||||
function createScopedAnimate(scope) {
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
|
||||
let animations = [];
|
||||
if (isSequence(subjectOrSequence)) {
|
||||
animations = animateSequence(subjectOrSequence, optionsOrKeyframes, scope);
|
||||
}
|
||||
else {
|
||||
animations = animateSubject(subjectOrSequence, optionsOrKeyframes, options, scope);
|
||||
}
|
||||
const animation = new GroupPlaybackControls(animations);
|
||||
if (scope) {
|
||||
scope.animations.push(animation);
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
return scopedAnimate;
|
||||
}
|
||||
const animate = createScopedAnimate();
|
||||
|
||||
export { animate, createScopedAnimate };
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { resolveElements } from '../../render/dom/utils/resolve-element.mjs';
|
||||
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
|
||||
|
||||
function resolveSubjects(subject, keyframes, scope, selectorCache) {
|
||||
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
|
||||
return resolveElements(subject, scope, selectorCache);
|
||||
}
|
||||
else if (subject instanceof NodeList) {
|
||||
return Array.from(subject);
|
||||
}
|
||||
else if (Array.isArray(subject)) {
|
||||
return subject;
|
||||
}
|
||||
else {
|
||||
return [subject];
|
||||
}
|
||||
}
|
||||
|
||||
export { resolveSubjects };
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { spring } from '../generators/spring/index.mjs';
|
||||
import { createAnimationsFromSequence } from '../sequence/create.mjs';
|
||||
import { animateSubject } from './subject.mjs';
|
||||
|
||||
function animateSequence(sequence, options, scope) {
|
||||
const animations = [];
|
||||
const animationDefinitions = createAnimationsFromSequence(sequence, options, scope, { spring });
|
||||
animationDefinitions.forEach(({ keyframes, transition }, subject) => {
|
||||
animations.push(...animateSubject(subject, keyframes, transition));
|
||||
});
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateSequence };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { animateMotionValue } from '../interfaces/motion-value.mjs';
|
||||
import { motionValue } from '../../value/index.mjs';
|
||||
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
|
||||
|
||||
function animateSingleValue(value, keyframes, options) {
|
||||
const motionValue$1 = isMotionValue(value) ? value : motionValue(value);
|
||||
motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options));
|
||||
return motionValue$1.animation;
|
||||
}
|
||||
|
||||
export { animateSingleValue };
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { visualElementStore } from '../../render/store.mjs';
|
||||
import { invariant } from '../../utils/errors.mjs';
|
||||
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
|
||||
import { animateTarget } from '../interfaces/visual-element-target.mjs';
|
||||
import { createDOMVisualElement, createObjectVisualElement } from '../utils/create-visual-element.mjs';
|
||||
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
|
||||
import { resolveSubjects } from './resolve-subjects.mjs';
|
||||
import { animateSingleValue } from './single-value.mjs';
|
||||
|
||||
function isSingleValue(subject, keyframes) {
|
||||
return (isMotionValue(subject) ||
|
||||
typeof subject === "number" ||
|
||||
(typeof subject === "string" && !isDOMKeyframes(keyframes)));
|
||||
}
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
function animateSubject(subject, keyframes, options, scope) {
|
||||
const animations = [];
|
||||
if (isSingleValue(subject, keyframes)) {
|
||||
animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes)
|
||||
? keyframes.default || keyframes
|
||||
: keyframes, options ? options.default || options : options));
|
||||
}
|
||||
else {
|
||||
const subjects = resolveSubjects(subject, keyframes, scope);
|
||||
const numSubjects = subjects.length;
|
||||
invariant(Boolean(numSubjects), "No valid elements provided.");
|
||||
for (let i = 0; i < numSubjects; i++) {
|
||||
const thisSubject = subjects[i];
|
||||
const createVisualElement = thisSubject instanceof Element
|
||||
? createDOMVisualElement
|
||||
: createObjectVisualElement;
|
||||
if (!visualElementStore.has(thisSubject)) {
|
||||
createVisualElement(thisSubject);
|
||||
}
|
||||
const visualElement = visualElementStore.get(thisSubject);
|
||||
const transition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if ("delay" in transition &&
|
||||
typeof transition.delay === "function") {
|
||||
transition.delay = transition.delay(i, numSubjects);
|
||||
}
|
||||
animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {}));
|
||||
}
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateSubject };
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import { anticipate } from '../../easing/anticipate.mjs';
|
||||
import { backInOut } from '../../easing/back.mjs';
|
||||
import { circInOut } from '../../easing/circ.mjs';
|
||||
import { DOMKeyframesResolver } from '../../render/dom/DOMKeyframesResolver.mjs';
|
||||
import { noop } from '../../utils/noop.mjs';
|
||||
import { millisecondsToSeconds, secondsToMilliseconds } from '../../utils/time-conversion.mjs';
|
||||
import { isGenerator } from '../generators/utils/is-generator.mjs';
|
||||
import { BaseAnimation } from './BaseAnimation.mjs';
|
||||
import { MainThreadAnimation } from './MainThreadAnimation.mjs';
|
||||
import { acceleratedValues } from './utils/accelerated-values.mjs';
|
||||
import { startWaapiAnimation } from './waapi/index.mjs';
|
||||
import { isWaapiSupportedEasing } from './waapi/easing.mjs';
|
||||
import { attachTimeline } from './waapi/utils/attach-timeline.mjs';
|
||||
import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
|
||||
import { supportsLinearEasing } from './waapi/utils/supports-linear-easing.mjs';
|
||||
import { supportsWaapi } from './waapi/utils/supports-waapi.mjs';
|
||||
|
||||
/**
|
||||
* 10ms is chosen here as it strikes a balance between smooth
|
||||
* results (more than one keyframe per frame at 60fps) and
|
||||
* keyframe quantity.
|
||||
*/
|
||||
const sampleDelta = 10; //ms
|
||||
/**
|
||||
* Implement a practical max duration for keyframe generation
|
||||
* to prevent infinite loops
|
||||
*/
|
||||
const maxDuration = 20000;
|
||||
/**
|
||||
* Check if an animation can run natively via WAAPI or requires pregenerated keyframes.
|
||||
* WAAPI doesn't support spring or function easings so we run these as JS animation before
|
||||
* handing off.
|
||||
*/
|
||||
function requiresPregeneratedKeyframes(options) {
|
||||
return (isGenerator(options.type) ||
|
||||
options.type === "spring" ||
|
||||
!isWaapiSupportedEasing(options.ease));
|
||||
}
|
||||
function pregenerateKeyframes(keyframes, options) {
|
||||
/**
|
||||
* Create a main-thread animation to pregenerate keyframes.
|
||||
* We sample this at regular intervals to generate keyframes that we then
|
||||
* linearly interpolate between.
|
||||
*/
|
||||
const sampleAnimation = new MainThreadAnimation({
|
||||
...options,
|
||||
keyframes,
|
||||
repeat: 0,
|
||||
delay: 0,
|
||||
isGenerator: true,
|
||||
});
|
||||
let state = { done: false, value: keyframes[0] };
|
||||
const pregeneratedKeyframes = [];
|
||||
/**
|
||||
* Bail after 20 seconds of pre-generated keyframes as it's likely
|
||||
* we're heading for an infinite loop.
|
||||
*/
|
||||
let t = 0;
|
||||
while (!state.done && t < maxDuration) {
|
||||
state = sampleAnimation.sample(t);
|
||||
pregeneratedKeyframes.push(state.value);
|
||||
t += sampleDelta;
|
||||
}
|
||||
return {
|
||||
times: undefined,
|
||||
keyframes: pregeneratedKeyframes,
|
||||
duration: t - sampleDelta,
|
||||
ease: "linear",
|
||||
};
|
||||
}
|
||||
const unsupportedEasingFunctions = {
|
||||
anticipate,
|
||||
backInOut,
|
||||
circInOut,
|
||||
};
|
||||
function isUnsupportedEase(key) {
|
||||
return key in unsupportedEasingFunctions;
|
||||
}
|
||||
class AcceleratedAnimation extends BaseAnimation {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const { name, motionValue, element, keyframes } = this.options;
|
||||
this.resolver = new DOMKeyframesResolver(keyframes, (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe), name, motionValue, element);
|
||||
this.resolver.scheduleResolve();
|
||||
}
|
||||
initPlayback(keyframes, finalKeyframe) {
|
||||
var _a;
|
||||
let { duration = 300, times, ease, type, motionValue, name, startTime, } = this.options;
|
||||
/**
|
||||
* If element has since been unmounted, return false to indicate
|
||||
* the animation failed to initialised.
|
||||
*/
|
||||
if (!((_a = motionValue.owner) === null || _a === void 0 ? void 0 : _a.current)) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* If the user has provided an easing function name that isn't supported
|
||||
* by WAAPI (like "anticipate"), we need to provide the corressponding
|
||||
* function. This will later get converted to a linear() easing function.
|
||||
*/
|
||||
if (typeof ease === "string" &&
|
||||
supportsLinearEasing() &&
|
||||
isUnsupportedEase(ease)) {
|
||||
ease = unsupportedEasingFunctions[ease];
|
||||
}
|
||||
/**
|
||||
* If this animation needs pre-generated keyframes then generate.
|
||||
*/
|
||||
if (requiresPregeneratedKeyframes(this.options)) {
|
||||
const { onComplete, onUpdate, motionValue, element, ...options } = this.options;
|
||||
const pregeneratedAnimation = pregenerateKeyframes(keyframes, options);
|
||||
keyframes = pregeneratedAnimation.keyframes;
|
||||
// If this is a very short animation, ensure we have
|
||||
// at least two keyframes to animate between as older browsers
|
||||
// can't animate between a single keyframe.
|
||||
if (keyframes.length === 1) {
|
||||
keyframes[1] = keyframes[0];
|
||||
}
|
||||
duration = pregeneratedAnimation.duration;
|
||||
times = pregeneratedAnimation.times;
|
||||
ease = pregeneratedAnimation.ease;
|
||||
type = "keyframes";
|
||||
}
|
||||
const animation = startWaapiAnimation(motionValue.owner.current, name, keyframes, { ...this.options, duration, times, ease });
|
||||
// Override the browser calculated startTime with one synchronised to other JS
|
||||
// and WAAPI animations starting this event loop.
|
||||
animation.startTime = startTime !== null && startTime !== void 0 ? startTime : this.calcStartTime();
|
||||
if (this.pendingTimeline) {
|
||||
attachTimeline(animation, this.pendingTimeline);
|
||||
this.pendingTimeline = undefined;
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* Prefer the `onfinish` prop as it's more widely supported than
|
||||
* the `finished` promise.
|
||||
*
|
||||
* Here, we synchronously set the provided MotionValue to the end
|
||||
* keyframe. If we didn't, when the WAAPI animation is finished it would
|
||||
* be removed from the element which would then revert to its old styles.
|
||||
*/
|
||||
animation.onfinish = () => {
|
||||
const { onComplete } = this.options;
|
||||
motionValue.set(getFinalKeyframe(keyframes, this.options, finalKeyframe));
|
||||
onComplete && onComplete();
|
||||
this.cancel();
|
||||
this.resolveFinishedPromise();
|
||||
};
|
||||
}
|
||||
return {
|
||||
animation,
|
||||
duration,
|
||||
times,
|
||||
type,
|
||||
ease,
|
||||
keyframes: keyframes,
|
||||
};
|
||||
}
|
||||
get duration() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return 0;
|
||||
const { duration } = resolved;
|
||||
return millisecondsToSeconds(duration);
|
||||
}
|
||||
get time() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return 0;
|
||||
const { animation } = resolved;
|
||||
return millisecondsToSeconds(animation.currentTime || 0);
|
||||
}
|
||||
set time(newTime) {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
const { animation } = resolved;
|
||||
animation.currentTime = secondsToMilliseconds(newTime);
|
||||
}
|
||||
get speed() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return 1;
|
||||
const { animation } = resolved;
|
||||
return animation.playbackRate;
|
||||
}
|
||||
set speed(newSpeed) {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
const { animation } = resolved;
|
||||
animation.playbackRate = newSpeed;
|
||||
}
|
||||
get state() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return "idle";
|
||||
const { animation } = resolved;
|
||||
return animation.playState;
|
||||
}
|
||||
get startTime() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return null;
|
||||
const { animation } = resolved;
|
||||
// Coerce to number as TypeScript incorrectly types this
|
||||
// as CSSNumberish
|
||||
return animation.startTime;
|
||||
}
|
||||
/**
|
||||
* Replace the default DocumentTimeline with another AnimationTimeline.
|
||||
* Currently used for scroll animations.
|
||||
*/
|
||||
attachTimeline(timeline) {
|
||||
if (!this._resolved) {
|
||||
this.pendingTimeline = timeline;
|
||||
}
|
||||
else {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return noop;
|
||||
const { animation } = resolved;
|
||||
attachTimeline(animation, timeline);
|
||||
}
|
||||
return noop;
|
||||
}
|
||||
play() {
|
||||
if (this.isStopped)
|
||||
return;
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
const { animation } = resolved;
|
||||
if (animation.playState === "finished") {
|
||||
this.updateFinishedPromise();
|
||||
}
|
||||
animation.play();
|
||||
}
|
||||
pause() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
const { animation } = resolved;
|
||||
animation.pause();
|
||||
}
|
||||
stop() {
|
||||
this.resolver.cancel();
|
||||
this.isStopped = true;
|
||||
if (this.state === "idle")
|
||||
return;
|
||||
this.resolveFinishedPromise();
|
||||
this.updateFinishedPromise();
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
const { animation, keyframes, duration, type, ease, times } = resolved;
|
||||
if (animation.playState === "idle" ||
|
||||
animation.playState === "finished") {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* WAAPI doesn't natively have any interruption capabilities.
|
||||
*
|
||||
* Rather than read commited styles back out of the DOM, we can
|
||||
* create a renderless JS animation and sample it twice to calculate
|
||||
* its current value, "previous" value, and therefore allow
|
||||
* Motion to calculate velocity for any subsequent animation.
|
||||
*/
|
||||
if (this.time) {
|
||||
const { motionValue, onUpdate, onComplete, element, ...options } = this.options;
|
||||
const sampleAnimation = new MainThreadAnimation({
|
||||
...options,
|
||||
keyframes,
|
||||
duration,
|
||||
type,
|
||||
ease,
|
||||
times,
|
||||
isGenerator: true,
|
||||
});
|
||||
const sampleTime = secondsToMilliseconds(this.time);
|
||||
motionValue.setWithVelocity(sampleAnimation.sample(sampleTime - sampleDelta).value, sampleAnimation.sample(sampleTime).value, sampleDelta);
|
||||
}
|
||||
const { onStop } = this.options;
|
||||
onStop && onStop();
|
||||
this.cancel();
|
||||
}
|
||||
complete() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
resolved.animation.finish();
|
||||
}
|
||||
cancel() {
|
||||
const { resolved } = this;
|
||||
if (!resolved)
|
||||
return;
|
||||
resolved.animation.cancel();
|
||||
}
|
||||
static supports(options) {
|
||||
const { motionValue, name, repeatDelay, repeatType, damping, type } = options;
|
||||
return (supportsWaapi() &&
|
||||
name &&
|
||||
acceleratedValues.has(name) &&
|
||||
motionValue &&
|
||||
motionValue.owner &&
|
||||
motionValue.owner.current instanceof HTMLElement &&
|
||||
/**
|
||||
* If we're outputting values to onUpdate then we can't use WAAPI as there's
|
||||
* no way to read the value from WAAPI every frame.
|
||||
*/
|
||||
!motionValue.owner.getProps().onUpdate &&
|
||||
!repeatDelay &&
|
||||
repeatType !== "mirror" &&
|
||||
damping !== 0 &&
|
||||
type !== "inertia");
|
||||
}
|
||||
}
|
||||
|
||||
export { AcceleratedAnimation };
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { time } from '../../frameloop/sync-time.mjs';
|
||||
import { flushKeyframeResolvers } from '../../render/utils/KeyframesResolver.mjs';
|
||||
import { instantAnimationState } from '../../utils/use-instant-transition-state.mjs';
|
||||
import { canAnimate } from './utils/can-animate.mjs';
|
||||
import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
|
||||
|
||||
/**
|
||||
* Maximum time allowed between an animation being created and it being
|
||||
* resolved for us to use the latter as the start time.
|
||||
*
|
||||
* This is to ensure that while we prefer to "start" an animation as soon
|
||||
* as it's triggered, we also want to avoid a visual jump if there's a big delay
|
||||
* between these two moments.
|
||||
*/
|
||||
const MAX_RESOLVE_DELAY = 40;
|
||||
class BaseAnimation {
|
||||
constructor({ autoplay = true, delay = 0, type = "keyframes", repeat = 0, repeatDelay = 0, repeatType = "loop", ...options }) {
|
||||
// Track whether the animation has been stopped. Stopped animations won't restart.
|
||||
this.isStopped = false;
|
||||
this.hasAttemptedResolve = false;
|
||||
this.createdAt = time.now();
|
||||
this.options = {
|
||||
autoplay,
|
||||
delay,
|
||||
type,
|
||||
repeat,
|
||||
repeatDelay,
|
||||
repeatType,
|
||||
...options,
|
||||
};
|
||||
this.updateFinishedPromise();
|
||||
}
|
||||
/**
|
||||
* This method uses the createdAt and resolvedAt to calculate the
|
||||
* animation startTime. *Ideally*, we would use the createdAt time as t=0
|
||||
* as the following frame would then be the first frame of the animation in
|
||||
* progress, which would feel snappier.
|
||||
*
|
||||
* However, if there's a delay (main thread work) between the creation of
|
||||
* the animation and the first commited frame, we prefer to use resolvedAt
|
||||
* to avoid a sudden jump into the animation.
|
||||
*/
|
||||
calcStartTime() {
|
||||
if (!this.resolvedAt)
|
||||
return this.createdAt;
|
||||
return this.resolvedAt - this.createdAt > MAX_RESOLVE_DELAY
|
||||
? this.resolvedAt
|
||||
: this.createdAt;
|
||||
}
|
||||
/**
|
||||
* A getter for resolved data. If keyframes are not yet resolved, accessing
|
||||
* this.resolved will synchronously flush all pending keyframe resolvers.
|
||||
* This is a deoptimisation, but at its worst still batches read/writes.
|
||||
*/
|
||||
get resolved() {
|
||||
if (!this._resolved && !this.hasAttemptedResolve) {
|
||||
flushKeyframeResolvers();
|
||||
}
|
||||
return this._resolved;
|
||||
}
|
||||
/**
|
||||
* A method to be called when the keyframes resolver completes. This method
|
||||
* will check if its possible to run the animation and, if not, skip it.
|
||||
* Otherwise, it will call initPlayback on the implementing class.
|
||||
*/
|
||||
onKeyframesResolved(keyframes, finalKeyframe) {
|
||||
this.resolvedAt = time.now();
|
||||
this.hasAttemptedResolve = true;
|
||||
const { name, type, velocity, delay, onComplete, onUpdate, isGenerator, } = this.options;
|
||||
/**
|
||||
* If we can't animate this value with the resolved keyframes
|
||||
* then we should complete it immediately.
|
||||
*/
|
||||
if (!isGenerator && !canAnimate(keyframes, name, type, velocity)) {
|
||||
// Finish immediately
|
||||
if (instantAnimationState.current || !delay) {
|
||||
onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate(getFinalKeyframe(keyframes, this.options, finalKeyframe));
|
||||
onComplete === null || onComplete === void 0 ? void 0 : onComplete();
|
||||
this.resolveFinishedPromise();
|
||||
return;
|
||||
}
|
||||
// Finish after a delay
|
||||
else {
|
||||
this.options.duration = 0;
|
||||
}
|
||||
}
|
||||
const resolvedAnimation = this.initPlayback(keyframes, finalKeyframe);
|
||||
if (resolvedAnimation === false)
|
||||
return;
|
||||
this._resolved = {
|
||||
keyframes,
|
||||
finalKeyframe,
|
||||
...resolvedAnimation,
|
||||
};
|
||||
this.onPostResolved();
|
||||
}
|
||||
onPostResolved() { }
|
||||
/**
|
||||
* Allows the returned animation to be awaited or promise-chained. Currently
|
||||
* resolves when the animation finishes at all but in a future update could/should
|
||||
* reject if its cancels.
|
||||
*/
|
||||
then(resolve, reject) {
|
||||
return this.currentFinishedPromise.then(resolve, reject);
|
||||
}
|
||||
updateFinishedPromise() {
|
||||
this.currentFinishedPromise = new Promise((resolve) => {
|
||||
this.resolveFinishedPromise = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseAnimation };
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
import { KeyframeResolver } from '../../render/utils/KeyframesResolver.mjs';
|
||||
import { spring } from '../generators/spring/index.mjs';
|
||||
import { inertia } from '../generators/inertia.mjs';
|
||||
import { keyframes } from '../generators/keyframes.mjs';
|
||||
import { BaseAnimation } from './BaseAnimation.mjs';
|
||||
import { pipe } from '../../utils/pipe.mjs';
|
||||
import { mix } from '../../utils/mix/index.mjs';
|
||||
import { calcGeneratorDuration } from '../generators/utils/calc-duration.mjs';
|
||||
import { millisecondsToSeconds, secondsToMilliseconds } from '../../utils/time-conversion.mjs';
|
||||
import { clamp } from '../../utils/clamp.mjs';
|
||||
import { invariant } from '../../utils/errors.mjs';
|
||||
import { frameloopDriver } from './drivers/driver-frameloop.mjs';
|
||||
import { getFinalKeyframe } from './waapi/utils/get-final-keyframe.mjs';
|
||||
import { isGenerator } from '../generators/utils/is-generator.mjs';
|
||||
|
||||
const generators = {
|
||||
decay: inertia,
|
||||
inertia,
|
||||
tween: keyframes,
|
||||
keyframes: keyframes,
|
||||
spring,
|
||||
};
|
||||
const percentToProgress = (percent) => percent / 100;
|
||||
/**
|
||||
* Animation that runs on the main thread. Designed to be WAAPI-spec in the subset of
|
||||
* features we expose publically. Mostly the compatibility is to ensure visual identity
|
||||
* between both WAAPI and main thread animations.
|
||||
*/
|
||||
class MainThreadAnimation extends BaseAnimation {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
/**
|
||||
* The time at which the animation was paused.
|
||||
*/
|
||||
this.holdTime = null;
|
||||
/**
|
||||
* The time at which the animation was cancelled.
|
||||
*/
|
||||
this.cancelTime = null;
|
||||
/**
|
||||
* The current time of the animation.
|
||||
*/
|
||||
this.currentTime = 0;
|
||||
/**
|
||||
* Playback speed as a factor. 0 would be stopped, -1 reverse and 2 double speed.
|
||||
*/
|
||||
this.playbackSpeed = 1;
|
||||
/**
|
||||
* The state of the animation to apply when the animation is resolved. This
|
||||
* allows calls to the public API to control the animation before it is resolved,
|
||||
* without us having to resolve it first.
|
||||
*/
|
||||
this.pendingPlayState = "running";
|
||||
/**
|
||||
* The time at which the animation was started.
|
||||
*/
|
||||
this.startTime = null;
|
||||
this.state = "idle";
|
||||
/**
|
||||
* This method is bound to the instance to fix a pattern where
|
||||
* animation.stop is returned as a reference from a useEffect.
|
||||
*/
|
||||
this.stop = () => {
|
||||
this.resolver.cancel();
|
||||
this.isStopped = true;
|
||||
if (this.state === "idle")
|
||||
return;
|
||||
this.teardown();
|
||||
const { onStop } = this.options;
|
||||
onStop && onStop();
|
||||
};
|
||||
const { name, motionValue, element, keyframes } = this.options;
|
||||
const KeyframeResolver$1 = (element === null || element === void 0 ? void 0 : element.KeyframeResolver) || KeyframeResolver;
|
||||
const onResolved = (resolvedKeyframes, finalKeyframe) => this.onKeyframesResolved(resolvedKeyframes, finalKeyframe);
|
||||
this.resolver = new KeyframeResolver$1(keyframes, onResolved, name, motionValue, element);
|
||||
this.resolver.scheduleResolve();
|
||||
}
|
||||
initPlayback(keyframes$1) {
|
||||
const { type = "keyframes", repeat = 0, repeatDelay = 0, repeatType, velocity = 0, } = this.options;
|
||||
const generatorFactory = isGenerator(type)
|
||||
? type
|
||||
: generators[type] || keyframes;
|
||||
/**
|
||||
* If our generator doesn't support mixing numbers, we need to replace keyframes with
|
||||
* [0, 100] and then make a function that maps that to the actual keyframes.
|
||||
*
|
||||
* 100 is chosen instead of 1 as it works nicer with spring animations.
|
||||
*/
|
||||
let mapPercentToKeyframes;
|
||||
let mirroredGenerator;
|
||||
if (generatorFactory !== keyframes &&
|
||||
typeof keyframes$1[0] !== "number") {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
invariant(keyframes$1.length === 2, `Only two keyframes currently supported with spring and inertia animations. Trying to animate ${keyframes$1}`);
|
||||
}
|
||||
mapPercentToKeyframes = pipe(percentToProgress, mix(keyframes$1[0], keyframes$1[1]));
|
||||
keyframes$1 = [0, 100];
|
||||
}
|
||||
const generator = generatorFactory({ ...this.options, keyframes: keyframes$1 });
|
||||
/**
|
||||
* If we have a mirror repeat type we need to create a second generator that outputs the
|
||||
* mirrored (not reversed) animation and later ping pong between the two generators.
|
||||
*/
|
||||
if (repeatType === "mirror") {
|
||||
mirroredGenerator = generatorFactory({
|
||||
...this.options,
|
||||
keyframes: [...keyframes$1].reverse(),
|
||||
velocity: -velocity,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* If duration is undefined and we have repeat options,
|
||||
* we need to calculate a duration from the generator.
|
||||
*
|
||||
* We set it to the generator itself to cache the duration.
|
||||
* Any timeline resolver will need to have already precalculated
|
||||
* the duration by this step.
|
||||
*/
|
||||
if (generator.calculatedDuration === null) {
|
||||
generator.calculatedDuration = calcGeneratorDuration(generator);
|
||||
}
|
||||
const { calculatedDuration } = generator;
|
||||
const resolvedDuration = calculatedDuration + repeatDelay;
|
||||
const totalDuration = resolvedDuration * (repeat + 1) - repeatDelay;
|
||||
return {
|
||||
generator,
|
||||
mirroredGenerator,
|
||||
mapPercentToKeyframes,
|
||||
calculatedDuration,
|
||||
resolvedDuration,
|
||||
totalDuration,
|
||||
};
|
||||
}
|
||||
onPostResolved() {
|
||||
const { autoplay = true } = this.options;
|
||||
this.play();
|
||||
if (this.pendingPlayState === "paused" || !autoplay) {
|
||||
this.pause();
|
||||
}
|
||||
else {
|
||||
this.state = this.pendingPlayState;
|
||||
}
|
||||
}
|
||||
tick(timestamp, sample = false) {
|
||||
const { resolved } = this;
|
||||
// If the animations has failed to resolve, return the final keyframe.
|
||||
if (!resolved) {
|
||||
const { keyframes } = this.options;
|
||||
return { done: true, value: keyframes[keyframes.length - 1] };
|
||||
}
|
||||
const { finalKeyframe, generator, mirroredGenerator, mapPercentToKeyframes, keyframes, calculatedDuration, totalDuration, resolvedDuration, } = resolved;
|
||||
if (this.startTime === null)
|
||||
return generator.next(0);
|
||||
const { delay, repeat, repeatType, repeatDelay, onUpdate } = this.options;
|
||||
/**
|
||||
* requestAnimationFrame timestamps can come through as lower than
|
||||
* the startTime as set by performance.now(). Here we prevent this,
|
||||
* though in the future it could be possible to make setting startTime
|
||||
* a pending operation that gets resolved here.
|
||||
*/
|
||||
if (this.speed > 0) {
|
||||
this.startTime = Math.min(this.startTime, timestamp);
|
||||
}
|
||||
else if (this.speed < 0) {
|
||||
this.startTime = Math.min(timestamp - totalDuration / this.speed, this.startTime);
|
||||
}
|
||||
// Update currentTime
|
||||
if (sample) {
|
||||
this.currentTime = timestamp;
|
||||
}
|
||||
else if (this.holdTime !== null) {
|
||||
this.currentTime = this.holdTime;
|
||||
}
|
||||
else {
|
||||
// Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
|
||||
// 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
|
||||
// example.
|
||||
this.currentTime =
|
||||
Math.round(timestamp - this.startTime) * this.speed;
|
||||
}
|
||||
// Rebase on delay
|
||||
const timeWithoutDelay = this.currentTime - delay * (this.speed >= 0 ? 1 : -1);
|
||||
const isInDelayPhase = this.speed >= 0
|
||||
? timeWithoutDelay < 0
|
||||
: timeWithoutDelay > totalDuration;
|
||||
this.currentTime = Math.max(timeWithoutDelay, 0);
|
||||
// If this animation has finished, set the current time to the total duration.
|
||||
if (this.state === "finished" && this.holdTime === null) {
|
||||
this.currentTime = totalDuration;
|
||||
}
|
||||
let elapsed = this.currentTime;
|
||||
let frameGenerator = generator;
|
||||
if (repeat) {
|
||||
/**
|
||||
* Get the current progress (0-1) of the animation. If t is >
|
||||
* than duration we'll get values like 2.5 (midway through the
|
||||
* third iteration)
|
||||
*/
|
||||
const progress = Math.min(this.currentTime, totalDuration) / resolvedDuration;
|
||||
/**
|
||||
* Get the current iteration (0 indexed). For instance the floor of
|
||||
* 2.5 is 2.
|
||||
*/
|
||||
let currentIteration = Math.floor(progress);
|
||||
/**
|
||||
* Get the current progress of the iteration by taking the remainder
|
||||
* so 2.5 is 0.5 through iteration 2
|
||||
*/
|
||||
let iterationProgress = progress % 1.0;
|
||||
/**
|
||||
* If iteration progress is 1 we count that as the end
|
||||
* of the previous iteration.
|
||||
*/
|
||||
if (!iterationProgress && progress >= 1) {
|
||||
iterationProgress = 1;
|
||||
}
|
||||
iterationProgress === 1 && currentIteration--;
|
||||
currentIteration = Math.min(currentIteration, repeat + 1);
|
||||
/**
|
||||
* Reverse progress if we're not running in "normal" direction
|
||||
*/
|
||||
const isOddIteration = Boolean(currentIteration % 2);
|
||||
if (isOddIteration) {
|
||||
if (repeatType === "reverse") {
|
||||
iterationProgress = 1 - iterationProgress;
|
||||
if (repeatDelay) {
|
||||
iterationProgress -= repeatDelay / resolvedDuration;
|
||||
}
|
||||
}
|
||||
else if (repeatType === "mirror") {
|
||||
frameGenerator = mirroredGenerator;
|
||||
}
|
||||
}
|
||||
elapsed = clamp(0, 1, iterationProgress) * resolvedDuration;
|
||||
}
|
||||
/**
|
||||
* If we're in negative time, set state as the initial keyframe.
|
||||
* This prevents delay: x, duration: 0 animations from finishing
|
||||
* instantly.
|
||||
*/
|
||||
const state = isInDelayPhase
|
||||
? { done: false, value: keyframes[0] }
|
||||
: frameGenerator.next(elapsed);
|
||||
if (mapPercentToKeyframes) {
|
||||
state.value = mapPercentToKeyframes(state.value);
|
||||
}
|
||||
let { done } = state;
|
||||
if (!isInDelayPhase && calculatedDuration !== null) {
|
||||
done =
|
||||
this.speed >= 0
|
||||
? this.currentTime >= totalDuration
|
||||
: this.currentTime <= 0;
|
||||
}
|
||||
const isAnimationFinished = this.holdTime === null &&
|
||||
(this.state === "finished" || (this.state === "running" && done));
|
||||
if (isAnimationFinished && finalKeyframe !== undefined) {
|
||||
state.value = getFinalKeyframe(keyframes, this.options, finalKeyframe);
|
||||
}
|
||||
if (onUpdate) {
|
||||
onUpdate(state.value);
|
||||
}
|
||||
if (isAnimationFinished) {
|
||||
this.finish();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
get duration() {
|
||||
const { resolved } = this;
|
||||
return resolved ? millisecondsToSeconds(resolved.calculatedDuration) : 0;
|
||||
}
|
||||
get time() {
|
||||
return millisecondsToSeconds(this.currentTime);
|
||||
}
|
||||
set time(newTime) {
|
||||
newTime = secondsToMilliseconds(newTime);
|
||||
this.currentTime = newTime;
|
||||
if (this.holdTime !== null || this.speed === 0) {
|
||||
this.holdTime = newTime;
|
||||
}
|
||||
else if (this.driver) {
|
||||
this.startTime = this.driver.now() - newTime / this.speed;
|
||||
}
|
||||
}
|
||||
get speed() {
|
||||
return this.playbackSpeed;
|
||||
}
|
||||
set speed(newSpeed) {
|
||||
const hasChanged = this.playbackSpeed !== newSpeed;
|
||||
this.playbackSpeed = newSpeed;
|
||||
if (hasChanged) {
|
||||
this.time = millisecondsToSeconds(this.currentTime);
|
||||
}
|
||||
}
|
||||
play() {
|
||||
if (!this.resolver.isScheduled) {
|
||||
this.resolver.resume();
|
||||
}
|
||||
if (!this._resolved) {
|
||||
this.pendingPlayState = "running";
|
||||
return;
|
||||
}
|
||||
if (this.isStopped)
|
||||
return;
|
||||
const { driver = frameloopDriver, onPlay, startTime } = this.options;
|
||||
if (!this.driver) {
|
||||
this.driver = driver((timestamp) => this.tick(timestamp));
|
||||
}
|
||||
onPlay && onPlay();
|
||||
const now = this.driver.now();
|
||||
if (this.holdTime !== null) {
|
||||
this.startTime = now - this.holdTime;
|
||||
}
|
||||
else if (!this.startTime) {
|
||||
this.startTime = startTime !== null && startTime !== void 0 ? startTime : this.calcStartTime();
|
||||
}
|
||||
else if (this.state === "finished") {
|
||||
this.startTime = now;
|
||||
}
|
||||
if (this.state === "finished") {
|
||||
this.updateFinishedPromise();
|
||||
}
|
||||
this.cancelTime = this.startTime;
|
||||
this.holdTime = null;
|
||||
/**
|
||||
* Set playState to running only after we've used it in
|
||||
* the previous logic.
|
||||
*/
|
||||
this.state = "running";
|
||||
this.driver.start();
|
||||
}
|
||||
pause() {
|
||||
var _a;
|
||||
if (!this._resolved) {
|
||||
this.pendingPlayState = "paused";
|
||||
return;
|
||||
}
|
||||
this.state = "paused";
|
||||
this.holdTime = (_a = this.currentTime) !== null && _a !== void 0 ? _a : 0;
|
||||
}
|
||||
complete() {
|
||||
if (this.state !== "running") {
|
||||
this.play();
|
||||
}
|
||||
this.pendingPlayState = this.state = "finished";
|
||||
this.holdTime = null;
|
||||
}
|
||||
finish() {
|
||||
this.teardown();
|
||||
this.state = "finished";
|
||||
const { onComplete } = this.options;
|
||||
onComplete && onComplete();
|
||||
}
|
||||
cancel() {
|
||||
if (this.cancelTime !== null) {
|
||||
this.tick(this.cancelTime);
|
||||
}
|
||||
this.teardown();
|
||||
this.updateFinishedPromise();
|
||||
}
|
||||
teardown() {
|
||||
this.state = "idle";
|
||||
this.stopDriver();
|
||||
this.resolveFinishedPromise();
|
||||
this.updateFinishedPromise();
|
||||
this.startTime = this.cancelTime = null;
|
||||
this.resolver.cancel();
|
||||
}
|
||||
stopDriver() {
|
||||
if (!this.driver)
|
||||
return;
|
||||
this.driver.stop();
|
||||
this.driver = undefined;
|
||||
}
|
||||
sample(time) {
|
||||
this.startTime = 0;
|
||||
return this.tick(time, true);
|
||||
}
|
||||
}
|
||||
// Legacy interface
|
||||
function animateValue(options) {
|
||||
return new MainThreadAnimation(options);
|
||||
}
|
||||
|
||||
export { MainThreadAnimation, animateValue };
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import { time } from '../../../frameloop/sync-time.mjs';
|
||||
import { frame, cancelFrame, frameData } from '../../../frameloop/frame.mjs';
|
||||
|
||||
const frameloopDriver = (update) => {
|
||||
const passTimestamp = ({ timestamp }) => update(timestamp);
|
||||
return {
|
||||
start: () => frame.update(passTimestamp, true),
|
||||
stop: () => cancelFrame(passTimestamp),
|
||||
/**
|
||||
* If we're processing this frame we can use the
|
||||
* framelocked timestamp to keep things in sync.
|
||||
*/
|
||||
now: () => (frameData.isProcessing ? frameData.timestamp : time.now()),
|
||||
};
|
||||
};
|
||||
|
||||
export { frameloopDriver };
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* A list of values that can be hardware-accelerated.
|
||||
*/
|
||||
const acceleratedValues = new Set([
|
||||
"opacity",
|
||||
"clipPath",
|
||||
"filter",
|
||||
"transform",
|
||||
// TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
|
||||
// or until we implement support for linear() easing.
|
||||
// "background-color"
|
||||
]);
|
||||
|
||||
export { acceleratedValues };
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { warning } from '../../../utils/errors.mjs';
|
||||
import { isGenerator } from '../../generators/utils/is-generator.mjs';
|
||||
import { isAnimatable } from '../../utils/is-animatable.mjs';
|
||||
|
||||
function hasKeyframesChanged(keyframes) {
|
||||
const current = keyframes[0];
|
||||
if (keyframes.length === 1)
|
||||
return true;
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
if (keyframes[i] !== current)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function canAnimate(keyframes, name, type, velocity) {
|
||||
/**
|
||||
* Check if we're able to animate between the start and end keyframes,
|
||||
* and throw a warning if we're attempting to animate between one that's
|
||||
* animatable and another that isn't.
|
||||
*/
|
||||
const originKeyframe = keyframes[0];
|
||||
if (originKeyframe === null)
|
||||
return false;
|
||||
/**
|
||||
* These aren't traditionally animatable but we do support them.
|
||||
* In future we could look into making this more generic or replacing
|
||||
* this function with mix() === mixImmediate
|
||||
*/
|
||||
if (name === "display" || name === "visibility")
|
||||
return true;
|
||||
const targetKeyframe = keyframes[keyframes.length - 1];
|
||||
const isOriginAnimatable = isAnimatable(originKeyframe, name);
|
||||
const isTargetAnimatable = isAnimatable(targetKeyframe, name);
|
||||
warning(isOriginAnimatable === isTargetAnimatable, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". ${originKeyframe} is not an animatable value - to enable this animation set ${originKeyframe} to a value animatable to ${targetKeyframe} via the \`style\` property.`);
|
||||
// Always skip if any of these are true
|
||||
if (!isOriginAnimatable || !isTargetAnimatable) {
|
||||
return false;
|
||||
}
|
||||
return (hasKeyframesChanged(keyframes) ||
|
||||
((type === "spring" || isGenerator(type)) && velocity));
|
||||
}
|
||||
|
||||
export { canAnimate };
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import { startWaapiAnimation } from './index.mjs';
|
||||
import { createGeneratorEasing } from '../../../easing/utils/create-generator-easing.mjs';
|
||||
import { browserNumberValueTypes } from '../../../render/dom/value-types/number-browser.mjs';
|
||||
import { invariant } from '../../../utils/errors.mjs';
|
||||
import { noop } from '../../../utils/noop.mjs';
|
||||
import { secondsToMilliseconds, millisecondsToSeconds } from '../../../utils/time-conversion.mjs';
|
||||
import { isGenerator } from '../../generators/utils/is-generator.mjs';
|
||||
import { attachTimeline } from './utils/attach-timeline.mjs';
|
||||
import { getFinalKeyframe } from './utils/get-final-keyframe.mjs';
|
||||
import { setCSSVar, setStyle } from './utils/style.mjs';
|
||||
import { supportsLinearEasing } from './utils/supports-linear-easing.mjs';
|
||||
import { supportsPartialKeyframes } from './utils/supports-partial-keyframes.mjs';
|
||||
import { supportsWaapi } from './utils/supports-waapi.mjs';
|
||||
|
||||
const state = new WeakMap();
|
||||
function hydrateKeyframes(valueName, keyframes, read) {
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
if (keyframes[i] === null) {
|
||||
keyframes[i] = i === 0 ? read() : keyframes[i - 1];
|
||||
}
|
||||
if (typeof keyframes[i] === "number" &&
|
||||
browserNumberValueTypes[valueName]) {
|
||||
keyframes[i] = browserNumberValueTypes[valueName].transform(keyframes[i]);
|
||||
}
|
||||
}
|
||||
if (!supportsPartialKeyframes() && keyframes.length < 2) {
|
||||
keyframes.unshift(read());
|
||||
}
|
||||
}
|
||||
const defaultEasing = "easeOut";
|
||||
function getElementAnimationState(element) {
|
||||
const animationState = state.get(element) || new Map();
|
||||
state.set(element, animationState);
|
||||
return state.get(element);
|
||||
}
|
||||
class NativeAnimation {
|
||||
constructor(element, valueName, valueKeyframes, options) {
|
||||
const isCSSVar = valueName.startsWith("--");
|
||||
this.setValue = isCSSVar ? setCSSVar : setStyle;
|
||||
this.options = options;
|
||||
this.updateFinishedPromise();
|
||||
invariant(typeof options.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "framer-motion"?`);
|
||||
const existingAnimation = getElementAnimationState(element).get(valueName);
|
||||
existingAnimation && existingAnimation.stop();
|
||||
const readInitialKeyframe = () => {
|
||||
return valueName.startsWith("--")
|
||||
? element.style.getPropertyValue(valueName)
|
||||
: window.getComputedStyle(element)[valueName];
|
||||
};
|
||||
if (!Array.isArray(valueKeyframes)) {
|
||||
valueKeyframes = [valueKeyframes];
|
||||
}
|
||||
hydrateKeyframes(valueName, valueKeyframes, readInitialKeyframe);
|
||||
if (isGenerator(options.type)) {
|
||||
const generatorOptions = createGeneratorEasing(options, 100, options.type);
|
||||
options.ease = supportsLinearEasing()
|
||||
? generatorOptions.ease
|
||||
: defaultEasing;
|
||||
options.duration = secondsToMilliseconds(generatorOptions.duration);
|
||||
options.type = "keyframes";
|
||||
}
|
||||
else {
|
||||
options.ease = options.ease || defaultEasing;
|
||||
}
|
||||
this.removeAnimation = () => { var _a; return (_a = state.get(element)) === null || _a === void 0 ? void 0 : _a.delete(valueName); };
|
||||
const onFinish = () => {
|
||||
this.setValue(element, valueName, getFinalKeyframe(valueKeyframes, this.options));
|
||||
this.cancel();
|
||||
this.resolveFinishedPromise();
|
||||
};
|
||||
if (!supportsWaapi()) {
|
||||
onFinish();
|
||||
}
|
||||
else {
|
||||
this.animation = startWaapiAnimation(element, valueName, valueKeyframes, options);
|
||||
if (options.autoplay === false) {
|
||||
this.animation.pause();
|
||||
}
|
||||
this.animation.onfinish = onFinish;
|
||||
if (this.pendingTimeline) {
|
||||
attachTimeline(this.animation, this.pendingTimeline);
|
||||
}
|
||||
getElementAnimationState(element).set(valueName, this);
|
||||
}
|
||||
}
|
||||
get duration() {
|
||||
return millisecondsToSeconds(this.options.duration || 300);
|
||||
}
|
||||
get time() {
|
||||
var _a;
|
||||
if (this.animation) {
|
||||
return millisecondsToSeconds(((_a = this.animation) === null || _a === void 0 ? void 0 : _a.currentTime) || 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
set time(newTime) {
|
||||
if (this.animation) {
|
||||
this.animation.currentTime = secondsToMilliseconds(newTime);
|
||||
}
|
||||
}
|
||||
get speed() {
|
||||
return this.animation ? this.animation.playbackRate : 1;
|
||||
}
|
||||
set speed(newSpeed) {
|
||||
if (this.animation) {
|
||||
this.animation.playbackRate = newSpeed;
|
||||
}
|
||||
}
|
||||
get state() {
|
||||
return this.animation ? this.animation.playState : "finished";
|
||||
}
|
||||
get startTime() {
|
||||
return this.animation ? this.animation.startTime : null;
|
||||
}
|
||||
play() {
|
||||
if (this.state === "finished") {
|
||||
this.updateFinishedPromise();
|
||||
}
|
||||
this.animation && this.animation.play();
|
||||
}
|
||||
pause() {
|
||||
this.animation && this.animation.pause();
|
||||
}
|
||||
stop() {
|
||||
if (!this.animation ||
|
||||
this.state === "idle" ||
|
||||
this.state === "finished") {
|
||||
return;
|
||||
}
|
||||
if (this.animation.commitStyles) {
|
||||
this.animation.commitStyles();
|
||||
}
|
||||
this.cancel();
|
||||
}
|
||||
complete() {
|
||||
this.animation && this.animation.finish();
|
||||
}
|
||||
cancel() {
|
||||
this.removeAnimation();
|
||||
try {
|
||||
this.animation && this.animation.cancel();
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
/**
|
||||
* Allows the returned animation to be awaited or promise-chained. Currently
|
||||
* resolves when the animation finishes at all but in a future update could/should
|
||||
* reject if its cancels.
|
||||
*/
|
||||
then(resolve, reject) {
|
||||
return this.currentFinishedPromise.then(resolve, reject);
|
||||
}
|
||||
updateFinishedPromise() {
|
||||
this.currentFinishedPromise = new Promise((resolve) => {
|
||||
this.resolveFinishedPromise = resolve;
|
||||
});
|
||||
}
|
||||
attachTimeline(timeline) {
|
||||
if (!this.animation) {
|
||||
this.pendingTimeline = timeline;
|
||||
}
|
||||
else {
|
||||
attachTimeline(this.animation, timeline);
|
||||
}
|
||||
return noop;
|
||||
}
|
||||
}
|
||||
|
||||
export { NativeAnimation };
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { resolveElements } from '../../../render/dom/utils/resolve-element.mjs';
|
||||
import { invariant } from '../../../utils/errors.mjs';
|
||||
import { secondsToMilliseconds } from '../../../utils/time-conversion.mjs';
|
||||
import { getValueTransition } from '../../utils/get-value-transition.mjs';
|
||||
import { NativeAnimation } from './NativeAnimation.mjs';
|
||||
|
||||
function animateElements(elementOrSelector, keyframes, options, scope) {
|
||||
const elements = resolveElements(elementOrSelector, scope);
|
||||
const numElements = elements.length;
|
||||
invariant(Boolean(numElements), "No valid element provided.");
|
||||
const animations = [];
|
||||
for (let i = 0; i < numElements; i++) {
|
||||
const element = elements[i];
|
||||
const elementTransition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if (typeof elementTransition.delay === "function") {
|
||||
elementTransition.delay = elementTransition.delay(i, numElements);
|
||||
}
|
||||
for (const valueName in keyframes) {
|
||||
const valueKeyframes = keyframes[valueName];
|
||||
const valueOptions = {
|
||||
...getValueTransition(options, valueName),
|
||||
};
|
||||
valueOptions.duration = valueOptions.duration
|
||||
? secondsToMilliseconds(valueOptions.duration)
|
||||
: valueOptions.duration;
|
||||
valueOptions.delay = secondsToMilliseconds(valueOptions.delay || 0);
|
||||
animations.push(new NativeAnimation(element, valueName, valueKeyframes, valueOptions));
|
||||
}
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateElements };
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { GroupPlaybackControls } from '../../GroupPlaybackControls.mjs';
|
||||
import { animateElements } from './animate-elements.mjs';
|
||||
|
||||
const createScopedWaapiAnimate = (scope) => {
|
||||
function scopedAnimate(elementOrSelector, keyframes, options) {
|
||||
return new GroupPlaybackControls(animateElements(elementOrSelector, keyframes, options, scope));
|
||||
}
|
||||
return scopedAnimate;
|
||||
};
|
||||
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
|
||||
|
||||
export { animateMini, createScopedWaapiAnimate };
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { isBezierDefinition } from '../../../easing/utils/is-bezier-definition.mjs';
|
||||
import { generateLinearEasing } from './utils/linear.mjs';
|
||||
import { supportsLinearEasing } from './utils/supports-linear-easing.mjs';
|
||||
|
||||
function isWaapiSupportedEasing(easing) {
|
||||
return Boolean((typeof easing === "function" && supportsLinearEasing()) ||
|
||||
!easing ||
|
||||
(typeof easing === "string" &&
|
||||
(easing in supportedWaapiEasing || supportsLinearEasing())) ||
|
||||
isBezierDefinition(easing) ||
|
||||
(Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
|
||||
}
|
||||
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
|
||||
const supportedWaapiEasing = {
|
||||
linear: "linear",
|
||||
ease: "ease",
|
||||
easeIn: "ease-in",
|
||||
easeOut: "ease-out",
|
||||
easeInOut: "ease-in-out",
|
||||
circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
|
||||
circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
|
||||
backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
|
||||
backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
|
||||
};
|
||||
function mapEasingToNativeEasing(easing, duration) {
|
||||
if (!easing) {
|
||||
return undefined;
|
||||
}
|
||||
else if (typeof easing === "function" && supportsLinearEasing()) {
|
||||
return generateLinearEasing(easing, duration);
|
||||
}
|
||||
else if (isBezierDefinition(easing)) {
|
||||
return cubicBezierAsString(easing);
|
||||
}
|
||||
else if (Array.isArray(easing)) {
|
||||
return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
|
||||
supportedWaapiEasing.easeOut);
|
||||
}
|
||||
else {
|
||||
return supportedWaapiEasing[easing];
|
||||
}
|
||||
}
|
||||
|
||||
export { cubicBezierAsString, isWaapiSupportedEasing, mapEasingToNativeEasing, supportedWaapiEasing };
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { mapEasingToNativeEasing } from './easing.mjs';
|
||||
|
||||
function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease, times, } = {}) {
|
||||
const keyframeOptions = { [valueName]: keyframes };
|
||||
if (times)
|
||||
keyframeOptions.offset = times;
|
||||
const easing = mapEasingToNativeEasing(ease, duration);
|
||||
/**
|
||||
* If this is an easing array, apply to keyframes, not animation as a whole
|
||||
*/
|
||||
if (Array.isArray(easing))
|
||||
keyframeOptions.easing = easing;
|
||||
return element.animate(keyframeOptions, {
|
||||
delay,
|
||||
duration,
|
||||
easing: !Array.isArray(easing) ? easing : "linear",
|
||||
fill: "both",
|
||||
iterations: repeat + 1,
|
||||
direction: repeatType === "reverse" ? "alternate" : "normal",
|
||||
});
|
||||
}
|
||||
|
||||
export { startWaapiAnimation };
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
function attachTimeline(animation, timeline) {
|
||||
animation.timeline = timeline;
|
||||
animation.onfinish = null;
|
||||
}
|
||||
|
||||
export { attachTimeline };
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
const isNotNull = (value) => value !== null;
|
||||
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
|
||||
const resolvedKeyframes = keyframes.filter(isNotNull);
|
||||
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
|
||||
? 0
|
||||
: resolvedKeyframes.length - 1;
|
||||
return !index || finalKeyframe === undefined
|
||||
? resolvedKeyframes[index]
|
||||
: finalKeyframe;
|
||||
}
|
||||
|
||||
export { getFinalKeyframe };
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { progress } from '../../../../utils/progress.mjs';
|
||||
|
||||
// Create a linear easing point for every 10 ms
|
||||
const resolution = 10;
|
||||
const generateLinearEasing = (easing, duration // as milliseconds
|
||||
) => {
|
||||
let points = "";
|
||||
const numPoints = Math.max(Math.round(duration / resolution), 2);
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
points += easing(progress(0, numPoints - 1, i)) + ", ";
|
||||
}
|
||||
return `linear(${points.substring(0, points.length - 2)})`;
|
||||
};
|
||||
|
||||
export { generateLinearEasing };
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { memo } from '../../../../utils/memo.mjs';
|
||||
import { supportsFlags } from './supports-flags.mjs';
|
||||
|
||||
function memoSupports(callback, supportsFlag) {
|
||||
const memoized = memo(callback);
|
||||
return () => { var _a; return (_a = supportsFlags[supportsFlag]) !== null && _a !== void 0 ? _a : memoized(); };
|
||||
}
|
||||
|
||||
export { memoSupports };
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
function setCSSVar(element, name, value) {
|
||||
element.style.setProperty(`--${name}`, value);
|
||||
}
|
||||
function setStyle(element, name, value) {
|
||||
element.style[name] = value;
|
||||
}
|
||||
|
||||
export { setCSSVar, setStyle };
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Add the ability for test suites to manually set support flags
|
||||
* to better test more environments.
|
||||
*/
|
||||
const supportsFlags = {
|
||||
linearEasing: undefined,
|
||||
};
|
||||
|
||||
export { supportsFlags };
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import { memoSupports } from './memo-supports.mjs';
|
||||
|
||||
const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
|
||||
try {
|
||||
document
|
||||
.createElement("div")
|
||||
.animate({ opacity: 0 }, { easing: "linear(0, 1)" });
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, "linearEasing");
|
||||
|
||||
export { supportsLinearEasing };
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { memo } from '../../../../utils/memo.mjs';
|
||||
|
||||
const supportsPartialKeyframes = /*@__PURE__*/ memo(() => {
|
||||
try {
|
||||
document.createElement("div").animate({ opacity: [1] });
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export { supportsPartialKeyframes };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { memo } from '../../../../utils/memo.mjs';
|
||||
|
||||
const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
|
||||
|
||||
export { supportsWaapi };
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { spring } from './spring/index.mjs';
|
||||
import { calcGeneratorVelocity } from './utils/velocity.mjs';
|
||||
|
||||
function inertia({ keyframes, velocity = 0.0, power = 0.8, timeConstant = 325, bounceDamping = 10, bounceStiffness = 500, modifyTarget, min, max, restDelta = 0.5, restSpeed, }) {
|
||||
const origin = keyframes[0];
|
||||
const state = {
|
||||
done: false,
|
||||
value: origin,
|
||||
};
|
||||
const isOutOfBounds = (v) => (min !== undefined && v < min) || (max !== undefined && v > max);
|
||||
const nearestBoundary = (v) => {
|
||||
if (min === undefined)
|
||||
return max;
|
||||
if (max === undefined)
|
||||
return min;
|
||||
return Math.abs(min - v) < Math.abs(max - v) ? min : max;
|
||||
};
|
||||
let amplitude = power * velocity;
|
||||
const ideal = origin + amplitude;
|
||||
const target = modifyTarget === undefined ? ideal : modifyTarget(ideal);
|
||||
/**
|
||||
* If the target has changed we need to re-calculate the amplitude, otherwise
|
||||
* the animation will start from the wrong position.
|
||||
*/
|
||||
if (target !== ideal)
|
||||
amplitude = target - origin;
|
||||
const calcDelta = (t) => -amplitude * Math.exp(-t / timeConstant);
|
||||
const calcLatest = (t) => target + calcDelta(t);
|
||||
const applyFriction = (t) => {
|
||||
const delta = calcDelta(t);
|
||||
const latest = calcLatest(t);
|
||||
state.done = Math.abs(delta) <= restDelta;
|
||||
state.value = state.done ? target : latest;
|
||||
};
|
||||
/**
|
||||
* Ideally this would resolve for t in a stateless way, we could
|
||||
* do that by always precalculating the animation but as we know
|
||||
* this will be done anyway we can assume that spring will
|
||||
* be discovered during that.
|
||||
*/
|
||||
let timeReachedBoundary;
|
||||
let spring$1;
|
||||
const checkCatchBoundary = (t) => {
|
||||
if (!isOutOfBounds(state.value))
|
||||
return;
|
||||
timeReachedBoundary = t;
|
||||
spring$1 = spring({
|
||||
keyframes: [state.value, nearestBoundary(state.value)],
|
||||
velocity: calcGeneratorVelocity(calcLatest, t, state.value), // TODO: This should be passing * 1000
|
||||
damping: bounceDamping,
|
||||
stiffness: bounceStiffness,
|
||||
restDelta,
|
||||
restSpeed,
|
||||
});
|
||||
};
|
||||
checkCatchBoundary(0);
|
||||
return {
|
||||
calculatedDuration: null,
|
||||
next: (t) => {
|
||||
/**
|
||||
* We need to resolve the friction to figure out if we need a
|
||||
* spring but we don't want to do this twice per frame. So here
|
||||
* we flag if we updated for this frame and later if we did
|
||||
* we can skip doing it again.
|
||||
*/
|
||||
let hasUpdatedFrame = false;
|
||||
if (!spring$1 && timeReachedBoundary === undefined) {
|
||||
hasUpdatedFrame = true;
|
||||
applyFriction(t);
|
||||
checkCatchBoundary(t);
|
||||
}
|
||||
/**
|
||||
* If we have a spring and the provided t is beyond the moment the friction
|
||||
* animation crossed the min/max boundary, use the spring.
|
||||
*/
|
||||
if (timeReachedBoundary !== undefined && t >= timeReachedBoundary) {
|
||||
return spring$1.next(t - timeReachedBoundary);
|
||||
}
|
||||
else {
|
||||
!hasUpdatedFrame && applyFriction(t);
|
||||
return state;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { inertia };
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { easeInOut } from '../../easing/ease.mjs';
|
||||
import { isEasingArray } from '../../easing/utils/is-easing-array.mjs';
|
||||
import { easingDefinitionToFunction } from '../../easing/utils/map.mjs';
|
||||
import { interpolate } from '../../utils/interpolate.mjs';
|
||||
import { defaultOffset } from '../../utils/offsets/default.mjs';
|
||||
import { convertOffsetToTimes } from '../../utils/offsets/time.mjs';
|
||||
|
||||
function defaultEasing(values, easing) {
|
||||
return values.map(() => easing || easeInOut).splice(0, values.length - 1);
|
||||
}
|
||||
function keyframes({ duration = 300, keyframes: keyframeValues, times, ease = "easeInOut", }) {
|
||||
/**
|
||||
* Easing functions can be externally defined as strings. Here we convert them
|
||||
* into actual functions.
|
||||
*/
|
||||
const easingFunctions = isEasingArray(ease)
|
||||
? ease.map(easingDefinitionToFunction)
|
||||
: easingDefinitionToFunction(ease);
|
||||
/**
|
||||
* 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: keyframeValues[0],
|
||||
};
|
||||
/**
|
||||
* Create a times array based on the provided 0-1 offsets
|
||||
*/
|
||||
const absoluteTimes = convertOffsetToTimes(
|
||||
// Only use the provided offsets if they're the correct length
|
||||
// TODO Maybe we should warn here if there's a length mismatch
|
||||
times && times.length === keyframeValues.length
|
||||
? times
|
||||
: defaultOffset(keyframeValues), duration);
|
||||
const mapTimeToKeyframe = interpolate(absoluteTimes, keyframeValues, {
|
||||
ease: Array.isArray(easingFunctions)
|
||||
? easingFunctions
|
||||
: defaultEasing(keyframeValues, easingFunctions),
|
||||
});
|
||||
return {
|
||||
calculatedDuration: duration,
|
||||
next: (t) => {
|
||||
state.value = mapTimeToKeyframe(t);
|
||||
state.done = t >= duration;
|
||||
return state;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { defaultEasing, keyframes };
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { warning } from '../../../utils/errors.mjs';
|
||||
import { clamp } from '../../../utils/clamp.mjs';
|
||||
import { secondsToMilliseconds, millisecondsToSeconds } from '../../../utils/time-conversion.mjs';
|
||||
|
||||
const safeMin = 0.001;
|
||||
const minDuration = 0.01;
|
||||
const maxDuration = 10.0;
|
||||
const minDamping = 0.05;
|
||||
const maxDamping = 1;
|
||||
function findSpring({ duration = 800, bounce = 0.25, velocity = 0, mass = 1, }) {
|
||||
let envelope;
|
||||
let derivative;
|
||||
warning(duration <= secondsToMilliseconds(maxDuration), "Spring duration must be 10 seconds or less");
|
||||
let dampingRatio = 1 - bounce;
|
||||
/**
|
||||
* Restrict dampingRatio and duration to within acceptable ranges.
|
||||
*/
|
||||
dampingRatio = clamp(minDamping, maxDamping, dampingRatio);
|
||||
duration = clamp(minDuration, maxDuration, millisecondsToSeconds(duration));
|
||||
if (dampingRatio < 1) {
|
||||
/**
|
||||
* Underdamped spring
|
||||
*/
|
||||
envelope = (undampedFreq) => {
|
||||
const exponentialDecay = undampedFreq * dampingRatio;
|
||||
const delta = exponentialDecay * duration;
|
||||
const a = exponentialDecay - velocity;
|
||||
const b = calcAngularFreq(undampedFreq, dampingRatio);
|
||||
const c = Math.exp(-delta);
|
||||
return safeMin - (a / b) * c;
|
||||
};
|
||||
derivative = (undampedFreq) => {
|
||||
const exponentialDecay = undampedFreq * dampingRatio;
|
||||
const delta = exponentialDecay * duration;
|
||||
const d = delta * velocity + velocity;
|
||||
const e = Math.pow(dampingRatio, 2) * Math.pow(undampedFreq, 2) * duration;
|
||||
const f = Math.exp(-delta);
|
||||
const g = calcAngularFreq(Math.pow(undampedFreq, 2), dampingRatio);
|
||||
const factor = -envelope(undampedFreq) + safeMin > 0 ? -1 : 1;
|
||||
return (factor * ((d - e) * f)) / g;
|
||||
};
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* Critically-damped spring
|
||||
*/
|
||||
envelope = (undampedFreq) => {
|
||||
const a = Math.exp(-undampedFreq * duration);
|
||||
const b = (undampedFreq - velocity) * duration + 1;
|
||||
return -safeMin + a * b;
|
||||
};
|
||||
derivative = (undampedFreq) => {
|
||||
const a = Math.exp(-undampedFreq * duration);
|
||||
const b = (velocity - undampedFreq) * (duration * duration);
|
||||
return a * b;
|
||||
};
|
||||
}
|
||||
const initialGuess = 5 / duration;
|
||||
const undampedFreq = approximateRoot(envelope, derivative, initialGuess);
|
||||
duration = secondsToMilliseconds(duration);
|
||||
if (isNaN(undampedFreq)) {
|
||||
return {
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
else {
|
||||
const stiffness = Math.pow(undampedFreq, 2) * mass;
|
||||
return {
|
||||
stiffness,
|
||||
damping: dampingRatio * 2 * Math.sqrt(mass * stiffness),
|
||||
duration,
|
||||
};
|
||||
}
|
||||
}
|
||||
const rootIterations = 12;
|
||||
function approximateRoot(envelope, derivative, initialGuess) {
|
||||
let result = initialGuess;
|
||||
for (let i = 1; i < rootIterations; i++) {
|
||||
result = result - envelope(result) / derivative(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function calcAngularFreq(undampedFreq, dampingRatio) {
|
||||
return undampedFreq * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
}
|
||||
|
||||
export { calcAngularFreq, findSpring, maxDamping, maxDuration, minDamping, minDuration };
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { millisecondsToSeconds, secondsToMilliseconds } from '../../../utils/time-conversion.mjs';
|
||||
import { calcGeneratorVelocity } from '../utils/velocity.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: 0.0,
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
mass: 1.0,
|
||||
isResolvedFromDuration: false,
|
||||
...options,
|
||||
};
|
||||
// stiffness/damping/mass overrides duration/bounce
|
||||
if (!isSpringType(options, physicsKeys) &&
|
||||
isSpringType(options, durationKeys)) {
|
||||
const derived = findSpring(options);
|
||||
springOptions = {
|
||||
...springOptions,
|
||||
...derived,
|
||||
mass: 1.0,
|
||||
};
|
||||
springOptions.isResolvedFromDuration = true;
|
||||
}
|
||||
return springOptions;
|
||||
}
|
||||
function spring({ keyframes, restDelta, restSpeed, ...options }) {
|
||||
const origin = keyframes[0];
|
||||
const target = keyframes[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 ? 0.01 : 2);
|
||||
restDelta || (restDelta = isGranularScale ? 0.005 : 0.5);
|
||||
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);
|
||||
};
|
||||
}
|
||||
return {
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { spring };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Implement a practical max duration for keyframe generation
|
||||
* to prevent infinite loops
|
||||
*/
|
||||
const maxGeneratorDuration = 20000;
|
||||
function calcGeneratorDuration(generator) {
|
||||
let duration = 0;
|
||||
const timeStep = 50;
|
||||
let state = generator.next(duration);
|
||||
while (!state.done && duration < maxGeneratorDuration) {
|
||||
duration += timeStep;
|
||||
state = generator.next(duration);
|
||||
}
|
||||
return duration >= maxGeneratorDuration ? Infinity : duration;
|
||||
}
|
||||
|
||||
export { calcGeneratorDuration, maxGeneratorDuration };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
function isGenerator(type) {
|
||||
return typeof type === "function";
|
||||
}
|
||||
|
||||
export { isGenerator };
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { velocityPerSecond } from '../../../utils/velocity-per-second.mjs';
|
||||
|
||||
const velocitySampleDuration = 5; // ms
|
||||
function calcGeneratorVelocity(resolveValue, t, current) {
|
||||
const prevT = Math.max(t - velocitySampleDuration, 0);
|
||||
return velocityPerSecond(current - resolveValue(prevT), t - prevT);
|
||||
}
|
||||
|
||||
export { calcGeneratorVelocity };
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { invariant } from '../../utils/errors.mjs';
|
||||
import { setTarget } from '../../render/utils/setters.mjs';
|
||||
import { animateVisualElement } from '../interfaces/visual-element.mjs';
|
||||
|
||||
function stopAnimation(visualElement) {
|
||||
visualElement.values.forEach((value) => value.stop());
|
||||
}
|
||||
function setVariants(visualElement, variantLabels) {
|
||||
const reversedLabels = [...variantLabels].reverse();
|
||||
reversedLabels.forEach((key) => {
|
||||
const variant = visualElement.getVariant(key);
|
||||
variant && setTarget(visualElement, variant);
|
||||
if (visualElement.variantChildren) {
|
||||
visualElement.variantChildren.forEach((child) => {
|
||||
setVariants(child, variantLabels);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function setValues(visualElement, definition) {
|
||||
if (Array.isArray(definition)) {
|
||||
return setVariants(visualElement, definition);
|
||||
}
|
||||
else if (typeof definition === "string") {
|
||||
return setVariants(visualElement, [definition]);
|
||||
}
|
||||
else {
|
||||
setTarget(visualElement, definition);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
function animationControls() {
|
||||
/**
|
||||
* Track whether the host component has mounted.
|
||||
*/
|
||||
let hasMounted = false;
|
||||
/**
|
||||
* A collection of linked component animation controls.
|
||||
*/
|
||||
const subscribers = new Set();
|
||||
const controls = {
|
||||
subscribe(visualElement) {
|
||||
subscribers.add(visualElement);
|
||||
return () => void subscribers.delete(visualElement);
|
||||
},
|
||||
start(definition, transitionOverride) {
|
||||
invariant(hasMounted, "controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook.");
|
||||
const animations = [];
|
||||
subscribers.forEach((visualElement) => {
|
||||
animations.push(animateVisualElement(visualElement, definition, {
|
||||
transitionOverride,
|
||||
}));
|
||||
});
|
||||
return Promise.all(animations);
|
||||
},
|
||||
set(definition) {
|
||||
invariant(hasMounted, "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.");
|
||||
return subscribers.forEach((visualElement) => {
|
||||
setValues(visualElement, definition);
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
subscribers.forEach((visualElement) => {
|
||||
stopAnimation(visualElement);
|
||||
});
|
||||
},
|
||||
mount() {
|
||||
hasMounted = true;
|
||||
return () => {
|
||||
hasMounted = false;
|
||||
controls.stop();
|
||||
};
|
||||
},
|
||||
};
|
||||
return controls;
|
||||
}
|
||||
|
||||
export { animationControls, setValues };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
|
||||
import { createScopedWaapiAnimate } from '../animators/waapi/animate-style.mjs';
|
||||
|
||||
function useAnimateMini() {
|
||||
const scope = useConstant(() => ({
|
||||
current: null, // Will be hydrated by React
|
||||
animations: [],
|
||||
}));
|
||||
const animate = useConstant(() => createScopedWaapiAnimate(scope));
|
||||
useUnmountEffect(() => {
|
||||
scope.animations.forEach((animation) => animation.stop());
|
||||
});
|
||||
return [scope, animate];
|
||||
}
|
||||
|
||||
export { useAnimateMini };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
|
||||
import { createScopedAnimate } from '../animate/index.mjs';
|
||||
|
||||
function useAnimate() {
|
||||
const scope = useConstant(() => ({
|
||||
current: null, // Will be hydrated by React
|
||||
animations: [],
|
||||
}));
|
||||
const animate = useConstant(() => createScopedAnimate(scope));
|
||||
useUnmountEffect(() => {
|
||||
scope.animations.forEach((animation) => animation.stop());
|
||||
});
|
||||
return [scope, animate];
|
||||
}
|
||||
|
||||
export { useAnimate };
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { useState, useLayoutEffect } from 'react';
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { makeUseVisualState } from '../../motion/utils/use-visual-state.mjs';
|
||||
import { createBox } from '../../projection/geometry/models.mjs';
|
||||
import { VisualElement } from '../../render/VisualElement.mjs';
|
||||
import { animateVisualElement } from '../interfaces/visual-element.mjs';
|
||||
|
||||
const createObject = () => ({});
|
||||
class StateVisualElement extends VisualElement {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.measureInstanceViewportBox = createBox;
|
||||
}
|
||||
build() { }
|
||||
resetTransform() { }
|
||||
restoreTransform() { }
|
||||
removeValueFromRenderState() { }
|
||||
renderInstance() { }
|
||||
scrapeMotionValuesFromProps() {
|
||||
return createObject();
|
||||
}
|
||||
getBaseTargetFromProps() {
|
||||
return undefined;
|
||||
}
|
||||
readValueFromInstance(_state, key, options) {
|
||||
return options.initialState[key] || 0;
|
||||
}
|
||||
sortInstanceNodePosition() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
const useVisualState = makeUseVisualState({
|
||||
scrapeMotionValuesFromProps: createObject,
|
||||
createRenderState: createObject,
|
||||
});
|
||||
/**
|
||||
* This is not an officially supported API and may be removed
|
||||
* on any version.
|
||||
*/
|
||||
function useAnimatedState(initialState) {
|
||||
const [animationState, setAnimationState] = useState(initialState);
|
||||
const visualState = useVisualState({}, false);
|
||||
const element = useConstant(() => {
|
||||
return new StateVisualElement({
|
||||
props: {
|
||||
onUpdate: (v) => {
|
||||
setAnimationState({ ...v });
|
||||
},
|
||||
},
|
||||
visualState,
|
||||
presenceContext: null,
|
||||
}, { initialState });
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
element.mount({});
|
||||
return () => element.unmount();
|
||||
}, [element]);
|
||||
const startAnimation = useConstant(() => (animationDefinition) => {
|
||||
return animateVisualElement(element, animationDefinition);
|
||||
});
|
||||
return [animationState, startAnimation];
|
||||
}
|
||||
|
||||
export { useAnimatedState };
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { animationControls } from './animation-controls.mjs';
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
|
||||
|
||||
/**
|
||||
* Creates `AnimationControls`, which can be used to manually start, stop
|
||||
* and sequence animations on one or more components.
|
||||
*
|
||||
* The returned `AnimationControls` should be passed to the `animate` property
|
||||
* of the components you want to animate.
|
||||
*
|
||||
* These components can then be animated with the `start` method.
|
||||
*
|
||||
* ```jsx
|
||||
* import * as React from 'react'
|
||||
* import { motion, useAnimation } from 'framer-motion'
|
||||
*
|
||||
* export function MyComponent(props) {
|
||||
* const controls = useAnimation()
|
||||
*
|
||||
* controls.start({
|
||||
* x: 100,
|
||||
* transition: { duration: 0.5 },
|
||||
* })
|
||||
*
|
||||
* return <motion.div animate={controls} />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns Animation controller with `start` and `stop` methods
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function useAnimationControls() {
|
||||
const controls = useConstant(animationControls);
|
||||
useIsomorphicLayoutEffect(controls.mount, []);
|
||||
return controls;
|
||||
}
|
||||
const useAnimation = useAnimationControls;
|
||||
|
||||
export { useAnimation, useAnimationControls };
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { secondsToMilliseconds } from '../../utils/time-conversion.mjs';
|
||||
import { getDefaultTransition } from '../utils/default-transitions.mjs';
|
||||
import { getValueTransition } from '../utils/get-value-transition.mjs';
|
||||
import { MotionGlobalConfig } from '../../utils/GlobalConfig.mjs';
|
||||
import { instantAnimationState } from '../../utils/use-instant-transition-state.mjs';
|
||||
import { getFinalKeyframe } from '../animators/waapi/utils/get-final-keyframe.mjs';
|
||||
import { frame } from '../../frameloop/frame.mjs';
|
||||
import { AcceleratedAnimation } from '../animators/AcceleratedAnimation.mjs';
|
||||
import { MainThreadAnimation } from '../animators/MainThreadAnimation.mjs';
|
||||
import { GroupPlaybackControls } from '../GroupPlaybackControls.mjs';
|
||||
import { isTransitionDefined } from '../utils/is-transition-defined.mjs';
|
||||
|
||||
const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
|
||||
const valueTransition = getValueTransition(transition, name) || {};
|
||||
/**
|
||||
* Most transition values are currently completely overwritten by value-specific
|
||||
* transitions. In the future it'd be nicer to blend these transitions. But for now
|
||||
* delay actually does inherit from the root transition if not value-specific.
|
||||
*/
|
||||
const delay = valueTransition.delay || transition.delay || 0;
|
||||
/**
|
||||
* Elapsed isn't a public transition option but can be passed through from
|
||||
* optimized appear effects in milliseconds.
|
||||
*/
|
||||
let { elapsed = 0 } = transition;
|
||||
elapsed = elapsed - secondsToMilliseconds(delay);
|
||||
let options = {
|
||||
keyframes: Array.isArray(target) ? target : [null, target],
|
||||
ease: "easeOut",
|
||||
velocity: value.getVelocity(),
|
||||
...valueTransition,
|
||||
delay: -elapsed,
|
||||
onUpdate: (v) => {
|
||||
value.set(v);
|
||||
valueTransition.onUpdate && valueTransition.onUpdate(v);
|
||||
},
|
||||
onComplete: () => {
|
||||
onComplete();
|
||||
valueTransition.onComplete && valueTransition.onComplete();
|
||||
},
|
||||
name,
|
||||
motionValue: value,
|
||||
element: isHandoff ? undefined : element,
|
||||
};
|
||||
/**
|
||||
* If there's no transition defined for this value, we can generate
|
||||
* unqiue transition settings for this value.
|
||||
*/
|
||||
if (!isTransitionDefined(valueTransition)) {
|
||||
options = {
|
||||
...options,
|
||||
...getDefaultTransition(name, options),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Both WAAPI and our internal animation functions use durations
|
||||
* as defined by milliseconds, while our external API defines them
|
||||
* as seconds.
|
||||
*/
|
||||
if (options.duration) {
|
||||
options.duration = secondsToMilliseconds(options.duration);
|
||||
}
|
||||
if (options.repeatDelay) {
|
||||
options.repeatDelay = secondsToMilliseconds(options.repeatDelay);
|
||||
}
|
||||
if (options.from !== undefined) {
|
||||
options.keyframes[0] = options.from;
|
||||
}
|
||||
let shouldSkip = false;
|
||||
if (options.type === false ||
|
||||
(options.duration === 0 && !options.repeatDelay)) {
|
||||
options.duration = 0;
|
||||
if (options.delay === 0) {
|
||||
shouldSkip = true;
|
||||
}
|
||||
}
|
||||
if (instantAnimationState.current ||
|
||||
MotionGlobalConfig.skipAnimations) {
|
||||
shouldSkip = true;
|
||||
options.duration = 0;
|
||||
options.delay = 0;
|
||||
}
|
||||
/**
|
||||
* If we can or must skip creating the animation, and apply only
|
||||
* the final keyframe, do so. We also check once keyframes are resolved but
|
||||
* this early check prevents the need to create an animation at all.
|
||||
*/
|
||||
if (shouldSkip && !isHandoff && value.get() !== undefined) {
|
||||
const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
|
||||
if (finalKeyframe !== undefined) {
|
||||
frame.update(() => {
|
||||
options.onUpdate(finalKeyframe);
|
||||
options.onComplete();
|
||||
});
|
||||
// We still want to return some animation controls here rather
|
||||
// than returning undefined
|
||||
return new GroupPlaybackControls([]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Animate via WAAPI if possible. If this is a handoff animation, the optimised animation will be running via
|
||||
* WAAPI. Therefore, this animation must be JS to ensure it runs "under" the
|
||||
* optimised animation.
|
||||
*/
|
||||
if (!isHandoff && AcceleratedAnimation.supports(options)) {
|
||||
return new AcceleratedAnimation(options);
|
||||
}
|
||||
else {
|
||||
return new MainThreadAnimation(options);
|
||||
}
|
||||
};
|
||||
|
||||
export { animateMotionValue };
|
||||
Generated
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
import { transformProps } from '../../render/html/utils/transform.mjs';
|
||||
import { animateMotionValue } from './motion-value.mjs';
|
||||
import { setTarget } from '../../render/utils/setters.mjs';
|
||||
import { getValueTransition } from '../utils/get-value-transition.mjs';
|
||||
import { getOptimisedAppearId } from '../optimized-appear/get-appear-id.mjs';
|
||||
import { addValueToWillChange } from '../../value/use-will-change/add-will-change.mjs';
|
||||
import { frame } from '../../frameloop/frame.mjs';
|
||||
|
||||
/**
|
||||
* Decide whether we should block this animation. Previously, we achieved this
|
||||
* just by checking whether the key was listed in protectedKeys, but this
|
||||
* posed problems if an animation was triggered by afterChildren and protectedKeys
|
||||
* had been set to true in the meantime.
|
||||
*/
|
||||
function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) {
|
||||
const shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true;
|
||||
needsAnimating[key] = false;
|
||||
return shouldBlock;
|
||||
}
|
||||
function animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) {
|
||||
var _a;
|
||||
let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target } = targetAndTransition;
|
||||
if (transitionOverride)
|
||||
transition = transitionOverride;
|
||||
const animations = [];
|
||||
const animationTypeState = type &&
|
||||
visualElement.animationState &&
|
||||
visualElement.animationState.getState()[type];
|
||||
for (const key in target) {
|
||||
const value = visualElement.getValue(key, (_a = visualElement.latestValues[key]) !== null && _a !== void 0 ? _a : null);
|
||||
const valueTarget = target[key];
|
||||
if (valueTarget === undefined ||
|
||||
(animationTypeState &&
|
||||
shouldBlockAnimation(animationTypeState, key))) {
|
||||
continue;
|
||||
}
|
||||
const valueTransition = {
|
||||
delay,
|
||||
...getValueTransition(transition || {}, key),
|
||||
};
|
||||
/**
|
||||
* If this is the first time a value is being animated, check
|
||||
* to see if we're handling off from an existing animation.
|
||||
*/
|
||||
let isHandoff = false;
|
||||
if (window.MotionHandoffAnimation) {
|
||||
const appearId = getOptimisedAppearId(visualElement);
|
||||
if (appearId) {
|
||||
const startTime = window.MotionHandoffAnimation(appearId, key, frame);
|
||||
if (startTime !== null) {
|
||||
valueTransition.startTime = startTime;
|
||||
isHandoff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
addValueToWillChange(visualElement, key);
|
||||
value.start(animateMotionValue(key, value, valueTarget, visualElement.shouldReduceMotion && transformProps.has(key)
|
||||
? { type: false }
|
||||
: valueTransition, visualElement, isHandoff));
|
||||
const animation = value.animation;
|
||||
if (animation) {
|
||||
animations.push(animation);
|
||||
}
|
||||
}
|
||||
if (transitionEnd) {
|
||||
Promise.all(animations).then(() => {
|
||||
frame.update(() => {
|
||||
transitionEnd && setTarget(visualElement, transitionEnd);
|
||||
});
|
||||
});
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateTarget };
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
|
||||
import { animateTarget } from './visual-element-target.mjs';
|
||||
|
||||
function animateVariant(visualElement, variant, options = {}) {
|
||||
var _a;
|
||||
const resolved = resolveVariant(visualElement, variant, options.type === "exit"
|
||||
? (_a = visualElement.presenceContext) === null || _a === void 0 ? void 0 : _a.custom
|
||||
: undefined);
|
||||
let { transition = visualElement.getDefaultTransition() || {} } = resolved || {};
|
||||
if (options.transitionOverride) {
|
||||
transition = options.transitionOverride;
|
||||
}
|
||||
/**
|
||||
* If we have a variant, create a callback that runs it as an animation.
|
||||
* Otherwise, we resolve a Promise immediately for a composable no-op.
|
||||
*/
|
||||
const getAnimation = resolved
|
||||
? () => Promise.all(animateTarget(visualElement, resolved, options))
|
||||
: () => Promise.resolve();
|
||||
/**
|
||||
* If we have children, create a callback that runs all their animations.
|
||||
* Otherwise, we resolve a Promise immediately for a composable no-op.
|
||||
*/
|
||||
const getChildAnimations = visualElement.variantChildren && visualElement.variantChildren.size
|
||||
? (forwardDelay = 0) => {
|
||||
const { delayChildren = 0, staggerChildren, staggerDirection, } = transition;
|
||||
return animateChildren(visualElement, variant, delayChildren + forwardDelay, staggerChildren, staggerDirection, options);
|
||||
}
|
||||
: () => Promise.resolve();
|
||||
/**
|
||||
* If the transition explicitly defines a "when" option, we need to resolve either
|
||||
* this animation or all children animations before playing the other.
|
||||
*/
|
||||
const { when } = transition;
|
||||
if (when) {
|
||||
const [first, last] = when === "beforeChildren"
|
||||
? [getAnimation, getChildAnimations]
|
||||
: [getChildAnimations, getAnimation];
|
||||
return first().then(() => last());
|
||||
}
|
||||
else {
|
||||
return Promise.all([getAnimation(), getChildAnimations(options.delay)]);
|
||||
}
|
||||
}
|
||||
function animateChildren(visualElement, variant, delayChildren = 0, staggerChildren = 0, staggerDirection = 1, options) {
|
||||
const animations = [];
|
||||
const maxStaggerDuration = (visualElement.variantChildren.size - 1) * staggerChildren;
|
||||
const generateStaggerDuration = staggerDirection === 1
|
||||
? (i = 0) => i * staggerChildren
|
||||
: (i = 0) => maxStaggerDuration - i * staggerChildren;
|
||||
Array.from(visualElement.variantChildren)
|
||||
.sort(sortByTreeOrder)
|
||||
.forEach((child, i) => {
|
||||
child.notify("AnimationStart", variant);
|
||||
animations.push(animateVariant(child, variant, {
|
||||
...options,
|
||||
delay: delayChildren + generateStaggerDuration(i),
|
||||
}).then(() => child.notify("AnimationComplete", variant)));
|
||||
});
|
||||
return Promise.all(animations);
|
||||
}
|
||||
function sortByTreeOrder(a, b) {
|
||||
return a.sortNodePosition(b);
|
||||
}
|
||||
|
||||
export { animateVariant, sortByTreeOrder };
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
|
||||
import { animateTarget } from './visual-element-target.mjs';
|
||||
import { animateVariant } from './visual-element-variant.mjs';
|
||||
|
||||
function animateVisualElement(visualElement, definition, options = {}) {
|
||||
visualElement.notify("AnimationStart", definition);
|
||||
let animation;
|
||||
if (Array.isArray(definition)) {
|
||||
const animations = definition.map((variant) => animateVariant(visualElement, variant, options));
|
||||
animation = Promise.all(animations);
|
||||
}
|
||||
else if (typeof definition === "string") {
|
||||
animation = animateVariant(visualElement, definition, options);
|
||||
}
|
||||
else {
|
||||
const resolvedDefinition = typeof definition === "function"
|
||||
? resolveVariant(visualElement, definition, options.custom)
|
||||
: definition;
|
||||
animation = Promise.all(animateTarget(visualElement, resolvedDefinition, options));
|
||||
}
|
||||
return animation.then(() => {
|
||||
visualElement.notify("AnimationComplete", definition);
|
||||
});
|
||||
}
|
||||
|
||||
export { animateVisualElement };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
|
||||
|
||||
const optimizedAppearDataId = "framerAppearId";
|
||||
const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
|
||||
|
||||
export { optimizedAppearDataAttribute, optimizedAppearDataId };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { optimizedAppearDataAttribute } from './data-id.mjs';
|
||||
|
||||
function getOptimisedAppearId(visualElement) {
|
||||
return visualElement.props[optimizedAppearDataAttribute];
|
||||
}
|
||||
|
||||
export { getOptimisedAppearId };
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { appearAnimationStore } from './store.mjs';
|
||||
import { appearStoreId } from './store-id.mjs';
|
||||
|
||||
function handoffOptimizedAppearAnimation(elementId, valueName, frame) {
|
||||
var _a;
|
||||
const storeId = appearStoreId(elementId, valueName);
|
||||
const optimisedAnimation = appearAnimationStore.get(storeId);
|
||||
if (!optimisedAnimation) {
|
||||
return null;
|
||||
}
|
||||
const { animation, startTime } = optimisedAnimation;
|
||||
function cancelAnimation() {
|
||||
var _a;
|
||||
(_a = window.MotionCancelOptimisedAnimation) === null || _a === void 0 ? void 0 : _a.call(window, elementId, valueName, frame);
|
||||
}
|
||||
/**
|
||||
* We can cancel the animation once it's finished now that we've synced
|
||||
* with Motion.
|
||||
*
|
||||
* Prefer onfinish over finished as onfinish is backwards compatible with
|
||||
* older browsers.
|
||||
*/
|
||||
animation.onfinish = cancelAnimation;
|
||||
if (startTime === null || ((_a = window.MotionHandoffIsComplete) === null || _a === void 0 ? void 0 : _a.call(window, elementId))) {
|
||||
/**
|
||||
* If the startTime is null, this animation is the Paint Ready detection animation
|
||||
* and we can cancel it immediately without handoff.
|
||||
*
|
||||
* Or if we've already handed off the animation then we're now interrupting it.
|
||||
* In which case we need to cancel it.
|
||||
*/
|
||||
cancelAnimation();
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return startTime;
|
||||
}
|
||||
}
|
||||
|
||||
export { handoffOptimizedAppearAnimation };
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { appearStoreId } from './store-id.mjs';
|
||||
import { startWaapiAnimation } from '../animators/waapi/index.mjs';
|
||||
import { optimizedAppearDataId } from './data-id.mjs';
|
||||
import { handoffOptimizedAppearAnimation } from './handoff.mjs';
|
||||
import { appearAnimationStore, appearComplete } from './store.mjs';
|
||||
import { noop } from '../../utils/noop.mjs';
|
||||
import { getOptimisedAppearId } from './get-appear-id.mjs';
|
||||
|
||||
/**
|
||||
* A single time to use across all animations to manually set startTime
|
||||
* and ensure they're all in sync.
|
||||
*/
|
||||
let startFrameTime;
|
||||
/**
|
||||
* A dummy animation to detect when Chrome is ready to start
|
||||
* painting the page and hold off from triggering the real animation
|
||||
* until then. We only need one animation to detect paint ready.
|
||||
*
|
||||
* https://bugs.chromium.org/p/chromium/issues/detail?id=1406850
|
||||
*/
|
||||
let readyAnimation;
|
||||
/**
|
||||
* Keep track of animations that were suspended vs cancelled so we
|
||||
* can easily resume them when we're done measuring layout.
|
||||
*/
|
||||
const suspendedAnimations = new Set();
|
||||
function resumeSuspendedAnimations() {
|
||||
suspendedAnimations.forEach((data) => {
|
||||
data.animation.play();
|
||||
data.animation.startTime = data.startTime;
|
||||
});
|
||||
suspendedAnimations.clear();
|
||||
}
|
||||
function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
|
||||
// Prevent optimised appear animations if Motion has already started animating.
|
||||
if (window.MotionIsMounted) {
|
||||
return;
|
||||
}
|
||||
const id = element.dataset[optimizedAppearDataId];
|
||||
if (!id)
|
||||
return;
|
||||
window.MotionHandoffAnimation = handoffOptimizedAppearAnimation;
|
||||
const storeId = appearStoreId(id, name);
|
||||
if (!readyAnimation) {
|
||||
readyAnimation = startWaapiAnimation(element, name, [keyframes[0], keyframes[0]],
|
||||
/**
|
||||
* 10 secs is basically just a super-safe duration to give Chrome
|
||||
* long enough to get the animation ready.
|
||||
*/
|
||||
{ duration: 10000, ease: "linear" });
|
||||
appearAnimationStore.set(storeId, {
|
||||
animation: readyAnimation,
|
||||
startTime: null,
|
||||
});
|
||||
/**
|
||||
* If there's no readyAnimation then there's been no instantiation
|
||||
* of handoff animations.
|
||||
*/
|
||||
window.MotionHandoffAnimation = handoffOptimizedAppearAnimation;
|
||||
window.MotionHasOptimisedAnimation = (elementId, valueName) => {
|
||||
if (!elementId)
|
||||
return false;
|
||||
/**
|
||||
* Keep a map of elementIds that have started animating. We check
|
||||
* via ID instead of Element because of hydration errors and
|
||||
* pre-hydration checks. We also actively record IDs as they start
|
||||
* animating rather than simply checking for data-appear-id as
|
||||
* this attrbute might be present but not lead to an animation, for
|
||||
* instance if the element's appear animation is on a different
|
||||
* breakpoint.
|
||||
*/
|
||||
if (!valueName) {
|
||||
return appearComplete.has(elementId);
|
||||
}
|
||||
const animationId = appearStoreId(elementId, valueName);
|
||||
return Boolean(appearAnimationStore.get(animationId));
|
||||
};
|
||||
window.MotionHandoffMarkAsComplete = (elementId) => {
|
||||
if (appearComplete.has(elementId)) {
|
||||
appearComplete.set(elementId, true);
|
||||
}
|
||||
};
|
||||
window.MotionHandoffIsComplete = (elementId) => {
|
||||
return appearComplete.get(elementId) === true;
|
||||
};
|
||||
/**
|
||||
* We only need to cancel transform animations as
|
||||
* they're the ones that will interfere with the
|
||||
* layout animation measurements.
|
||||
*/
|
||||
window.MotionCancelOptimisedAnimation = (elementId, valueName, frame, canResume) => {
|
||||
const animationId = appearStoreId(elementId, valueName);
|
||||
const data = appearAnimationStore.get(animationId);
|
||||
if (!data)
|
||||
return;
|
||||
if (frame && canResume === undefined) {
|
||||
/**
|
||||
* Wait until the end of the subsequent frame to cancel the animation
|
||||
* to ensure we don't remove the animation before the main thread has
|
||||
* had a chance to resolve keyframes and render.
|
||||
*/
|
||||
frame.postRender(() => {
|
||||
frame.postRender(() => {
|
||||
data.animation.cancel();
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
data.animation.cancel();
|
||||
}
|
||||
if (frame && canResume) {
|
||||
suspendedAnimations.add(data);
|
||||
frame.render(resumeSuspendedAnimations);
|
||||
}
|
||||
else {
|
||||
appearAnimationStore.delete(animationId);
|
||||
/**
|
||||
* If there are no more animations left, we can remove the cancel function.
|
||||
* This will let us know when we can stop checking for conflicting layout animations.
|
||||
*/
|
||||
if (!appearAnimationStore.size) {
|
||||
window.MotionCancelOptimisedAnimation = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
window.MotionCheckAppearSync = (visualElement, valueName, value) => {
|
||||
var _a, _b;
|
||||
const appearId = getOptimisedAppearId(visualElement);
|
||||
if (!appearId)
|
||||
return;
|
||||
const valueIsOptimised = (_a = window.MotionHasOptimisedAnimation) === null || _a === void 0 ? void 0 : _a.call(window, appearId, valueName);
|
||||
const externalAnimationValue = (_b = visualElement.props.values) === null || _b === void 0 ? void 0 : _b[valueName];
|
||||
if (!valueIsOptimised || !externalAnimationValue)
|
||||
return;
|
||||
const removeSyncCheck = value.on("change", (latestValue) => {
|
||||
var _a;
|
||||
if (externalAnimationValue.get() !== latestValue) {
|
||||
(_a = window.MotionCancelOptimisedAnimation) === null || _a === void 0 ? void 0 : _a.call(window, appearId, valueName);
|
||||
removeSyncCheck();
|
||||
}
|
||||
});
|
||||
return removeSyncCheck;
|
||||
};
|
||||
}
|
||||
const startAnimation = () => {
|
||||
readyAnimation.cancel();
|
||||
const appearAnimation = startWaapiAnimation(element, name, keyframes, options);
|
||||
/**
|
||||
* Record the time of the first started animation. We call performance.now() once
|
||||
* here and once in handoff to ensure we're getting
|
||||
* close to a frame-locked time. This keeps all animations in sync.
|
||||
*/
|
||||
if (startFrameTime === undefined) {
|
||||
startFrameTime = performance.now();
|
||||
}
|
||||
appearAnimation.startTime = startFrameTime;
|
||||
appearAnimationStore.set(storeId, {
|
||||
animation: appearAnimation,
|
||||
startTime: startFrameTime,
|
||||
});
|
||||
if (onReady)
|
||||
onReady(appearAnimation);
|
||||
};
|
||||
appearComplete.set(id, false);
|
||||
if (readyAnimation.ready) {
|
||||
readyAnimation.ready.then(startAnimation).catch(noop);
|
||||
}
|
||||
else {
|
||||
startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
export { startOptimizedAppearAnimation };
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { transformProps } from '../../render/html/utils/transform.mjs';
|
||||
|
||||
const appearStoreId = (elementId, valueName) => {
|
||||
const key = transformProps.has(valueName) ? "transform" : valueName;
|
||||
return `${elementId}: ${key}`;
|
||||
};
|
||||
|
||||
export { appearStoreId };
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
const appearAnimationStore = new Map();
|
||||
const appearComplete = new Map();
|
||||
|
||||
export { appearAnimationStore, appearComplete };
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import { createGeneratorEasing } from '../../easing/utils/create-generator-easing.mjs';
|
||||
import { defaultOffset } from '../../utils/offsets/default.mjs';
|
||||
import { fillOffset } from '../../utils/offsets/fill.mjs';
|
||||
import { progress } from '../../utils/progress.mjs';
|
||||
import { secondsToMilliseconds } from '../../utils/time-conversion.mjs';
|
||||
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
|
||||
import { resolveSubjects } from '../animate/resolve-subjects.mjs';
|
||||
import { isGenerator } from '../generators/utils/is-generator.mjs';
|
||||
import { calcNextTime } from './utils/calc-time.mjs';
|
||||
import { addKeyframes } from './utils/edit.mjs';
|
||||
import { compareByTime } from './utils/sort.mjs';
|
||||
|
||||
const defaultSegmentEasing = "easeInOut";
|
||||
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
|
||||
const defaultDuration = defaultTransition.duration || 0.3;
|
||||
const animationDefinitions = new Map();
|
||||
const sequences = new Map();
|
||||
const elementCache = {};
|
||||
const timeLabels = new Map();
|
||||
let prevTime = 0;
|
||||
let currentTime = 0;
|
||||
let totalDuration = 0;
|
||||
/**
|
||||
* Build the timeline by mapping over the sequence array and converting
|
||||
* the definitions into keyframes and offsets with absolute time values.
|
||||
* These will later get converted into relative offsets in a second pass.
|
||||
*/
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const segment = sequence[i];
|
||||
/**
|
||||
* If this is a timeline label, mark it and skip the rest of this iteration.
|
||||
*/
|
||||
if (typeof segment === "string") {
|
||||
timeLabels.set(segment, currentTime);
|
||||
continue;
|
||||
}
|
||||
else if (!Array.isArray(segment)) {
|
||||
timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
|
||||
continue;
|
||||
}
|
||||
let [subject, keyframes, transition = {}] = segment;
|
||||
/**
|
||||
* If a relative or absolute time value has been specified we need to resolve
|
||||
* it in relation to the currentTime.
|
||||
*/
|
||||
if (transition.at !== undefined) {
|
||||
currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
|
||||
}
|
||||
/**
|
||||
* Keep track of the maximum duration in this definition. This will be
|
||||
* applied to currentTime once the definition has been parsed.
|
||||
*/
|
||||
let maxDuration = 0;
|
||||
const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
|
||||
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
|
||||
const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = "keyframes", ...remainingTransition } = valueTransition;
|
||||
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
|
||||
/**
|
||||
* Resolve stagger() if defined.
|
||||
*/
|
||||
const calculatedDelay = typeof delay === "function"
|
||||
? delay(elementIndex, numSubjects)
|
||||
: delay;
|
||||
/**
|
||||
* If this animation should and can use a spring, generate a spring easing function.
|
||||
*/
|
||||
const numKeyframes = valueKeyframesAsList.length;
|
||||
const createGenerator = isGenerator(type)
|
||||
? type
|
||||
: generators === null || generators === void 0 ? void 0 : generators[type];
|
||||
if (numKeyframes <= 2 && createGenerator) {
|
||||
/**
|
||||
* As we're creating an easing function from a spring,
|
||||
* ideally we want to generate it using the real distance
|
||||
* between the two keyframes. However this isn't always
|
||||
* possible - in these situations we use 0-100.
|
||||
*/
|
||||
let absoluteDelta = 100;
|
||||
if (numKeyframes === 2 &&
|
||||
isNumberKeyframesArray(valueKeyframesAsList)) {
|
||||
const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
|
||||
absoluteDelta = Math.abs(delta);
|
||||
}
|
||||
const springTransition = { ...remainingTransition };
|
||||
if (duration !== undefined) {
|
||||
springTransition.duration = secondsToMilliseconds(duration);
|
||||
}
|
||||
const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
|
||||
ease = springEasing.ease;
|
||||
duration = springEasing.duration;
|
||||
}
|
||||
duration !== null && duration !== void 0 ? duration : (duration = defaultDuration);
|
||||
const startTime = currentTime + calculatedDelay;
|
||||
const targetTime = startTime + duration;
|
||||
/**
|
||||
* If there's only one time offset of 0, fill in a second with length 1
|
||||
*/
|
||||
if (times.length === 1 && times[0] === 0) {
|
||||
times[1] = 1;
|
||||
}
|
||||
/**
|
||||
* Fill out if offset if fewer offsets than keyframes
|
||||
*/
|
||||
const remainder = times.length - valueKeyframesAsList.length;
|
||||
remainder > 0 && fillOffset(times, remainder);
|
||||
/**
|
||||
* If only one value has been set, ie [1], push a null to the start of
|
||||
* the keyframe array. This will let us mark a keyframe at this point
|
||||
* that will later be hydrated with the previous value.
|
||||
*/
|
||||
valueKeyframesAsList.length === 1 &&
|
||||
valueKeyframesAsList.unshift(null);
|
||||
/**
|
||||
* Add keyframes, mapping offsets to absolute time.
|
||||
*/
|
||||
addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
|
||||
maxDuration = Math.max(calculatedDelay + duration, maxDuration);
|
||||
totalDuration = Math.max(targetTime, totalDuration);
|
||||
};
|
||||
if (isMotionValue(subject)) {
|
||||
const subjectSequence = getSubjectSequence(subject, sequences);
|
||||
resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
|
||||
}
|
||||
else {
|
||||
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
|
||||
const numSubjects = subjects.length;
|
||||
/**
|
||||
* For every element in this segment, process the defined values.
|
||||
*/
|
||||
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
|
||||
/**
|
||||
* Cast necessary, but we know these are of this type
|
||||
*/
|
||||
keyframes = keyframes;
|
||||
transition = transition;
|
||||
const thisSubject = subjects[subjectIndex];
|
||||
const subjectSequence = getSubjectSequence(thisSubject, sequences);
|
||||
for (const key in keyframes) {
|
||||
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
prevTime = currentTime;
|
||||
currentTime += maxDuration;
|
||||
}
|
||||
/**
|
||||
* For every element and value combination create a new animation.
|
||||
*/
|
||||
sequences.forEach((valueSequences, element) => {
|
||||
for (const key in valueSequences) {
|
||||
const valueSequence = valueSequences[key];
|
||||
/**
|
||||
* Arrange all the keyframes in ascending time order.
|
||||
*/
|
||||
valueSequence.sort(compareByTime);
|
||||
const keyframes = [];
|
||||
const valueOffset = [];
|
||||
const valueEasing = [];
|
||||
/**
|
||||
* For each keyframe, translate absolute times into
|
||||
* relative offsets based on the total duration of the timeline.
|
||||
*/
|
||||
for (let i = 0; i < valueSequence.length; i++) {
|
||||
const { at, value, easing } = valueSequence[i];
|
||||
keyframes.push(value);
|
||||
valueOffset.push(progress(0, totalDuration, at));
|
||||
valueEasing.push(easing || "easeOut");
|
||||
}
|
||||
/**
|
||||
* If the first keyframe doesn't land on offset: 0
|
||||
* provide one by duplicating the initial keyframe. This ensures
|
||||
* it snaps to the first keyframe when the animation starts.
|
||||
*/
|
||||
if (valueOffset[0] !== 0) {
|
||||
valueOffset.unshift(0);
|
||||
keyframes.unshift(keyframes[0]);
|
||||
valueEasing.unshift(defaultSegmentEasing);
|
||||
}
|
||||
/**
|
||||
* If the last keyframe doesn't land on offset: 1
|
||||
* provide one with a null wildcard value. This will ensure it
|
||||
* stays static until the end of the animation.
|
||||
*/
|
||||
if (valueOffset[valueOffset.length - 1] !== 1) {
|
||||
valueOffset.push(1);
|
||||
keyframes.push(null);
|
||||
}
|
||||
if (!animationDefinitions.has(element)) {
|
||||
animationDefinitions.set(element, {
|
||||
keyframes: {},
|
||||
transition: {},
|
||||
});
|
||||
}
|
||||
const definition = animationDefinitions.get(element);
|
||||
definition.keyframes[key] = keyframes;
|
||||
definition.transition[key] = {
|
||||
...defaultTransition,
|
||||
duration: totalDuration,
|
||||
ease: valueEasing,
|
||||
times: valueOffset,
|
||||
...sequenceTransition,
|
||||
};
|
||||
}
|
||||
});
|
||||
return animationDefinitions;
|
||||
}
|
||||
function getSubjectSequence(subject, sequences) {
|
||||
!sequences.has(subject) && sequences.set(subject, {});
|
||||
return sequences.get(subject);
|
||||
}
|
||||
function getValueSequence(name, sequences) {
|
||||
if (!sequences[name])
|
||||
sequences[name] = [];
|
||||
return sequences[name];
|
||||
}
|
||||
function keyframesAsList(keyframes) {
|
||||
return Array.isArray(keyframes) ? keyframes : [keyframes];
|
||||
}
|
||||
function getValueTransition(transition, key) {
|
||||
return transition && transition[key]
|
||||
? {
|
||||
...transition,
|
||||
...transition[key],
|
||||
}
|
||||
: { ...transition };
|
||||
}
|
||||
const isNumber = (keyframe) => typeof keyframe === "number";
|
||||
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
|
||||
|
||||
export { createAnimationsFromSequence, getValueTransition };
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Given a absolute or relative time definition and current/prev time state of the sequence,
|
||||
* calculate an absolute time for the next keyframes.
|
||||
*/
|
||||
function calcNextTime(current, next, prev, labels) {
|
||||
var _a;
|
||||
if (typeof next === "number") {
|
||||
return next;
|
||||
}
|
||||
else if (next.startsWith("-") || next.startsWith("+")) {
|
||||
return Math.max(0, current + parseFloat(next));
|
||||
}
|
||||
else if (next === "<") {
|
||||
return prev;
|
||||
}
|
||||
else {
|
||||
return (_a = labels.get(next)) !== null && _a !== void 0 ? _a : current;
|
||||
}
|
||||
}
|
||||
|
||||
export { calcNextTime };
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { getEasingForSegment } from '../../../easing/utils/get-easing-for-segment.mjs';
|
||||
import { removeItem } from '../../../utils/array.mjs';
|
||||
import { mixNumber } from '../../../utils/mix/number.mjs';
|
||||
|
||||
function eraseKeyframes(sequence, startTime, endTime) {
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const keyframe = sequence[i];
|
||||
if (keyframe.at > startTime && keyframe.at < endTime) {
|
||||
removeItem(sequence, keyframe);
|
||||
// If we remove this item we have to push the pointer back one
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
|
||||
/**
|
||||
* Erase every existing value between currentTime and targetTime,
|
||||
* this will essentially splice this timeline into any currently
|
||||
* defined ones.
|
||||
*/
|
||||
eraseKeyframes(sequence, startTime, endTime);
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
sequence.push({
|
||||
value: keyframes[i],
|
||||
at: mixNumber(startTime, endTime, offset[i]),
|
||||
easing: getEasingForSegment(easing, i),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { addKeyframes, eraseKeyframes };
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
function compareByTime(a, b) {
|
||||
if (a.at === b.at) {
|
||||
if (a.value === null)
|
||||
return 1;
|
||||
if (b.value === null)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return a.at - b.at;
|
||||
}
|
||||
}
|
||||
|
||||
export { compareByTime };
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { isSVGElement } from '../../render/dom/utils/is-svg-element.mjs';
|
||||
import { SVGVisualElement } from '../../render/svg/SVGVisualElement.mjs';
|
||||
import { HTMLVisualElement } from '../../render/html/HTMLVisualElement.mjs';
|
||||
import { visualElementStore } from '../../render/store.mjs';
|
||||
import { ObjectVisualElement } from '../../render/object/ObjectVisualElement.mjs';
|
||||
|
||||
function createDOMVisualElement(element) {
|
||||
const options = {
|
||||
presenceContext: null,
|
||||
props: {},
|
||||
visualState: {
|
||||
renderState: {
|
||||
transform: {},
|
||||
transformOrigin: {},
|
||||
style: {},
|
||||
vars: {},
|
||||
attrs: {},
|
||||
},
|
||||
latestValues: {},
|
||||
},
|
||||
};
|
||||
const node = isSVGElement(element)
|
||||
? new SVGVisualElement(options)
|
||||
: new HTMLVisualElement(options);
|
||||
node.mount(element);
|
||||
visualElementStore.set(element, node);
|
||||
}
|
||||
function createObjectVisualElement(subject) {
|
||||
const options = {
|
||||
presenceContext: null,
|
||||
props: {},
|
||||
visualState: {
|
||||
renderState: {
|
||||
output: {},
|
||||
},
|
||||
latestValues: {},
|
||||
},
|
||||
};
|
||||
const node = new ObjectVisualElement(options);
|
||||
node.mount(subject);
|
||||
visualElementStore.set(subject, node);
|
||||
}
|
||||
|
||||
export { createDOMVisualElement, createObjectVisualElement };
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { transformProps } from '../../render/html/utils/transform.mjs';
|
||||
|
||||
const underDampedSpring = {
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 25,
|
||||
restSpeed: 10,
|
||||
};
|
||||
const criticallyDampedSpring = (target) => ({
|
||||
type: "spring",
|
||||
stiffness: 550,
|
||||
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
|
||||
restSpeed: 10,
|
||||
});
|
||||
const keyframesTransition = {
|
||||
type: "keyframes",
|
||||
duration: 0.8,
|
||||
};
|
||||
/**
|
||||
* Default easing curve is a slightly shallower version of
|
||||
* the default browser easing curve.
|
||||
*/
|
||||
const ease = {
|
||||
type: "keyframes",
|
||||
ease: [0.25, 0.1, 0.35, 1],
|
||||
duration: 0.3,
|
||||
};
|
||||
const getDefaultTransition = (valueKey, { keyframes }) => {
|
||||
if (keyframes.length > 2) {
|
||||
return keyframesTransition;
|
||||
}
|
||||
else if (transformProps.has(valueKey)) {
|
||||
return valueKey.startsWith("scale")
|
||||
? criticallyDampedSpring(keyframes[1])
|
||||
: underDampedSpring;
|
||||
}
|
||||
return ease;
|
||||
};
|
||||
|
||||
export { getDefaultTransition };
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
function getValueTransition(transition, key) {
|
||||
return transition
|
||||
? transition[key] ||
|
||||
transition["default"] ||
|
||||
transition
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export { getValueTransition };
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { complex } from '../../value/types/complex/index.mjs';
|
||||
|
||||
/**
|
||||
* Check if a value is animatable. Examples:
|
||||
*
|
||||
* ✅: 100, "100px", "#fff"
|
||||
* ❌: "block", "url(2.jpg)"
|
||||
* @param value
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
const isAnimatable = (value, name) => {
|
||||
// If the list of keys tat might be non-animatable grows, replace with Set
|
||||
if (name === "zIndex")
|
||||
return false;
|
||||
// If it's a number or a keyframes array, we can animate it. We might at some point
|
||||
// need to do a deep isAnimatable check of keyframes, or let Popmotion handle this,
|
||||
// but for now lets leave it like this for performance reasons
|
||||
if (typeof value === "number" || Array.isArray(value))
|
||||
return true;
|
||||
if (typeof value === "string" && // It's animatable if we have a string
|
||||
(complex.test(value) || value === "0") && // And it contains numbers and/or colors
|
||||
!value.startsWith("url(") // Unless it starts with "url("
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export { isAnimatable };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
function isAnimationControls(v) {
|
||||
return (v !== null &&
|
||||
typeof v === "object" &&
|
||||
typeof v.start === "function");
|
||||
}
|
||||
|
||||
export { isAnimationControls };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
function isDOMKeyframes(keyframes) {
|
||||
return typeof keyframes === "object" && !Array.isArray(keyframes);
|
||||
}
|
||||
|
||||
export { isDOMKeyframes };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
const isKeyframesTarget = (v) => {
|
||||
return Array.isArray(v);
|
||||
};
|
||||
|
||||
export { isKeyframesTarget };
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { isZeroValueString } from '../../utils/is-zero-value-string.mjs';
|
||||
|
||||
function isNone(value) {
|
||||
if (typeof value === "number") {
|
||||
return value === 0;
|
||||
}
|
||||
else if (value !== null) {
|
||||
return value === "none" || value === "0" || isZeroValueString(value);
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export { isNone };
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Decide whether a transition is defined on a given Transition.
|
||||
* This filters out orchestration options and returns true
|
||||
* if any options are left.
|
||||
*/
|
||||
function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) {
|
||||
return !!Object.keys(transition).length;
|
||||
}
|
||||
|
||||
export { isTransitionDefined };
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { easingDefinitionToFunction } from '../../easing/utils/map.mjs';
|
||||
|
||||
function getOriginIndex(from, total) {
|
||||
if (from === "first") {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
const lastIndex = total - 1;
|
||||
return from === "last" ? lastIndex : lastIndex / 2;
|
||||
}
|
||||
}
|
||||
function stagger(duration = 0.1, { startDelay = 0, from = 0, ease } = {}) {
|
||||
return (i, total) => {
|
||||
const fromIndex = typeof from === "number" ? from : getOriginIndex(from, total);
|
||||
const distance = Math.abs(fromIndex - i);
|
||||
let delay = duration * distance;
|
||||
if (ease) {
|
||||
const maxDelay = total * duration;
|
||||
const easingFunction = easingDefinitionToFunction(ease);
|
||||
delay = easingFunction(delay / maxDelay) * maxDelay;
|
||||
}
|
||||
return startDelay + delay;
|
||||
};
|
||||
}
|
||||
|
||||
export { getOriginIndex, stagger };
|
||||
Reference in new issue
Block a user