|
1 | | -import React from 'react' |
2 | | -import Controller from '../animated/Controller' |
3 | | -import * as Globals from '../animated/Globals' |
| 1 | +import { |
| 2 | + useRef, |
| 3 | + useState, |
| 4 | + useMemo, |
| 5 | + useCallback, |
| 6 | + useImperativeMethods, |
| 7 | + useEffect, |
| 8 | +} from 'react' |
| 9 | +import Ctrl from '../animated/Controller' |
| 10 | +import { requestFrame } from '../animated/Globals' |
4 | 11 |
|
5 | | -const map = new Map([]) |
6 | | -export function useTrail (count, params) { |
7 | | - const isFunctionProps = typeof params === 'function' |
8 | | - const { |
9 | | - delay, |
10 | | - reverse, |
11 | | - onKeyframesHalt = () => null, |
12 | | - onRest, |
13 | | - ...props |
14 | | - } = isFunctionProps ? params() : params |
15 | | - const instances = React.useRef(map) |
16 | | - const mounted = React.useRef(false) |
17 | | - const endResolver = React.useRef() |
18 | | - const [, forceUpdate] = React.useState() |
19 | | - |
20 | | - const onHalt = onRest |
| 12 | +export function useTrail(length, args) { |
| 13 | + const [, forceUpdate] = useState() |
| 14 | + // Extract animation props and hook-specific props, can be a function or an obj |
| 15 | + const isFn = typeof args === 'function' |
| 16 | + const { reverse, onKeyframesHalt, onRest, ...props } = isFn ? args() : args |
| 17 | + // The controller maintains the animation values, starts and tops animations |
| 18 | + const instances = useMemo( |
| 19 | + () => { |
| 20 | + const instances = [] |
| 21 | + for (let i = 0; i < length; i++) |
| 22 | + instances.push( |
| 23 | + new Ctrl({ ...props, attach: i > 0 && (() => instances[i - 1]) }) |
| 24 | + ) |
| 25 | + return instances |
| 26 | + }, |
| 27 | + [length] |
| 28 | + ) |
| 29 | + // Define onEnd callbacks and resolvers |
| 30 | + const endResolver = useRef() |
| 31 | + const onHalt = onKeyframesHalt |
21 | 32 | ? ctrl => ({ finished }) => { |
22 | | - finished && endResolver.current && endResolver.current() |
23 | | - finished && mounted.current && onRest(ctrl.merged) |
24 | | - } |
25 | | - : onKeyframesHalt |
26 | | - |
27 | | - if (count > instances.current.size) { |
28 | | - for (let i = instances.current.size; i < count; i++) { |
29 | | - instances.current.set( |
30 | | - i, |
31 | | - new Controller({ |
32 | | - ...props, |
33 | | - attach: i === 0 ? undefined : () => instances.current.get(i - 1) |
34 | | - }) |
35 | | - ) |
36 | | - } |
37 | | - } |
38 | | - |
39 | | - const update = React.useCallback( |
40 | | - /** resolve and last are passed to the update function from the keyframes controller */ |
41 | | - props => { |
42 | | - for (let [idx, ctrl] of instances.current.entries()) { |
43 | | - ctrl.update(props) |
44 | | - if (!props.ref) { |
45 | | - ctrl.start(instances.current.size - 1 === idx && onHalt(ctrl)) |
| 33 | + if (finished) { |
| 34 | + if (endResolver.current) endResolver.current() |
| 35 | + if (onRest) onRest(ctrl.merged) |
46 | 36 | } |
47 | 37 | } |
48 | | - Globals.requestFrame(() => props.reset && forceUpdate()) |
49 | | - }, |
50 | | - [onRest] |
51 | | - ) |
| 38 | + : onKeyframesHalt || (() => null) |
52 | 39 |
|
53 | | - React.useImperativeMethods(props.ref, () => ({ |
| 40 | + // The hooks explcit API gets defined here ... |
| 41 | + useImperativeMethods(props.ref, () => ({ |
54 | 42 | start: resolve => { |
55 | 43 | endResolver.current = resolve |
56 | | - for (let [idx, ctrl] of instances.current.entries()) { |
57 | | - ctrl.start(instances.current.size - 1 === idx && onHalt(ctrl)) |
58 | | - } |
| 44 | + instances.forEach((ctrl, i) => |
| 45 | + ctrl.start(instances.length - 1 === i && onHalt(ctrl)) |
| 46 | + ) |
59 | 47 | }, |
60 | | - tag: 'TrailHook' |
61 | 48 | })) |
62 | 49 |
|
63 | | - /** must hoooks always return something? */ |
64 | | - React.useEffect(() => { |
65 | | - mounted.current = true |
66 | | - return () => void (mounted.current = false) |
67 | | - }, []) |
68 | | - |
69 | | - React.useLayoutEffect(() => void (!isFunctionProps && update(props))) |
70 | | - |
71 | | - const propValues = Array.from(instances.current.values()).reduce( |
72 | | - (acc, ctrl) => { |
73 | | - reverse ? acc.unshift(ctrl.getValues()) : acc.push(ctrl.getValues()) |
74 | | - return acc |
| 50 | + // Defines the hooks setter, which updates the controller |
| 51 | + const updateCtrl = useCallback( |
| 52 | + props => { |
| 53 | + instances.forEach((ctrl, i) => { |
| 54 | + const last = instances.length - 1 === i |
| 55 | + ctrl.update(props) |
| 56 | + if (!ctrl.props.ref) ctrl.start(last && onHalt(ctrl)) |
| 57 | + if (last && ctrl.props.reset) requestFrame(forceUpdate) |
| 58 | + }) |
75 | 59 | }, |
76 | | - [] |
| 60 | + [onRest, onKeyframesHalt, props.ref] |
77 | 61 | ) |
78 | 62 |
|
79 | | - return isFunctionProps |
| 63 | + // Update next frame is props aren't functional |
| 64 | + useEffect(() => void (!isFn && updateCtrl(props))) |
| 65 | + // Return animated props, or, anim-props + the update-setter above |
| 66 | + const propValues = instances.reduce((acc, ctrl) => { |
| 67 | + reverse ? acc.unshift(ctrl.getValues()) : acc.push(ctrl.getValues()) |
| 68 | + return acc |
| 69 | + }, []) |
| 70 | + return isFn |
80 | 71 | ? [ |
81 | | - propValues, |
82 | | - props => update(props), |
83 | | - (finished = false) => { |
84 | | - for (let [, ctrl] of instances.current.entries()) { |
85 | | - ctrl.stop(finished) |
86 | | - } |
87 | | - } |
88 | | - ] |
| 72 | + propValues, |
| 73 | + updateCtrl, |
| 74 | + (finished = false) => instances.forEach(ctrl => ctrl.stop(finished)), |
| 75 | + ] |
89 | 76 | : propValues |
90 | 77 | } |
0 commit comments