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,509 @@
import { warning, invariant } from '../utils/errors.mjs';
import { createBox } from '../projection/geometry/models.mjs';
import { isRefObject } from '../utils/is-ref-object.mjs';
import { initPrefersReducedMotion } from '../utils/reduced-motion/index.mjs';
import { hasReducedMotionListener, prefersReducedMotion } from '../utils/reduced-motion/state.mjs';
import { SubscriptionManager } from '../utils/subscription-manager.mjs';
import { motionValue } from '../value/index.mjs';
import { isWillChangeMotionValue } from '../value/use-will-change/is.mjs';
import { isMotionValue } from '../value/utils/is-motion-value.mjs';
import { transformProps } from './html/utils/transform.mjs';
import { isControllingVariants, isVariantNode } from './utils/is-controlling-variants.mjs';
import { isVariantLabel } from './utils/is-variant-label.mjs';
import { updateMotionValuesFromProps } from './utils/motion-values.mjs';
import { resolveVariantFromProps } from './utils/resolve-variants.mjs';
import { warnOnce } from '../utils/warn-once.mjs';
import { featureDefinitions } from '../motion/features/definitions.mjs';
import { variantProps } from './utils/variant-props.mjs';
import { visualElementStore } from './store.mjs';
import { frame, cancelFrame } from '../frameloop/frame.mjs';
const featureNames = Object.keys(featureDefinitions);
const numFeatures = featureNames.length;
const propEventHandlers = [
"AnimationStart",
"AnimationComplete",
"Update",
"BeforeLayoutMeasure",
"LayoutMeasure",
"LayoutAnimationStart",
"LayoutAnimationComplete",
];
const numVariantProps = variantProps.length;
/**
* A VisualElement is an imperative abstraction around UI elements such as
* HTMLElement, SVGElement, Three.Object3D etc.
*/
class VisualElement {
constructor({ parent, props, presenceContext, reducedMotionConfig, visualState, }, options = {}) {
/**
* A reference to the current underlying Instance, e.g. a HTMLElement
* or Three.Mesh etc.
*/
this.current = null;
/**
* A set containing references to this VisualElement's children.
*/
this.children = new Set();
/**
* Determine what role this visual element should take in the variant tree.
*/
this.isVariantNode = false;
this.isControllingVariants = false;
/**
* Decides whether this VisualElement should animate in reduced motion
* mode.
*
* TODO: This is currently set on every individual VisualElement but feels
* like it could be set globally.
*/
this.shouldReduceMotion = null;
/**
* A map of all motion values attached to this visual element. Motion
* values are source of truth for any given animated value. A motion
* value might be provided externally by the component via props.
*/
this.values = new Map();
/**
* Cleanup functions for active features (hover/tap/exit etc)
*/
this.features = {};
/**
* A map of every subscription that binds the provided or generated
* motion values onChange listeners to this visual element.
*/
this.valueSubscriptions = new Map();
/**
* A reference to the previously-provided motion values as returned
* from scrapeMotionValuesFromProps. We use the keys in here to determine
* if any motion values need to be removed after props are updated.
*/
this.prevMotionValues = {};
/**
* An object containing a SubscriptionManager for each active event.
*/
this.events = {};
/**
* An object containing an unsubscribe function for each prop event subscription.
* For example, every "Update" event can have multiple subscribers via
* VisualElement.on(), but only one of those can be defined via the onUpdate prop.
*/
this.propEventSubscriptions = {};
this.notifyUpdate = () => this.notify("Update", this.latestValues);
this.render = () => {
if (!this.current)
return;
this.triggerBuild();
this.renderInstance(this.current, this.renderState, this.props.style, this.projection);
};
this.scheduleRender = () => frame.render(this.render, false, true);
const { latestValues, renderState } = visualState;
this.latestValues = latestValues;
this.baseTarget = { ...latestValues };
this.initialValues = props.initial ? { ...latestValues } : {};
this.renderState = renderState;
this.parent = parent;
this.props = props;
this.presenceContext = presenceContext;
this.depth = parent ? parent.depth + 1 : 0;
this.reducedMotionConfig = reducedMotionConfig;
this.options = options;
this.isControllingVariants = isControllingVariants(props);
this.isVariantNode = isVariantNode(props);
if (this.isVariantNode) {
this.variantChildren = new Set();
}
this.manuallyAnimateOnMount = Boolean(parent && parent.current);
/**
* Any motion values that are provided to the element when created
* aren't yet bound to the element, as this would technically be impure.
* However, we iterate through the motion values and set them to the
* initial values for this component.
*
* TODO: This is impure and we should look at changing this to run on mount.
* Doing so will break some tests but this isn't neccessarily a breaking change,
* more a reflection of the test.
*/
const { willChange, ...initialMotionValues } = this.scrapeMotionValuesFromProps(props, {});
for (const key in initialMotionValues) {
const value = initialMotionValues[key];
if (latestValues[key] !== undefined && isMotionValue(value)) {
value.set(latestValues[key], false);
if (isWillChangeMotionValue(willChange)) {
willChange.add(key);
}
}
}
}
/**
* This method takes React props and returns found MotionValues. For example, HTML
* MotionValues will be found within the style prop, whereas for Three.js within attribute arrays.
*
* This isn't an abstract method as it needs calling in the constructor, but it is
* intended to be one.
*/
scrapeMotionValuesFromProps(_props, _prevProps) {
return {};
}
mount(instance) {
this.current = instance;
visualElementStore.set(instance, this);
if (this.projection && !this.projection.instance) {
this.projection.mount(instance);
}
if (this.parent && this.isVariantNode && !this.isControllingVariants) {
this.removeFromVariantTree = this.parent.addVariantChild(this);
}
this.values.forEach((value, key) => this.bindToMotionValue(key, value));
if (!hasReducedMotionListener.current) {
initPrefersReducedMotion();
}
this.shouldReduceMotion =
this.reducedMotionConfig === "never"
? false
: this.reducedMotionConfig === "always"
? true
: prefersReducedMotion.current;
if (process.env.NODE_ENV !== "production") {
warnOnce(this.shouldReduceMotion !== true, "You have Reduced Motion enabled on your device. Animations may not appear as expected.");
}
if (this.parent)
this.parent.children.add(this);
this.update(this.props, this.presenceContext);
}
unmount() {
visualElementStore.delete(this.current);
this.projection && this.projection.unmount();
cancelFrame(this.notifyUpdate);
cancelFrame(this.render);
this.valueSubscriptions.forEach((remove) => remove());
this.removeFromVariantTree && this.removeFromVariantTree();
this.parent && this.parent.children.delete(this);
for (const key in this.events) {
this.events[key].clear();
}
for (const key in this.features) {
this.features[key].unmount();
}
this.current = null;
}
bindToMotionValue(key, value) {
const valueIsTransform = transformProps.has(key);
const removeOnChange = value.on("change", (latestValue) => {
this.latestValues[key] = latestValue;
this.props.onUpdate &&
frame.update(this.notifyUpdate, false, true);
if (valueIsTransform && this.projection) {
this.projection.isTransformDirty = true;
}
});
const removeOnRenderRequest = value.on("renderRequest", this.scheduleRender);
this.valueSubscriptions.set(key, () => {
removeOnChange();
removeOnRenderRequest();
});
}
sortNodePosition(other) {
/**
* If these nodes aren't even of the same type we can't compare their depth.
*/
if (!this.current ||
!this.sortInstanceNodePosition ||
this.type !== other.type) {
return 0;
}
return this.sortInstanceNodePosition(this.current, other.current);
}
loadFeatures({ children, ...renderedProps }, isStrict, preloadedFeatures, initialLayoutGroupConfig) {
let ProjectionNodeConstructor;
let MeasureLayout;
/**
* If we're in development mode, check to make sure we're not rendering a motion component
* as a child of LazyMotion, as this will break the file-size benefits of using it.
*/
if (process.env.NODE_ENV !== "production" &&
preloadedFeatures &&
isStrict) {
const strictMessage = "You have rendered a `motion` component within a `LazyMotion` component. This will break tree shaking. Import and render a `m` component instead.";
renderedProps.ignoreStrict
? warning(false, strictMessage)
: invariant(false, strictMessage);
}
for (let i = 0; i < numFeatures; i++) {
const name = featureNames[i];
const { isEnabled, Feature: FeatureConstructor, ProjectionNode, MeasureLayout: MeasureLayoutComponent, } = featureDefinitions[name];
if (ProjectionNode)
ProjectionNodeConstructor = ProjectionNode;
if (isEnabled(renderedProps)) {
if (!this.features[name] && FeatureConstructor) {
this.features[name] = new FeatureConstructor(this);
}
if (MeasureLayoutComponent) {
MeasureLayout = MeasureLayoutComponent;
}
}
}
if ((this.type === "html" || this.type === "svg") &&
!this.projection &&
ProjectionNodeConstructor) {
this.projection = new ProjectionNodeConstructor(this.latestValues, this.parent && this.parent.projection);
const { layoutId, layout, drag, dragConstraints, layoutScroll, layoutRoot, } = renderedProps;
this.projection.setOptions({
layoutId,
layout,
alwaysMeasureLayout: Boolean(drag) ||
(dragConstraints && isRefObject(dragConstraints)),
visualElement: this,
scheduleRender: () => this.scheduleRender(),
/**
* TODO: Update options in an effect. This could be tricky as it'll be too late
* to update by the time layout animations run.
* We also need to fix this safeToRemove by linking it up to the one returned by usePresence,
* ensuring it gets called if there's no potential layout animations.
*
*/
animationType: typeof layout === "string" ? layout : "both",
initialPromotionConfig: initialLayoutGroupConfig,
layoutScroll,
layoutRoot,
});
}
return MeasureLayout;
}
updateFeatures() {
for (const key in this.features) {
const feature = this.features[key];
if (feature.isMounted) {
feature.update();
}
else {
feature.mount();
feature.isMounted = true;
}
}
}
triggerBuild() {
this.build(this.renderState, this.latestValues, this.options, this.props);
}
/**
* Measure the current viewport box with or without transforms.
* Only measures axis-aligned boxes, rotate and skew must be manually
* removed with a re-render to work.
*/
measureViewportBox() {
return this.current
? this.measureInstanceViewportBox(this.current, this.props)
: createBox();
}
getStaticValue(key) {
return this.latestValues[key];
}
setStaticValue(key, value) {
this.latestValues[key] = value;
}
/**
* Make a target animatable by Popmotion. For instance, if we're
* trying to animate width from 100px to 100vw we need to measure 100vw
* in pixels to determine what we really need to animate to. This is also
* pluggable to support Framer's custom value types like Color,
* and CSS variables.
*/
makeTargetAnimatable(target, canMutate = true) {
return this.makeTargetAnimatableFromInstance(target, this.props, canMutate);
}
/**
* Update the provided props. Ensure any newly-added motion values are
* added to our map, old ones removed, and listeners updated.
*/
update(props, presenceContext) {
if (props.transformTemplate || this.props.transformTemplate) {
this.scheduleRender();
}
this.prevProps = this.props;
this.props = props;
this.prevPresenceContext = this.presenceContext;
this.presenceContext = presenceContext;
/**
* Update prop event handlers ie onAnimationStart, onAnimationComplete
*/
for (let i = 0; i < propEventHandlers.length; i++) {
const key = propEventHandlers[i];
if (this.propEventSubscriptions[key]) {
this.propEventSubscriptions[key]();
delete this.propEventSubscriptions[key];
}
const listener = props["on" + key];
if (listener) {
this.propEventSubscriptions[key] = this.on(key, listener);
}
}
this.prevMotionValues = updateMotionValuesFromProps(this, this.scrapeMotionValuesFromProps(props, this.prevProps), this.prevMotionValues);
if (this.handleChildMotionValue) {
this.handleChildMotionValue();
}
}
getProps() {
return this.props;
}
/**
* Returns the variant definition with a given name.
*/
getVariant(name) {
return this.props.variants ? this.props.variants[name] : undefined;
}
/**
* Returns the defined default transition on this component.
*/
getDefaultTransition() {
return this.props.transition;
}
getTransformPagePoint() {
return this.props.transformPagePoint;
}
getClosestVariantNode() {
return this.isVariantNode
? this
: this.parent
? this.parent.getClosestVariantNode()
: undefined;
}
getVariantContext(startAtParent = false) {
if (startAtParent) {
return this.parent ? this.parent.getVariantContext() : undefined;
}
if (!this.isControllingVariants) {
const context = this.parent
? this.parent.getVariantContext() || {}
: {};
if (this.props.initial !== undefined) {
context.initial = this.props.initial;
}
return context;
}
const context = {};
for (let i = 0; i < numVariantProps; i++) {
const name = variantProps[i];
const prop = this.props[name];
if (isVariantLabel(prop) || prop === false) {
context[name] = prop;
}
}
return context;
}
/**
* Add a child visual element to our set of children.
*/
addVariantChild(child) {
const closestVariantNode = this.getClosestVariantNode();
if (closestVariantNode) {
closestVariantNode.variantChildren &&
closestVariantNode.variantChildren.add(child);
return () => closestVariantNode.variantChildren.delete(child);
}
}
/**
* Add a motion value and bind it to this visual element.
*/
addValue(key, value) {
// Remove existing value if it exists
if (value !== this.values.get(key)) {
this.removeValue(key);
this.bindToMotionValue(key, value);
}
this.values.set(key, value);
this.latestValues[key] = value.get();
}
/**
* Remove a motion value and unbind any active subscriptions.
*/
removeValue(key) {
this.values.delete(key);
const unsubscribe = this.valueSubscriptions.get(key);
if (unsubscribe) {
unsubscribe();
this.valueSubscriptions.delete(key);
}
delete this.latestValues[key];
this.removeValueFromRenderState(key, this.renderState);
}
/**
* Check whether we have a motion value for this key
*/
hasValue(key) {
return this.values.has(key);
}
getValue(key, defaultValue) {
if (this.props.values && this.props.values[key]) {
return this.props.values[key];
}
let value = this.values.get(key);
if (value === undefined && defaultValue !== undefined) {
value = motionValue(defaultValue, { owner: this });
this.addValue(key, value);
}
return value;
}
/**
* If we're trying to animate to a previously unencountered value,
* we need to check for it in our state and as a last resort read it
* directly from the instance (which might have performance implications).
*/
readValue(key) {
var _a;
return this.latestValues[key] !== undefined || !this.current
? this.latestValues[key]
: (_a = this.getBaseTargetFromProps(this.props, key)) !== null && _a !== void 0 ? _a : this.readValueFromInstance(this.current, key, this.options);
}
/**
* Set the base target to later animate back to. This is currently
* only hydrated on creation and when we first read a value.
*/
setBaseTarget(key, value) {
this.baseTarget[key] = value;
}
/**
* Find the base target for a value thats been removed from all animation
* props.
*/
getBaseTarget(key) {
var _a;
const { initial } = this.props;
const valueFromInitial = typeof initial === "string" || typeof initial === "object"
? (_a = resolveVariantFromProps(this.props, initial)) === null || _a === void 0 ? void 0 : _a[key]
: undefined;
/**
* If this value still exists in the current initial variant, read that.
*/
if (initial && valueFromInitial !== undefined) {
return valueFromInitial;
}
/**
* Alternatively, if this VisualElement config has defined a getBaseTarget
* so we can read the value from an alternative source, try that.
*/
const target = this.getBaseTargetFromProps(this.props, key);
if (target !== undefined && !isMotionValue(target))
return target;
/**
* If the value was initially defined on initial, but it doesn't any more,
* return undefined. Otherwise return the value as initially read from the DOM.
*/
return this.initialValues[key] !== undefined &&
valueFromInitial === undefined
? undefined
: this.baseTarget[key];
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = new SubscriptionManager();
}
return this.events[eventName].add(callback);
}
notify(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].notify(...args);
}
}
}
export { VisualElement };

