@humanspeak/svelte-motion
Version:
Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values
255 lines (254 loc) • 10.5 kB
JavaScript
/**
* Resolves a variant key to its keyframes definition.
*
* Looks up `key` in `variants`. When the entry is a function (dynamic
* variant), it's invoked with `custom` to produce keyframes — matching
* framer-motion's per-instance variant pattern.
*
* @param variants - The variants object containing named animation states.
* @param key - The variant key to look up.
* @param custom - Value forwarded to function-form variants. Pass-through
* `undefined` when no `custom` is in scope; the dynamic variant itself
* decides how to handle the absent input.
* @returns The keyframes definition for the variant, or `undefined` if the
* key is missing.
*
* @example
* ```ts
* const variants = {
* visible: (i: number) => ({ x: i * 50 }),
* hidden: { opacity: 0 }
* }
* resolveVariant(variants, 'visible', 3) // { x: 150 }
* resolveVariant(variants, 'hidden') // { opacity: 0 }
* resolveVariant(undefined, 'visible') // undefined
* ```
*/
export const resolveVariant = (variants, key, custom) => {
if (!variants || !key)
return undefined;
// Guard against built-in / inherited keys like 'toString' or
// 'constructor' — without this, `whileHover="toString"` would
// resolve to `Function.prototype.toString` and leak a function into
// the merge path.
if (!Object.prototype.hasOwnProperty.call(variants, key))
return undefined;
const entry = variants[key];
if (typeof entry === 'function')
return entry(custom);
return entry;
};
/**
* Resolves a single variant key or an ordered list of keys to merged
* keyframes. Matches framer-motion's `VariantLabels = string | string[]`
* surface: later keys in the list override earlier ones on key
* collisions (`Object.assign` semantics).
*
* Missing keys are skipped. An empty list, an empty string, or an
* undefined argument all resolve to `undefined`.
*
* @param variants - The variants object containing named animation states.
* @param keys - A single variant key or an array of variant keys.
* @param custom - Forwarded to function-form variants (per-entry).
* @returns Merged keyframes definition, or `undefined` when nothing resolved.
*
* @example
* ```ts
* const variants = {
* hover: { scale: 1.1 },
* active: { scale: 1.2, color: 'red' }
* }
* resolveVariantList(variants, 'hover') // { scale: 1.1 }
* resolveVariantList(variants, ['hover', 'active']) // { scale: 1.2, color: 'red' }
* resolveVariantList(variants, ['hover', 'missing']) // { scale: 1.1 }
* resolveVariantList(variants, []) // undefined
* ```
*/
export const resolveVariantList = (variants, keys, custom) => {
if (keys === undefined)
return undefined;
if (typeof keys === 'string')
return resolveVariant(variants, keys, custom);
if (keys.length === 0)
return undefined;
let merged;
for (const key of keys) {
const entry = resolveVariant(variants, key, custom);
// Defensive: only merge plain keyframe objects. A function-form
// variant could return something else (array, class instance,
// string) under a misuse, and spreading those would corrupt the
// merged result. Reject arrays explicitly and require the
// prototype to be `Object.prototype` (or `null` for objects
// created via `Object.create(null)`).
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
continue;
const proto = Object.getPrototypeOf(entry);
if (proto !== Object.prototype && proto !== null)
continue;
const obj = entry;
merged = merged ? { ...merged, ...obj } : { ...obj };
}
return merged;
};
/**
* Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
*
* When `initial` is a string, looks it up in the variants object (invoking
* dynamic variants with `custom`). When `initial={false}`, returns `false` to
* skip the initial animation. Otherwise returns the keyframes directly.
*
* @param initial - The initial prop value (keyframes, variant key, false, or undefined).
* @param variants - The variants object for resolving string keys.
* @param custom - Forwarded to function-form variants.
* @returns Keyframes definition, `false` to skip animation, or undefined.
*
* @example
* ```ts
* const variants = { hidden: (i: number) => ({ x: -i * 100 }) }
* resolveInitial('hidden', variants, 2) // { x: -200 }
* resolveInitial({ x: 0 }, variants) // { x: 0 }
* resolveInitial(false, variants) // false
* resolveInitial(undefined, variants) // undefined
* ```
*/
export const resolveInitial = (initial, variants, custom) => {
if (initial === false)
return false;
if (initial === undefined)
return undefined;
if (typeof initial === 'string' || Array.isArray(initial))
return resolveVariantList(variants, initial, custom);
return initial;
};
/**
* Resolves the animate prop to keyframes, handling variant keys.
*
* When `animate` is a string (or array of strings), looks it up in the
* variants object (invoking dynamic variants with `custom`). Otherwise
* returns the keyframes directly.
*
* @param animate - The animate prop value (keyframes, variant key, array
* of variant keys, or undefined).
* @param variants - The variants object for resolving string keys.
* @param custom - Forwarded to function-form variants.
* @returns Keyframes definition or undefined.
*
* @example
* ```ts
* const variants = { visible: { opacity: 1 }, shifted: { x: 100 } }
* resolveAnimate('visible', variants) // { opacity: 1 }
* resolveAnimate(['visible', 'shifted'], variants) // { opacity: 1, x: 100 }
* resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
* resolveAnimate(undefined, variants) // undefined
* ```
*/
export const resolveAnimate = (animate, variants, custom) => {
if (animate === undefined)
return undefined;
if (typeof animate === 'string' || Array.isArray(animate))
return resolveVariantList(variants, animate, custom);
return animate;
};
/**
* Resolves the exit prop to keyframes, handling variant keys.
*
* When `exit` is a string (or array of strings), looks it up in the
* variants object (invoking dynamic variants with `custom`). Otherwise
* returns the keyframes directly. Used by AnimatePresence for exit
* animations.
*
* @param exit - The exit prop value (keyframes, variant key, array of
* variant keys, or undefined).
* @param variants - The variants object for resolving string keys.
* @param custom - Forwarded to function-form variants.
* @returns Keyframes definition or undefined.
*
* @example
* ```ts
* const variants = { hidden: { opacity: 0 }, small: { scale: 0.8 } }
* resolveExit('hidden', variants) // { opacity: 0 }
* resolveExit(['hidden', 'small'], variants) // { opacity: 0, scale: 0.8 }
* resolveExit({ y: -20 }, variants) // { y: -20 }
* resolveExit(undefined, variants) // undefined
* ```
*/
export const resolveExit = (exit, variants, custom) => {
if (exit === undefined)
return undefined;
if (typeof exit === 'string' || Array.isArray(exit))
return resolveVariantList(variants, exit, custom);
return exit;
};
/**
* Resolves a `whileX` prop (hover, tap, focus, drag, in-view) to
* keyframes. Mirrors `resolveAnimate` — pass-through for inline
* keyframes, look up variant keys via `resolveVariantList` (single
* string or array of strings, merged left-to-right).
*
* Used by `_MotionContainer.svelte` to feed the gesture attach helpers
* a consistent keyframes object regardless of whether the consumer
* wrote inline keyframes or a variant reference.
*
* @param value - The whileX prop value.
* @param variants - The variants object for resolving string keys.
* @param custom - Forwarded to function-form variants.
* @returns Keyframes definition or `undefined` when nothing applies.
*
* @example
* ```ts
* const variants = { hover: { scale: 1.1 }, active: { color: 'red' } }
* resolveWhile('hover', variants) // { scale: 1.1 }
* resolveWhile(['hover', 'active'], variants) // { scale: 1.1, color: 'red' }
* resolveWhile({ scale: 1.2 }, variants) // { scale: 1.2 } (pass-through)
* resolveWhile(undefined, variants) // undefined
* ```
*/
export const resolveWhile = (value, variants, custom) => {
if (value === undefined)
return undefined;
if (typeof value === 'string' || Array.isArray(value))
return resolveVariantList(variants, value, custom);
return value;
};
/**
* Collapse each keyframe value to the value the element comes to REST at
* — the last element of a keyframe array, or the value itself otherwise.
*
* Used when deriving the post-animation inline style baseline: an
* `animate={{ x: [0, 100, 50] }}` settles at `50`, so the resting inline
* transform must reflect `50`, not the first keyframe. Mirrors
* framer-motion, whose `buildTransform` reads the motion value as a
* scalar that has already settled at the final keyframe
* (`motion-dom/.../build-transform.ts`).
*
* @param keyframes - Resolved animate keyframes (scalars and/or arrays),
* or `undefined`.
* @returns A new object with each value collapsed to its resting scalar,
* or `undefined` when given `undefined`. Keys whose value is an empty
* array are omitted (no resting value).
*
* @example
* ```ts
* resolveRestingValues({ x: [0, 100, 50], scaleX: 1 }) // { x: 50, scaleX: 1 }
* resolveRestingValues({ opacity: 0.5 }) // { opacity: 0.5 }
* resolveRestingValues({ x: [], y: 5 }) // { y: 5 } (empty array dropped)
* resolveRestingValues(undefined) // undefined
* ```
*/
export const resolveRestingValues = (keyframes) => {
if (keyframes === undefined)
return undefined;
const out = {};
for (const [key, value] of Object.entries(keyframes)) {
if (Array.isArray(value)) {
// An empty array has no resting value — omit the key rather than
// emitting `value[-1]` (undefined).
if (value.length > 0)
out[key] = value[value.length - 1];
}
else {
out[key] = value;
}
}
return out;
};