This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
import { SubscriptionManager } from '../utils/subscription-manager.mjs';
import { velocityPerSecond } from '../utils/velocity-per-second.mjs';
import { warnOnce } from '../utils/warn-once.mjs';
import { frame, frameData } from '../frameloop/frame.mjs';
const isFloat = (value) => {
return !isNaN(parseFloat(value));
};
const collectMotionValues = {
current: undefined,
};
/**
* `MotionValue` is used to track the state and velocity of motion values.
*
* @public
*/
class MotionValue {
/**
* @param init - The initiating value
* @param config - Optional configuration options
*
* - `transformer`: A function to transform incoming values with.
*
* @internal
*/
constructor(init, options = {}) {
/**
* This will be replaced by the build step with the latest version number.
* When MotionValues are provided to motion components, warn if versions are mixed.
*/
this.version = "10.18.0";
/**
* Duration, in milliseconds, since last updating frame.
*
* @internal
*/
this.timeDelta = 0;
/**
* Timestamp of the last time this `MotionValue` was updated.
*
* @internal
*/
this.lastUpdated = 0;
/**
* 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;
/**
* An object containing a SubscriptionManager for each active event.
*/
this.events = {};
this.updateAndNotify = (v, render = true) => {
this.prev = this.current;
this.current = v;
// Update timestamp
const { delta, timestamp } = frameData;
if (this.lastUpdated !== timestamp) {
this.timeDelta = delta;
this.lastUpdated = timestamp;
frame.postRender(this.scheduleVelocityCheck);
}
// Update update subscribers
if (this.prev !== this.current && this.events.change) {
this.events.change.notify(this.current);
}
// Update velocity subscribers
if (this.events.velocityChange) {
this.events.velocityChange.notify(this.getVelocity());
}
// Update render subscribers
if (render && this.events.renderRequest) {
this.events.renderRequest.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 = () => frame.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 = ({ timestamp }) => {
if (timestamp !== this.lastUpdated) {
this.prev = this.current;
if (this.events.velocityChange) {
this.events.velocityChange.notify(this.getVelocity());
}
}
};
this.hasAnimated = false;
this.prev = this.current = init;
this.canTrackVelocity = isFloat(this.current);
this.owner = options.owner;
}
/**
* 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..
*
* ```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.on("change", updateOpacity)
* const unsubscribeY = y.on("change", updateOpacity)
*
* return () => {
* unsubscribeX()
* unsubscribeY()
* }
* }, [])
*
* return <motion.div style={{ x }} />
* }
* ```
*
* @param subscriber - A function that receives the latest value.
* @returns A function that, when called, will cancel this subscription.
*
* @deprecated
*/
onChange(subscription) {
if (process.env.NODE_ENV !== "production") {
warnOnce(false, `value.onChange(callback) is deprecated. Switch to value.on("change", callback).`);
}
return this.on("change", subscription);
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = new SubscriptionManager();
}
const unsubscribe = this.events[eventName].add(callback);
if (eventName === "change") {
return () => {
unsubscribe();
/**
* If we have no more change listeners by the start
* of the next frame, stop active animations.
*/
frame.read(() => {
if (!this.events.change.getSize()) {
this.stop();
}
});
};
}
return unsubscribe;
}
clearListeners() {
for (const eventManagers in this.events) {
this.events[eventManagers].clear();
}
}
/**
* Attaches a passive effect to the `MotionValue`.
*
* @internal
*/
attach(passiveEffect, stopPassiveEffect) {
this.passiveEffect = passiveEffect;
this.stopPassiveEffect = stopPassiveEffect;
}
/**
* 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
*/
set(v, render = true) {
if (!render || !this.passiveEffect) {
this.updateAndNotify(v, render);
}
else {
this.passiveEffect(v, this.updateAndNotify);
}
}
setWithVelocity(prev, current, delta) {
this.set(current);
this.prev = prev;
this.timeDelta = delta;
}
/**
* Set the state of the `MotionValue`, stopping any active animations,
* effects, and resets velocity to `0`.
*/
jump(v) {
this.updateAndNotify(v);
this.prev = v;
this.stop();
if (this.stopPassiveEffect)
this.stopPassiveEffect();
}
/**
* Returns the latest state of `MotionValue`
*
* @returns - The latest state of `MotionValue`
*
* @public
*/
get() {
if (collectMotionValues.current) {
collectMotionValues.current.push(this);
}
return this.current;
}
/**
* @public
*/
getPrevious() {
return this.prev;
}
/**
* Returns the latest velocity of `MotionValue`
*
* @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
*
* @public
*/
getVelocity() {
// This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful
return this.canTrackVelocity
? // These casts could be avoided if parseFloat would be typed better
velocityPerSecond(parseFloat(this.current) -
parseFloat(this.prev), this.timeDelta)
: 0;
}
/**
* 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
*/
start(startAnimation) {
this.stop();
return new Promise((resolve) => {
this.hasAnimated = true;
this.animation = startAnimation(resolve);
if (this.events.animationStart) {
this.events.animationStart.notify();
}
}).then(() => {
if (this.events.animationComplete) {
this.events.animationComplete.notify();
}
this.clearAnimation();
});
}
/**
* Stop the currently active animation.
*
* @public
*/
stop() {
if (this.animation) {
this.animation.stop();
if (this.events.animationCancel) {
this.events.animationCancel.notify();
}
}
this.clearAnimation();
}
/**
* Returns `true` if this value is currently animating.
*
* @public
*/
isAnimating() {
return !!this.animation;
}
clearAnimation() {
delete this.animation;
}
/**
* 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
*/
destroy() {
this.clearListeners();
this.stop();
if (this.stopPassiveEffect) {
this.stopPassiveEffect();
}
}
}
function motionValue(init, options) {
return new MotionValue(init, options);
}
export { MotionValue, collectMotionValues, motionValue };

View File

@@ -0,0 +1,14 @@
import { warnOnce } from '../../utils/warn-once.mjs';
import { useScroll } from '../use-scroll.mjs';
/**
* @deprecated useElementScroll is deprecated. Convert to useScroll({ container: ref })
*/
function useElementScroll(ref) {
if (process.env.NODE_ENV === "development") {
warnOnce(false, "useElementScroll is deprecated. Convert to useScroll({ container: ref }).");
}
return useScroll({ container: ref });
}
export { useElementScroll };

View File

@@ -0,0 +1,14 @@
import { warnOnce } from '../../utils/warn-once.mjs';
import { useScroll } from '../use-scroll.mjs';
/**
* @deprecated useViewportScroll is deprecated. Convert to useScroll()
*/
function useViewportScroll() {
if (process.env.NODE_ENV !== "production") {
warnOnce(false, "useViewportScroll is deprecated. Convert to useScroll().");
}
return useScroll();
}
export { useViewportScroll };

View File

@@ -0,0 +1,40 @@
import { rgba } from './rgba.mjs';
import { isColorString } from './utils.mjs';
function parseHex(v) {
let r = "";
let g = "";
let b = "";
let a = "";
// If we have 6 characters, ie #FF0000
if (v.length > 5) {
r = v.substring(1, 3);
g = v.substring(3, 5);
b = v.substring(5, 7);
a = v.substring(7, 9);
// Or we have 3 characters, ie #F00
}
else {
r = v.substring(1, 2);
g = v.substring(2, 3);
b = v.substring(3, 4);
a = v.substring(4, 5);
r += r;
g += g;
b += b;
a += a;
}
return {
red: parseInt(r, 16),
green: parseInt(g, 16),
blue: parseInt(b, 16),
alpha: a ? parseInt(a, 16) / 255 : 1,
};
}
const hex = {
test: isColorString("#"),
parse: parseHex,
transform: rgba.transform,
};
export { hex };

View File

@@ -0,0 +1,22 @@
import { alpha } from '../numbers/index.mjs';
import { percent } from '../numbers/units.mjs';
import { sanitize } from '../utils.mjs';
import { isColorString, splitColor } from './utils.mjs';
const hsla = {
test: isColorString("hsl", "hue"),
parse: splitColor("hue", "saturation", "lightness"),
transform: ({ hue, saturation, lightness, alpha: alpha$1 = 1 }) => {
return ("hsla(" +
Math.round(hue) +
", " +
percent.transform(sanitize(saturation)) +
", " +
percent.transform(sanitize(lightness)) +
", " +
sanitize(alpha.transform(alpha$1)) +
")");
},
};
export { hsla };

View File

@@ -0,0 +1,28 @@
import { isString } from '../utils.mjs';
import { hex } from './hex.mjs';
import { hsla } from './hsla.mjs';
import { rgba } from './rgba.mjs';
const color = {
test: (v) => rgba.test(v) || hex.test(v) || hsla.test(v),
parse: (v) => {
if (rgba.test(v)) {
return rgba.parse(v);
}
else if (hsla.test(v)) {
return hsla.parse(v);
}
else {
return hex.parse(v);
}
},
transform: (v) => {
return isString(v)
? v
: v.hasOwnProperty("red")
? rgba.transform(v)
: hsla.transform(v);
},
};
export { color };

View File

@@ -0,0 +1,25 @@
import { clamp } from '../../../utils/clamp.mjs';
import { number, alpha } from '../numbers/index.mjs';
import { sanitize } from '../utils.mjs';
import { isColorString, splitColor } from './utils.mjs';
const clampRgbUnit = (v) => clamp(0, 255, v);
const rgbUnit = {
...number,
transform: (v) => Math.round(clampRgbUnit(v)),
};
const rgba = {
test: isColorString("rgb", "red"),
parse: splitColor("red", "green", "blue"),
transform: ({ red, green, blue, alpha: alpha$1 = 1 }) => "rgba(" +
rgbUnit.transform(red) +
", " +
rgbUnit.transform(green) +
", " +
rgbUnit.transform(blue) +
", " +
sanitize(alpha.transform(alpha$1)) +
")",
};
export { rgbUnit, rgba };

View File

@@ -0,0 +1,23 @@
import { isString, singleColorRegex, floatRegex } from '../utils.mjs';
/**
* Returns true if the provided string is a color, ie rgba(0,0,0,0) or #000,
* but false if a number or multiple colors
*/
const isColorString = (type, testProp) => (v) => {
return Boolean((isString(v) && singleColorRegex.test(v) && v.startsWith(type)) ||
(testProp && Object.prototype.hasOwnProperty.call(v, testProp)));
};
const splitColor = (aName, bName, cName) => (v) => {
if (!isString(v))
return v;
const [a, b, c, alpha] = v.match(floatRegex);
return {
[aName]: parseFloat(a),
[bName]: parseFloat(b),
[cName]: parseFloat(c),
alpha: alpha !== undefined ? parseFloat(alpha) : 1,
};
};
export { isColorString, splitColor };

View File

@@ -0,0 +1,30 @@
import { complex } from './index.mjs';
import { floatRegex } from '../utils.mjs';
/**
* Properties that should default to 1 or 100%
*/
const maxDefaults = new Set(["brightness", "contrast", "saturate", "opacity"]);
function applyDefaultFilter(v) {
const [name, value] = v.slice(0, -1).split("(");
if (name === "drop-shadow")
return v;
const [number] = value.match(floatRegex) || [];
if (!number)
return v;
const unit = value.replace(number, "");
let defaultValue = maxDefaults.has(name) ? 1 : 0;
if (number !== value)
defaultValue *= 100;
return name + "(" + defaultValue + unit + ")";
}
const functionRegex = /([a-z-]*)\(.*?\)/g;
const filter = {
...complex,
getAnimatableNone: (v) => {
const functions = v.match(functionRegex);
return functions ? functions.map(applyDefaultFilter).join(" ") : v;
},
};
export { filter };

View File

@@ -0,0 +1,92 @@
import { cssVariableRegex } from '../../../render/dom/utils/is-css-variable.mjs';
import { noop } from '../../../utils/noop.mjs';
import { color } from '../color/index.mjs';
import { number } from '../numbers/index.mjs';
import { colorRegex, floatRegex, isString, sanitize } from '../utils.mjs';
function test(v) {
var _a, _b;
return (isNaN(v) &&
isString(v) &&
(((_a = v.match(floatRegex)) === null || _a === void 0 ? void 0 : _a.length) || 0) +
(((_b = v.match(colorRegex)) === null || _b === void 0 ? void 0 : _b.length) || 0) >
0);
}
const cssVarTokeniser = {
regex: cssVariableRegex,
countKey: "Vars",
token: "${v}",
parse: noop,
};
const colorTokeniser = {
regex: colorRegex,
countKey: "Colors",
token: "${c}",
parse: color.parse,
};
const numberTokeniser = {
regex: floatRegex,
countKey: "Numbers",
token: "${n}",
parse: number.parse,
};
function tokenise(info, { regex, countKey, token, parse }) {
const matches = info.tokenised.match(regex);
if (!matches)
return;
info["num" + countKey] = matches.length;
info.tokenised = info.tokenised.replace(regex, token);
info.values.push(...matches.map(parse));
}
function analyseComplexValue(value) {
const originalValue = value.toString();
const info = {
value: originalValue,
tokenised: originalValue,
values: [],
numVars: 0,
numColors: 0,
numNumbers: 0,
};
if (info.value.includes("var(--"))
tokenise(info, cssVarTokeniser);
tokenise(info, colorTokeniser);
tokenise(info, numberTokeniser);
return info;
}
function parseComplexValue(v) {
return analyseComplexValue(v).values;
}
function createTransformer(source) {
const { values, numColors, numVars, tokenised } = analyseComplexValue(source);
const numValues = values.length;
return (v) => {
let output = tokenised;
for (let i = 0; i < numValues; i++) {
if (i < numVars) {
output = output.replace(cssVarTokeniser.token, v[i]);
}
else if (i < numVars + numColors) {
output = output.replace(colorTokeniser.token, color.transform(v[i]));
}
else {
output = output.replace(numberTokeniser.token, sanitize(v[i]));
}
}
return output;
};
}
const convertNumbersToZero = (v) => typeof v === "number" ? 0 : v;
function getAnimatableNone(v) {
const parsed = parseComplexValue(v);
const transformer = createTransformer(v);
return transformer(parsed.map(convertNumbersToZero));
}
const complex = {
test,
parse: parseComplexValue,
createTransformer,
getAnimatableNone,
};
export { analyseComplexValue, complex };

View File

@@ -0,0 +1,17 @@
import { clamp } from '../../../utils/clamp.mjs';
const number = {
test: (v) => typeof v === "number",
parse: parseFloat,
transform: (v) => v,
};
const alpha = {
...number,
transform: (v) => clamp(0, 1, v),
};
const scale = {
...number,
default: 1,
};
export { alpha, number, scale };

View File

@@ -0,0 +1,19 @@
import { isString } from '../utils.mjs';
const createUnitType = (unit) => ({
test: (v) => isString(v) && v.endsWith(unit) && v.split(" ").length === 1,
parse: parseFloat,
transform: (v) => `${v}${unit}`,
});
const degrees = createUnitType("deg");
const percent = createUnitType("%");
const px = createUnitType("px");
const vh = createUnitType("vh");
const vw = createUnitType("vw");
const progressPercentage = {
...percent,
parse: (v) => percent.parse(v) / 100,
transform: (v) => percent.transform(v * 100),
};
export { degrees, percent, progressPercentage, px, vh, vw };

View File

@@ -0,0 +1,15 @@
/**
* TODO: When we move from string as a source of truth to data models
* everything in this folder should probably be referred to as models vs types
*/
// If this number is a decimal, make it just five decimal places
// to avoid exponents
const sanitize = (v) => Math.round(v * 100000) / 100000;
const floatRegex = /(-)?([\d]*\.?[\d])+/g;
const colorRegex = /(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))/gi;
const singleColorRegex = /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))$/i;
function isString(v) {
return typeof v === "string";
}
export { colorRegex, floatRegex, isString, sanitize, singleColorRegex };