View File

@@ -0,0 +1,48 @@
import { getOrigin, checkTargetForNewValues } from '../utils/setters.mjs';
import { parseDomVariant } from './utils/parse-dom-variant.mjs';
import { VisualElement } from '../VisualElement.mjs';
class DOMVisualElement extends VisualElement {
sortInstanceNodePosition(a, b) {
/**
* compareDocumentPosition returns a bitmask, by using the bitwise &
* we're returning true if 2 in that bitmask is set to true. 2 is set
* to true if b preceeds a.
*/
return a.compareDocumentPosition(b) & 2 ? 1 : -1;
}
getBaseTargetFromProps(props, key) {
return props.style ? props.style[key] : undefined;
}
removeValueFromRenderState(key, { vars, style }) {
delete vars[key];
delete style[key];
}
makeTargetAnimatableFromInstance({ transition, transitionEnd, ...target }, { transformValues }, isMounted) {
let origin = getOrigin(target, transition || {}, this);
/**
* If Framer has provided a function to convert `Color` etc value types, convert them
*/
if (transformValues) {
if (transitionEnd)
transitionEnd = transformValues(transitionEnd);
if (target)
target = transformValues(target);
if (origin)
origin = transformValues(origin);
}
if (isMounted) {
checkTargetForNewValues(this, target, origin);
const parsed = parseDomVariant(this, target, origin, transitionEnd);
transitionEnd = parsed.transitionEnd;
target = parsed.target;
}
return {
transition,
transitionEnd,
...target,
};
}
}
export { DOMVisualElement };

View File

@@ -0,0 +1,11 @@
import { HTMLVisualElement } from '../html/HTMLVisualElement.mjs';
import { SVGVisualElement } from '../svg/SVGVisualElement.mjs';
import { isSVGComponent } from './utils/is-svg-component.mjs';
const createDomVisualElement = (Component, options) => {
return isSVGComponent(Component)
? new SVGVisualElement(options, { enableHardwareAcceleration: false })
: new HTMLVisualElement(options, { enableHardwareAcceleration: true });
};
export { createDomVisualElement };

View File

@@ -0,0 +1,14 @@
import { animations } from '../../motion/features/animations.mjs';
import { gestureAnimations } from '../../motion/features/gestures.mjs';
import { createDomVisualElement } from './create-visual-element.mjs';
/**
* @public
*/
const domAnimation = {
renderer: createDomVisualElement,
...animations,
...gestureAnimations,
};
export { domAnimation };

View File

@@ -0,0 +1,14 @@
import { drag } from '../../motion/features/drag.mjs';
import { layout } from '../../motion/features/layout.mjs';
import { domAnimation } from './features-animation.mjs';
/**
* @public
*/
const domMax = {
...domAnimation,
...drag,
...layout,
};
export { domMax };

View File

@@ -0,0 +1,9 @@
import { createMotionProxy } from './motion-proxy.mjs';
import { createDomMotionConfig } from './utils/create-config.mjs';
/**
* @public
*/
const m = createMotionProxy(createDomMotionConfig);
export { m };

View File

@@ -0,0 +1,47 @@
import { createMotionComponent } from '../../motion/index.mjs';
/**
* Convert any React component into a `motion` component. The provided component
* **must** use `React.forwardRef` to the underlying DOM component you want to animate.
*
* ```jsx
* const Component = React.forwardRef((props, ref) => {
* return <div ref={ref} />
* })
*
* const MotionComponent = motion(Component)
* ```
*
* @public
*/
function createMotionProxy(createConfig) {
function custom(Component, customMotionComponentConfig = {}) {
return createMotionComponent(createConfig(Component, customMotionComponentConfig));
}
if (typeof Proxy === "undefined") {
return custom;
}
/**
* A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc.
* Rather than generating them anew every render.
*/
const componentCache = new Map();
return new Proxy(custom, {
/**
* Called when `motion` is referenced with a prop: `motion.div`, `motion.input` etc.
* The prop name is passed through as `key` and we can use that to generate a `motion`
* DOM component with that name.
*/
get: (_target, key) => {
/**
* If this element doesn't exist in the component cache, create it and cache.
*/
if (!componentCache.has(key)) {
componentCache.set(key, custom(key));
}
return componentCache.get(key);
},
});
}
export { createMotionProxy };

View File

@@ -0,0 +1,42 @@
import { createMotionComponent } from '../../motion/index.mjs';
import { createMotionProxy } from './motion-proxy.mjs';
import { createDomMotionConfig } from './utils/create-config.mjs';
import { gestureAnimations } from '../../motion/features/gestures.mjs';
import { animations } from '../../motion/features/animations.mjs';
import { drag } from '../../motion/features/drag.mjs';
import { createDomVisualElement } from './create-visual-element.mjs';
import { layout } from '../../motion/features/layout.mjs';
const preloadedFeatures = {
...animations,
...gestureAnimations,
...drag,
...layout,
};
/**
* HTML & SVG components, optimised for use with gestures and animation. These can be used as
* drop-in replacements for any HTML & SVG component, all CSS & SVG properties are supported.
*
* @public
*/
const motion = /*@__PURE__*/ createMotionProxy((Component, config) => createDomMotionConfig(Component, config, preloadedFeatures, createDomVisualElement));
/**
* Create a DOM `motion` component with the provided string. This is primarily intended
* as a full alternative to `motion` for consumers who have to support environments that don't
* support `Proxy`.
*
* ```javascript
* import { createDomMotionComponent } from "framer-motion"
*
* const motion = {
* div: createDomMotionComponent('div')
* }
* ```
*
* @public
*/
function createDomMotionComponent(key) {
return createMotionComponent(createDomMotionConfig(key, { forwardMotionProps: false }, preloadedFeatures, createDomVisualElement));
}
export { createDomMotionComponent, motion };

View File

@@ -0,0 +1,64 @@
import { resolveElements } from '../utils/resolve-element.mjs';
const resizeHandlers = new WeakMap();
let observer;
function getElementSize(target, borderBoxSize) {
if (borderBoxSize) {
const { inlineSize, blockSize } = borderBoxSize[0];
return { width: inlineSize, height: blockSize };
}
else if (target instanceof SVGElement && "getBBox" in target) {
return target.getBBox();
}
else {
return {
width: target.offsetWidth,
height: target.offsetHeight,
};
}
}
function notifyTarget({ target, contentRect, borderBoxSize, }) {
var _a;
(_a = resizeHandlers.get(target)) === null || _a === void 0 ? void 0 : _a.forEach((handler) => {
handler({
target,
contentSize: contentRect,
get size() {
return getElementSize(target, borderBoxSize);
},
});
});
}
function notifyAll(entries) {
entries.forEach(notifyTarget);
}
function createResizeObserver() {
if (typeof ResizeObserver === "undefined")
return;
observer = new ResizeObserver(notifyAll);
}
function resizeElement(target, handler) {
if (!observer)
createResizeObserver();
const elements = resolveElements(target);
elements.forEach((element) => {
let elementHandlers = resizeHandlers.get(element);
if (!elementHandlers) {
elementHandlers = new Set();
resizeHandlers.set(element, elementHandlers);
}
elementHandlers.add(handler);
observer === null || observer === void 0 ? void 0 : observer.observe(element);
});
return () => {
elements.forEach((element) => {
const elementHandlers = resizeHandlers.get(element);
elementHandlers === null || elementHandlers === void 0 ? void 0 : elementHandlers.delete(handler);
if (!(elementHandlers === null || elementHandlers === void 0 ? void 0 : elementHandlers.size)) {
observer === null || observer === void 0 ? void 0 : observer.unobserve(element);
}
});
};
}
export { resizeElement };

View File

@@ -0,0 +1,30 @@
const windowCallbacks = new Set();
let windowResizeHandler;
function createWindowResizeHandler() {
windowResizeHandler = () => {
const size = {
width: window.innerWidth,
height: window.innerHeight,
};
const info = {
target: window,
size,
contentSize: size,
};
windowCallbacks.forEach((callback) => callback(info));
};
window.addEventListener("resize", windowResizeHandler);
}
function resizeWindow(callback) {
windowCallbacks.add(callback);
if (!windowResizeHandler)
createWindowResizeHandler();
return () => {
windowCallbacks.delete(callback);
if (!windowCallbacks.size && windowResizeHandler) {
windowResizeHandler = undefined;
}
};
}
export { resizeWindow };

View File

@@ -0,0 +1,8 @@
import { resizeElement } from './handle-element.mjs';
import { resizeWindow } from './handle-window.mjs';
function resize(a, b) {
return typeof a === "function" ? resizeWindow(a) : resizeElement(a, b);
}
export { resize };

View File

