UNPKG

@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

218 lines (217 loc) 8.18 kB
import { isMotionValue } from 'motion-dom'; /** * Merge inline CSS styles from an existing style string with a Motion initial definition. * This is used during SSR to reflect the initial state in server-rendered markup. */ export const mergeInlineStyles = (existingStyle, initial, animateFallback) => { const base = parseStyleString(typeof existingStyle === 'string' ? existingStyle : ''); const source = initial && Object.keys(initial).length > 0 ? initial : (animateFallback ?? null); if (!source) return stringifyStyleObject(base); // Collect transform parts so we can emit a single transform string const transformParts = []; const setProp = (cssProp, value) => { if (value == null) return; const v = Array.isArray(value) ? value[0] : value; if (v == null) return; base[cssProp] = String(v); }; const setPx = (cssProp, value) => { if (value == null) return; const v = Array.isArray(value) ? value[0] : value; if (v == null) return; base[cssProp] = typeof v === 'number' ? `${v}px` : String(v); }; const addTransform = (fn, value, unit) => { if (value == null) return; const v = Array.isArray(value) ? value[0] : value; if (v == null) return; const val = typeof v === 'number' ? `${v}${unit}` : String(v); transformParts.push(`${fn}(${val})`); }; for (const key of Object.keys(source)) { const value = source[key]; switch (key) { case 'opacity': setProp('opacity', value); break; case 'backgroundColor': setProp('background-color', value); break; case 'borderRadius': setProp('border-radius', value); break; case 'width': setPx('width', value); break; case 'height': setPx('height', value); break; case 'x': addTransform('translateX', value, 'px'); break; case 'y': addTransform('translateY', value, 'px'); break; case 'z': addTransform('translateZ', value, 'px'); break; case 'scale': // scale is unitless addTransform('scale', value, ''); break; case 'scaleX': addTransform('scaleX', value, ''); break; case 'scaleY': addTransform('scaleY', value, ''); break; case 'rotate': addTransform('rotate', value, typeof (Array.isArray(value) ? value[0] : value) === 'number' ? 'deg' : ''); break; case 'rotateX': addTransform('rotateX', value, typeof (Array.isArray(value) ? value[0] : value) === 'number' ? 'deg' : ''); break; case 'rotateY': addTransform('rotateY', value, typeof (Array.isArray(value) ? value[0] : value) === 'number' ? 'deg' : ''); break; case 'rotateZ': addTransform('rotateZ', value, typeof (Array.isArray(value) ? value[0] : value) === 'number' ? 'deg' : ''); break; case 'skew': case 'skewX': case 'skewY': addTransform(key, value, typeof (Array.isArray(value) ? value[0] : value) === 'number' ? 'deg' : ''); break; case 'pointerEvents': base['pointer-events'] = String(Array.isArray(value) ? value[0] : value); break; case 'cursor': setProp('cursor', value); break; // Skip SVG path animation properties - they'll be set by animate() case 'pathLength': case 'pathOffset': case 'pathSpacing': case 'strokeDasharray': case 'stroke-dasharray': case 'strokeDashoffset': case 'stroke-dashoffset': // Don't add these to inline styles - they interfere with animation break; default: // Fallback: write raw as-is for simple CSS props if (typeof value === 'string' || typeof value === 'number') { base[toKebabCase(key)] = String(value); } break; } } if (transformParts.length > 0) { base['transform'] = transformParts.join(' '); } return stringifyStyleObject(base); }; /** * Extract the user-authored `transform` declaration from a `style` prop. * * Used by the projection system as the "base" transform a node resets to * while measuring — the value the user wrote, independent of any * motion-applied transform (`initial`/`animate`/FLIP/drag) that lands on * `element.style.transform` after mount. Reading it from the `style` prop * rather than the live inline style is what keeps an `initial={{ x }}` (or * any transform-type initial/animate) from being mistaken for the base. * * Returns `''` when the prop is not a string or carries no transform. * * @param style The component's `style` prop. * @returns The user's `transform` value, or `''`. * @example * ```ts * extractTransform('opacity: 0.5; transform: translateX(10px) scale(2)') * // => 'translateX(10px) scale(2)' * extractTransform('color: red') // => '' * extractTransform({ color: 'red' }) // => '' (non-string) * ``` */ export const extractTransform = (style) => { if (typeof style !== 'string') { if (!isMotionStyleObject(style)) return ''; const transform = resolveStyleValue(style.transform); return transform == null ? '' : String(transform); } return parseStyleString(style).transform ?? ''; }; /** * Serialize a string or object-form Motion style prop into inline CSS. * * @param style The consumer-provided style prop. * @returns Inline CSS suitable for Svelte's `style` attribute. */ export const serializeMotionStyle = (style) => { if (typeof style === 'string') return style; if (!isMotionStyleObject(style)) return ''; return mergeInlineStyles('', resolveMotionStyle(style), null); }; /** * Collect MotionValue entries from object-form style props. * * @param style The consumer-provided style prop. * @returns A map of style keys to MotionValues, or `undefined` when no live * style values are present. */ export const collectMotionStyleValues = (style) => { if (!isMotionStyleObject(style)) return undefined; const values = {}; for (const [key, value] of Object.entries(style)) { if (isMotionValue(value)) { values[key] = value; } } return Object.keys(values).length ? values : undefined; }; const isMotionStyleObject = (style) => !!style && typeof style === 'object' && !Array.isArray(style); const resolveMotionStyle = (style) => { const resolved = {}; for (const [key, value] of Object.entries(style)) { resolved[key] = resolveStyleValue(value); } return resolved; }; const resolveStyleValue = (value) => { if (isMotionStyleMotionValue(value)) return value.get(); return value; }; const isMotionStyleMotionValue = (value) => !!value && typeof value === 'object' && 'get' in value && typeof value.get === 'function'; const parseStyleString = (style) => { const out = {}; style .split(';') .map((s) => s.trim()) .filter(Boolean) .forEach((decl) => { const idx = decl.indexOf(':'); if (idx === -1) return; const prop = decl.slice(0, idx).trim(); const value = decl.slice(idx + 1).trim(); if (prop) out[prop] = value; }); return out; }; const stringifyStyleObject = (obj) => Object.entries(obj) .map(([k, v]) => `${k}: ${v}`) .join('; '); const toKebabCase = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);