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

190 lines (189 loc) 6.97 kB
// Utilities for building and validating transform strings import { isMotionValue, mapValue, transformValue } from 'motion-dom'; import {} from 'svelte/store'; import { augmentMotionValue, bridgeReadableToMotionValue } from './augmentMotionValue.svelte.js'; const DEFAULTS = { x: 0, y: 0, scale: 1, scaleX: 1, scaleY: 1, rotate: 0 }; /** * Build a CSS transform string from numeric values (no matrices). * * @param values Partial map of translate/scale/rotate values. * @returns A space-separated CSS `transform` string, or `""` when all values are defaults. * * @example * ```ts * buildTransform({ x: 10, y: 20, scale: 1.5 }) * // => "translate(10px, 20px) scale(1.5)" * * buildTransform({ x: 0, y: 0, rotate: 0 }) // all defaults * // => "" * ``` */ export const buildTransform = (values) => { const v = { ...DEFAULTS, ...values }; const useAxes = values.scaleX !== undefined || values.scaleY !== undefined; const parts = []; if (v.x !== 0 || v.y !== 0) parts.push(`translate(${round(v.x)}px, ${round(v.y)}px)`); if (v.rotate !== 0) parts.push(`rotate(${round(v.rotate)}deg)`); if (useAxes) { parts.push(`scaleX(${round(v.scaleX)})`); parts.push(`scaleY(${round(v.scaleY)})`); } else if (v.scale !== 1) { parts.push(`scale(${round(v.scale)})`); } return parts.join(' ').trim(); }; /** * Lightweight safety check for transform magnitudes and NaN values. * * @param values Transform values to validate. * @param opts Optional configuration; `maxScale` caps allowable absolute scale (default 8). * @returns `true` if all scale values are finite and within bounds. * * @example * ```ts * isSafeTransform({ scale: 2 }) // true * isSafeTransform({ scale: 100 }) // false (exceeds default maxScale=8) * isSafeTransform({ scale: 100 }, { maxScale: 200 }) // true * isSafeTransform({ scale: NaN }) // false * ``` */ export const isSafeTransform = (values, opts) => { const maxScale = opts?.maxScale ?? 8; const entries = [ ['scale', values.scale], ['scaleX', values.scaleX], ['scaleY', values.scaleY] ]; for (const [, val] of entries) { if (val === undefined) continue; if (!Number.isFinite(val)) return false; if (Math.abs(val) > maxScale) return false; } return true; }; /** * Extract the uniform scale factor from a CSS `matrix()` string. * * @param matrix A CSS `matrix(...)` value, `"none"`, `null`, or `undefined`. * @returns The `a` component of the matrix (uniform scale), or `null` if unparseable. * * @example * ```ts * parseMatrixScale('matrix(1.5, 0, 0, 1.5, 0, 0)') // 1.5 * parseMatrixScale('none') // null * parseMatrixScale(null) // null * ``` */ export const parseMatrixScale = (matrix) => { if (!matrix || matrix === 'none') return null; const m = matrix.match(/matrix\(([^)]+)\)/); if (!m) return null; const [a] = m[1].split(',').map((s) => parseFloat(s.trim())); return Number.isFinite(a) ? a : null; }; /** * Round a number to six decimal places to avoid excessive precision in CSS strings. * * @param n The number to round. * @returns The rounded value. */ const round = (n) => { return Math.round(n * 1e6) / 1e6; }; /** * Normalizes a `TransformSource<T>` into a `MotionValue<T>`. Returns the * source as-is for any motion-value input (no bridge), or a bridge * `MotionValue` + cleanup for `Readable` inputs. The cast at the * motion-value branch is safe — both `MotionValue` and `AugmentedMotionValue` * are the same instance at runtime. */ const toMotionValue = (source) => { if (isMotionValue(source)) { return { value: source, dispose: () => undefined }; } return bridgeReadableToMotionValue(source); }; export function useTransform(sourceOrCompute, inputOrTransformer, outputOrOutputMap, options) { // Compute form: useTransform(() => compute). if (typeof sourceOrCompute === 'function') { const compute = sourceOrCompute; const value = transformValue(compute); $effect(() => () => value.destroy()); return augmentMotionValue(value); } // Multi-input list form: useTransform([mv, mv, …], ([a, b, …]) => O). if (Array.isArray(sourceOrCompute) && typeof inputOrTransformer === 'function') { const sources = sourceOrCompute; const transformer = inputOrTransformer; const value = transformValue(() => { const latest = []; for (let i = 0; i < sources.length; i++) { latest.push(sources[i].get()); } return transformer(latest); }); $effect(() => () => value.destroy()); return augmentMotionValue(value); } // Single-MV transformer form: useTransform(mv, (latest) => O). if (typeof inputOrTransformer === 'function') { const source = sourceOrCompute; const transformer = inputOrTransformer; const value = transformValue(() => transformer(source.get())); $effect(() => () => value.destroy()); return augmentMotionValue(value); } // Mapping forms (single output array or output map). const source = sourceOrCompute; const input = inputOrTransformer ?? []; const { value: numericSource, dispose: disposeBridge } = toMotionValue(source); // Multi-output mapping form: useTransform(source, [range], { key: [out], … }, options). // The `outputOrOutputMap !== null` check is load-bearing — `typeof null` // is `'object'`, so a `null` argument would otherwise enter this branch // and crash at `Object.keys(null)`. if (outputOrOutputMap !== undefined && outputOrOutputMap !== null && !Array.isArray(outputOrOutputMap) && typeof outputOrOutputMap === 'object') { const outputMap = outputOrOutputMap; const keys = Object.keys(outputMap); const result = {}; const inners = []; for (const key of keys) { const inner = mapValue(numericSource, input, outputMap[key], options); inners.push(inner); result[key] = augmentMotionValue(inner); } // One cleanup effect for all per-key MVs plus the shared bridge — // saves N effect nodes vs. registering one per key. $effect(() => () => { for (const inner of inners) inner.destroy(); disposeBridge(); }); return result; } // Single-output mapping form: useTransform(source, [range], [out], options). const output = outputOrOutputMap ?? []; const value = mapValue(numericSource, input, output, options); $effect(() => () => { value.destroy(); disposeBridge(); }); return augmentMotionValue(value); }