@@ -0,0 +1,36 @@
import { scrollInfo } from './track.mjs';
import { observeTimeline } from './observe.mjs';
import { supportsScrollTimeline } from './supports.mjs';
function scrollTimelineFallback({ source, axis = "y" }) {
// ScrollTimeline records progress as a percentage CSSUnitValue
const currentTime = { value: 0 };
const cancel = scrollInfo((info) => {
currentTime.value = info[axis].progress * 100;
}, { container: source, axis });
return { currentTime, cancel };
}
const timelineCache = new Map();
function getTimeline({ source = document.documentElement, axis = "y", } = {}) {
if (!timelineCache.has(source)) {
timelineCache.set(source, {});
}
const elementCache = timelineCache.get(source);
if (!elementCache[axis]) {
elementCache[axis] = supportsScrollTimeline()
? new ScrollTimeline({ source, axis })
: scrollTimelineFallback({ source, axis });
}
return elementCache[axis];
}
function scroll(onScroll, options) {
const timeline = getTimeline(options);
if (typeof onScroll === "function") {
return observeTimeline(onScroll, timeline);
}
else {
return onScroll.attachTimeline(timeline);
}
}
export { scroll };

View File

@@ -0,0 +1,56 @@
import { progress } from '../../../utils/progress.mjs';
import { velocityPerSecond } from '../../../utils/velocity-per-second.mjs';
/**
* A time in milliseconds, beyond which we consider the scroll velocity to be 0.
*/
const maxElapsed = 50;
const createAxisInfo = () => ({
current: 0,
offset: [],
progress: 0,
scrollLength: 0,
targetOffset: 0,
targetLength: 0,
containerLength: 0,
velocity: 0,
});
const createScrollInfo = () => ({
time: 0,
x: createAxisInfo(),
y: createAxisInfo(),
});
const keys = {
x: {
length: "Width",
position: "Left",
},
y: {
length: "Height",
position: "Top",
},
};
function updateAxisInfo(element, axisName, info, time) {
const axis = info[axisName];
const { length, position } = keys[axisName];
const prev = axis.current;
const prevTime = info.time;
axis.current = element["scroll" + position];
axis.scrollLength = element["scroll" + length] - element["client" + length];
axis.offset.length = 0;
axis.offset[0] = 0;
axis.offset[1] = axis.scrollLength;
axis.progress = progress(0, axis.scrollLength, axis.current);
const elapsed = time - prevTime;
axis.velocity =
elapsed > maxElapsed
? 0
: velocityPerSecond(axis.current - prev, elapsed);
}
function updateScrollInfo(element, info, time) {
updateAxisInfo(element, "x", info, time);
updateAxisInfo(element, "y", info, time);
info.time = time;
}
export { createScrollInfo, updateScrollInfo };

View File

@@ -0,0 +1,18 @@
import { frame, cancelFrame } from '../../../frameloop/frame.mjs';
function observeTimeline(update, timeline) {
let prevProgress;
const onFrame = () => {
const { currentTime } = timeline;
const percentage = currentTime === null ? 0 : currentTime.value;
const progress = percentage / 100;
if (prevProgress !== progress) {
update(progress);
}
prevProgress = progress;
};
frame.update(onFrame, true);
return () => cancelFrame(onFrame);
}
export { observeTimeline };

View File

@@ -0,0 +1,45 @@
const namedEdges = {
start: 0,
center: 0.5,
end: 1,
};
function resolveEdge(edge, length, inset = 0) {
let delta = 0;
/**
* If we have this edge defined as a preset, replace the definition
* with the numerical value.
*/
if (namedEdges[edge] !== undefined) {
edge = namedEdges[edge];
}
/**
* Handle unit values
*/
if (typeof edge === "string") {
const asNumber = parseFloat(edge);
if (edge.endsWith("px")) {
delta = asNumber;
}
else if (edge.endsWith("%")) {
edge = asNumber / 100;
}
else if (edge.endsWith("vw")) {
delta = (asNumber / 100) * document.documentElement.clientWidth;
}
else if (edge.endsWith("vh")) {
delta = (asNumber / 100) * document.documentElement.clientHeight;
}
else {
edge = asNumber;
}
}
/**
* If the edge is defined as a number, handle as a progress value.
*/
if (typeof edge === "number") {
delta = length * edge;
}
return inset + delta;
}
export { namedEdges, resolveEdge };

View File

@@ -0,0 +1,59 @@
import { calcInset } from './inset.mjs';
import { ScrollOffset } from './presets.mjs';
import { resolveOffset } from './offset.mjs';
import { interpolate } from '../../../../utils/interpolate.mjs';
import { defaultOffset } from '../../../../utils/offsets/default.mjs';
const point = { x: 0, y: 0 };
function getTargetSize(target) {
return "getBBox" in target && target.tagName !== "svg"
? target.getBBox()
: { width: target.clientWidth, height: target.clientHeight };
}
function resolveOffsets(container, info, options) {
let { offset: offsetDefinition = ScrollOffset.All } = options;
const { target = container, axis = "y" } = options;
const lengthLabel = axis === "y" ? "height" : "width";
const inset = target !== container ? calcInset(target, container) : point;
/**
* Measure the target and container. If they're the same thing then we
* use the container's scrollWidth/Height as the target, from there
* all other calculations can remain the same.
*/
const targetSize = target === container
? { width: container.scrollWidth, height: container.scrollHeight }
: getTargetSize(target);
const containerSize = {
width: container.clientWidth,
height: container.clientHeight,
};
/**
* Reset the length of the resolved offset array rather than creating a new one.
* TODO: More reusable data structures for targetSize/containerSize would also be good.
*/
info[axis].offset.length = 0;
/**
* Populate the offset array by resolving the user's offset definition into
* a list of pixel scroll offets.
*/
let hasChanged = !info[axis].interpolate;
const numOffsets = offsetDefinition.length;
for (let i = 0; i < numOffsets; i++) {
const offset = resolveOffset(offsetDefinition[i], containerSize[lengthLabel], targetSize[lengthLabel], inset[axis]);
if (!hasChanged && offset !== info[axis].interpolatorOffsets[i]) {
hasChanged = true;
}
info[axis].offset[i] = offset;
}
/**
* If the pixel scroll offsets have changed, create a new interpolator function
* to map scroll value into a progress.
*/
if (hasChanged) {
info[axis].interpolate = interpolate(info[axis].offset, defaultOffset(offsetDefinition));
info[axis].interpolatorOffsets = [...info[axis].offset];
}
info[axis].progress = info[axis].interpolate(info[axis].current);
}
export { resolveOffsets };

View File

@@ -0,0 +1,45 @@
function calcInset(element, container) {
const inset = { x: 0, y: 0 };
let current = element;
while (current && current !== container) {
if (current instanceof HTMLElement) {
inset.x += current.offsetLeft;
inset.y += current.offsetTop;
current = current.offsetParent;
}
else if (current.tagName === "svg") {
/**
* This isn't an ideal approach to measuring the offset of <svg /> tags.
* It would be preferable, given they behave like HTMLElements in most ways
* to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
* can't use .getBBox() like most SVG elements as these provide the offset
* relative to the SVG itself, which for <svg /> is usually 0x0.
*/
const svgBoundingBox = current.getBoundingClientRect();
current = current.parentElement;
const parentBoundingBox = current.getBoundingClientRect();
inset.x += svgBoundingBox.left - parentBoundingBox.left;
inset.y += svgBoundingBox.top - parentBoundingBox.top;
}
else if (current instanceof SVGGraphicsElement) {
const { x, y } = current.getBBox();
inset.x += x;
inset.y += y;
let svg = null;
let parent = current.parentNode;
while (!svg) {
if (parent.tagName === "svg") {
svg = parent;
}
parent = current.parentNode;
}
current = svg;
}
else {
break;
}
}
return inset;
}
export { calcInset };

View File

@@ -0,0 +1,35 @@
import { namedEdges, resolveEdge } from './edge.mjs';
const defaultOffset = [0, 0];
function resolveOffset(offset, containerLength, targetLength, targetInset) {
let offsetDefinition = Array.isArray(offset) ? offset : defaultOffset;
let targetPoint = 0;
let containerPoint = 0;
if (typeof offset === "number") {
/**
* If we're provided offset: [0, 0.5, 1] then each number x should become
* [x, x], so we default to the behaviour of mapping 0 => 0 of both target
* and container etc.
*/
offsetDefinition = [offset, offset];
}
else if (typeof offset === "string") {
offset = offset.trim();
if (offset.includes(" ")) {
offsetDefinition = offset.split(" ");
}
else {
/**
* If we're provided a definition like "100px" then we want to apply
* that only to the top of the target point, leaving the container at 0.
* Whereas a named offset like "end" should be applied to both.
*/
offsetDefinition = [offset, namedEdges[offset] ? offset : `0`];
}
}
targetPoint = resolveEdge(offsetDefinition[0], targetLength, targetInset);
containerPoint = resolveEdge(offsetDefinition[1], containerLength);
return targetPoint - containerPoint;
}
export { resolveOffset };

View File

@@ -0,0 +1,20 @@
const ScrollOffset = {
Enter: [
[0, 1],
[1, 1],
],
Exit: [
[0, 0],
[1, 0],
],
Any: [
[1, 0],
[0, 1],
],
All: [
[0, 0],
[1, 1],
],
};
export { ScrollOffset };

View File

@@ -0,0 +1,48 @@
import { warnOnce } from '../../../utils/warn-once.mjs';
import { updateScrollInfo } from './info.mjs';
import { resolveOffsets } from './offsets/index.mjs';
function measure(container, target = container, info) {
/**
* Find inset of target within scrollable container
*/
info.x.targetOffset = 0;
info.y.targetOffset = 0;
if (target !== container) {
let node = target;
while (node && node !== container) {
info.x.targetOffset += node.offsetLeft;
info.y.targetOffset += node.offsetTop;
node = node.offsetParent;
}
}
info.x.targetLength =
target === container ? target.scrollWidth : target.clientWidth;
info.y.targetLength =
target === container ? target.scrollHeight : target.clientHeight;
info.x.containerLength = container.clientWidth;
info.y.containerLength = container.clientHeight;
/**
* In development mode ensure scroll containers aren't position: static as this makes
* it difficult to measure their relative positions.
*/
if (process.env.NODE_ENV !== "production") {
if (container && target && target !== container) {
warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
}
}
}
function createOnScrollHandler(element, onScroll, info, options = {}) {
return {
measure: () => measure(element, options.target, info),
update: (time) => {
updateScrollInfo(element, info, time);
if (options.offset || options.target) {
resolveOffsets(element, info, options);
}
},
notify: () => onScroll(info),
};
}
export { createOnScrollHandler };

View File

@@ -0,0 +1,5 @@
import { memo } from '../../../utils/memo.mjs';
const supportsScrollTimeline = memo(() => window.ScrollTimeline !== undefined);
export { supportsScrollTimeline };

View File

@@ -0,0 +1,84 @@
import { resize } from '../resize/index.mjs';
import { createScrollInfo } from './info.mjs';
import { createOnScrollHandler } from './on-scroll-handler.mjs';
import { frame, cancelFrame, frameData } from '../../../frameloop/frame.mjs';
const scrollListeners = new WeakMap();
const resizeListeners = new WeakMap();
const onScrollHandlers = new WeakMap();
const getEventTarget = (element) => element === document.documentElement ? window : element;
function scrollInfo(onScroll, { container = document.documentElement, ...options } = {}) {
let containerHandlers = onScrollHandlers.get(container);
/**
* Get the onScroll handlers for this container.
* If one isn't found, create a new one.
*/
if (!containerHandlers) {
containerHandlers = new Set();
onScrollHandlers.set(container, containerHandlers);
}
/**
* Create a new onScroll handler for the provided callback.
*/
const info = createScrollInfo();
const containerHandler = createOnScrollHandler(container, onScroll, info, options);
containerHandlers.add(containerHandler);
/**
* Check if there's a scroll event listener for this container.
* If not, create one.
*/
if (!scrollListeners.has(container)) {
const measureAll = () => {
for (const handler of containerHandlers)
handler.measure();
};
const updateAll = () => {
for (const handler of containerHandlers) {
handler.update(frameData.timestamp);
}
};
const notifyAll = () => {
for (const handler of containerHandlers)
handler.notify();
};
const listener = () => {
frame.read(measureAll, false, true);
frame.read(updateAll, false, true);
frame.update(notifyAll, false, true);
};
scrollListeners.set(container, listener);
const target = getEventTarget(container);
window.addEventListener("resize", listener, { passive: true });
if (container !== document.documentElement) {
resizeListeners.set(container, resize(container, listener));
}
target.addEventListener("scroll", listener, { passive: true });
}
const listener = scrollListeners.get(container);
frame.read(listener, false, true);
return () => {
var _a;
cancelFrame(listener);
/**
* Check if we even have any handlers for this container.
*/
const currentHandlers = onScrollHandlers.get(container);
if (!currentHandlers)
return;
currentHandlers.delete(containerHandler);
if (currentHandlers.size)
return;
/**
* If no more handlers, remove the scroll listener too.
*/
const scrollListener = scrollListeners.get(container);
scrollListeners.delete(container);
if (scrollListener) {
getEventTarget(container).removeEventListener("scroll", scrollListener);
(_a = resizeListeners.get(container)) === null || _a === void 0 ? void 0 : _a();
window.removeEventListener("resize", scrollListener);
}
};
}
export { scrollInfo };

