svelte-motion
Version:
Svelte animation library based on the React library framer-motion.
366 lines (357 loc) • 11.6 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 { velocityPerSecond } from 'popmotion';
import { SubscriptionManager } from '../utils/subscription-manager.js';
var isFloat = function (value) {
return !isNaN(parseFloat(value));
};
/**
* `MotionValue` is used to track the state and velocity of motion values.
*
* @public
*/
var MotionValue = /** @class */ (function () {
/**
* @param init - The initiating value
* @param config - Optional configuration options
*
* - `transformer`: A function to transform incoming values with.
*
* @internal
*/
function MotionValue(init, startStopNotifier) {
var _this = this;
/**
* Duration, in milliseconds, since last updating frame.
*
* @internal
*/
this.timeDelta = 0;
/**
* Timestamp of the last time this `MotionValue` was updated.
*
* @internal
*/
this.lastUpdated = 0;
/**
* Functions to notify when the `MotionValue` updates.
*
* @internal
*/
this.updateSubscribers = new SubscriptionManager();
/**
* Functions to notify when the velocity updates.
*
* @internal
*/
this.velocityUpdateSubscribers = new SubscriptionManager();
/**
* Functions to notify when the `MotionValue` updates and `render` is set to `true`.
*
* @internal
*/
this.renderSubscribers = new SubscriptionManager();
/**
* Tracks whether this value can output a velocity. Currently this is only true
* if the value is numerical, but we might be able to widen the scope here and support
* other value types.
*
* @internal
*/
this.canTrackVelocity = false;
this.updateAndNotify = function (v, render) {
if (render === void 0) { render = true; }
_this.prev = _this.current;
_this.current = v;
// Update timestamp
var _a = getFrameData(), delta = _a.delta, timestamp = _a.timestamp;
if (_this.lastUpdated !== timestamp) {
_this.timeDelta = delta;
_this.lastUpdated = timestamp;
sync.postRender(_this.scheduleVelocityCheck);
}
// Update update subscribers
if (_this.prev !== _this.current) {
_this.updateSubscribers.notify(_this.current);
}
// Update velocity subscribers
if (_this.velocityUpdateSubscribers.getSize()) {
_this.velocityUpdateSubscribers.notify(_this.getVelocity());
}
// Update render subscribers
if (render) {
_this.renderSubscribers.notify(_this.current);
}
};
/**
* Schedule a velocity check for the next frame.
*
* This is an instanced and bound function to prevent generating a new
* function once per frame.
*
* @internal
*/
this.scheduleVelocityCheck = function () { return sync.postRender(_this.velocityCheck); };
/**
* Updates `prev` with `current` if the value hasn't been updated this frame.
* This ensures velocity calculations return `0`.
*
* This is an instanced and bound function to prevent generating a new
* function once per frame.
*
* @internal
*/
this.velocityCheck = function (_a) {
var timestamp = _a.timestamp;
if (timestamp !== _this.lastUpdated) {
_this.prev = _this.current;
_this.velocityUpdateSubscribers.notify(_this.getVelocity());
}
};
this.hasAnimated = false;
this.prev = this.current = init;
this.canTrackVelocity = isFloat(this.current);
this.onSubscription = () => { }
this.onUnsubscription = () => { }
if (startStopNotifier) {
this.onSubscription = () => {
if (this.updateSubscribers.getSize() + this.velocityUpdateSubscribers.getSize() + this.renderSubscribers.getSize() === 0) {
const unsub = startStopNotifier()
this.onUnsubscription = () => { }
if (unsub) {
this.onUnsubscription = () => {
if (this.updateSubscribers.getSize() + this.velocityUpdateSubscribers.getSize() + this.renderSubscribers.getSize() === 0) {
unsub()
}
}
}
}
}
}
}
/**
* Adds a function that will be notified when the `MotionValue` is updated.
*
* It returns a function that, when called, will cancel the subscription.
*
* When calling `onChange` inside a React component, it should be wrapped with the
* `useEffect` hook. As it returns an unsubscribe function, this should be returned
* from the `useEffect` function to ensure you don't add duplicate subscribers..
*
* @motion
*
* ```jsx
* export const MyComponent = () => {
* const x = useMotionValue(0)
* const y = useMotionValue(0)
* const opacity = useMotionValue(1)
*
* useEffect(() => {
* function updateOpacity() {
* const maxXY = Math.max(x.get(), y.get())
* const newOpacity = transform(maxXY, [0, 100], [1, 0])
* opacity.set(newOpacity)
* }
*
* const unsubscribeX = x.onChange(updateOpacity)
* const unsubscribeY = y.onChange(updateOpacity)
*
* return () => {
* unsubscribeX()
* unsubscribeY()
* }
* }, [])
*
* return <MotionDiv style={{ x }} />
* }
* ```
*
* @internalremarks
*
* We could look into a `useOnChange` hook if the above lifecycle management proves confusing.
*
* ```jsx
* useOnChange(x, () => {})
* ```
*
* @param subscriber - A function that receives the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @public
*/
MotionValue.prototype.onChange = function (subscription) {
this.onSubscription();
const unsub = this.updateSubscribers.add(subscription);
return () => {
unsub()
this.onUnsubscription()
}
};
/** Add subscribe method for Svelte store interface */
MotionValue.prototype.subscribe = function (subscription) {
return this.onChange(subscription);
};
MotionValue.prototype.clearListeners = function () {
this.updateSubscribers.clear();
this.onUnsubscription()
};
/**
* Adds a function that will be notified when the `MotionValue` requests a render.
*
* @param subscriber - A function that's provided the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @internal
*/
MotionValue.prototype.onRenderRequest = function (subscription) {
this.onSubscription()
// Render immediately
subscription(this.get());
const unsub = this.renderSubscribers.add(subscription);
return () => {
unsub()
this.onUnsubscription()
}
};
/**
* Attaches a passive effect to the `MotionValue`.
*
* @internal
*/
MotionValue.prototype.attach = function (passiveEffect) {
this.passiveEffect = passiveEffect;
};
/**
* Sets the state of the `MotionValue`.
*
* @remarks
*
* ```jsx
* const x = useMotionValue(0)
* x.set(10)
* ```
*
* @param latest - Latest value to set.
* @param render - Whether to notify render subscribers. Defaults to `true`
*
* @public
*/
MotionValue.prototype.set = function (v, render) {
if (render === void 0) { render = true; }
if (!render || !this.passiveEffect) {
this.updateAndNotify(v, render);
}
else {
this.passiveEffect(v, this.updateAndNotify);
}
};
/** Add update method for Svelte Store behavior */
MotionValue.prototype.update = function (v) {
this.set(v(this.get()));
}
/**
* Returns the latest state of `MotionValue`
*
* @returns - The latest state of `MotionValue`
*
* @public
*/
MotionValue.prototype.get = function () {
this.onSubscription()
const curr = this.current;
this.onUnsubscription()
return curr
};
/**
* @public
*/
MotionValue.prototype.getPrevious = function () {
return this.prev;
};
/**
* Returns the latest velocity of `MotionValue`
*
* @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
*
* @public
*/
MotionValue.prototype.getVelocity = function () {
// This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful
this.onSubscription()
const vel = this.canTrackVelocity
? // These casts could be avoided if parseFloat would be typed better
velocityPerSecond(parseFloat(this.current) -
parseFloat(this.prev), this.timeDelta)
: 0;
this.onUnsubscription()
return vel;
};
/**
* Registers a new animation to control this `MotionValue`. Only one
* animation can drive a `MotionValue` at one time.
*
* ```jsx
* value.start()
* ```
*
* @param animation - A function that starts the provided animation
*
* @internal
*/
MotionValue.prototype.start = function (animation) {
var _this = this;
this.stop();
return new Promise(function (resolve) {
_this.hasAnimated = true;
_this.stopAnimation = animation(resolve);
}).then(function () { return _this.clearAnimation(); });
};
/**
* Stop the currently active animation.
*
* @public
*/
MotionValue.prototype.stop = function () {
if (this.stopAnimation)
this.stopAnimation();
this.clearAnimation();
};
/**
* Returns `true` if this value is currently animating.
*
* @public
*/
MotionValue.prototype.isAnimating = function () {
return !!this.stopAnimation;
};
MotionValue.prototype.clearAnimation = function () {
this.stopAnimation = null;
};
/**
* Destroy and clean up subscribers to this `MotionValue`.
*
* The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
* handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
* created a `MotionValue` via the `motionValue` function.
*
* @public
*/
MotionValue.prototype.destroy = function () {
this.updateSubscribers.clear();
this.renderSubscribers.clear();
this.stop();
this.onUnsubscription()
};
return MotionValue;
}());
/**
* @internal
*/
function motionValue(init, startStopNotifier) {
return new MotionValue(init, startStopNotifier);
}
export { MotionValue, motionValue };