View File

@@ -0,0 +1,37 @@
import { useMotionValue } from './use-motion-value.mjs';
import { useIsomorphicLayoutEffect } from '../utils/use-isomorphic-effect.mjs';
import { cancelFrame, frame } from '../frameloop/frame.mjs';
function useCombineMotionValues(values, combineValues) {
/**
* Initialise the returned motion value. This remains the same between renders.
*/
const value = useMotionValue(combineValues());
/**
* Create a function that will update the template motion value with the latest values.
* This is pre-bound so whenever a motion value updates it can schedule its
* execution in Framesync. If it's already been scheduled it won't be fired twice
* in a single frame.
*/
const updateValue = () => value.set(combineValues());
/**
* Synchronously update the motion value with the latest values during the render.
* This ensures that within a React render, the styles applied to the DOM are up-to-date.
*/
updateValue();
/**
* Subscribe to all motion values found within the template. Whenever any of them change,
* schedule an update.
*/
useIsomorphicLayoutEffect(() => {
const scheduleUpdate = () => frame.update(updateValue, false, true);
const subscriptions = values.map((v) => v.on("change", scheduleUpdate));
return () => {
subscriptions.forEach((unsubscribe) => unsubscribe());
cancelFrame(updateValue);
};
});
return value;
}
export { useCombineMotionValues };