View File

@@ -0,0 +1,35 @@
import { useMemo, createElement } from 'react';
import { useHTMLProps } from '../html/use-props.mjs';
import { filterProps } from './utils/filter-props.mjs';
import { isSVGComponent } from './utils/is-svg-component.mjs';
import { useSVGProps } from '../svg/use-props.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
function createUseRender(forwardMotionProps = false) {
const useRender = (Component, props, ref, { latestValues }, isStatic) => {
const useVisualProps = isSVGComponent(Component)
? useSVGProps
: useHTMLProps;
const visualProps = useVisualProps(props, latestValues, isStatic, Component);
const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps);
const elementProps = {
...filteredProps,
...visualProps,
ref,
};
/**
* If component has been handed a motion value as its child,
* memoise its initial value and render that. Subsequent updates
* will be handled by the onChange handler
*/
const { children } = props;
const renderedChildren = useMemo(() => (isMotionValue(children) ? children.get() : children), [children]);
return createElement(Component, {
...elementProps,
children: renderedChildren,
});
};
return useRender;
}
export { createUseRender };

View File

@@ -0,0 +1,6 @@
/**
* Convert camelCase to dash-case properties.
*/
const camelToDash = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
export { camelToDash };

View File

@@ -0,0 +1,19 @@
import { isSVGComponent } from './is-svg-component.mjs';
import { createUseRender } from '../use-render.mjs';
import { svgMotionConfig } from '../../svg/config-motion.mjs';
import { htmlMotionConfig } from '../../html/config-motion.mjs';
function createDomMotionConfig(Component, { forwardMotionProps = false }, preloadedFeatures, createVisualElement) {
const baseConfig = isSVGComponent(Component)
? svgMotionConfig
: htmlMotionConfig;
return {
...baseConfig,
preloadedFeatures,
useRender: createUseRender(forwardMotionProps),
createVisualElement,
Component,
};
}
export { createDomMotionConfig };

View File

