101 | {IMAGES.map((image, index) => {
102 | const imageStyle =
103 | activeIndex === index
104 | ? {
105 | backgroundColor: 'white',
106 | }
107 | : {
108 | backgroundImage: `url(${image})`,
109 | backgroundSize: 'cover',
110 | };
111 |
112 | return (
113 |
setActiveIndex(index)}
124 | />
125 | );
126 | })}
127 |
128 |
129 | {activeIndex !== null && (
130 |
140 |
159 | Pull Down
160 |
161 |
162 | )}
163 | >
164 | );
165 | }
166 |
167 | export default Example;
168 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React UI Animate
2 |
3 | [](https://badge.fury.io/js/react-ui-animate)
4 |
5 | > Create smooth animations and interactive gestures in React applications effortlessly.
6 |
7 | ### Install
8 |
9 | You can install `react-ui-animate` via `npm` or `yarn`:
10 |
11 | ```sh
12 | npm install react-ui-animate
13 | ```
14 |
15 | ```sh
16 | yarn add react-ui-animate
17 | ```
18 |
19 | ---
20 |
21 | ## Getting Started
22 |
23 | The `react-ui-animate` library provides a straightforward way to add animations and gestures to your React components. Below are some common use cases.
24 |
25 | ### 1. useValue
26 |
27 | Use `useValue` to initialize and update an animated value.
28 |
29 | ```tsx
30 | import React from 'react';
31 | import {
32 | animate,
33 | useValue,
34 | withSpring,
35 | withTiming,
36 | withSequence,
37 | } from 'react-ui-animate';
38 |
39 | export const UseValue: React.FC = () => {
40 | const [width, setWidth] = useValue(100);
41 |
42 | return (
43 | <>
44 |
51 |
58 |
65 |
66 |
75 | >
76 | );
77 | };
78 | ```
79 |
80 | ### 2. useMount
81 |
82 | Use `useMount` to animate component mount and unmount transitions.
83 |
84 | ```tsx
85 | import React from 'react';
86 | import {
87 | animate,
88 | useMount,
89 | withDecay,
90 | withSequence,
91 | withSpring,
92 | withTiming,
93 | } from 'react-ui-animate';
94 |
95 | export const UseMount: React.FC = () => {
96 | const [open, setOpen] = React.useState(true);
97 | const mounted = useMount(open, { from: 0, enter: 1, exit: 0 });
98 |
99 | return (
100 | <>
101 | {mounted(
102 | (animation, isMounted) =>
103 | isMounted && (
104 |
112 | )
113 | )}
114 |
115 |
116 | >
117 | );
118 | };
119 | ```
120 |
121 | ### 3. Interpolation
122 |
123 | Interpolate values for complex mappings like color transitions or movement.
124 |
125 | ```tsx
126 | import React, { useLayoutEffect, useState } from 'react';
127 | import { animate, useValue, withSpring } from 'react-ui-animate';
128 |
129 | export const Interpolation: React.FC = () => {
130 | const [open, setOpen] = useState(false);
131 | const [x, setX] = useValue(0);
132 |
133 | useLayoutEffect(() => {
134 | setX(withSpring(open ? 500 : 0));
135 | }, [open, setX]);
136 |
137 | return (
138 | <>
139 |
147 |
148 |
149 | >
150 | );
151 | };
152 | ```
153 |
154 | ---
155 |
156 | ## API Overview
157 |
158 | - **`useValue(initial)`**: Initializes an animated value.
159 | - **`animate`**: JSX wrapper for animatable elements (`animate.div`, `animate.span`, etc.).
160 | - **Modifiers**: `withSpring`, `withTiming`, `withDecay`, `withSequence` — functions to define animation behavior.
161 | - **`useMount(state, config)`**: Manages mount/unmount transitions. `config` includes `from`, `enter`, and `exit` values.
162 |
163 | ## Gestures
164 |
165 | `react-ui-animate` also provides hooks for handling gestures:
166 |
167 | - `useDrag`
168 | - `useMove`
169 | - `useScroll`
170 | - `useWheel`
171 |
172 | **Example: `useDrag`**
173 |
174 | ```tsx
175 | import React from 'react';
176 | import { useValue, animate, useDrag, withSpring } from 'react-ui-animate';
177 |
178 | export const Draggable: React.FC = () => {
179 | const ref = useRef(null);
180 | const [translateX, setTranslateX] = useValue(0);
181 |
182 | useDrag(ref, ({ down, movement }) => {
183 | setTranslateX(down ? movement.x : withSpring(0));
184 | });
185 |
186 | return (
187 |
196 | );
197 | };
198 | ```
199 |
200 | ## Documentation
201 |
202 | For detailed documentation and examples, visit the official [react-ui-animate documentation](https://react-ui-animate.js.org/).
203 |
204 | ## License
205 |
206 | This library is licensed under the MIT License.
207 |
--------------------------------------------------------------------------------
/src/gestures/controllers/DragGesture.ts:
--------------------------------------------------------------------------------
1 | import { clamp } from '../../utils';
2 | import { Gesture } from './Gesture';
3 |
4 | export interface DragEvent {
5 | down: boolean;
6 | movement: { x: number; y: number };
7 | offset: { x: number; y: number };
8 | velocity: { x: number; y: number };
9 | event: PointerEvent;
10 | cancel: () => void;
11 | }
12 |
13 | export interface DragConfig {
14 | threshold?: number;
15 | axis?: 'x' | 'y';
16 | initial?: () => { x: number; y: number };
17 | }
18 |
19 | export class DragGesture extends Gesture
{
20 | private config: DragConfig;
21 | private prev = { x: 0, y: 0 };
22 | private lastTime = 0;
23 |
24 | private movement = { x: 0, y: 0 };
25 | private velocity = { x: 0, y: 0 };
26 | private start = { x: 0, y: 0 };
27 | private offset = { x: 0, y: 0 };
28 |
29 | private pointerCaptured = false;
30 | private activePointerId: number | null = null;
31 | private attachedEls = new Set();
32 | private activeEl: HTMLElement | null = null;
33 | private pointerDownPos = { x: 0, y: 0 };
34 | private thresholdPassed = false;
35 |
36 | constructor(config: DragConfig = {}) {
37 | super();
38 | this.config = config;
39 | }
40 |
41 | attach(elements: HTMLElement | HTMLElement[] | Window): () => void {
42 | if (elements === window) return () => {};
43 |
44 | const els = Array.isArray(elements) ? elements : [elements as HTMLElement];
45 | const down = this.onDown.bind(this);
46 | const move = this.onMove.bind(this);
47 | const up = this.onUp.bind(this);
48 |
49 | els.forEach((el) => {
50 | this.attachedEls.add(el);
51 | el.addEventListener('pointerdown', down, { passive: false });
52 | });
53 |
54 | window.addEventListener('pointermove', move, { passive: false });
55 | window.addEventListener('pointerup', up);
56 | window.addEventListener('pointercancel', up);
57 |
58 | return () => {
59 | els.forEach((el) => {
60 | el.removeEventListener('pointerdown', down);
61 | this.attachedEls.delete(el);
62 | });
63 |
64 | window.removeEventListener('pointermove', move);
65 | window.removeEventListener('pointerup', up);
66 | window.removeEventListener('pointercancel', up);
67 | };
68 | }
69 |
70 | private onDown(e: PointerEvent) {
71 | if (e.button !== 0) return;
72 |
73 | const target = e.currentTarget as HTMLElement;
74 | if (!this.attachedEls.has(target)) return;
75 |
76 | this.activeEl = target;
77 | this.activePointerId = e.pointerId;
78 | this.pointerCaptured = false;
79 |
80 | this.start =
81 | this.thresholdPassed === false && this.start.x === 0 && this.start.y === 0
82 | ? this.config.initial?.() ?? { x: 0, y: 0 }
83 | : { ...this.offset };
84 | this.offset = { ...this.start };
85 | this.movement = { x: 0, y: 0 };
86 | this.velocity = { x: 0, y: 0 };
87 |
88 | this.pointerDownPos = { x: e.clientX, y: e.clientY };
89 | this.thresholdPassed = false;
90 | this.prev = { x: e.clientX, y: e.clientY };
91 | this.lastTime = e.timeStamp;
92 |
93 | this.emitChange({
94 | down: true,
95 | movement: { x: 0, y: 0 },
96 | offset: { ...this.offset },
97 | velocity: { x: 0, y: 0 },
98 | event: e,
99 | cancel: this.cancel.bind(this),
100 | });
101 | }
102 |
103 | private onMove(e: PointerEvent) {
104 | if (this.activePointerId !== e.pointerId || !this.activeEl) return;
105 |
106 | const threshold = this.config.threshold ?? 0;
107 | if (!this.thresholdPassed) {
108 | const dxTotal = e.clientX - this.pointerDownPos.x;
109 | const dyTotal = e.clientY - this.pointerDownPos.y;
110 | const dist = Math.hypot(dxTotal, dyTotal);
111 | if (dist < threshold) return;
112 | this.thresholdPassed = true;
113 |
114 | this.activeEl.setPointerCapture(e.pointerId);
115 | this.pointerCaptured = true;
116 | }
117 |
118 | if (this.pointerCaptured) {
119 | e.preventDefault();
120 | }
121 |
122 | const dt = Math.max((e.timeStamp - this.lastTime) / 1000, 1e-6);
123 | this.lastTime = e.timeStamp;
124 | const dx = e.clientX - this.prev.x;
125 | const dy = e.clientY - this.prev.y;
126 | const rawX = dx / dt / 1000;
127 | const rawY = dy / dt / 1000;
128 | this.velocity = {
129 | x: clamp(rawX, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT),
130 | y: clamp(rawY, -Gesture.VELOCITY_LIMIT, Gesture.VELOCITY_LIMIT),
131 | };
132 |
133 | const moveRaw = {
134 | x: e.clientX - this.pointerDownPos.x,
135 | y: e.clientY - this.pointerDownPos.y,
136 | };
137 | this.movement = {
138 | x: this.config.axis === 'y' ? 0 : moveRaw.x,
139 | y: this.config.axis === 'x' ? 0 : moveRaw.y,
140 | };
141 |
142 | this.offset = {
143 | x: this.start.x + this.movement.x,
144 | y: this.start.y + this.movement.y,
145 | };
146 |
147 | this.prev = { x: e.clientX, y: e.clientY };
148 |
149 | this.emitChange({
150 | down: true,
151 | movement: { ...this.movement },
152 | offset: { ...this.offset },
153 | velocity: { ...this.velocity },
154 | event: e,
155 | cancel: this.cancel.bind(this),
156 | });
157 | }
158 |
159 | private onUp(e: PointerEvent) {
160 | if (this.activePointerId !== e.pointerId || !this.activeEl) return;
161 | this.activeEl.releasePointerCapture(e.pointerId);
162 |
163 | this.emitEnd({
164 | down: false,
165 | movement: { ...this.movement },
166 | offset: { ...this.offset },
167 | velocity: { ...this.velocity },
168 | event: e,
169 | cancel: this.cancel.bind(this),
170 | });
171 |
172 | this.activePointerId = null;
173 | this.pointerCaptured = false;
174 | }
175 |
176 | cancel() {
177 | if (this.activeEl && this.activePointerId !== null) {
178 | this.activeEl.releasePointerCapture(this.activePointerId);
179 | this.activePointerId = null;
180 | this.activeEl = null;
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/hooks/observers/useScrollProgress.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useRef } from 'react';
2 | import { MotionValue } from '@raidipesh78/re-motion';
3 |
4 | import { useValue, withSpring } from '../../animation';
5 | import { ScrollGesture } from '../../gestures/controllers/ScrollGesture';
6 | import { useRecognizer } from '../../gestures/hooks/useRecognizer';
7 | import { type Descriptor } from '../../animation/types';
8 |
9 | type SupportedEdgeUnit = 'px' | 'vw' | 'vh' | '%';
10 | type EdgeUnit = `${number}${SupportedEdgeUnit}`;
11 | type NamedEdges = 'start' | 'end' | 'center';
12 | type EdgeString = NamedEdges | EdgeUnit | `${number}`;
13 | type Edge = EdgeString | number;
14 | type ProgressIntersection = [number, number];
15 | type Intersection = `${Edge} ${Edge}`;
16 | type ScrollOffset = Array;
17 |
18 | export interface UseScrollProgressOptions {
19 | target?: RefObject;
20 | axis?: 'x' | 'y';
21 | offset?: ScrollOffset;
22 | animate?: boolean;
23 | toDescriptor?: (t: number) => Descriptor;
24 | }
25 |
26 | export function useScrollProgress(
27 | refs: Window | RefObject,
28 | {
29 | target,
30 | axis = 'y',
31 | offset = ['start start', 'end end'],
32 | animate = true,
33 | toDescriptor = (v: number) => withSpring(v),
34 | }: UseScrollProgressOptions = {}
35 | ): {
36 | scrollYProgress: MotionValue;
37 | scrollXProgress: MotionValue;
38 | } {
39 | const [yProgress, setYProgress] = useValue(0);
40 | const [xProgress, setXProgress] = useValue(0);
41 | const rangeRef = useRef<[number, number]>([0, 0]);
42 |
43 | useEffect(() => {
44 | const containerEl =
45 | refs instanceof Window ? window : (refs.current as HTMLElement);
46 | const targetEl = target?.current ?? document.documentElement;
47 |
48 | rangeRef.current = getScrollRange(
49 | offset as [Intersection, Intersection],
50 | targetEl,
51 | containerEl,
52 | axis
53 | );
54 | }, [refs, target, axis, offset]);
55 |
56 | useRecognizer(ScrollGesture, refs, (e) => {
57 | const pos = axis === 'y' ? e.offset.y : e.offset.x;
58 | const [start, end] = rangeRef.current;
59 |
60 | const raw =
61 | end === start ? (pos < start ? 0 : 1) : (pos - start) / (end - start);
62 |
63 | const t = Math.min(Math.max(raw, 0), 1);
64 | const apply = animate ? toDescriptor : (v: number) => v;
65 |
66 | if (axis === 'y') {
67 | setYProgress(apply(t));
68 | setXProgress(0);
69 | } else {
70 | setXProgress(apply(t));
71 | setYProgress(0);
72 | }
73 | });
74 |
75 | return { scrollYProgress: yProgress, scrollXProgress: xProgress };
76 | }
77 |
78 | function getScroll(el: HTMLElement | Window, axis: 'x' | 'y') {
79 | if (el instanceof HTMLElement) {
80 | return axis === 'y' ? el.scrollTop : el.scrollLeft;
81 | }
82 | return axis === 'y' ? window.scrollY : window.scrollX;
83 | }
84 |
85 | function getSize(el: HTMLElement | Window, axis: 'x' | 'y') {
86 | if (el instanceof HTMLElement) {
87 | return axis === 'y' ? el.clientHeight : el.clientWidth;
88 | }
89 | return axis === 'y' ? window.innerHeight : window.innerWidth;
90 | }
91 |
92 | function getScrollRange(
93 | [startMarker, endMarker]: [Intersection, Intersection],
94 | targetEl: HTMLElement,
95 | containerEl: HTMLElement | Window,
96 | axis: 'x' | 'y'
97 | ): [number, number] {
98 | return [
99 | resolveMarker(startMarker, targetEl, containerEl, axis),
100 | resolveMarker(endMarker, targetEl, containerEl, axis),
101 | ];
102 | }
103 |
104 | function resolveMarker(
105 | marker: Intersection,
106 | targetEl: HTMLElement,
107 | containerEl: HTMLElement | Window,
108 | axis: 'x' | 'y'
109 | ): number {
110 | const [tMark, cMark = tMark] = marker.trim().split(/\s+/) as [
111 | EdgeString,
112 | EdgeString
113 | ];
114 |
115 | if (containerEl instanceof HTMLElement) {
116 | const tRect = targetEl.getBoundingClientRect();
117 | const cRect = containerEl.getBoundingClientRect();
118 | const scroll = getScroll(containerEl, axis);
119 | const elementStart =
120 | (axis === 'y' ? tRect.top - cRect.top : tRect.left - cRect.left) + scroll;
121 | const elementSize = axis === 'y' ? tRect.height : tRect.width;
122 | const containerSize = getSize(containerEl, axis);
123 |
124 | const elemPos = resolveEdge(
125 | tMark,
126 | elementStart,
127 | elementSize,
128 | containerSize
129 | );
130 | const contPos = resolveEdge(cMark, 0, containerSize, containerSize);
131 | return elemPos - contPos;
132 | } else {
133 | const elemPos = parseEdgeValue(tMark, axis, targetEl, false);
134 | const contPos = parseEdgeValue(cMark, axis, window, true);
135 | return elemPos - contPos;
136 | }
137 | }
138 |
139 | function resolveEdge(
140 | edge: EdgeString,
141 | base: number,
142 | size: number,
143 | containerSize: number
144 | ): number {
145 | if (edge === 'start') return base;
146 | if (edge === 'center') return base + size / 2;
147 | if (edge === 'end') return base + size;
148 |
149 | const m = edge.match(/^(-?\d+(?:\.\d+)?)(px|%|vw|vh)?$/);
150 | if (!m) throw new Error(`Invalid edge marker “${edge}”`);
151 |
152 | const n = parseFloat(m[1]);
153 | const unit = m[2] as SupportedEdgeUnit | undefined;
154 |
155 | switch (unit) {
156 | case 'px':
157 | return base + n;
158 | case '%':
159 | return base + (n / 100) * size;
160 | case 'vw':
161 | return base + (n / 100) * containerSize;
162 | case 'vh':
163 | return base + (n / 100) * containerSize;
164 | default:
165 | return base + n * size;
166 | }
167 | }
168 |
169 | function parseEdgeValue(
170 | edge: EdgeString,
171 | axis: 'x' | 'y',
172 | el: HTMLElement | Window,
173 | isContainer: boolean
174 | ): number {
175 | const scrollTarget = isContainer ? el : (el as HTMLElement);
176 | const base = isContainer
177 | ? 0
178 | : (() => {
179 | if (!(el instanceof HTMLElement))
180 | throw new Error('Expected HTMLElement for element-relative edge');
181 | const rect = el.getBoundingClientRect();
182 | const pageScroll =
183 | axis === 'y'
184 | ? window.pageYOffset || window.scrollY
185 | : window.pageXOffset || window.scrollX;
186 | return (axis === 'y' ? rect.top : rect.left) + pageScroll;
187 | })();
188 |
189 | const size = isContainer
190 | ? getSize(el, axis)
191 | : (() => {
192 | if (!(el instanceof HTMLElement)) throw new Error();
193 | const rect = el.getBoundingClientRect();
194 | return axis === 'y' ? rect.height : rect.width;
195 | })();
196 |
197 | return resolveEdge(edge, base, size, getSize(scrollTarget, axis));
198 | }
199 |
--------------------------------------------------------------------------------
/src/animation/hooks/useValue.ts:
--------------------------------------------------------------------------------
1 | import { useMemo, useRef } from 'react';
2 | import { delay, sequence, loop, MotionValue } from '@raidipesh78/re-motion';
3 |
4 | import { buildAnimation, buildParallel } from '../drivers';
5 | import { filterCallbackOptions, isDescriptor } from '../helpers';
6 | import type { Primitive, Descriptor, Controls } from '../types';
7 |
8 | type Widen = T extends number ? number : T extends string ? string : T;
9 |
10 | type ValueReturn = T extends Primitive
11 | ? MotionValue>
12 | : T extends Primitive[]
13 | ? MotionValue>[]
14 | : { [K in keyof T]: MotionValue> };
15 |
16 | type Base = Primitive | Primitive[] | Record;
17 |
18 | export function useValue(
19 | initial: T
20 | ): [ValueReturn, (to: Base | Descriptor) => void, Controls] {
21 | const controllerRef = useRef(null);
22 |
23 | const value = useMemo(() => {
24 | if (Array.isArray(initial)) {
25 | return initial.map((v) => new MotionValue(v));
26 | }
27 |
28 | if (typeof initial === 'object') {
29 | return Object.fromEntries(
30 | Object.entries(initial).map(([k, v]) => [k, new MotionValue(v)])
31 | );
32 | }
33 |
34 | return new MotionValue(initial);
35 | }, []) as ValueReturn;
36 |
37 | function set(to: Base | Descriptor) {
38 | let ctrl: Controls | null = null;
39 |
40 | if (Array.isArray(initial)) {
41 | ctrl = handleArray(
42 | value as Array>,
43 | to as Primitive[] | Descriptor
44 | );
45 | } else if (typeof initial === 'object') {
46 | ctrl = handleObject(
47 | value as Record>,
48 | to as Record | Descriptor
49 | );
50 | } else {
51 | ctrl = handlePrimitive(
52 | value as MotionValue,
53 | to as Primitive | Descriptor
54 | );
55 | }
56 |
57 | controllerRef.current = ctrl;
58 | if (ctrl) ctrl.start();
59 | }
60 |
61 | const controls = {
62 | start: () => controllerRef.current?.start(),
63 | pause: () => controllerRef.current?.pause(),
64 | resume: () => controllerRef.current?.resume(),
65 | cancel: () => controllerRef.current?.cancel(),
66 | reset: () => controllerRef.current?.reset(),
67 | };
68 |
69 | return [value, set, controls] as const;
70 | }
71 |
72 | function handlePrimitive(
73 | mv: MotionValue,
74 | to: Primitive | Descriptor
75 | ) {
76 | if (typeof to === 'number' || typeof to === 'string') {
77 | mv.set(to);
78 | return null;
79 | }
80 |
81 | if (to.type === 'sequence') {
82 | const animations = to.options?.animations ?? [];
83 | const ctrls = animations.map((step) => buildAnimation(mv, step));
84 | return sequence(ctrls, to.options);
85 | }
86 |
87 | if (to.type === 'loop') {
88 | const animation = to.options?.animation;
89 | if (!animation) return null;
90 |
91 | if (animation.type === 'sequence') {
92 | const animations = animation.options?.animations ?? [];
93 | const ctrls = animations.map((step) => buildAnimation(mv, step));
94 | return loop(sequence(ctrls), to.options?.iterations ?? 0, to.options);
95 | }
96 |
97 | return loop(
98 | buildAnimation(mv, animation),
99 | to.options?.iterations ?? 0,
100 | to.options
101 | );
102 | }
103 |
104 | return buildAnimation(mv, to);
105 | }
106 |
107 | function handleArray(
108 | mvs: Array>,
109 | to: Primitive[] | Descriptor
110 | ) {
111 | if (!isDescriptor(to)) {
112 | (to as Primitive[]).forEach((val, i) => {
113 | mvs[i]?.set(val);
114 | });
115 | return null;
116 | }
117 |
118 | const desc = to as Descriptor;
119 |
120 | const mvsRecord = Object.fromEntries(
121 | mvs.map((mv, idx) => [idx.toString(), mv])
122 | ) as Record>;
123 |
124 | switch (desc.type) {
125 | case 'sequence': {
126 | const ctrls = desc.options!.animations!.map((step) =>
127 | step.type === 'delay'
128 | ? delay(step.options?.delay ?? 0)
129 | : buildParallel(mvsRecord, {
130 | ...step,
131 | to: Array.isArray(step.to)
132 | ? Object.fromEntries(
133 | (step.to as Primitive[]).map((v, i) => [i.toString(), v])
134 | )
135 | : step.to,
136 | })
137 | );
138 |
139 | return sequence(ctrls, desc.options);
140 | }
141 |
142 | case 'loop': {
143 | const inner = desc.options!.animation!;
144 |
145 | if (inner.type === 'sequence') {
146 | const seqCtrls = inner.options!.animations!.map((step) =>
147 | buildParallel(mvsRecord, {
148 | ...step,
149 | to: Array.isArray(step.to)
150 | ? Object.fromEntries(
151 | (step.to as Primitive[]).map((v, i) => [i.toString(), v])
152 | )
153 | : step.to,
154 | })
155 | );
156 |
157 | const seq = sequence(
158 | seqCtrls,
159 | filterCallbackOptions(inner.options, true)
160 | );
161 |
162 | return loop(
163 | seq,
164 | desc.options!.iterations ?? 0,
165 | filterCallbackOptions(desc.options, true)
166 | );
167 | }
168 |
169 | const par = buildParallel(mvsRecord, inner);
170 | return loop(
171 | par,
172 | desc.options!.iterations ?? 0,
173 | filterCallbackOptions(desc.options, true)
174 | );
175 | }
176 |
177 | case 'decay':
178 | return buildParallel(mvsRecord, desc);
179 |
180 | default:
181 | return buildParallel(mvsRecord, desc);
182 | }
183 | }
184 |
185 | function handleObject(
186 | mvs: Record>,
187 | to: Record | Descriptor
188 | ) {
189 | if (isDescriptor(to)) {
190 | switch (to.type) {
191 | case 'sequence': {
192 | const ctrls = to.options!.animations!.map((step) =>
193 | step.type === 'delay'
194 | ? delay(step.options!.delay ?? 0)
195 | : buildParallel(mvs, step)
196 | );
197 | return sequence(ctrls, to.options);
198 | }
199 |
200 | case 'loop': {
201 | const inner = to.options!.animation!;
202 | if (inner.type === 'sequence') {
203 | const ctrls = inner.options!.animations!.map((s) =>
204 | buildParallel(mvs, s)
205 | );
206 | return loop(
207 | sequence(ctrls, filterCallbackOptions(inner.options, true)),
208 | to.options!.iterations ?? 0,
209 | filterCallbackOptions(to.options, true)
210 | );
211 | }
212 | return loop(
213 | buildParallel(mvs, inner),
214 | to.options!.iterations ?? 0,
215 | filterCallbackOptions(to.options, true)
216 | );
217 | }
218 |
219 | case 'decay':
220 | return buildParallel(mvs, to);
221 |
222 | default:
223 | return buildParallel(mvs, to);
224 | }
225 | }
226 |
227 | Object.entries(to).forEach(([k, v]) => {
228 | mvs[k]?.set(v);
229 | });
230 |
231 | return null;
232 | }
233 |
--------------------------------------------------------------------------------