View File

@@ -0,0 +1,19 @@
import { collectMotionValues } from './index.mjs';
import { useCombineMotionValues } from './use-combine-values.mjs';
function useComputed(compute) {
/**
* Open session of collectMotionValues. Any MotionValue that calls get()
* will be saved into this array.
*/
collectMotionValues.current = [];
compute();
const value = useCombineMotionValues(collectMotionValues.current, compute);
/**
* Synchronously close session of collectMotionValues.
*/
collectMotionValues.current = undefined;
return value;
}
export { useComputed };

View File

@@ -0,0 +1,52 @@
import { useTransform } from './use-transform.mjs';
import { invariant, warning } from '../utils/errors.mjs';
import { useMotionValue } from './use-motion-value.mjs';
import { MotionContext } from '../context/MotionContext/index.mjs';
import { useContext } from 'react';
// Keep things reasonable and avoid scale: Infinity. In practise we might need
// to add another value, opacity, that could interpolate scaleX/Y [0,0.01] => [0,1]
// to simply hide content at unreasonable scales.
const maxScale = 100000;
const invertScale = (scale) => scale > 0.001 ? 1 / scale : maxScale;
let hasWarned = false;
/**
* Returns a `MotionValue` each for `scaleX` and `scaleY` that update with the inverse
* of their respective parent scales.
*
* This is useful for undoing the distortion of content when scaling a parent component.
*
* By default, `useInvertedScale` will automatically fetch `scaleX` and `scaleY` from the nearest parent.
* By passing other `MotionValue`s in as `useInvertedScale({ scaleX, scaleY })`, it will invert the output
* of those instead.
*
* ```jsx
* const MyComponent = () => {
* const { scaleX, scaleY } = useInvertedScale()
* return <motion.div style={{ scaleX, scaleY }} />
* }
* ```
*
* @deprecated
*/
function useInvertedScale(scale) {
let parentScaleX = useMotionValue(1);
let parentScaleY = useMotionValue(1);
const { visualElement } = useContext(MotionContext);
invariant(!!(scale || visualElement), "If no scale values are provided, useInvertedScale must be used within a child of another motion component.");
warning(hasWarned, "useInvertedScale is deprecated and will be removed in 3.0. Use the layout prop instead.");
hasWarned = true;
if (scale) {
parentScaleX = scale.scaleX || parentScaleX;
parentScaleY = scale.scaleY || parentScaleY;
}
else if (visualElement) {
parentScaleX = visualElement.getValue("scaleX", 1);
parentScaleY = visualElement.getValue("scaleY", 1);
}
const scaleX = useTransform(parentScaleX, invertScale);
const scaleY = useTransform(parentScaleY, invertScale);
return { scaleX, scaleY };
}
export { invertScale, useInvertedScale };

