svelte-motion
Version:
Svelte animation library based on the React library framer-motion.
234 lines (231 loc) • 9.24 kB
JavaScript
/**
based on framer-motion@4.0.3,
Copyright (c) 2018 Framer B.V.
*/
import {fixed} from '../../../utils/fix-process-env';
import sync, { getFrameData } from 'framesync';
import { mix, progress, linear, mixColor, circOut } from 'popmotion';
import { animate } from '../../../animation/animate.js';
import { getValueTransition } from '../../../animation/utils/transitions.js';
import { motionValue } from '../../../value/index.js';
function createCrossfader() {
/**
* The current state of the crossfade as a value between 0 and 1
*/
var progress = motionValue(1);
var options = {
lead: undefined,
follow: undefined,
crossfadeOpacity: false,
preserveFollowOpacity: false,
};
var prevOptions = Object.assign({}, options);
var leadState = {};
var followState = {};
/**
*
*/
var isActive = false;
/**
*
*/
var finalCrossfadeFrame = null;
/**
* Framestamp of the last frame we updated values at.
*/
var prevUpdate = 0;
function startCrossfadeAnimation(target, transition) {
var lead = options.lead, follow = options.follow;
isActive = true;
finalCrossfadeFrame = null;
var hasUpdated = false;
var onUpdate = function () {
hasUpdated = true;
lead && lead.scheduleRender();
follow && follow.scheduleRender();
};
var onComplete = function () {
isActive = false;
/**
* If the crossfade animation is no longer active, flag a frame
* that we're still allowed to crossfade
*/
finalCrossfadeFrame = getFrameData().timestamp;
};
transition = transition && getValueTransition(transition, "crossfade");
return animate(progress, target, Object.assign(Object.assign({}, transition), { onUpdate: onUpdate, onComplete: function () {
if (!hasUpdated) {
progress.set(target);
/**
* If we never ran an update, for instance if this was an instant transition, fire a
* simulated final frame to ensure the crossfade gets applied correctly.
*/
sync.read(onComplete);
}
else {
onComplete();
}
onUpdate();
} }));
}
function updateCrossfade() {
var _a, _b;
/**
* We only want to compute the crossfade once per frame, so we
* compare the previous update framestamp with the current frame
* and early return if they're the same.
*/
var timestamp = getFrameData().timestamp;
var lead = options.lead, follow = options.follow;
if (timestamp === prevUpdate || !lead)
return;
prevUpdate = timestamp;
/**
* Merge each component's latest values into our crossfaded state
* before crossfading.
*/
var latestLeadValues = lead.getLatestValues();
Object.assign(leadState, latestLeadValues);
var latestFollowValues = follow
? follow.getLatestValues()
: options.prevValues;
Object.assign(followState, latestFollowValues);
var p = progress.get();
/**
* Crossfade the opacity between the two components. This will result
* in a different opacity for each component.
*/
var leadTargetOpacity = (_a = latestLeadValues.opacity) !== null && _a !== void 0 ? _a : 1;
var followTargetOpacity = (_b = latestFollowValues === null || latestFollowValues === void 0 ? void 0 : latestFollowValues.opacity) !== null && _b !== void 0 ? _b : 1;
if (options.crossfadeOpacity && follow) {
leadState.opacity = mix(
/**
* If the follow child has been completely hidden we animate
* this opacity from its previous opacity, but otherwise from completely transparent.
*/
follow.isVisible !== false ? 0 : followTargetOpacity, leadTargetOpacity, easeCrossfadeIn(p));
followState.opacity = options.preserveFollowOpacity
? followTargetOpacity
: mix(followTargetOpacity, 0, easeCrossfadeOut(p));
}
else if (!follow) {
leadState.opacity = mix(followTargetOpacity, leadTargetOpacity, p);
}
mixValues(leadState, followState, latestLeadValues, latestFollowValues || {}, Boolean(follow), p);
}
return {
isActive: function () {
return leadState &&
(isActive || getFrameData().timestamp === finalCrossfadeFrame);
},
fromLead: function (transition) {
return startCrossfadeAnimation(0, transition);
},
toLead: function (transition) {
var initialProgress = 0;
if (!options.prevValues && !options.follow) {
/**
* If we're not coming from anywhere, start at the end of the animation.
*/
initialProgress = 1;
}
else if (prevOptions.lead === options.follow &&
prevOptions.follow === options.lead) {
/**
* If we're swapping follow/lead we can reverse the progress
*/
initialProgress = 1 - progress.get();
}
progress.set(initialProgress);
return startCrossfadeAnimation(1, transition);
},
reset: function () { return progress.set(1); },
stop: function () { return progress.stop(); },
getCrossfadeState: function (element) {
updateCrossfade();
if (element === options.lead) {
return leadState;
}
else if (element === options.follow) {
return followState;
}
},
setOptions: function (newOptions) {
prevOptions = options;
options = newOptions;
leadState = {};
followState = {};
},
getLatestValues: function () {
return leadState;
},
};
}
var easeCrossfadeIn = compress(0, 0.5, circOut);
var easeCrossfadeOut = compress(0.5, 0.95, linear);
function compress(min, max, easing) {
return function (p) {
// Could replace ifs with clamp
if (p < min)
return 0;
if (p > max)
return 1;
return easing(progress(min, max, p));
};
}
var borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"];
var numBorders = borders.length;
function mixValues(leadState, followState, latestLeadValues, latestFollowValues, hasFollowElement, p) {
/**
* Mix border radius
*/
for (var i = 0; i < numBorders; i++) {
var borderLabel = "border" + borders[i] + "Radius";
var followRadius = getRadius(latestFollowValues, borderLabel);
var leadRadius = getRadius(latestLeadValues, borderLabel);
if (followRadius === undefined && leadRadius === undefined)
continue;
followRadius || (followRadius = 0);
leadRadius || (leadRadius = 0);
/**
* Currently we're only crossfading between numerical border radius.
* It would be possible to crossfade between percentages for a little
* extra bundle size.
*/
if (typeof followRadius === "number" &&
typeof leadRadius === "number") {
var radius = Math.max(mix(followRadius, leadRadius, p), 0);
leadState[borderLabel] = followState[borderLabel] = radius;
}
}
/**
* Mix rotation
*/
if (latestFollowValues.rotate || latestLeadValues.rotate) {
var rotate = mix(latestFollowValues.rotate || 0, latestLeadValues.rotate || 0, p);
leadState.rotate = followState.rotate = rotate;
}
/**
* We only want to mix the background color if there's a follow element
* that we're not crossfading opacity between. For instance with switch
* AnimateSharedLayout animations, this helps the illusion of a continuous
* element being animated but also cuts down on the number of paints triggered
* for elements where opacity is doing that work for us.
*/
if (!hasFollowElement &&
latestLeadValues.backgroundColor &&
latestFollowValues.backgroundColor) {
/**
* This isn't ideal performance-wise as mixColor is creating a new function every frame.
* We could probably create a mixer that runs at the start of the animation but
* the idea behind the crossfader is that it runs dynamically between two potentially
* changing targets (ie opacity or borderRadius may be animating independently via variants)
*/
leadState.backgroundColor = followState.backgroundColor = mixColor(latestFollowValues.backgroundColor, latestLeadValues.backgroundColor)(p);
}
}
function getRadius(values, radiusName) {
var _a;
return (_a = values[radiusName]) !== null && _a !== void 0 ? _a : values.borderRadius;
}
export { createCrossfader };