@@ -0,0 +1,89 @@
import { invariant } from '../../../utils/errors.mjs';
import { isNumericalString } from '../../../utils/is-numerical-string.mjs';
import { isCSSVariableToken } from './is-css-variable.mjs';
/**
* Parse Framer's special CSS variable format into a CSS token and a fallback.
*
* ```
* `var(--foo, #fff)` => [`--foo`, '#fff']
* ```
*
* @param current
*/
const splitCSSVariableRegex = /var\((--[a-zA-Z0-9-_]+),? ?([a-zA-Z0-9 ()%#.,-]+)?\)/;
function parseCSSVariable(current) {
const match = splitCSSVariableRegex.exec(current);
if (!match)
return [,];
const [, token, fallback] = match;
return [token, fallback];
}
const maxDepth = 4;
function getVariableValue(current, element, depth = 1) {
invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`);
const [token, fallback] = parseCSSVariable(current);
// No CSS variable detected
if (!token)
return;
// Attempt to read this CSS variable off the element
const resolved = window.getComputedStyle(element).getPropertyValue(token);
if (resolved) {
const trimmed = resolved.trim();
return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
}
else if (isCSSVariableToken(fallback)) {
// The fallback might itself be a CSS variable, in which case we attempt to resolve it too.
return getVariableValue(fallback, element, depth + 1);
}
else {
return fallback;
}
}
/**
* Resolve CSS variables from
*
* @internal
*/
function resolveCSSVariables(visualElement, { ...target }, transitionEnd) {
const element = visualElement.current;
if (!(element instanceof Element))
return { target, transitionEnd };
// If `transitionEnd` isn't `undefined`, clone it. We could clone `target` and `transitionEnd`
// only if they change but I think this reads clearer and this isn't a performance-critical path.
if (transitionEnd) {
transitionEnd = { ...transitionEnd };
}
// Go through existing `MotionValue`s and ensure any existing CSS variables are resolved
visualElement.values.forEach((value) => {
const current = value.get();
if (!isCSSVariableToken(current))
return;
const resolved = getVariableValue(current, element);
if (resolved)
value.set(resolved);
});
// Cycle through every target property and resolve CSS variables. Currently
// we only read single-var properties like `var(--foo)`, not `calc(var(--foo) + 20px)`
for (const key in target) {
const current = target[key];
if (!isCSSVariableToken(current))
continue;
const resolved = getVariableValue(current, element);
if (!resolved)
continue;
// Clone target if it hasn't already been
target[key] = resolved;
if (!transitionEnd)
transitionEnd = {};
// If the user hasn't already set this key on `transitionEnd`, set it to the unresolved
// CSS variable. This will ensure that after the animation the component will reflect
// changes in the value of the CSS variable.
if (transitionEnd[key] === undefined) {
transitionEnd[key] = current;
}
}
return { target, transitionEnd };
}
export { parseCSSVariable, resolveCSSVariables };

View File

@@ -0,0 +1,57 @@
import { isValidMotionProp } from '../../../motion/utils/valid-prop.mjs';
let shouldForward = (key) => !isValidMotionProp(key);
function loadExternalIsValidProp(isValidProp) {
if (!isValidProp)
return;
// Explicitly filter our events
shouldForward = (key) => key.startsWith("on") ? !isValidMotionProp(key) : isValidProp(key);
}
/**
* Emotion and Styled Components both allow users to pass through arbitrary props to their components
* to dynamically generate CSS. They both use the `@emotion/is-prop-valid` package to determine which
* of these should be passed to the underlying DOM node.
*
* However, when styling a Motion component `styled(motion.div)`, both packages pass through *all* props
* as it's seen as an arbitrary component rather than a DOM node. Motion only allows arbitrary props
* passed through the `custom` prop so it doesn't *need* the payload or computational overhead of
* `@emotion/is-prop-valid`, however to fix this problem we need to use it.
*
* By making it an optionalDependency we can offer this functionality only in the situations where it's
* actually required.
*/
try {
/**
* We attempt to import this package but require won't be defined in esm environments, in that case
* isPropValid will have to be provided via `MotionContext`. In a 6.0.0 this should probably be removed
* in favour of explicit injection.
*/
loadExternalIsValidProp(require("@emotion/is-prop-valid").default);
}
catch (_a) {
// We don't need to actually do anything here - the fallback is the existing `isPropValid`.
}
function filterProps(props, isDom, forwardMotionProps) {
const filteredProps = {};
for (const key in props) {
/**
* values is considered a valid prop by Emotion, so if it's present
* this will be rendered out to the DOM unless explicitly filtered.
*
* We check the type as it could be used with the `feColorMatrix`
* element, which we support.
*/
if (key === "values" && typeof props.values === "object")
continue;
if (shouldForward(key) ||
(forwardMotionProps === true && isValidMotionProp(key)) ||
(!isDom && !isValidMotionProp(key)) ||
// If trying to use native HTML drag events, forward drag listeners
(props["draggable"] && key.startsWith("onDrag"))) {
filteredProps[key] = props[key];
}
}
return filteredProps;
}
export { filterProps, loadExternalIsValidProp };

View File

@@ -0,0 +1,6 @@
const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
const isCSSVariableName = checkStringStartsWith("--");
const isCSSVariableToken = checkStringStartsWith("var(--");
const cssVariableRegex = /var\s*\(\s*--[\w-]+(\s*,\s*(?:(?:[^)(]|\((?:[^)(]+|\([^)(]*\))*\))*)+)?\s*\)/g;
export { cssVariableRegex, isCSSVariableName, isCSSVariableToken };

View File

@@ -0,0 +1,30 @@
import { lowercaseSVGElements } from '../../svg/lowercase-elements.mjs';
function isSVGComponent(Component) {
if (
/**
* If it's not a string, it's a custom React component. Currently we only support
* HTML custom React components.
*/
typeof Component !== "string" ||
/**
* If it contains a dash, the element is a custom HTML webcomponent.
*/
Component.includes("-")) {
return false;
}
else if (
/**
* If it's in our list of lowercase SVG tags, it's an SVG component
*/
lowercaseSVGElements.indexOf(Component) > -1 ||
/**
* If it contains a capital letter, it's an SVG component
*/
/[A-Z]/.test(Component)) {
return true;
}
return false;
}
export { isSVGComponent };

View File

@@ -0,0 +1,5 @@
function isSVGElement(element) {
return element instanceof SVGElement && element.tagName !== "svg";
}
export { isSVGElement };

View File

@@ -0,0 +1,15 @@
import { resolveCSSVariables } from './css-variables-conversion.mjs';
import { unitConversion } from './unit-conversion.mjs';
/**
* Parse a DOM variant to make it animatable. This involves resolving CSS variables
* and ensuring animations like "20%" => "calc(50vw)" are performed in pixels.
*/
const parseDomVariant = (visualElement, target, origin, transitionEnd) => {
const resolved = resolveCSSVariables(visualElement, target, transitionEnd);
target = resolved.target;
transitionEnd = resolved.transitionEnd;
return unitConversion(visualElement, target, origin, transitionEnd);
};
export { parseDomVariant };

View File

@@ -0,0 +1,28 @@
import { invariant } from '../../../utils/errors.mjs';
function resolveElements(elements, scope, selectorCache) {
var _a;
if (typeof elements === "string") {
let root = document;
if (scope) {
invariant(Boolean(scope.current), "Scope provided, but no element detected.");
root = scope.current;
}
if (selectorCache) {
(_a = selectorCache[elements]) !== null && _a !== void 0 ? _a : (selectorCache[elements] = root.querySelectorAll(elements));
elements = selectorCache[elements];
}
else {
elements = root.querySelectorAll(elements);
}
}
else if (elements instanceof Element) {
elements = [elements];
}
/**
* Return an empty array
*/
return Array.from(elements || []);
}
export { resolveElements };

View File

@@ -0,0 +1,230 @@
import { isKeyframesTarget } from '../../../animation/utils/is-keyframes-target.mjs';
import { invariant } from '../../../utils/errors.mjs';
import { transformPropOrder } from '../../html/utils/transform.mjs';
import { findDimensionValueType } from '../value-types/dimensions.mjs';
import { isBrowser } from '../../../utils/is-browser.mjs';
import { number } from '../../../value/types/numbers/index.mjs';
import { px } from '../../../value/types/numbers/units.mjs';
const positionalKeys = new Set([
"width",
"height",
"top",
"left",
"right",
"bottom",
"x",
"y",
"translateX",
"translateY",
]);
const isPositionalKey = (key) => positionalKeys.has(key);
const hasPositionalKey = (target) => {
return Object.keys(target).some(isPositionalKey);
};
const isNumOrPxType = (v) => v === number || v === px;
const getPosFromMatrix = (matrix, pos) => parseFloat(matrix.split(", ")[pos]);
const getTranslateFromMatrix = (pos2, pos3) => (_bbox, { transform }) => {
if (transform === "none" || !transform)
return 0;
const matrix3d = transform.match(/^matrix3d\((.+)\)$/);
if (matrix3d) {
return getPosFromMatrix(matrix3d[1], pos3);
}
else {
const matrix = transform.match(/^matrix\((.+)\)$/);
if (matrix) {
return getPosFromMatrix(matrix[1], pos2);
}
else {
return 0;
}
}
};
const transformKeys = new Set(["x", "y", "z"]);
const nonTranslationalTransformKeys = transformPropOrder.filter((key) => !transformKeys.has(key));
function removeNonTranslationalTransform(visualElement) {
const removedTransforms = [];
nonTranslationalTransformKeys.forEach((key) => {
const value = visualElement.getValue(key);
if (value !== undefined) {
removedTransforms.push([key, value.get()]);
value.set(key.startsWith("scale") ? 1 : 0);
}
});
// Apply changes to element before measurement
if (removedTransforms.length)
visualElement.render();
return removedTransforms;
}
const positionalValues = {
// Dimensions
width: ({ x }, { paddingLeft = "0", paddingRight = "0" }) => x.max - x.min - parseFloat(paddingLeft) - parseFloat(paddingRight),
height: ({ y }, { paddingTop = "0", paddingBottom = "0" }) => y.max - y.min - parseFloat(paddingTop) - parseFloat(paddingBottom),
top: (_bbox, { top }) => parseFloat(top),
left: (_bbox, { left }) => parseFloat(left),
bottom: ({ y }, { top }) => parseFloat(top) + (y.max - y.min),
right: ({ x }, { left }) => parseFloat(left) + (x.max - x.min),
// Transform
x: getTranslateFromMatrix(4, 13),
y: getTranslateFromMatrix(5, 14),
};
// Alias translate longform names
positionalValues.translateX = positionalValues.x;
positionalValues.translateY = positionalValues.y;
const convertChangedValueTypes = (target, visualElement, changedKeys) => {
const originBbox = visualElement.measureViewportBox();
const element = visualElement.current;
const elementComputedStyle = getComputedStyle(element);
const { display } = elementComputedStyle;
const origin = {};
// If the element is currently set to display: "none", make it visible before
// measuring the target bounding box
if (display === "none") {
visualElement.setStaticValue("display", target.display || "block");
}
/**
* Record origins before we render and update styles
*/
changedKeys.forEach((key) => {
origin[key] = positionalValues[key](originBbox, elementComputedStyle);
});
// Apply the latest values (as set in checkAndConvertChangedValueTypes)
visualElement.render();
const targetBbox = visualElement.measureViewportBox();
changedKeys.forEach((key) => {
// Restore styles to their **calculated computed style**, not their actual
// originally set style. This allows us to animate between equivalent pixel units.
const value = visualElement.getValue(key);
value && value.jump(origin[key]);
target[key] = positionalValues[key](targetBbox, elementComputedStyle);
});
return target;
};
const checkAndConvertChangedValueTypes = (visualElement, target, origin = {}, transitionEnd = {}) => {
target = { ...target };
transitionEnd = { ...transitionEnd };
const targetPositionalKeys = Object.keys(target).filter(isPositionalKey);
// We want to remove any transform values that could affect the element's bounding box before
// it's measured. We'll reapply these later.
let removedTransformValues = [];
let hasAttemptedToRemoveTransformValues = false;
const changedValueTypeKeys = [];
targetPositionalKeys.forEach((key) => {
const value = visualElement.getValue(key);
if (!visualElement.hasValue(key))
return;
let from = origin[key];
let fromType = findDimensionValueType(from);
const to = target[key];
let toType;
// TODO: The current implementation of this basically throws an error
// if you try and do value conversion via keyframes. There's probably
// a way of doing this but the performance implications would need greater scrutiny,
// as it'd be doing multiple resize-remeasure operations.
if (isKeyframesTarget(to)) {
const numKeyframes = to.length;
const fromIndex = to[0] === null ? 1 : 0;
from = to[fromIndex];
fromType = findDimensionValueType(from);
for (let i = fromIndex; i < numKeyframes; i++) {
/**
* Don't allow wildcard keyframes to be used to detect
* a difference in value types.
*/
if (to[i] === null)
break;
if (!toType) {
toType = findDimensionValueType(to[i]);
invariant(toType === fromType ||
(isNumOrPxType(fromType) && isNumOrPxType(toType)), "Keyframes must be of the same dimension as the current value");
}
else {
invariant(findDimensionValueType(to[i]) === toType, "All keyframes must be of the same type");
}
}
}
else {
toType = findDimensionValueType(to);
}
if (fromType !== toType) {
// If they're both just number or px, convert them both to numbers rather than
// relying on resize/remeasure to convert (which is wasteful in this situation)
if (isNumOrPxType(fromType) && isNumOrPxType(toType)) {
const current = value.get();
if (typeof current === "string") {
value.set(parseFloat(current));
}
if (typeof to === "string") {
target[key] = parseFloat(to);
}
else if (Array.isArray(to) && toType === px) {
target[key] = to.map(parseFloat);
}
}
else if ((fromType === null || fromType === void 0 ? void 0 : fromType.transform) &&
(toType === null || toType === void 0 ? void 0 : toType.transform) &&
(from === 0 || to === 0)) {
// If one or the other value is 0, it's safe to coerce it to the
// type of the other without measurement
if (from === 0) {
value.set(toType.transform(from));
}
else {
target[key] = fromType.transform(to);
}
}
else {
// If we're going to do value conversion via DOM measurements, we first
// need to remove non-positional transform values that could affect the bbox measurements.
if (!hasAttemptedToRemoveTransformValues) {
removedTransformValues =
removeNonTranslationalTransform(visualElement);
hasAttemptedToRemoveTransformValues = true;
}
changedValueTypeKeys.push(key);
transitionEnd[key] =
transitionEnd[key] !== undefined
? transitionEnd[key]
: target[key];
value.jump(to);
}
}
});
if (changedValueTypeKeys.length) {
const scrollY = changedValueTypeKeys.indexOf("height") >= 0
? window.pageYOffset
: null;
const convertedTarget = convertChangedValueTypes(target, visualElement, changedValueTypeKeys);
// If we removed transform values, reapply them before the next render
if (removedTransformValues.length) {
removedTransformValues.forEach(([key, value]) => {
visualElement.getValue(key).set(value);
});
}
// Reapply original values
visualElement.render();
// Restore scroll position
if (isBrowser && scrollY !== null) {
window.scrollTo({ top: scrollY });
}
return { target: convertedTarget, transitionEnd };
}
else {
return { target, transitionEnd };
}
};
/**
* Convert value types for x/y/width/height/top/left/bottom/right
*
* Allows animation between `'auto'` -> `'100%'` or `0` -> `'calc(50% - 10vw)'`
*
* @internal
*/
function unitConversion(visualElement, target, origin, transitionEnd) {
return hasPositionalKey(target)
? checkAndConvertChangedValueTypes(visualElement, target, origin, transitionEnd)
: { target, transitionEnd };
}
export { positionalValues, unitConversion };

View File

@@ -0,0 +1,15 @@
import { complex } from '../../../value/types/complex/index.mjs';
import { filter } from '../../../value/types/complex/filter.mjs';
import { getDefaultValueType } from './defaults.mjs';
function getAnimatableNone(key, value) {
let defaultValueType = getDefaultValueType(key);
if (defaultValueType !== filter)
defaultValueType = complex;
// If value is not recognised as animatable, ie "none", create an animatable version origin based on the target
return defaultValueType.getAnimatableNone
? defaultValueType.getAnimatableNone(value)
: undefined;
}
export { getAnimatableNone };

View File

@@ -0,0 +1,30 @@
import { color } from '../../../value/types/color/index.mjs';
import { filter } from '../../../value/types/complex/filter.mjs';
import { numberValueTypes } from './number.mjs';
/**
* A map of default value types for common values
*/
const defaultValueTypes = {
...numberValueTypes,
// Color props
color,
backgroundColor: color,
outlineColor: color,
fill: color,
stroke: color,
// Border props
borderColor: color,
borderTopColor: color,
borderRightColor: color,
borderBottomColor: color,
borderLeftColor: color,
filter,
WebkitFilter: filter,
};
/**
* Gets the default ValueType for the provided value key
*/
const getDefaultValueType = (key) => defaultValueTypes[key];
export { defaultValueTypes, getDefaultValueType };

View File

@@ -0,0 +1,15 @@
import { number } from '../../../value/types/numbers/index.mjs';
import { px, percent, degrees, vw, vh } from '../../../value/types/numbers/units.mjs';
import { testValueType } from './test.mjs';
import { auto } from './type-auto.mjs';
/**
* A list of value types commonly used for dimensions
*/
const dimensionValueTypes = [number, px, percent, degrees, vw, vh, auto];
/**
* Tests a dimensional value against the list of dimension ValueTypes
*/
const findDimensionValueType = (v) => dimensionValueTypes.find(testValueType(v));
export { dimensionValueTypes, findDimensionValueType };

View File

@@ -0,0 +1,15 @@
import { color } from '../../../value/types/color/index.mjs';
import { complex } from '../../../value/types/complex/index.mjs';
import { dimensionValueTypes } from './dimensions.mjs';
import { testValueType } from './test.mjs';
/**
* A list of all ValueTypes
*/
const valueTypes = [...dimensionValueTypes, color, complex];
/**
* Tests a value against the list of ValueTypes
*/
const findValueType = (v) => valueTypes.find(testValueType(v));
export { findValueType };

View File

@@ -0,0 +1,10 @@
/**
* Provided a value and a ValueType, returns the value as that value type.
*/
const getValueAsType = (value, type) => {
return type && typeof value === "number"
? type.transform(value)
: value;
};
export { getValueAsType };

View File

@@ -0,0 +1,72 @@
import { scale, alpha } from '../../../value/types/numbers/index.mjs';
import { px, degrees, progressPercentage } from '../../../value/types/numbers/units.mjs';
import { int } from './type-int.mjs';
const numberValueTypes = {
// Border props
borderWidth: px,
borderTopWidth: px,
borderRightWidth: px,
borderBottomWidth: px,
borderLeftWidth: px,
borderRadius: px,
radius: px,
borderTopLeftRadius: px,
borderTopRightRadius: px,
borderBottomRightRadius: px,
borderBottomLeftRadius: px,
// Positioning props
width: px,
maxWidth: px,
height: px,
maxHeight: px,
size: px,
top: px,
right: px,
bottom: px,
left: px,
// Spacing props
padding: px,
paddingTop: px,
paddingRight: px,
paddingBottom: px,
paddingLeft: px,
margin: px,
marginTop: px,
marginRight: px,
marginBottom: px,
marginLeft: px,
// Transform props
rotate: degrees,
rotateX: degrees,
rotateY: degrees,
rotateZ: degrees,
scale,
scaleX: scale,
scaleY: scale,
scaleZ: scale,
skew: degrees,
skewX: degrees,
skewY: degrees,
distance: px,
translateX: px,
translateY: px,
translateZ: px,
x: px,
y: px,
z: px,
perspective: px,
transformPerspective: px,
opacity: alpha,
originX: progressPercentage,
originY: progressPercentage,
originZ: px,
// Misc
zIndex: int,
// SVG
fillOpacity: alpha,
strokeOpacity: alpha,
numOctaves: int,
};
export { numberValueTypes };

View File

@@ -0,0 +1,6 @@
/**
* Tests a provided value against a ValueType
*/
const testValueType = (v) => (type) => type.test(v);
export { testValueType };

View File

@@ -0,0 +1,9 @@
/**
* ValueType for "auto"
*/
const auto = {
test: (v) => v === "auto",
parse: (v) => v,
};
export { auto };

View File

@@ -0,0 +1,8 @@
import { number } from '../../../value/types/numbers/index.mjs';
const int = {
...number,
transform: Math.round,
};
export { int };

View File

@@ -0,0 +1,43 @@
import { resolveElements } from '../utils/resolve-element.mjs';
const thresholds = {
some: 0,
all: 1,
};
function inView(elementOrSelector, onStart, { root, margin: rootMargin, amount = "some" } = {}) {
const elements = resolveElements(elementOrSelector);
const activeIntersections = new WeakMap();
const onIntersectionChange = (entries) => {
entries.forEach((entry) => {
const onEnd = activeIntersections.get(entry.target);
/**
* If there's no change to the intersection, we don't need to
* do anything here.
*/
if (entry.isIntersecting === Boolean(onEnd))
return;
if (entry.isIntersecting) {
const newOnEnd = onStart(entry);
if (typeof newOnEnd === "function") {
activeIntersections.set(entry.target, newOnEnd);
}
else {
observer.unobserve(entry.target);
}
}
else if (onEnd) {
onEnd(entry);
activeIntersections.delete(entry.target);
}
});
};
const observer = new IntersectionObserver(onIntersectionChange, {
root,
rootMargin,
threshold: typeof amount === "number" ? amount : thresholds[amount],
});
elements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
}
export { inView };

View File

@@ -0,0 +1,59 @@
import { buildHTMLStyles } from './utils/build-styles.mjs';
import { isCSSVariableName } from '../dom/utils/is-css-variable.mjs';
import { transformProps } from './utils/transform.mjs';
import { scrapeMotionValuesFromProps } from './utils/scrape-motion-values.mjs';
import { renderHTML } from './utils/render.mjs';
import { getDefaultValueType } from '../dom/value-types/defaults.mjs';
import { measureViewportBox } from '../../projection/utils/measure.mjs';
import { DOMVisualElement } from '../dom/DOMVisualElement.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
function getComputedStyle(element) {
return window.getComputedStyle(element);
}
class HTMLVisualElement extends DOMVisualElement {
constructor() {
super(...arguments);
this.type = "html";
}
readValueFromInstance(instance, key) {
if (transformProps.has(key)) {
const defaultType = getDefaultValueType(key);
return defaultType ? defaultType.default || 0 : 0;
}
else {
const computedStyle = getComputedStyle(instance);
const value = (isCSSVariableName(key)
? computedStyle.getPropertyValue(key)
: computedStyle[key]) || 0;
return typeof value === "string" ? value.trim() : value;
}
}
measureInstanceViewportBox(instance, { transformPagePoint }) {
return measureViewportBox(instance, transformPagePoint);
}
build(renderState, latestValues, options, props) {
buildHTMLStyles(renderState, latestValues, options, props.transformTemplate);
}
scrapeMotionValuesFromProps(props, prevProps) {
return scrapeMotionValuesFromProps(props, prevProps);
}
handleChildMotionValue() {
if (this.childSubscription) {
this.childSubscription();
delete this.childSubscription;
}
const { children } = this.props;
if (isMotionValue(children)) {
this.childSubscription = children.on("change", (latest) => {
if (this.current)
this.current.textContent = `${latest}`;
});
}
}
renderInstance(instance, renderState, styleProp, projection) {
renderHTML(instance, renderState, styleProp, projection);
}
}
export { HTMLVisualElement, getComputedStyle };

View File

@@ -0,0 +1,12 @@
import { makeUseVisualState } from '../../motion/utils/use-visual-state.mjs';
import { scrapeMotionValuesFromProps } from './utils/scrape-motion-values.mjs';
import { createHtmlRenderState } from './utils/create-render-state.mjs';
const htmlMotionConfig = {
useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps,
createRenderState: createHtmlRenderState,
}),
};
export { htmlMotionConfig };

View File

@@ -0,0 +1,57 @@
import { useMemo } from 'react';
import { isForcedMotionValue } from '../../motion/utils/is-forced-motion-value.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
import { buildHTMLStyles } from './utils/build-styles.mjs';
import { createHtmlRenderState } from './utils/create-render-state.mjs';
function copyRawValuesOnly(target, source, props) {
for (const key in source) {
if (!isMotionValue(source[key]) && !isForcedMotionValue(key, props)) {
target[key] = source[key];
}
}
}
function useInitialMotionValues({ transformTemplate }, visualState, isStatic) {
return useMemo(() => {
const state = createHtmlRenderState();
buildHTMLStyles(state, visualState, { enableHardwareAcceleration: !isStatic }, transformTemplate);
return Object.assign({}, state.vars, state.style);
}, [visualState]);
}
function useStyle(props, visualState, isStatic) {
const styleProp = props.style || {};
const style = {};
/**
* Copy non-Motion Values straight into style
*/
copyRawValuesOnly(style, styleProp, props);
Object.assign(style, useInitialMotionValues(props, visualState, isStatic));
return props.transformValues ? props.transformValues(style) : style;
}
function useHTMLProps(props, visualState, isStatic) {
// The `any` isn't ideal but it is the type of createElement props argument
const htmlProps = {};
const style = useStyle(props, visualState, isStatic);
if (props.drag && props.dragListener !== false) {
// Disable the ghost element when a user drags
htmlProps.draggable = false;
// Disable text selection
style.userSelect =
style.WebkitUserSelect =
style.WebkitTouchCallout =
"none";
// Disable scrolling on the draggable direction
style.touchAction =
props.drag === true
? "none"
: `pan-${props.drag === "x" ? "y" : "x"}`;
}
if (props.tabIndex === undefined &&
(props.onTap || props.onTapStart || props.whileTap)) {
htmlProps.tabIndex = 0;
}
htmlProps.style = style;
return htmlProps;
}
export { copyRawValuesOnly, useHTMLProps };

View File

@@ -0,0 +1,74 @@
import { buildTransform } from './build-transform.mjs';
import { isCSSVariableName } from '../../dom/utils/is-css-variable.mjs';
import { transformProps } from './transform.mjs';
import { getValueAsType } from '../../dom/value-types/get-as-type.mjs';
import { numberValueTypes } from '../../dom/value-types/number.mjs';
function buildHTMLStyles(state, latestValues, options, transformTemplate) {
const { style, vars, transform, transformOrigin } = state;
// Track whether we encounter any transform or transformOrigin values.
let hasTransform = false;
let hasTransformOrigin = false;
// Does the calculated transform essentially equal "none"?
let transformIsNone = true;
/**
* Loop over all our latest animated values and decide whether to handle them
* as a style or CSS variable.
*
* Transforms and transform origins are kept seperately for further processing.
*/
for (const key in latestValues) {
const value = latestValues[key];
/**
* If this is a CSS variable we don't do any further processing.
*/
if (isCSSVariableName(key)) {
vars[key] = value;
continue;
}
// Convert the value to its default value type, ie 0 -> "0px"
const valueType = numberValueTypes[key];
const valueAsType = getValueAsType(value, valueType);
if (transformProps.has(key)) {
// If this is a transform, flag to enable further transform processing
hasTransform = true;
transform[key] = valueAsType;
// If we already know we have a non-default transform, early return
if (!transformIsNone)
continue;
// Otherwise check to see if this is a default transform
if (value !== (valueType.default || 0))
transformIsNone = false;
}
else if (key.startsWith("origin")) {
// If this is a transform origin, flag and enable further transform-origin processing
hasTransformOrigin = true;
transformOrigin[key] = valueAsType;
}
else {
style[key] = valueAsType;
}
}
if (!latestValues.transform) {
if (hasTransform || transformTemplate) {
style.transform = buildTransform(state.transform, options, transformIsNone, transformTemplate);
}
else if (style.transform) {
/**
* If we have previously created a transform but currently don't have any,
* reset transform style to none.
*/
style.transform = "none";
}
}
/**
* Build a transformOrigin style. Uses the same defaults as the browser for
* undefined origins.
*/
if (hasTransformOrigin) {
const { originX = "50%", originY = "50%", originZ = 0, } = transformOrigin;
style.transformOrigin = `${originX} ${originY} ${originZ}`;
}
}
export { buildHTMLStyles };

View File

@@ -0,0 +1,45 @@
import { transformPropOrder } from './transform.mjs';
const translateAlias = {
x: "translateX",
y: "translateY",
z: "translateZ",
transformPerspective: "perspective",
};
const numTransforms = transformPropOrder.length;
/**
* Build a CSS transform style from individual x/y/scale etc properties.
*
* This outputs with a default order of transforms/scales/rotations, this can be customised by
* providing a transformTemplate function.
*/
function buildTransform(transform, { enableHardwareAcceleration = true, allowTransformNone = true, }, transformIsDefault, transformTemplate) {
// The transform string we're going to build into.
let transformString = "";
/**
* Loop over all possible transforms in order, adding the ones that
* are present to the transform string.
*/
for (let i = 0; i < numTransforms; i++) {
const key = transformPropOrder[i];
if (transform[key] !== undefined) {
const transformName = translateAlias[key] || key;
transformString += `${transformName}(${transform[key]}) `;
}
}
if (enableHardwareAcceleration && !transform.z) {
transformString += "translateZ(0)";
}
transformString = transformString.trim();
// If we have a custom `transform` template, pass our transform values and
// generated transformString to that before returning
if (transformTemplate) {
transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
}
else if (allowTransformNone && transformIsDefault) {
transformString = "none";
}
return transformString;
}
export { buildTransform };

View File

@@ -0,0 +1,8 @@
const createHtmlRenderState = () => ({
style: {},
transform: {},
transformOrigin: {},
vars: {},
});
export { createHtmlRenderState };

View File

@@ -0,0 +1,9 @@
function renderHTML(element, { style, vars }, styleProp, projection) {
Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp));
// Loop over any CSS variables and assign those.
for (const key in vars) {
element.style.setProperty(key, vars[key]);
}
}
export { renderHTML };

View File

@@ -0,0 +1,17 @@
import { isForcedMotionValue } from '../../../motion/utils/is-forced-motion-value.mjs';
import { isMotionValue } from '../../../value/utils/is-motion-value.mjs';
function scrapeMotionValuesFromProps(props, prevProps) {
const { style } = props;
const newValues = {};
for (const key in style) {
if (isMotionValue(style[key]) ||
(prevProps.style && isMotionValue(prevProps.style[key])) ||
isForcedMotionValue(key, props)) {
newValues[key] = style[key];
}
}
return newValues;
}
export { scrapeMotionValuesFromProps };

View File

@@ -0,0 +1,28 @@
/**
* Generate a list of every possible transform key.
*/
const transformPropOrder = [
"transformPerspective",
"x",
"y",
"z",
"translateX",
"translateY",
"translateZ",
"scale",
"scaleX",
"scaleY",
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"skew",
"skewX",
"skewY",
];
/**
* A quick lookup for transform props.
*/
const transformProps = new Set(transformPropOrder);
export { transformPropOrder, transformProps };

View File

@@ -0,0 +1,3 @@
const visualElementStore = new WeakMap();
export { visualElementStore };

View File

@@ -0,0 +1,47 @@
import { scrapeMotionValuesFromProps } from './utils/scrape-motion-values.mjs';
import { DOMVisualElement } from '../dom/DOMVisualElement.mjs';
import { buildSVGAttrs } from './utils/build-attrs.mjs';
import { camelToDash } from '../dom/utils/camel-to-dash.mjs';
import { camelCaseAttributes } from './utils/camel-case-attrs.mjs';
import { transformProps } from '../html/utils/transform.mjs';
import { renderSVG } from './utils/render.mjs';
import { getDefaultValueType } from '../dom/value-types/defaults.mjs';
import { createBox } from '../../projection/geometry/models.mjs';
import { isSVGTag } from './utils/is-svg-tag.mjs';
class SVGVisualElement extends DOMVisualElement {
constructor() {
super(...arguments);
this.type = "svg";
this.isSVGTag = false;
}
getBaseTargetFromProps(props, key) {
return props[key];
}
readValueFromInstance(instance, key) {
if (transformProps.has(key)) {
const defaultType = getDefaultValueType(key);
return defaultType ? defaultType.default || 0 : 0;
}
key = !camelCaseAttributes.has(key) ? camelToDash(key) : key;
return instance.getAttribute(key);
}
measureInstanceViewportBox() {
return createBox();
}
scrapeMotionValuesFromProps(props, prevProps) {
return scrapeMotionValuesFromProps(props, prevProps);
}
build(renderState, latestValues, options, props) {
buildSVGAttrs(renderState, latestValues, options, this.isSVGTag, props.transformTemplate);
}
renderInstance(instance, renderState, styleProp, projection) {
renderSVG(instance, renderState, styleProp, projection);
}
mount(instance) {
this.isSVGTag = isSVGTag(instance.tagName);
super.mount(instance);
}
}
export { SVGVisualElement };

View File

@@ -0,0 +1,40 @@
import { renderSVG } from './utils/render.mjs';
import { scrapeMotionValuesFromProps } from './utils/scrape-motion-values.mjs';
import { makeUseVisualState } from '../../motion/utils/use-visual-state.mjs';
import { createSvgRenderState } from './utils/create-render-state.mjs';
import { buildSVGAttrs } from './utils/build-attrs.mjs';
import { isSVGTag } from './utils/is-svg-tag.mjs';
import { frame } from '../../frameloop/frame.mjs';
const svgMotionConfig = {
useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps: scrapeMotionValuesFromProps,
createRenderState: createSvgRenderState,
onMount: (props, instance, { renderState, latestValues }) => {
frame.read(() => {
try {
renderState.dimensions =
typeof instance.getBBox ===
"function"
? instance.getBBox()
: instance.getBoundingClientRect();
}
catch (e) {
// Most likely trying to measure an unrendered element under Firefox
renderState.dimensions = {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
});
frame.render(() => {
buildSVGAttrs(renderState, latestValues, { enableHardwareAcceleration: false }, isSVGTag(instance.tagName), props.transformTemplate);
renderSVG(instance, renderState);
});
},
}),
};
export { svgMotionConfig };

View File

@@ -0,0 +1,33 @@
/**
* We keep these listed seperately as we use the lowercase tag names as part
* of the runtime bundle to detect SVG components
*/
const lowercaseSVGElements = [
"animate",
"circle",
"defs",
"desc",
"ellipse",
"g",
"image",
"line",
"filter",
"marker",
"mask",
"metadata",
"path",
"pattern",
"polygon",
"polyline",
"rect",
"stop",
"switch",
"symbol",
"svg",
"text",
"tspan",
"use",
"view",
];
export { lowercaseSVGElements };

View File

@@ -0,0 +1,24 @@
import { useMemo } from 'react';
import { copyRawValuesOnly } from '../html/use-props.mjs';
import { buildSVGAttrs } from './utils/build-attrs.mjs';
import { createSvgRenderState } from './utils/create-render-state.mjs';
import { isSVGTag } from './utils/is-svg-tag.mjs';
function useSVGProps(props, visualState, _isStatic, Component) {
const visualProps = useMemo(() => {
const state = createSvgRenderState();
buildSVGAttrs(state, visualState, { enableHardwareAcceleration: false }, isSVGTag(Component), props.transformTemplate);
return {
...state.attrs,
style: { ...state.style },
};
}, [visualState]);
if (props.style) {
const rawStyles = {};
copyRawValuesOnly(rawStyles, props.style, props);
visualProps.style = { ...rawStyles, ...visualProps.style };
}
return visualProps;
}
export { useSVGProps };

View File

@@ -0,0 +1,52 @@
import { buildHTMLStyles } from '../../html/utils/build-styles.mjs';
import { calcSVGTransformOrigin } from './transform-origin.mjs';
import { buildSVGPath } from './path.mjs';
/**
* Build SVG visual attrbutes, like cx and style.transform
*/
function buildSVGAttrs(state, { attrX, attrY, attrScale, originX, originY, pathLength, pathSpacing = 1, pathOffset = 0,
// This is object creation, which we try to avoid per-frame.
...latest }, options, isSVGTag, transformTemplate) {
buildHTMLStyles(state, latest, options, transformTemplate);
/**
* For svg tags we just want to make sure viewBox is animatable and treat all the styles
* as normal HTML tags.
*/
if (isSVGTag) {
if (state.style.viewBox) {
state.attrs.viewBox = state.style.viewBox;
}
return;
}
state.attrs = state.style;
state.style = {};
const { attrs, style, dimensions } = state;
/**
* However, we apply transforms as CSS transforms. So if we detect a transform we take it from attrs
* and copy it into style.
*/
if (attrs.transform) {
if (dimensions)
style.transform = attrs.transform;
delete attrs.transform;
}
// Parse transformOrigin
if (dimensions &&
(originX !== undefined || originY !== undefined || style.transform)) {
style.transformOrigin = calcSVGTransformOrigin(dimensions, originX !== undefined ? originX : 0.5, originY !== undefined ? originY : 0.5);
}
// Render attrX/attrY/attrScale as attributes
if (attrX !== undefined)
attrs.x = attrX;
if (attrY !== undefined)
attrs.y = attrY;
if (attrScale !== undefined)
attrs.scale = attrScale;
// Build SVG path if one has been defined
if (pathLength !== undefined) {
buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false);
}
}
export { buildSVGAttrs };

View File

@@ -0,0 +1,30 @@
/**
* A set of attribute names that are always read/written as camel case.
*/
const camelCaseAttributes = new Set([
"baseFrequency",
"diffuseConstant",
"kernelMatrix",
"kernelUnitLength",
"keySplines",
"keyTimes",
"limitingConeAngle",
"markerHeight",
"markerWidth",
"numOctaves",
"targetX",
"targetY",
"surfaceScale",
"specularConstant",
"specularExponent",
"stdDeviation",
"tableValues",
"viewBox",
"gradientTransform",
"pathLength",
"startOffset",
"textLength",
"lengthAdjust",
]);
export { camelCaseAttributes };

View File

@@ -0,0 +1,8 @@
import { createHtmlRenderState } from '../../html/utils/create-render-state.mjs';
const createSvgRenderState = () => ({
...createHtmlRenderState(),
attrs: {},
});
export { createSvgRenderState };

View File

@@ -0,0 +1,3 @@
const isSVGTag = (tag) => typeof tag === "string" && tag.toLowerCase() === "svg";
export { isSVGTag };

View File

@@ -0,0 +1,32 @@
import { px } from '../../../value/types/numbers/units.mjs';
const dashKeys = {
offset: "stroke-dashoffset",
array: "stroke-dasharray",
};
const camelKeys = {
offset: "strokeDashoffset",
array: "strokeDasharray",
};
/**
* Build SVG path properties. Uses the path's measured length to convert
* our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset
* and stroke-dasharray attributes.
*
* This function is mutative to reduce per-frame GC.
*/
function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) {
// Normalise path length by setting SVG attribute pathLength to 1
attrs.pathLength = 1;
// We use dash case when setting attributes directly to the DOM node and camel case
// when defining props on a React component.
const keys = useDashCase ? dashKeys : camelKeys;
// Build the dash offset
attrs[keys.offset] = px.transform(-offset);
// Build the dash array
const pathLength = px.transform(length);
const pathSpacing = px.transform(spacing);
attrs[keys.array] = `${pathLength} ${pathSpacing}`;
}
export { buildSVGPath };

View File

@@ -0,0 +1,12 @@
import { camelToDash } from '../../dom/utils/camel-to-dash.mjs';
import { renderHTML } from '../../html/utils/render.mjs';
import { camelCaseAttributes } from './camel-case-attrs.mjs';
function renderSVG(element, renderState, _styleProp, projection) {
renderHTML(element, renderState, undefined, projection);
for (const key in renderState.attrs) {
element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]);
}
}
export { renderSVG };

View File

@@ -0,0 +1,18 @@
import { isMotionValue } from '../../../value/utils/is-motion-value.mjs';
import { scrapeMotionValuesFromProps as scrapeMotionValuesFromProps$1 } from '../../html/utils/scrape-motion-values.mjs';
import { transformPropOrder } from '../../html/utils/transform.mjs';
function scrapeMotionValuesFromProps(props, prevProps) {
const newValues = scrapeMotionValuesFromProps$1(props, prevProps);
for (const key in props) {
if (isMotionValue(props[key]) || isMotionValue(prevProps[key])) {
const targetKey = transformPropOrder.indexOf(key) !== -1
? "attr" + key.charAt(0).toUpperCase() + key.substring(1)
: key;
newValues[targetKey] = props[key];
}
}
return newValues;
}
export { scrapeMotionValuesFromProps };

View File

@@ -0,0 +1,18 @@
import { px } from '../../../value/types/numbers/units.mjs';
function calcOrigin(origin, offset, size) {
return typeof origin === "string"
? origin
: px.transform(offset + size * origin);
}
/**
* The SVG transform origin defaults are different to CSS and is less intuitive,
* so we use the measured dimensions of the SVG to reconcile these.
*/
function calcSVGTransformOrigin(dimensions, originX, originY) {
const pxOriginX = calcOrigin(originX, dimensions.x, dimensions.width);
const pxOriginY = calcOrigin(originY, dimensions.y, dimensions.height);
return `${pxOriginX} ${pxOriginY}`;
}
export { calcSVGTransformOrigin };

View File

@@ -0,0 +1,312 @@
import { isAnimationControls } from '../../animation/utils/is-animation-controls.mjs';
import { isKeyframesTarget } from '../../animation/utils/is-keyframes-target.mjs';
import { shallowCompare } from '../../utils/shallow-compare.mjs';
import { isVariantLabel } from './is-variant-label.mjs';
import { resolveVariant } from './resolve-dynamic-variants.mjs';
import { variantPriorityOrder } from './variant-props.mjs';
import { animateVisualElement } from '../../animation/interfaces/visual-element.mjs';
const reversePriorityOrder = [...variantPriorityOrder].reverse();
const numAnimationTypes = variantPriorityOrder.length;
function animateList(visualElement) {
return (animations) => Promise.all(animations.map(({ animation, options }) => animateVisualElement(visualElement, animation, options)));
}
function createAnimationState(visualElement) {
let animate = animateList(visualElement);
const state = createState();
let isInitialRender = true;
/**
* This function will be used to reduce the animation definitions for
* each active animation type into an object of resolved values for it.
*/
const buildResolvedTypeValues = (acc, definition) => {
const resolved = resolveVariant(visualElement, definition);
if (resolved) {
const { transition, transitionEnd, ...target } = resolved;
acc = { ...acc, ...target, ...transitionEnd };
}
return acc;
};
/**
* This just allows us to inject mocked animation functions
* @internal
*/
function setAnimateFunction(makeAnimator) {
animate = makeAnimator(visualElement);
}
/**
* When we receive new props, we need to:
* 1. Create a list of protected keys for each type. This is a directory of
* value keys that are currently being "handled" by types of a higher priority
* so that whenever an animation is played of a given type, these values are
* protected from being animated.
* 2. Determine if an animation type needs animating.
* 3. Determine if any values have been removed from a type and figure out
* what to animate those to.
*/
function animateChanges(options, changedActiveType) {
const props = visualElement.getProps();
const context = visualElement.getVariantContext(true) || {};
/**
* A list of animations that we'll build into as we iterate through the animation
* types. This will get executed at the end of the function.
*/
const animations = [];
/**
* Keep track of which values have been removed. Then, as we hit lower priority
* animation types, we can check if they contain removed values and animate to that.
*/
const removedKeys = new Set();
/**
* A dictionary of all encountered keys. This is an object to let us build into and
* copy it without iteration. Each time we hit an animation type we set its protected
* keys - the keys its not allowed to animate - to the latest version of this object.
*/
let encounteredKeys = {};
/**
* If a variant has been removed at a given index, and this component is controlling
* variant animations, we want to ensure lower-priority variants are forced to animate.
*/
let removedVariantIndex = Infinity;
/**
* Iterate through all animation types in reverse priority order. For each, we want to
* detect which values it's handling and whether or not they've changed (and therefore
* need to be animated). If any values have been removed, we want to detect those in
* lower priority props and flag for animation.
*/
for (let i = 0; i < numAnimationTypes; i++) {
const type = reversePriorityOrder[i];
const typeState = state[type];
const prop = props[type] !== undefined ? props[type] : context[type];
const propIsVariant = isVariantLabel(prop);
/**
* If this type has *just* changed isActive status, set activeDelta
* to that status. Otherwise set to null.
*/
const activeDelta = type === changedActiveType ? typeState.isActive : null;
if (activeDelta === false)
removedVariantIndex = i;
/**
* If this prop is an inherited variant, rather than been set directly on the
* component itself, we want to make sure we allow the parent to trigger animations.
*
* TODO: Can probably change this to a !isControllingVariants check
*/
let isInherited = prop === context[type] && prop !== props[type] && propIsVariant;
/**
*
*/
if (isInherited &&
isInitialRender &&
visualElement.manuallyAnimateOnMount) {
isInherited = false;
}
/**
* Set all encountered keys so far as the protected keys for this type. This will
* be any key that has been animated or otherwise handled by active, higher-priortiy types.
*/
typeState.protectedKeys = { ...encounteredKeys };
// Check if we can skip analysing this prop early
if (
// If it isn't active and hasn't *just* been set as inactive
(!typeState.isActive && activeDelta === null) ||
// If we didn't and don't have any defined prop for this animation type
(!prop && !typeState.prevProp) ||
// Or if the prop doesn't define an animation
isAnimationControls(prop) ||
typeof prop === "boolean") {
continue;
}
/**
* As we go look through the values defined on this type, if we detect
* a changed value or a value that was removed in a higher priority, we set
* this to true and add this prop to the animation list.
*/
const variantDidChange = checkVariantsDidChange(typeState.prevProp, prop);
let shouldAnimateType = variantDidChange ||
// If we're making this variant active, we want to always make it active
(type === changedActiveType &&
typeState.isActive &&
!isInherited &&
propIsVariant) ||
// If we removed a higher-priority variant (i is in reverse order)
(i > removedVariantIndex && propIsVariant);
let handledRemovedValues = false;
/**
* As animations can be set as variant lists, variants or target objects, we
* coerce everything to an array if it isn't one already
*/
const definitionList = Array.isArray(prop) ? prop : [prop];
/**
* Build an object of all the resolved values. We'll use this in the subsequent
* animateChanges calls to determine whether a value has changed.
*/
let resolvedValues = definitionList.reduce(buildResolvedTypeValues, {});
if (activeDelta === false)
resolvedValues = {};
/**
* Now we need to loop through all the keys in the prev prop and this prop,
* and decide:
* 1. If the value has changed, and needs animating
* 2. If it has been removed, and needs adding to the removedKeys set
* 3. If it has been removed in a higher priority type and needs animating
* 4. If it hasn't been removed in a higher priority but hasn't changed, and
* needs adding to the type's protectedKeys list.
*/
const { prevResolvedValues = {} } = typeState;
const allKeys = {
...prevResolvedValues,
...resolvedValues,
};
const markToAnimate = (key) => {
shouldAnimateType = true;
if (removedKeys.has(key)) {
handledRemovedValues = true;
removedKeys.delete(key);
}
typeState.needsAnimating[key] = true;
};
for (const key in allKeys) {
const next = resolvedValues[key];
const prev = prevResolvedValues[key];
// If we've already handled this we can just skip ahead
if (encounteredKeys.hasOwnProperty(key))
continue;
/**
* If the value has changed, we probably want to animate it.
*/
let valueHasChanged = false;
if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
valueHasChanged = !shallowCompare(next, prev);
}
else {
valueHasChanged = next !== prev;
}
if (valueHasChanged) {
if (next !== undefined) {
// If next is defined and doesn't equal prev, it needs animating
markToAnimate(key);
}
else {
// If it's undefined, it's been removed.
removedKeys.add(key);
}
}
else if (next !== undefined && removedKeys.has(key)) {
/**
* If next hasn't changed and it isn't undefined, we want to check if it's
* been removed by a higher priority
*/
markToAnimate(key);
}
else {
/**
* If it hasn't changed, we add it to the list of protected values
* to ensure it doesn't get animated.
*/
typeState.protectedKeys[key] = true;
}
}
/**
* Update the typeState so next time animateChanges is called we can compare the
* latest prop and resolvedValues to these.
*/
typeState.prevProp = prop;
typeState.prevResolvedValues = resolvedValues;
/**
*
*/
if (typeState.isActive) {
encounteredKeys = { ...encounteredKeys, ...resolvedValues };
}
if (isInitialRender && visualElement.blockInitialAnimation) {
shouldAnimateType = false;
}
/**
* If this is an inherited prop we want to hard-block animations
*/
if (shouldAnimateType && (!isInherited || handledRemovedValues)) {
animations.push(...definitionList.map((animation) => ({
animation: animation,
options: { type, ...options },
})));
}
}
/**
* If there are some removed value that haven't been dealt with,
* we need to create a new animation that falls back either to the value
* defined in the style prop, or the last read value.
*/
if (removedKeys.size) {
const fallbackAnimation = {};
removedKeys.forEach((key) => {
const fallbackTarget = visualElement.getBaseTarget(key);
if (fallbackTarget !== undefined) {
fallbackAnimation[key] = fallbackTarget;
}
});
animations.push({ animation: fallbackAnimation });
}
let shouldAnimate = Boolean(animations.length);
if (isInitialRender &&
(props.initial === false || props.initial === props.animate) &&
!visualElement.manuallyAnimateOnMount) {
shouldAnimate = false;
}
isInitialRender = false;
return shouldAnimate ? animate(animations) : Promise.resolve();
}
/**
* Change whether a certain animation type is active.
*/
function setActive(type, isActive, options) {
var _a;
// If the active state hasn't changed, we can safely do nothing here
if (state[type].isActive === isActive)
return Promise.resolve();
// Propagate active change to children
(_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach((child) => { var _a; return (_a = child.animationState) === null || _a === void 0 ? void 0 : _a.setActive(type, isActive); });
state[type].isActive = isActive;
const animations = animateChanges(options, type);
for (const key in state) {
state[key].protectedKeys = {};
}
return animations;
}
return {
animateChanges,
setActive,
setAnimateFunction,
getState: () => state,
};
}
function checkVariantsDidChange(prev, next) {
if (typeof next === "string") {
return next !== prev;
}
else if (Array.isArray(next)) {
return !shallowCompare(next, prev);
}
return false;
}
function createTypeState(isActive = false) {
return {
isActive,
protectedKeys: {},
needsAnimating: {},
prevResolvedValues: {},
};
}
function createState() {
return {
animate: createTypeState(true),
whileInView: createTypeState(),
whileHover: createTypeState(),
whileTap: createTypeState(),
whileDrag: createTypeState(),
whileFocus: createTypeState(),
exit: createTypeState(),
};
}
export { checkVariantsDidChange, createAnimationState };

View File

@@ -0,0 +1,3 @@
const compareByDepth = (a, b) => a.depth - b.depth;
export { compareByDepth };

View File

@@ -0,0 +1,24 @@
import { addUniqueItem, removeItem } from '../../utils/array.mjs';
import { compareByDepth } from './compare-by-depth.mjs';
class FlatTree {
constructor() {
this.children = [];
this.isDirty = false;
}
add(child) {
addUniqueItem(this.children, child);
this.isDirty = true;
}
remove(child) {
removeItem(this.children, child);
this.isDirty = true;
}
forEach(callback) {
this.isDirty && this.children.sort(compareByDepth);
this.isDirty = false;
this.children.forEach(callback);
}
}
export { FlatTree };

View File

@@ -0,0 +1,13 @@
import { isAnimationControls } from '../../animation/utils/is-animation-controls.mjs';
import { isVariantLabel } from './is-variant-label.mjs';
import { variantProps } from './variant-props.mjs';
function isControllingVariants(props) {
return (isAnimationControls(props.animate) ||
variantProps.some((name) => isVariantLabel(props[name])));
}
function isVariantNode(props) {
return Boolean(isControllingVariants(props) || props.variants);
}
export { isControllingVariants, isVariantNode };

View File

@@ -0,0 +1,8 @@
/**
* Decides if the supplied variable is variant label
*/
function isVariantLabel(v) {
return typeof v === "string" || Array.isArray(v);
}
export { isVariantLabel };

View File

@@ -0,0 +1,63 @@
import { isWillChangeMotionValue } from '../../value/use-will-change/is.mjs';
import { warnOnce } from '../../utils/warn-once.mjs';
import { motionValue } from '../../value/index.mjs';
import { isMotionValue } from '../../value/utils/is-motion-value.mjs';
function updateMotionValuesFromProps(element, next, prev) {
const { willChange } = next;
for (const key in next) {
const nextValue = next[key];
const prevValue = prev[key];
if (isMotionValue(nextValue)) {
/**
* If this is a motion value found in props or style, we want to add it
* to our visual element's motion value map.
*/
element.addValue(key, nextValue);
if (isWillChangeMotionValue(willChange)) {
willChange.add(key);
}
/**
* Check the version of the incoming motion value with this version
* and warn against mismatches.
*/
if (process.env.NODE_ENV === "development") {
warnOnce(nextValue.version === "10.18.0", `Attempting to mix Framer Motion versions ${nextValue.version} with 10.18.0 may not work as expected.`);
}
}
else if (isMotionValue(prevValue)) {
/**
* If we're swapping from a motion value to a static value,
* create a new motion value from that
*/
element.addValue(key, motionValue(nextValue, { owner: element }));
if (isWillChangeMotionValue(willChange)) {
willChange.remove(key);
}
}
else if (prevValue !== nextValue) {
/**
* If this is a flat value that has changed, update the motion value
* or create one if it doesn't exist. We only want to do this if we're
* not handling the value with our animation state.
*/
if (element.hasValue(key)) {
const existingValue = element.getValue(key);
// TODO: Only update values that aren't being animated or even looked at
!existingValue.hasAnimated && existingValue.set(nextValue);
}
else {
const latestValue = element.getStaticValue(key);
element.addValue(key, motionValue(latestValue !== undefined ? latestValue : nextValue, { owner: element }));
}
}
}
// Handle removed values
for (const key in prev) {
if (next[key] === undefined)
element.removeValue(key);
}
return next;
}
export { updateMotionValuesFromProps };

View File

@@ -0,0 +1,24 @@
import { resolveVariantFromProps } from './resolve-variants.mjs';
/**
* Creates an object containing the latest state of every MotionValue on a VisualElement
*/
function getCurrent(visualElement) {
const current = {};
visualElement.values.forEach((value, key) => (current[key] = value.get()));
return current;
}
/**
* Creates an object containing the latest velocity of every MotionValue on a VisualElement
*/
function getVelocity(visualElement) {
const velocity = {};
visualElement.values.forEach((value, key) => (velocity[key] = value.getVelocity()));
return velocity;
}
function resolveVariant(visualElement, definition, custom) {
const props = visualElement.getProps();
return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, getCurrent(visualElement), getVelocity(visualElement));
}
export { resolveVariant };

View File

@@ -0,0 +1,26 @@
function resolveVariantFromProps(props, definition, custom, currentValues = {}, currentVelocity = {}) {
/**
* If the variant definition is a function, resolve.
*/
if (typeof definition === "function") {
definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity);
}
/**
* If the variant definition is a variant label, or
* the function returned a variant label, resolve.
*/
if (typeof definition === "string") {
definition = props.variants && props.variants[definition];
}
/**
* At this point we've resolved both functions and variant labels,
* but the resolved variant label might itself have been a function.
* If so, resolve. This can only have returned a valid target object.
*/
if (typeof definition === "function") {
definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity);
}
return definition;
}
export { resolveVariantFromProps };

View File

@@ -0,0 +1,124 @@
import { isNumericalString } from '../../utils/is-numerical-string.mjs';
import { isZeroValueString } from '../../utils/is-zero-value-string.mjs';
import { resolveFinalValueInKeyframes } from '../../utils/resolve-value.mjs';
import { motionValue } from '../../value/index.mjs';
import { complex } from '../../value/types/complex/index.mjs';
import { getAnimatableNone } from '../dom/value-types/animatable-none.mjs';
import { findValueType } from '../dom/value-types/find.mjs';
import { resolveVariant } from './resolve-dynamic-variants.mjs';
/**
* Set VisualElement's MotionValue, creating a new MotionValue for it if
* it doesn't exist.
*/
function setMotionValue(visualElement, key, value) {
if (visualElement.hasValue(key)) {
visualElement.getValue(key).set(value);
}
else {
visualElement.addValue(key, motionValue(value));
}
}
function setTarget(visualElement, definition) {
const resolved = resolveVariant(visualElement, definition);
let { transitionEnd = {}, transition = {}, ...target } = resolved ? visualElement.makeTargetAnimatable(resolved, false) : {};
target = { ...target, ...transitionEnd };
for (const key in target) {
const value = resolveFinalValueInKeyframes(target[key]);
setMotionValue(visualElement, key, value);
}
}
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);
}
}
function checkTargetForNewValues(visualElement, target, origin) {
var _a, _b;
const newValueKeys = Object.keys(target).filter((key) => !visualElement.hasValue(key));
const numNewValues = newValueKeys.length;
if (!numNewValues)
return;
for (let i = 0; i < numNewValues; i++) {
const key = newValueKeys[i];
const targetValue = target[key];
let value = null;
/**
* If the target is a series of keyframes, we can use the first value
* in the array. If this first value is null, we'll still need to read from the DOM.
*/
if (Array.isArray(targetValue)) {
value = targetValue[0];
}
/**
* If the target isn't keyframes, or the first keyframe was null, we need to
* first check if an origin value was explicitly defined in the transition as "from",
* if not read the value from the DOM. As an absolute fallback, take the defined target value.
*/
if (value === null) {
value = (_b = (_a = origin[key]) !== null && _a !== void 0 ? _a : visualElement.readValue(key)) !== null && _b !== void 0 ? _b : target[key];
}
/**
* If value is still undefined or null, ignore it. Preferably this would throw,
* but this was causing issues in Framer.
*/
if (value === undefined || value === null)
continue;
if (typeof value === "string" &&
(isNumericalString(value) || isZeroValueString(value))) {
// If this is a number read as a string, ie "0" or "200", convert it to a number
value = parseFloat(value);
}
else if (!findValueType(value) && complex.test(targetValue)) {
value = getAnimatableNone(key, targetValue);
}
visualElement.addValue(key, motionValue(value, { owner: visualElement }));
if (origin[key] === undefined) {
origin[key] = value;
}
if (value !== null)
visualElement.setBaseTarget(key, value);
}
}
function getOriginFromTransition(key, transition) {
if (!transition)
return;
const valueTransition = transition[key] || transition["default"] || transition;
return valueTransition.from;
}
function getOrigin(target, transition, visualElement) {
const origin = {};
for (const key in target) {
const transitionOrigin = getOriginFromTransition(key, transition);
if (transitionOrigin !== undefined) {
origin[key] = transitionOrigin;
}
else {
const value = visualElement.getValue(key);
if (value) {
origin[key] = value.get();
}
}
}
return origin;
}
export { checkTargetForNewValues, getOrigin, getOriginFromTransition, setTarget, setValues };

View File

@@ -0,0 +1,12 @@
const variantPriorityOrder = [
"animate",
"whileInView",
"whileFocus",
"whileHover",
"whileTap",
"whileDrag",
"exit",
];
const variantProps = ["initial", ...variantPriorityOrder];
export { variantPriorityOrder, variantProps };