View File

@@ -0,0 +1,45 @@
import { useCombineMotionValues } from './use-combine-values.mjs';
import { isMotionValue } from './utils/is-motion-value.mjs';
/**
* Combine multiple motion values into a new one using a string template literal.
*
* ```jsx
* import {
* motion,
* useSpring,
* useMotionValue,
* useMotionTemplate
* } from "framer-motion"
*
* function Component() {
* const shadowX = useSpring(0)
* const shadowY = useMotionValue(0)
* const shadow = useMotionTemplate`drop-shadow(${shadowX}px ${shadowY}px 20px rgba(0,0,0,0.3))`
*
* return <motion.div style={{ filter: shadow }} />
* }
* ```
*
* @public
*/
function useMotionTemplate(fragments, ...values) {
/**
* Create a function that will build a string from the latest motion values.
*/
const numFragments = fragments.length;
function buildValue() {
let output = ``;
for (let i = 0; i < numFragments; i++) {
output += fragments[i];
const value = values[i];
if (value) {
output += isMotionValue(value) ? value.get() : value;
}
}
return output;
}
return useCombineMotionValues(values.filter(isMotionValue), buildValue);
}
export { useMotionTemplate };

View File

@@ -0,0 +1,38 @@
import { useContext, useState, useEffect } from 'react';
import { motionValue } from './index.mjs';
import { MotionConfigContext } from '../context/MotionConfigContext.mjs';
import { useConstant } from '../utils/use-constant.mjs';
/**
* Creates a `MotionValue` to track the state and velocity of a value.
*
* Usually, these are created automatically. For advanced use-cases, like use with `useTransform`, you can create `MotionValue`s externally and pass them into the animated component via the `style` prop.
*
* ```jsx
* export const MyComponent = () => {
* const scale = useMotionValue(1)
*
* return <motion.div style={{ scale }} />
* }
* ```
*
* @param initial - The initial state.
*
* @public
*/
function useMotionValue(initial) {
const value = useConstant(() => motionValue(initial));
/**
* If this motion value is being used in static mode, like on
* the Framer canvas, force components to rerender when the motion
* value is updated.
*/
const { isStatic } = useContext(MotionConfigContext);
if (isStatic) {
const [, setLatest] = useState(initial);
useEffect(() => value.on("change", setLatest), []);
}
return value;
}
export { useMotionValue };

View File

@@ -0,0 +1,39 @@
import { motionValue } from './index.mjs';
import { useConstant } from '../utils/use-constant.mjs';
import { useEffect } from 'react';
import { warning } from '../utils/errors.mjs';
import { scrollInfo } from '../render/dom/scroll/track.mjs';
import { useIsomorphicLayoutEffect } from '../utils/use-isomorphic-effect.mjs';
function refWarning(name, ref) {
warning(Boolean(!ref || ref.current), `You have defined a ${name} options but the provided ref is not yet hydrated, probably because it's defined higher up the tree. Try calling useScroll() in the same component as the ref, or setting its \`layoutEffect: false\` option.`);
}
const createScrollMotionValues = () => ({
scrollX: motionValue(0),
scrollY: motionValue(0),
scrollXProgress: motionValue(0),
scrollYProgress: motionValue(0),
});
function useScroll({ container, target, layoutEffect = true, ...options } = {}) {
const values = useConstant(createScrollMotionValues);
const useLifecycleEffect = layoutEffect
? useIsomorphicLayoutEffect
: useEffect;
useLifecycleEffect(() => {
refWarning("target", target);
refWarning("container", container);
return scrollInfo(({ x, y }) => {
values.scrollX.set(x.current);
values.scrollXProgress.set(x.progress);
values.scrollY.set(y.current);
values.scrollYProgress.set(y.progress);
}, {
...options,
container: (container === null || container === void 0 ? void 0 : container.current) || undefined,
target: (target === null || target === void 0 ? void 0 : target.current) || undefined,
});
}, [container, target, JSON.stringify(options.offset)]);
return values;
}
export { useScroll };

View File

@@ -0,0 +1,77 @@
import { useContext, useRef, useInsertionEffect } from 'react';
import { isMotionValue } from './utils/is-motion-value.mjs';
import { useMotionValue } from './use-motion-value.mjs';
import { MotionConfigContext } from '../context/MotionConfigContext.mjs';
import { useIsomorphicLayoutEffect } from '../utils/use-isomorphic-effect.mjs';
import { animateValue } from '../animation/animators/js/index.mjs';
import { millisecondsToSeconds } from '../utils/time-conversion.mjs';
import { frameData } from '../frameloop/frame.mjs';
/**
* Creates a `MotionValue` that, when `set`, will use a spring animation to animate to its new state.
*
* It can either work as a stand-alone `MotionValue` by initialising it with a value, or as a subscriber
* to another `MotionValue`.
*
* @remarks
*
* ```jsx
* const x = useSpring(0, { stiffness: 300 })
* const y = useSpring(x, { damping: 10 })
* ```
*
* @param inputValue - `MotionValue` or number. If provided a `MotionValue`, when the input `MotionValue` changes, the created `MotionValue` will spring towards that value.
* @param springConfig - Configuration options for the spring.
* @returns `MotionValue`
*
* @public
*/
function useSpring(source, config = {}) {
const { isStatic } = useContext(MotionConfigContext);
const activeSpringAnimation = useRef(null);
const value = useMotionValue(isMotionValue(source) ? source.get() : source);
const stopAnimation = () => {
if (activeSpringAnimation.current) {
activeSpringAnimation.current.stop();
}
};
useInsertionEffect(() => {
return value.attach((v, set) => {
/**
* A more hollistic approach to this might be to use isStatic to fix VisualElement animations
* at that level, but this will work for now
*/
if (isStatic)
return set(v);
stopAnimation();
activeSpringAnimation.current = animateValue({
keyframes: [value.get(), v],
velocity: value.getVelocity(),
type: "spring",
restDelta: 0.001,
restSpeed: 0.01,
...config,
onUpdate: set,
});
/**
* If we're between frames, resync the animation to the frameloop.
*/
if (!frameData.isProcessing) {
const delta = performance.now() - frameData.timestamp;
if (delta < 30) {
activeSpringAnimation.current.time =
millisecondsToSeconds(delta);
}
}
return value.get();
}, stopAnimation);
}, [JSON.stringify(config)]);
useIsomorphicLayoutEffect(() => {
if (isMotionValue(source)) {
return source.on("change", (v) => value.set(parseFloat(v)));
}
}, [value]);
return value;
}
export { useSpring };

View File

@@ -0,0 +1,10 @@
import { useAnimationFrame } from '../utils/use-animation-frame.mjs';
import { useMotionValue } from './use-motion-value.mjs';
function useTime() {
const time = useMotionValue(0);
useAnimationFrame((t) => time.set(t));
return time;
}
export { useTime };

View File

@@ -0,0 +1,29 @@
import { transform } from '../utils/transform.mjs';
import { useCombineMotionValues } from './use-combine-values.mjs';
import { useConstant } from '../utils/use-constant.mjs';
import { useComputed } from './use-computed.mjs';
function useTransform(input, inputRangeOrTransformer, outputRange, options) {
if (typeof input === "function") {
return useComputed(input);
}
const transformer = typeof inputRangeOrTransformer === "function"
? inputRangeOrTransformer
: transform(inputRangeOrTransformer, outputRange, options);
return Array.isArray(input)
? useListTransform(input, transformer)
: useListTransform([input], ([latest]) => transformer(latest));
}
function useListTransform(values, transformer) {
const latest = useConstant(() => []);
return useCombineMotionValues(values, () => {
latest.length = 0;
const numValues = values.length;
for (let i = 0; i < numValues; i++) {
latest[i] = values[i].get();
}
return transformer(latest);
});
}
export { useTransform };

View File

@@ -0,0 +1,23 @@
import { useMotionValueEvent } from '../utils/use-motion-value-event.mjs';
import { useMotionValue } from './use-motion-value.mjs';
/**
* Creates a `MotionValue` that updates when the velocity of the provided `MotionValue` changes.
*
* ```javascript
* const x = useMotionValue(0)
* const xVelocity = useVelocity(x)
* const xAcceleration = useVelocity(xVelocity)
* ```
*
* @public
*/
function useVelocity(value) {
const velocity = useMotionValue(value.getVelocity());
useMotionValueEvent(value, "velocityChange", (newVelocity) => {
velocity.set(newVelocity);
});
return velocity;
}
export { useVelocity };

View File

@@ -0,0 +1,50 @@
import { isCSSVariableName } from '../../render/dom/utils/is-css-variable.mjs';
import { transformProps } from '../../render/html/utils/transform.mjs';
import { addUniqueItem, removeItem } from '../../utils/array.mjs';
import { useConstant } from '../../utils/use-constant.mjs';
import { MotionValue } from '../index.mjs';
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
class WillChangeMotionValue extends MotionValue {
constructor() {
super(...arguments);
this.members = [];
this.transforms = new Set();
}
add(name) {
let memberName;
if (transformProps.has(name)) {
this.transforms.add(name);
memberName = "transform";
}
else if (!name.startsWith("origin") &&
!isCSSVariableName(name) &&
name !== "willChange") {
memberName = camelToDash(name);
}
if (memberName) {
addUniqueItem(this.members, memberName);
this.update();
}
}
remove(name) {
if (transformProps.has(name)) {
this.transforms.delete(name);
if (!this.transforms.size) {
removeItem(this.members, "transform");
}
}
else {
removeItem(this.members, camelToDash(name));
}
this.update();
}
update() {
this.set(this.members.length ? this.members.join(", ") : "auto");
}
}
function useWillChange() {
return useConstant(() => new WillChangeMotionValue("auto"));
}
export { WillChangeMotionValue, useWillChange };

View File

@@ -0,0 +1,7 @@
import { isMotionValue } from '../utils/is-motion-value.mjs';
function isWillChangeMotionValue(value) {
return Boolean(isMotionValue(value) && value.add);
}
export { isWillChangeMotionValue };

View File

@@ -0,0 +1,3 @@
const isMotionValue = (value) => Boolean(value && value.getVelocity);
export { isMotionValue };

View File

@@ -0,0 +1,16 @@
import { isCustomValue } from '../../utils/resolve-value.mjs';
import { isMotionValue } from './is-motion-value.mjs';
/**
* If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
*
* TODO: Remove and move to library
*/
function resolveMotionValue(value) {
const unwrappedValue = isMotionValue(value) ? value.get() : value;
return isCustomValue(unwrappedValue)
? unwrappedValue.toValue()
: unwrappedValue;
}
export { resolveMotionValue };