` element.
159 |
160 | ```jsx
161 | import Lottie from "lottie-react";
162 | import groovyWalkAnimation from "./groovyWalk.json";
163 |
164 | const Example = () =>
165 |
169 | };
170 |
171 | export default Example;
172 | ```
173 |
174 | ## Interaction methods
175 |
176 | These methods are designed to give you more control over the Lottie animation, and fill in the gaps left by the props:
177 |
178 | ### `play()`
179 |
180 | ---
181 |
182 | ### `stop()`
183 |
184 | ---
185 |
186 | ### `pause()`
187 |
188 | ---
189 |
190 | ### `setSpeed(speed)`
191 |
192 | ```yaml
193 | speed: 1 is normal speed
194 | ```
195 |
196 | ---
197 |
198 | ### `goToAndPlay(value, isFrame)`
199 |
200 | ```yaml
201 | value: numeric value.
202 | isFrame: defines if first argument is a time based value or a frame based (default false).
203 | ```
204 |
205 | ---
206 |
207 | ### `goToAndStop(value, isFrame)`
208 |
209 | ```yaml
210 | value: numeric value.
211 | isFrame: defines if first argument is a time based value or a frame based (default false).
212 | ```
213 |
214 | ---
215 |
216 | ### `setDirection(direction)`
217 |
218 | ```yaml
219 | direction: 1 is forward, -1 is reverse.
220 | ```
221 |
222 | ---
223 |
224 | ### `playSegments(segments, forceFlag)`
225 |
226 | ```yaml
227 | segments: array. Can contain 2 numeric values that will be used as first and last frame of the animation. Or can contain a sequence of arrays each with 2 numeric values.
228 | forceFlag: boolean. If set to false, it will wait until the current segment is complete. If true, it will update values immediately.
229 | ```
230 |
231 | ---
232 |
233 | ### `setSubframe(useSubFrames)`
234 |
235 | ```yaml
236 | useSubFrames: If false, it will respect the original AE fps. If true, it will update on every requestAnimationFrame with intermediate values. Default is true.
237 | ```
238 |
239 | ---
240 |
241 | ### `getDuration(inFrames)`
242 |
243 | ```yaml
244 | inFrames: If true, returns duration in frames, if false, in seconds
245 | ```
246 |
247 | ---
248 |
249 | ### `destroy()`
250 |
251 | ### Calling the methods
252 |
253 | To use the interaction methods listed above, pass a reference object to the Lottie component by using the `ref` prop (see the React documentation to learn more about [Ref](https://reactjs.org/docs/refs-and-the-dom.html) or [useRef](https://reactjs.org/docs/hooks-reference.html#useref) hook):
254 |
255 | ```jsx
256 | import Lottie from "lottie-react";
257 | import groovyWalkAnimation from "./groovyWalk.json";
258 |
259 | const Example = () => {
260 | const lottieRef = useRef();
261 |
262 | return
;
263 | };
264 |
265 | export default Example;
266 | ```
267 |
268 | You can then use the interaction methods like this:
269 |
270 | ```jsx
271 | ...
272 | lottieRef.current.pause();
273 | ...
274 | ```
275 |
276 | ## Interactivity
277 |
278 | To sync animation with either scroll or cursor, you can pass the interactivity
279 | object.
280 |
281 | For more information please navigate to __useLottieInteractivity__ hook
282 |
283 |
284 |
285 | ```jsx
286 | import Lottie from "lottie-react";
287 | import robotAnimation from "./robotAnimation.json";
288 |
289 | const style = {
290 | height: 300,
291 | };
292 |
293 | const interactivity = {
294 | mode: "scroll",
295 | actions: [
296 | {
297 | visibility: [0, 0.2],
298 | type: "stop",
299 | frames: [0],
300 | },
301 | {
302 | visibility: [0.2, 0.45],
303 | type: "seek",
304 | frames: [0, 45],
305 | },
306 | {
307 | visibility: [0.45, 1.0],
308 | type: "loop",
309 | frames: [45, 60],
310 | },
311 | ],
312 | };
313 |
314 | const Example = () => {
315 | return (
316 |
321 | );
322 | };
323 |
324 | export default Example;
325 | ```
326 |
327 |
328 |
329 | ## Examples
330 |
331 | Soon :)
332 |
--------------------------------------------------------------------------------
/docs/hooks/useLottie/README.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: useLottie
3 | menu: Hooks
4 | route: /hooks/useLottie
5 | ---
6 |
7 | import UseLottieExamples from "./UseLottieExamples";
8 |
9 | # useLottie
10 |
11 | ## Getting Started
12 |
13 |
14 |
15 | ```jsx
16 | import { useLottie } from "lottie-react";
17 | import groovyWalkAnimation from "./groovyWalk.json";
18 |
19 | const style = {
20 | height: 300,
21 | };
22 |
23 | const Example = () => {
24 | const options = {
25 | animationData: groovyWalkAnimation,
26 | loop: true,
27 | autoplay: true,
28 | };
29 |
30 | const { View } = useLottie(options, style);
31 |
32 | return View;
33 | };
34 |
35 | export default Example;
36 | ```
37 |
38 | ## Params
39 |
40 | | Param | Type | Required | Default | Description |
41 | | ---------------------- | --------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42 | | options | Object | required | | Subset of the lottie-web options |
43 | | options.animationData | Object | required | | A JSON Object with the exported animation data |
44 | | options.loop | boolean\|number | optional | true | Set it to true for infinite amount of loops, or pass a number to specify how many times should the last segment played be looped ([More info](https://github.com/airbnb/lottie-web/issues/1450)) |
45 | | options.autoplay | boolean | optional | true | If set to true, animation will play as soon as it's loaded |
46 | | options.initialSegment | array | optional | | Expects an array of length 2. First value is the initial frame, second value is the final frame. If this is set, the animation will start at this position in time instead of the exported value from AE |
47 | | style | Object | optional | | Style object that applies to the animation wrapper (which is a `div`) |
48 |
49 | ## Returns
50 |
51 | | Property | Type |
52 | | ------------------- | ------------- |
53 | | Lottie | Object |
54 | | Lottie.View | React.Element |
55 | | Lottie.play | method |
56 | | Lottie.stop | method |
57 | | Lottie.pause | method |
58 | | Lottie.setSpeed | method |
59 | | Lottie.goToAndStop | method |
60 | | Lottie.goToAndPlay | method |
61 | | Lottie.setDirection | method |
62 | | Lottie.playSegments | method |
63 | | Lottie.setSubframe | method |
64 | | Lottie.getDuration | method |
65 | | Lottie.destroy | method |
66 |
--------------------------------------------------------------------------------
/docs/hooks/useLottie/UseLottieExamples.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import groovyWalkAnimation from "../../assets/groovyWalk.json";
3 |
4 | const style = {
5 | height: 300,
6 | border: 3,
7 | borderStyle: "solid",
8 | borderRadius: 7,
9 | };
10 |
11 | const UseLottieExamples = () => {
12 | const options = {
13 | animationData: groovyWalkAnimation,
14 | loop: true,
15 | autoplay: true,
16 | };
17 |
18 | const Lottie = useLottie(options, style);
19 |
20 | // useEffect(() => {
21 | // setTimeout(() => {
22 | // // Lottie.play();
23 | // // Lottie.stop();
24 | // // Lottie.pause();
25 | // // Lottie.setSpeed(5);
26 | // // Lottie.goToAndStop(6150);
27 | // // Lottie.goToAndPlay(6000);
28 | // // Lottie.setDirection(-1);
29 | // // Lottie.playSegments([350, 500]);
30 | // // Lottie.playSegments([350, 500], true);
31 | // // Lottie.setSubframe(true);
32 | // // console.log('Duration:', Lottie.getDuration());
33 | // // Lottie.destroy();
34 | // }, 2000);
35 | // });
36 |
37 | return Lottie.View;
38 | };
39 |
40 | export default UseLottieExamples;
41 |
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/CursorDiagonalSync.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import useLottieInteractivity from "../../../src/hooks/useLottieInteractivity";
3 | import robotAnimation from "../../assets/robotAnimation.json";
4 |
5 | const style = {
6 | height: 300,
7 | border: 3,
8 | borderStyle: "solid",
9 | borderRadius: 7,
10 | };
11 |
12 | const options = {
13 | animationData: robotAnimation,
14 | };
15 |
16 | const CursorDiagonalSync = () => {
17 | const lottieObj = useLottie(options, style);
18 | const Animation = useLottieInteractivity({
19 | lottieObj,
20 | mode: "cursor",
21 | actions: [
22 | {
23 | position: { x: [0, 1], y: [0, 1] },
24 | type: "seek",
25 | frames: [0, 180],
26 | },
27 | ],
28 | });
29 |
30 | return Animation;
31 | };
32 |
33 | export default CursorDiagonalSync;
34 |
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/CursorHorizontalSync.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import useLottieInteractivity from "../../../src/hooks/useLottieInteractivity";
3 | import hamsterAnimation from "../../assets/hamster.json";
4 |
5 | const style = {
6 | height: 300,
7 | border: 3,
8 | borderStyle: "solid",
9 | borderRadius: 7,
10 | };
11 |
12 | const options = {
13 | animationData: hamsterAnimation,
14 | };
15 |
16 | const CursorHorizontalSync = () => {
17 | const lottieObj = useLottie(options, style);
18 | const Animation = useLottieInteractivity({
19 | lottieObj,
20 | mode: "cursor",
21 | actions: [
22 | {
23 | position: { x: [0, 1], y: [-1, 2] },
24 | type: "seek",
25 | frames: [0, 179],
26 | },
27 | {
28 | position: { x: -1, y: -1 },
29 | type: "stop",
30 | frames: [0],
31 | },
32 | ],
33 | });
34 |
35 | return Animation;
36 | };
37 |
38 | export default CursorHorizontalSync;
39 |
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/PlaySegmentsOnHover.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import useLottieInteractivity from "../../../src/hooks/useLottieInteractivity";
3 | import robotAnimation from "../../assets/robotAnimation.json";
4 |
5 | const style = {
6 | height: 300,
7 | border: 3,
8 | borderStyle: "solid",
9 | borderRadius: 7,
10 | };
11 |
12 | const options = {
13 | animationData: robotAnimation,
14 | };
15 |
16 | const PlaySegmentsOnHover = () => {
17 | const lottieObj = useLottie(options, style);
18 | const Animation = useLottieInteractivity({
19 | lottieObj,
20 | mode: "cursor",
21 | actions: [
22 | {
23 | position: { x: [0, 1], y: [0, 1] },
24 | type: "loop",
25 | frames: [45, 60],
26 | },
27 | {
28 | position: { x: -1, y: -1 },
29 | type: "stop",
30 | frames: [45],
31 | },
32 | ],
33 | });
34 |
35 | return Animation;
36 | };
37 |
38 | export default PlaySegmentsOnHover;
39 |
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/README.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: useLottieInteractivity
3 | menu: Hooks
4 | route: /hooks/useLottieInteractivity
5 | ---
6 |
7 | import { Playground } from "docz";
8 | import UseInteractivityBasic from "./UseInteractivityBasic.js";
9 | import ScrollWithOffset from "./ScrollWithOffset.js";
10 | import ScrollWithOffsetAndLoop from "./ScrollWithOffsetAndLoop.js";
11 | import PlaySegmentsOnHover from "./PlaySegmentsOnHover.js";
12 | import CursorDiagonalSync from "./CursorDiagonalSync.js";
13 | import CursorHorizontalSync from "./CursorHorizontalSync.js";
14 |
15 | # useLottieInteractivity
16 |
17 | ## Getting Started
18 |
19 | Use this hook along with the __useLottie__ hook to add animations synced with
20 | scroll and cursor
21 |
22 | Also read [official lottie
23 | reference](https://lottiefiles.com/interactivity) for general, non-react
24 | solution.
25 |
26 |
27 |
28 | ```jsx
29 | import { useLottie, useLottieInteractivity } from "lottie-react";
30 | import likeButton from "./likeButton.json";
31 |
32 | const style = {
33 | height: 300,
34 | border: 3,
35 | borderStyle: "solid",
36 | borderRadius: 7,
37 | };
38 |
39 | const options = {
40 | animationData: likeButton,
41 | };
42 |
43 | const Example = () => {
44 | const lottieObj = useLottie(options, style);
45 | const Animation = useLottieInteractivity({
46 | lottieObj,
47 | mode: "scroll",
48 | actions: [
49 | {
50 | visibility: [0.4, 0.9],
51 | type: "seek",
52 | frames: [0, 38],
53 | },
54 | ],
55 | });
56 |
57 | return Animation;
58 | };
59 |
60 | export default Example;
61 | ```
62 |
63 | ## Params
64 |
65 | | Param | Type | Required | Default | Description |
66 | | ---------------------- | --------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
67 | | lottieObj | object | required | | Result returned from the useLottie() hook
68 | | mode | string | required | | Either "scroll" or "cursor". Event that will be synced with animation |
69 | | actions | array | required | | Array of actions that will run in sequence (SEE BELOW) |
70 |
71 | __actions__ is an array of objects that define how animation will
72 | be run based on the chosen mode. One action chains the next one.
73 |
74 | An action object is defined as:
75 |
76 | ```js
77 | {
78 | frames: [number] | [number, number];
79 | type: "seek" | "play" | "stop" | "loop";
80 | visibility?: [number, number];
81 | position?: { [axis in "x" | "y"]: number | [number, number] };
82 | }
83 | ```
84 |
85 | ### frames
86 |
87 | Animation frame range to play for the action.
88 |
89 | Let's say full animation has 150 frames.
90 | To sync all 150 frames with one action, you would pass [0, 150].
91 | To start animation from 50 frame and end at 120, you would pass [50, 120].
92 | To freeze animation at 80 frame, you would pass [80].
93 |
94 | ### type
95 |
96 | Action type.
97 |
98 | 'play', 'stop', 'loop' are pretty self-explanatory. With 'seek' passed, lottie
99 | will play animation frame by frame as you scroll down the page (mode: "scroll")
100 | or move cursor around (mode: "cursor").
101 |
102 | ### visibility
103 |
104 | Viewport of the action (mode "scroll" only)
105 |
106 | Each action has a start and end which is essentially a percentage for the height
107 | of the lottie container and is a value between 0 and 1.
108 | To start the action when animation is visible on the page, you would pass [0, 1].
109 | To start lottie after 40% scrolled and end at 85% scrolled, you would pass [0.4, 0.85].
110 |
111 |
112 | ### position
113 |
114 | Cursor viewport (mode "cursor" only)
115 |
116 | You can define how much of the viewport cursor movement will cover inside the
117 | animation element. To set cursor cover the entire animation element, you would
118 | pass `{ x: [0, 1], y: [0, 1]}`. To set cursor outside of the element, you would
119 | pass `{ x: -1, y: -1 }`.
120 |
121 |
122 | ## Returns
123 |
124 | ### React.Element
125 |
126 | You only need to render the returned value.
127 |
128 | ## Examples
129 |
130 | ### Lottie scroll with offset
131 |
132 | From 0 to 45% of the container the Lottie will be stopped, and from 45% to 100%
133 | of the container the Lottie will be synced with the scroll.
134 |
135 |
136 |
137 | ```jsx
138 | import { useLottie, useLottieInteractivity } from "lottie-react";
139 | import likeButton from "./likeButton.json";
140 |
141 | const options = {
142 | animationData: likeButton,
143 | };
144 |
145 | const Example = () => {
146 | const lottieObj = useLottie(options);
147 | const Animation = useLottieInteractivity({
148 | lottieObj,
149 | mode: "scroll",
150 | actions: [
151 | {
152 | visibility: [0, 0.45],
153 | type: "stop",
154 | frames: [0],
155 | },
156 | {
157 | visibility: [0.45, 1],
158 | type: "seek",
159 | frames: [0, 38],
160 | },
161 | ],
162 | });
163 |
164 | return Animation;
165 | };
166 |
167 | export default Example;
168 | ```
169 |
170 | ### Scroll effect with offset and segment looping
171 |
172 | In cases where you would like the animation to loop from a specific frame to a
173 | specific frame, you can add an additional object to actions in which you can
174 | specify the frames. In the example below, the Lottie loops frame 45 to 60 once
175 | 45% of the container is reached.
176 |
177 |
178 |
179 |
180 | ```jsx
181 | import { useLottie, useLottieInteractivity } from "lottie-react";
182 | import robotAnimation from "./robotAnimation.json";
183 |
184 | const options = {
185 | animationData: robotAnimation,
186 | };
187 |
188 | const Example = () => {
189 | const lottieObj = useLottie(options);
190 | const Animation = useLottieInteractivity({
191 | lottieObj,
192 | mode: "scroll",
193 | actions: [
194 | {
195 | visibility: [0, 0.2],
196 | type: "stop",
197 | frames: [0],
198 | },
199 | {
200 | visibility: [0.2, 0.45],
201 | type: "seek",
202 | frames: [0, 45],
203 | },
204 | {
205 | visibility: [0.45, 1.0],
206 | type: "loop",
207 | frames: [45, 60],
208 | },
209 | ],
210 | });
211 |
212 | return Animation;
213 | };
214 |
215 | export default Example;
216 | ```
217 |
218 | ### Play segments on hover
219 |
220 | When the cursor moves in to the container, the Lottie loops from frame 45 to 60
221 | as long as cursor is inside the container. The stop action as shown below is so
222 | that the animation is stopped at the 45th frame when cursor is outside.
223 |
224 |
225 |
226 |
227 | ```jsx
228 | import { useLottie, useLottieInteractivity } from "lottie-react";
229 | import robotAnimation from "./robotAnimation.json";
230 |
231 | const style = {
232 | height: 300,
233 | border: 3,
234 | borderStyle: "solid",
235 | borderRadius: 7,
236 | };
237 |
238 | const options = {
239 | animationData: robotAnimation,
240 | };
241 |
242 | const PlaySegmentsOnHover = () => {
243 | const lottieObj = useLottie(options, style);
244 | const Animation = useLottieInteractivity({
245 | lottieObj,
246 | mode: "cursor",
247 | actions: [
248 | {
249 | position: { x: [0, 1], y: [0, 1] },
250 | type: "loop",
251 | frames: [45, 60],
252 | },
253 | {
254 | position: { x: -1, y: -1 },
255 | type: "stop",
256 | frames: [45],
257 | },
258 | ],
259 | });
260 |
261 | return Animation;
262 | };
263 |
264 | export default PlaySegmentsOnHover;
265 | ```
266 |
267 | ### Sync cursor position with animation
268 |
269 | Moving the cursor from top left of the container to the bottom right of the
270 | container completes the animation. The seeking of the animation is synced to the
271 | diagonal movement of the cursor.
272 |
273 |
274 |
275 | ```jsx
276 | import { useLottie, useLottieInteractivity } from "lottie-react";
277 | import robotAnimation from "./robotAnimation.json";
278 |
279 | const style = {
280 | height: 300,
281 | border: 3,
282 | borderStyle: "solid",
283 | borderRadius: 7,
284 | };
285 |
286 | const options = {
287 | animationData: robotAnimation,
288 | };
289 |
290 | const CursorDiagonalSync = () => {
291 | const lottieObj = useLottie(options, style);
292 | const Animation = useLottieInteractivity({
293 | lottieObj,
294 | mode: "cursor",
295 | actions: [
296 | {
297 | position: { x: [0, 1], y: [0, 1] },
298 | type: "seek",
299 | frames: [0, 180],
300 | },
301 | ],
302 | });
303 |
304 | return Animation;
305 | };
306 |
307 | export default CursorDiagonalSync;
308 | ```
309 |
310 | ### Sync animation with cursor horizontal movement
311 |
312 | Move your cursor on the animation below. You may interchange the x and y arrays
313 | to get the vertical movement of the cursor synced with the animation.
314 |
315 |
316 |
317 |
318 | ```jsx
319 | import { useLottie, useLottieInteractivity } from "lottie-react";
320 | import hamsterAnimation from "./hamsterAnimation.json";
321 |
322 | const style = {
323 | height: 300,
324 | border: 3,
325 | borderStyle: "solid",
326 | borderRadius: 7,
327 | };
328 |
329 | const options = {
330 | animationData: hamsterAnimation,
331 | };
332 |
333 | const CursorHorizontalSync = () => {
334 | const lottieObj = useLottie(options, style);
335 | const Animation = useLottieInteractivity({
336 | lottieObj,
337 | mode: "cursor",
338 | actions: [
339 | {
340 | position: { x: [0, 1], y: [-1, 2] },
341 | type: "seek",
342 | frames: [0, 179],
343 | },
344 | {
345 | position: { x: -1, y: -1 },
346 | type: "stop",
347 | frames: [0],
348 | },
349 | ],
350 | });
351 |
352 | return Animation;
353 | };
354 |
355 | export default CursorHorizontalSync;
356 | ```
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/ScrollWithOffset.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import useLottieInteractivity from "../../../src/hooks/useLottieInteractivity";
3 | import likeButton from "../../assets/likeButton.json";
4 |
5 | const style = {
6 | height: 300,
7 | };
8 |
9 | const options = {
10 | animationData: likeButton,
11 | };
12 |
13 | const ScrollWithOffset = () => {
14 | const lottieObj = useLottie(options, style);
15 | const Animation = useLottieInteractivity({
16 | lottieObj,
17 | mode: "scroll",
18 | actions: [
19 | {
20 | visibility: [0, 0.45],
21 | type: "stop",
22 | frames: [0],
23 | },
24 | {
25 | visibility: [0.45, 1],
26 | type: "seek",
27 | frames: [0, 38],
28 | },
29 | ],
30 | });
31 |
32 | return Animation;
33 | };
34 |
35 | export default ScrollWithOffset;
36 |
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/ScrollWithOffsetAndLoop.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import useLottieInteractivity from "../../../src/hooks/useLottieInteractivity";
3 | import robotAnimation from "../../assets/robotAnimation.json";
4 |
5 | const style = {
6 | height: 450,
7 | };
8 |
9 | const options = {
10 | animationData: robotAnimation,
11 | loop: true,
12 | };
13 |
14 | const ScrollWithOffsetAndLoop = () => {
15 | const lottieObj = useLottie(options, style);
16 | const Animation = useLottieInteractivity({
17 | lottieObj,
18 | mode: "scroll",
19 | actions: [
20 | {
21 | visibility: [0, 0.2],
22 | type: "stop",
23 | frames: [0],
24 | },
25 | {
26 | visibility: [0.2, 0.45],
27 | type: "seek",
28 | frames: [0, 45],
29 | },
30 | {
31 | visibility: [0.45, 1.0],
32 | type: "loop",
33 | frames: [45, 60],
34 | },
35 | ],
36 | });
37 |
38 | return Animation;
39 | };
40 | // max 180
41 |
42 | export default ScrollWithOffsetAndLoop;
43 |
--------------------------------------------------------------------------------
/docs/hooks/useLottieInteractivity/UseInteractivityBasic.js:
--------------------------------------------------------------------------------
1 | import useLottie from "../../../src/hooks/useLottie";
2 | import useLottieInteractivity from "../../../src/hooks/useLottieInteractivity";
3 | import likeButton from "../../assets/likeButton.json";
4 |
5 | const style = {
6 | height: 300,
7 | border: 3,
8 | borderStyle: "solid",
9 | borderRadius: 7,
10 | };
11 |
12 | const options = {
13 | animationData: likeButton,
14 | };
15 |
16 | const UseInteractivityBasic = () => {
17 | const lottieObj = useLottie(options, style);
18 | const Animation = useLottieInteractivity({
19 | mode: "scroll",
20 | lottieObj,
21 | actions: [
22 | {
23 | visibility: [0.4, 0.9],
24 | type: "seek",
25 | frames: [0, 38],
26 | },
27 | ],
28 | });
29 |
30 | return Animation;
31 | };
32 |
33 | export default UseInteractivityBasic;
34 |
--------------------------------------------------------------------------------
/doczrc.js:
--------------------------------------------------------------------------------
1 | export default {
2 | menu: ["Components", "Hooks"],
3 | src: "docs",
4 | dest: "docs-dist",
5 | base: "/", // GitHub Pages sub-path
6 | ignore: ["README.md"],
7 | title: "Lottie for React",
8 | themeConfig: {
9 | initialColorMode: "light",
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "@jest/types";
2 |
3 | export default async (): Promise
=> {
4 | return {
5 | // The root of your source code, typically /src
6 | // `` is a token Jest substitutes
7 | roots: ["/src"],
8 |
9 | // Jest transformations -- this adds support for TypeScript
10 | // using ts-jest
11 | transform: {
12 | "^.+\\.tsx?$": "ts-jest",
13 | },
14 |
15 | // Runs special logic, such as cleaning up components
16 | // when using React Testing Library and adds special
17 | // extended assertions to Jest
18 | setupFilesAfterEnv: [
19 | // "@testing-library/react/cleanup-after-each",
20 | "@testing-library/jest-dom/extend-expect",
21 | "jest-canvas-mock",
22 | ],
23 |
24 | // Test spec file resolution pattern
25 | // Matches parent folder `__tests__` and filename
26 | // should contain `test` or `spec`.
27 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
28 |
29 | // Module file extensions for importing
30 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
31 |
32 | // All imported modules in your tests should be mocked automatically
33 | // automock: false,
34 |
35 | // Stop running tests after `n` failures
36 | // bail: 0,
37 |
38 | // The directory where Jest should store its cached dependency information
39 | // cacheDirectory: "/private/var/folders/t5/3rr3f6y11j33hb3zrmm66nh80000gn/T/jest_dx",
40 |
41 | // Automatically clear mock calls and instances between every test
42 | // clearMocks: true,
43 |
44 | // Indicates whether the coverage information should be collected while executing the test
45 | // collectCoverage: false,
46 |
47 | // An array of glob patterns indicating a set of files for which coverage information should be collected
48 | // collectCoverageFrom: undefined,
49 |
50 | // The directory where Jest should output its coverage files
51 | coverageDirectory: "coverage",
52 |
53 | // An array of regexp pattern strings used to skip coverage collection
54 | // coveragePathIgnorePatterns: [
55 | // "/node_modules/"
56 | // ],
57 |
58 | // A list of reporter names that Jest uses when writing coverage reports
59 | // coverageReporters: [
60 | // "json",
61 | // "text",
62 | // "lcov",
63 | // "clover"
64 | // ],
65 |
66 | // An object that configures minimum threshold enforcement for coverage results
67 | // coverageThreshold: undefined,
68 |
69 | // A path to a custom dependency extractor
70 | // dependencyExtractor: undefined,
71 |
72 | // Make calling deprecated APIs throw helpful error messages
73 | // errorOnDeprecated: false,
74 |
75 | // Force coverage collection from ignored files using an array of glob patterns
76 | // forceCoverageMatch: [],
77 |
78 | // A path to a module which exports an async function that is triggered once before all test suites
79 | // globalSetup: undefined,
80 |
81 | // A path to a module which exports an async function that is triggered once after all test suites
82 | // globalTeardown: undefined,
83 |
84 | // A set of global variables that need to be available in all test environments
85 | globals: {
86 | "ts-jest": {
87 | diagnostics: false,
88 | },
89 | },
90 |
91 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
92 | // maxWorkers: "50%",
93 |
94 | // An array of directory names to be searched recursively up from the requiring module's location
95 | // moduleDirectories: [
96 | // "node_modules"
97 | // ],
98 |
99 | // An array of file extensions your modules use
100 | // moduleFileExtensions: [
101 | // "js",
102 | // "json",
103 | // "jsx",
104 | // "ts",
105 | // "tsx",
106 | // "node"
107 | // ],
108 |
109 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
110 | // moduleNameMapper: {},
111 |
112 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
113 | // modulePathIgnorePatterns: [],
114 |
115 | // Activates notifications for test results
116 | // notify: false,
117 |
118 | // An enum that specifies notification mode. Requires { notify: true }
119 | // notifyMode: "failure-change",
120 |
121 | // A preset that is used as a base for Jest's configuration
122 | // preset: undefined,
123 |
124 | // Run tests from one or more projects
125 | // projects: undefined,
126 |
127 | // Use this configuration option to add custom reporters to Jest
128 | // reporters: undefined,
129 |
130 | // Automatically reset mock state between every test
131 | // resetMocks: false,
132 |
133 | // Reset the module registry before running each individual test
134 | // resetModules: false,
135 |
136 | // A path to a custom resolver
137 | // resolver: undefined,
138 |
139 | // Automatically restore mock state between every test
140 | // restoreMocks: false,
141 |
142 | // The root directory that Jest should scan for tests and modules within
143 | // rootDir: undefined,
144 |
145 | // A list of paths to directories that Jest should use to search for files in
146 | // roots: [
147 | // ""
148 | // ],
149 |
150 | // Allows you to use a custom runner instead of Jest's default test runner
151 | // runner: "jest-runner",
152 |
153 | // The paths to modules that run some code to configure or set up the testing environment before each test
154 | // setupFiles: [],
155 |
156 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
157 | // snapshotSerializers: [],
158 |
159 | // The test environment that will be used for testing
160 | // testEnvironment: "jest-environment-jsdom",
161 |
162 | // Options that will be passed to the testEnvironment
163 | // testEnvironmentOptions: {},
164 |
165 | // Adds a location field to test results
166 | // testLocationInResults: false,
167 |
168 | // The glob patterns Jest uses to detect test files
169 | // testMatch: [
170 | // "**/__tests__/**/*.[jt]s?(x)",
171 | // "**/?(*.)+(spec|test).[tj]s?(x)"
172 | // ],
173 |
174 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
175 | // testPathIgnorePatterns: [
176 | // "/node_modules/"
177 | // ],
178 |
179 | // The regexp pattern or array of patterns that Jest uses to detect test files
180 | // testRegex: [],
181 |
182 | // This option allows the use of a custom results processor
183 | // testResultsProcessor: undefined,
184 |
185 | // This option allows use of a custom test runner
186 | // testRunner: "jasmine2",
187 |
188 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
189 | // testURL: "http://localhost",
190 |
191 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
192 | // timers: "real",
193 |
194 | // A map from regular expressions to paths to transformers
195 | // transform: undefined,
196 |
197 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
198 | // transformIgnorePatterns: [
199 | // "/node_modules/"
200 | // ],
201 |
202 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
203 | // unmockedModulePathPatterns: undefined,
204 |
205 | // Indicates whether each individual test should be reported during the run
206 | // verbose: undefined,
207 |
208 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
209 | // watchPathIgnorePatterns: [],
210 |
211 | // Whether to use watchman for file crawling
212 | // watchman: true,
213 | };
214 | };
215 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lottie-react",
3 | "version": "2.4.1",
4 | "description": "Lottie for React",
5 | "keywords": [
6 | "lottie",
7 | "react",
8 | "lottie react",
9 | "react lottie",
10 | "lottie web",
11 | "animation",
12 | "component",
13 | "hook"
14 | ],
15 | "homepage": "https://lottiereact.com",
16 | "bugs": {
17 | "url": "https://github.com/Gamote/lottie-react/issues"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/Gamote/lottie-react.git"
22 | },
23 | "license": "MIT",
24 | "author": "David Gamote",
25 | "main": "build/index.js",
26 | "module": "build/index.es.js",
27 | "browser": "build/index.umd.js",
28 | "types": "build/index.d.ts",
29 | "style": "build/index.css",
30 | "files": [
31 | "/build"
32 | ],
33 | "scripts": {
34 | "build": "run-s tsc:compile rollup:compile",
35 | "postbuild": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz",
36 | "build:watch": "run-p tsc:compile:watch rollup:compile:watch",
37 | "coverage": "jest --coverage && cat ./coverage/lcov.info | coveralls",
38 | "docz:build": "docz build",
39 | "deploy:docs": "echo 'lottiereact.com' > ./docs-dist/CNAME && gh-pages -d docs-dist",
40 | "docz:dev": "docz dev",
41 | "docz:serve": "docz build && docz serve",
42 | "prepublishOnly": "rm -rf build && yarn build",
43 | "rollup:compile": "rollup -c",
44 | "rollup:compile:watch": "rollup -c -w",
45 | "test": "jest",
46 | "test:watch": "jest --watch",
47 | "tsc:compile": "tsc",
48 | "tsc:compile:watch": "tsc --watch"
49 | },
50 | "dependencies": {
51 | "lottie-web": "^5.10.2"
52 | },
53 | "devDependencies": {
54 | "@babel/core": "^7.16.7",
55 | "@babel/preset-env": "^7.16.8",
56 | "@babel/preset-react": "^7.16.7",
57 | "@jest/types": "^27.4.2",
58 | "@rollup/plugin-commonjs": "^21.0.1",
59 | "@rollup/plugin-node-resolve": "^13.1.3",
60 | "@testing-library/jest-dom": "^5.16.1",
61 | "@testing-library/react": "^12.1.2",
62 | "@testing-library/react-hooks": "^7.0.2",
63 | "@types/jest": "^27.4.0",
64 | "@types/react": "^18.0.14",
65 | "@types/react-dom": "^18.0.5",
66 | "@typescript-eslint/eslint-plugin": "^5.29.0",
67 | "@typescript-eslint/parser": "^5.29.0",
68 | "autoprefixer": "^10.4.2",
69 | "babel-loader": "^8.2.3",
70 | "coveralls": "^3.1.1",
71 | "docz": "^2.3.1",
72 | "eslint": "^8.18.0",
73 | "eslint-config-prettier": "^8.5.0",
74 | "eslint-plugin-import": "^2.26.0",
75 | "eslint-plugin-jsx-a11y": "^6.5.1",
76 | "eslint-plugin-prettier": "^4.0.0",
77 | "eslint-plugin-promise": "^6.0.0",
78 | "eslint-plugin-react": "^7.30.0",
79 | "eslint-plugin-react-hooks": "^4.6.0",
80 | "get-pkg-repo": "^5.0.0",
81 | "gh-pages": "^3.2.3",
82 | "jest": "^27.4.7",
83 | "jest-canvas-mock": "^2.3.1",
84 | "sass": "^1.83.4",
85 | "npm-run-all": "4.1.5",
86 | "prettier": "^2.8.4",
87 | "react": "^18.2.0",
88 | "react-dom": "^18.2.0",
89 | "react-test-renderer": "^17.0.2",
90 | "rollup": "^2.64.0",
91 | "rollup-plugin-babel": "^4.4.0",
92 | "rollup-plugin-dts": "^4.1.0",
93 | "rollup-plugin-peer-deps-external": "^2.2.4",
94 | "rollup-plugin-postcss": "^4.0.2",
95 | "rollup-plugin-terser": "^7.0.2",
96 | "ts-jest": "^27.1.3",
97 | "ts-node": "^10.9.1",
98 | "tslib": "^2.5.0",
99 | "typescript": "^4.9.5"
100 | },
101 | "peerDependencies": {
102 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
103 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
104 | },
105 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
106 | }
107 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import external from "rollup-plugin-peer-deps-external";
3 | import resolve from "@rollup/plugin-node-resolve";
4 | import commonjs from "@rollup/plugin-commonjs";
5 | import postcss from "rollup-plugin-postcss";
6 | import autoprefixer from "autoprefixer";
7 | import { terser } from "rollup-plugin-terser";
8 | import dts from "rollup-plugin-dts";
9 |
10 | import packageJSON from "./package.json";
11 |
12 | /**
13 | * We are using 'build/compiled/index.js' instead of 'src/index.tsx'
14 | * because we need to compile the code first.
15 | *
16 | * We could've used the '@rollup/plugin-typescript' but that plugin
17 | * doesn't allow us to rename the files on output. So we decided to
18 | * compile the code and after that to run the rollup command using
19 | * the index file generated by the compilation.
20 | *
21 | * @type {string}
22 | */
23 | const input = "./compiled/index.js";
24 |
25 | /**
26 | * Get the extension for the minified files
27 | * @param pathToFile
28 | * @return string
29 | */
30 | const minifyExtension = (pathToFile) => pathToFile.replace(/\.js$/, ".min.js");
31 |
32 | /**
33 | * Get the extension for the TS definition files
34 | * @param pathToFile
35 | * @return string
36 | */
37 | const dtsExtension = (pathToFile) => pathToFile.replace(".js", ".d.ts");
38 |
39 | /**
40 | * Definition of the common plugins used in the rollup configurations
41 | */
42 | const reusablePluginList = [
43 | postcss({
44 | plugins: [autoprefixer],
45 | }),
46 | babel({
47 | exclude: "node_modules/**",
48 | }),
49 | external(),
50 | resolve(),
51 | commonjs(),
52 | ];
53 |
54 | /**
55 | * Definition of the rollup configurations
56 | */
57 | const exports = {
58 | cjs: {
59 | input,
60 | output: {
61 | file: packageJSON.main,
62 | format: "cjs",
63 | sourcemap: true,
64 | exports: "named",
65 | },
66 | external: ["lottie-web"],
67 | plugins: reusablePluginList,
68 | },
69 | cjs_min: {
70 | input,
71 | output: {
72 | file: minifyExtension(packageJSON.main),
73 | format: "cjs",
74 | exports: "named",
75 | },
76 | external: ["lottie-web"],
77 | plugins: [...reusablePluginList, terser()],
78 | },
79 | umd: {
80 | input,
81 | output: {
82 | file: packageJSON.browser,
83 | format: "umd",
84 | sourcemap: true,
85 | name: "lottie-react",
86 | exports: "named",
87 | globals: {
88 | react: "React",
89 | "lottie-web": "Lottie",
90 | },
91 | },
92 | external: ["lottie-web"],
93 | plugins: reusablePluginList,
94 | },
95 | umd_min: {
96 | input,
97 | output: {
98 | file: minifyExtension(packageJSON.browser),
99 | format: "umd",
100 | exports: "named",
101 | name: "lottie-react",
102 | globals: {
103 | react: "React",
104 | "lottie-web": "Lottie",
105 | },
106 | },
107 | external: ["lottie-web"],
108 | plugins: [...reusablePluginList, terser()],
109 | },
110 | es: {
111 | input,
112 | output: {
113 | file: packageJSON.module,
114 | format: "es",
115 | sourcemap: true,
116 | exports: "named",
117 | },
118 | external: ["lottie-web"],
119 | plugins: reusablePluginList,
120 | },
121 | es_min: {
122 | input,
123 | output: {
124 | file: minifyExtension(packageJSON.module),
125 | format: "es",
126 | exports: "named",
127 | },
128 | external: ["lottie-web"],
129 | plugins: [...reusablePluginList, terser()],
130 | },
131 | dts: {
132 | input: dtsExtension(input),
133 | output: {
134 | file: packageJSON.types,
135 | format: "es",
136 | },
137 | plugins: [dts()],
138 | },
139 | };
140 |
141 | export default [
142 | exports.cjs,
143 | exports.cjs_min,
144 | exports.umd,
145 | exports.umd_min,
146 | exports.es,
147 | exports.es_min,
148 | exports.dts,
149 | ];
150 |
--------------------------------------------------------------------------------
/src/__tests__/Lottie.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from "react";
5 | import { render } from "@testing-library/react";
6 | import groovyWalk from "./assets/groovyWalk.json";
7 |
8 | import Lottie from "../components/Lottie";
9 | import { LottieRef, PartialLottieComponentProps } from "../types";
10 | import useLottieInteractivity from "../hooks/useLottieInteractivity";
11 |
12 | jest.mock("../hooks/useLottieInteractivity.tsx");
13 |
14 | function renderLottie(props?: PartialLottieComponentProps) {
15 | const defaultProps = {
16 | animationData: groovyWalk,
17 | };
18 |
19 | return render();
20 | }
21 |
22 | describe("", () => {
23 | test("should check if 'lottieRef' can be undefined", async () => {
24 | const component = renderLottie();
25 | expect(component.container).toBeDefined();
26 | });
27 |
28 | test("should check 'lottieRef' properties", async () => {
29 | const lottieRef: LottieRef = { current: null };
30 |
31 | renderLottie({ lottieRef });
32 |
33 | expect(Object.keys(lottieRef.current || {}).length).toBe(13);
34 |
35 | expect(lottieRef.current?.play).toBeDefined();
36 | expect(lottieRef.current?.stop).toBeDefined();
37 | expect(lottieRef.current?.pause).toBeDefined();
38 | expect(lottieRef.current?.setSpeed).toBeDefined();
39 | expect(lottieRef.current?.goToAndPlay).toBeDefined();
40 | expect(lottieRef.current?.goToAndStop).toBeDefined();
41 | expect(lottieRef.current?.setDirection).toBeDefined();
42 | expect(lottieRef.current?.playSegments).toBeDefined();
43 | expect(lottieRef.current?.setSubframe).toBeDefined();
44 | expect(lottieRef.current?.getDuration).toBeDefined();
45 | expect(lottieRef.current?.destroy).toBeDefined();
46 | expect(lottieRef.current?.animationLoaded).toBeDefined();
47 | expect(lottieRef.current?.animationItem).toBeDefined();
48 | });
49 |
50 | test("should pass HTML props to container ", () => {
51 | const { getByLabelText } = renderLottie({ "aria-label": "test" });
52 | expect(getByLabelText("test")).toBeTruthy();
53 | });
54 |
55 | test("should not pass non-HTML props to container
", () => {
56 | // TODO
57 | });
58 |
59 | test("should check if interactivity applied when passed as a prop", async () => {
60 | (useLottieInteractivity as jest.Mock).mockReturnValue(
);
61 | renderLottie({ interactivity: { actions: [], mode: "scroll" } });
62 | expect(useLottieInteractivity).toBeCalled();
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/src/__tests__/useLottie.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React, { CSSProperties } from "react";
5 | import { render } from "@testing-library/react";
6 | import { renderHook } from "@testing-library/react-hooks";
7 | import groovyWalk from "./assets/groovyWalk.json";
8 |
9 | import useLottie from "../hooks/useLottie";
10 | import { PartialLottieOptions } from "../types";
11 |
12 | function initUseLottie(props?: PartialLottieOptions, style?: CSSProperties) {
13 | const defaultProps = {
14 | animationData: groovyWalk,
15 | };
16 |
17 | return renderHook(
18 | (rerenderProps?) =>
19 | useLottie(
20 | {
21 | ...defaultProps,
22 | ...props,
23 | ...rerenderProps,
24 | },
25 | style,
26 | ),
27 | {
28 | initialProps: defaultProps as PartialLottieOptions,
29 | },
30 | );
31 | }
32 |
33 | /**
34 | * We need to render the returned 'View', otherwise the container's 'ref'
35 | * will remain 'null' so the animation will never be initialized
36 | * TODO: check if we can avoid a manual rerender
37 | */
38 | function renderUseLottie(hook: any, props?: PartialLottieOptions) {
39 | render(hook.result.current.View);
40 |
41 | /*
42 | * We need to manually trigger a rerender for the ref to be updated
43 | * by providing different props
44 | */
45 | hook.rerender({
46 | loop: true,
47 | ...props,
48 | });
49 | }
50 |
51 | describe("useLottie(...)", () => {
52 | describe("General", () => {
53 | test("should check the returned object", async () => {
54 | const { result } = initUseLottie();
55 |
56 | expect(Object.keys(result.current).length).toBe(14);
57 |
58 | expect(result.current.View).toBeDefined();
59 | expect(result.current.play).toBeDefined();
60 | expect(result.current.stop).toBeDefined();
61 | expect(result.current.pause).toBeDefined();
62 | expect(result.current.setSpeed).toBeDefined();
63 | expect(result.current.goToAndStop).toBeDefined();
64 | expect(result.current.goToAndPlay).toBeDefined();
65 | expect(result.current.setDirection).toBeDefined();
66 | expect(result.current.playSegments).toBeDefined();
67 | expect(result.current.setSubframe).toBeDefined();
68 | expect(result.current.getDuration).toBeDefined();
69 | expect(result.current.destroy).toBeDefined();
70 | expect(result.current.animationLoaded).toBeDefined();
71 | expect(result.current.animationItem || true).toBeDefined();
72 | });
73 | });
74 |
75 | describe("w/o animationInstanceRef", () => {
76 | test("should check the interaction methods", async () => {
77 | const { result } = initUseLottie();
78 |
79 | expect(result.current.play()).toBeUndefined();
80 | expect(result.current.stop()).toBeUndefined();
81 | expect(result.current.pause()).toBeUndefined();
82 | expect(result.current.setSpeed(1)).toBeUndefined();
83 | expect(result.current.goToAndStop(1)).toBeUndefined();
84 | expect(result.current.goToAndPlay(1)).toBeUndefined();
85 | expect(result.current.setDirection(1)).toBeUndefined();
86 | expect(result.current.playSegments([])).toBeUndefined();
87 | expect(result.current.setSubframe(true)).toBeUndefined();
88 | expect(result.current.getDuration()).toBeUndefined();
89 | expect(result.current.destroy()).toBeUndefined();
90 |
91 | expect(result.current.animationLoaded).toBe(false);
92 | });
93 |
94 | test("shouldn't return error when adding event listener", async () => {
95 | const hookFactory = () =>
96 | initUseLottie({
97 | onComplete: () => {},
98 | });
99 |
100 | expect(hookFactory).not.toThrow();
101 | });
102 | });
103 |
104 | describe("w/ animationInstanceRef", () => {
105 | test("should check the interaction methods", async () => {
106 | const hook = initUseLottie();
107 |
108 | renderUseLottie(hook);
109 |
110 | expect(hook.result.current.play()).toBeUndefined();
111 | expect(hook.result.current.stop()).toBeUndefined();
112 | expect(hook.result.current.pause()).toBeUndefined();
113 | expect(hook.result.current.setSpeed(1)).toBeUndefined();
114 | expect(hook.result.current.goToAndStop(1)).toBeUndefined();
115 | expect(hook.result.current.goToAndPlay(1)).toBeUndefined();
116 | expect(hook.result.current.setDirection(1)).toBeUndefined();
117 | expect(hook.result.current.playSegments([])).toBeUndefined();
118 | expect(hook.result.current.setSubframe(true)).toBeUndefined();
119 | expect(hook.result.current.getDuration()).not.toBeNaN();
120 | expect(hook.result.current.destroy()).toBeUndefined();
121 |
122 | expect(hook.result.current.animationLoaded).toBe(true);
123 | });
124 |
125 | test("should destroy the previous animation instance", async () => {
126 | const hook = initUseLottie();
127 |
128 | renderUseLottie(hook);
129 |
130 | expect(hook.result.current.animationItem).toBeDefined();
131 |
132 | if (hook.result.current.animationItem) {
133 | const mock = jest.spyOn(hook.result.current.animationItem, "destroy");
134 |
135 | renderUseLottie(hook, {
136 | loop: false,
137 | });
138 |
139 | expect(mock).toBeCalledTimes(1);
140 | }
141 | });
142 |
143 | test("should add event listener", async () => {
144 | const hook = initUseLottie();
145 |
146 | renderUseLottie(hook);
147 |
148 | expect(hook.result.current.animationItem).toBeDefined();
149 |
150 | if (hook.result.current.animationItem) {
151 | const mock = jest.spyOn(
152 | hook.result.current.animationItem,
153 | "addEventListener",
154 | );
155 |
156 | renderUseLottie(hook, {
157 | onComplete: () => {},
158 | });
159 |
160 | expect(mock).toBeCalledTimes(1);
161 | }
162 | });
163 |
164 | test("shouldn't add an undefined event listener type", async () => {
165 | const hook = initUseLottie();
166 |
167 | renderUseLottie(hook);
168 |
169 | expect(hook.result.current.animationItem).toBeDefined();
170 |
171 | if (hook.result.current.animationItem) {
172 | const mock = jest.spyOn(
173 | hook.result.current.animationItem,
174 | "addEventListener",
175 | );
176 |
177 | renderUseLottie(hook, {
178 | // @ts-ignore
179 | notDefined: () => {},
180 | });
181 |
182 | expect(mock).toBeCalledTimes(0);
183 | }
184 | });
185 |
186 | test("shouldn't add event listener w/ the handler as 'undefined'", async () => {
187 | const hook = initUseLottie();
188 |
189 | renderUseLottie(hook);
190 |
191 | expect(hook.result.current.animationItem).toBeDefined();
192 |
193 | if (hook.result.current.animationItem) {
194 | const mock = jest.spyOn(
195 | hook.result.current.animationItem,
196 | "addEventListener",
197 | );
198 |
199 | renderUseLottie(hook, {
200 | onComplete: undefined,
201 | });
202 |
203 | expect(mock).not.toBeCalled();
204 | }
205 | });
206 | });
207 | });
208 |
--------------------------------------------------------------------------------
/src/__tests__/useLottieInteractivity.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from "react";
5 | import { render, fireEvent } from "@testing-library/react";
6 | import { renderHook } from "@testing-library/react-hooks";
7 |
8 | import useLottieInteractivity, {
9 | getContainerVisibility,
10 | getContainerCursorPosition,
11 | useInitInteractivity,
12 | InitInteractivity,
13 | } from "../hooks/useLottieInteractivity";
14 | import { InteractivityProps } from "../types";
15 | import { act } from "react-dom/test-utils";
16 |
17 | function renderUseLottieInteractivity(props: InteractivityProps) {
18 | return renderHook(() => useLottieInteractivity(props));
19 | }
20 |
21 | function renderUseInitInteractivity(props: InitInteractivity) {
22 | return renderHook(() => useInitInteractivity(props));
23 | }
24 |
25 | describe("useLottieInteractivity", () => {
26 | describe("General", () => {
27 | test("mounts with a div wrapper around lottie element", async () => {
28 | const hook = renderUseLottieInteractivity({
29 | lottieObj: {
30 | View:
,
31 | } as any,
32 | mode: "scroll",
33 | actions: [],
34 | });
35 |
36 | const result = render(hook.result.current);
37 |
38 | expect(result.container.innerHTML).toEqual("
");
39 | });
40 | });
41 | });
42 |
43 | describe("useInitInteractivity", () => {
44 | const result = render(
);
45 |
46 | const wrapperRef = {
47 | current: result.getByRole("test"),
48 | };
49 | wrapperRef.current.getBoundingClientRect = jest.fn();
50 |
51 | const animationItem = {
52 | stop: jest.fn(),
53 | play: jest.fn(),
54 | goToAndStop: jest.fn(),
55 | playSegments: jest.fn(),
56 | resetSegments: jest.fn(),
57 | firstFrame: 0,
58 | isPaused: false,
59 | };
60 |
61 | beforeEach(() => {
62 | (wrapperRef.current.getBoundingClientRect as jest.Mock<
63 | any,
64 | any
65 | >).mockClear();
66 | // Object.values(wrapperRef.current).forEach((f) => {
67 | // f.mockClear();
68 | // });
69 | let { firstFrame, isPaused, ...itemMocks } = animationItem;
70 |
71 | Object.values(itemMocks).forEach((f) => {
72 | f.mockClear();
73 | });
74 |
75 | firstFrame = 0;
76 | isPaused = false;
77 | });
78 |
79 | describe("General", () => {
80 | test("does nothing if animationItem is not provided", () => {
81 | const stopSpy = jest.spyOn(animationItem, "stop");
82 |
83 | renderUseInitInteractivity({
84 | wrapperRef: wrapperRef as any,
85 | animationItem: undefined as any,
86 | mode: "scroll",
87 | actions: [],
88 | });
89 |
90 | expect(stopSpy).toHaveBeenCalledTimes(0);
91 | });
92 |
93 | test("calls animationItem.stop() when mounts", () => {
94 | const stopSpy = jest.spyOn(animationItem, "stop");
95 |
96 | renderUseInitInteractivity({
97 | wrapperRef: wrapperRef as any,
98 | animationItem: animationItem as any,
99 | mode: "scroll",
100 | actions: [],
101 | });
102 |
103 | expect(stopSpy).toHaveBeenCalledTimes(1);
104 | });
105 | });
106 |
107 | describe("scroll mode", () => {
108 | beforeAll(() => {
109 | window = Object.assign(window, { innerHeight: 1 });
110 | (wrapperRef.current.getBoundingClientRect as jest.Mock<
111 | any,
112 | any
113 | >).mockReturnValue({
114 | top: 0,
115 | left: 0,
116 | width: 0,
117 | height: 1,
118 | });
119 | // currentPercent/containerVisibility => 0.5
120 | });
121 |
122 | beforeEach(() => {
123 | animationItem.isPaused = false;
124 | });
125 |
126 | test("attaches and detaches eventListeners", () => {
127 | const AddLSpy = jest.spyOn(document, "addEventListener");
128 | const RmLSpy = jest.spyOn(document, "removeEventListener");
129 |
130 | const hook = renderUseInitInteractivity({
131 | wrapperRef: wrapperRef as any,
132 | animationItem: animationItem as any,
133 | mode: "scroll",
134 | actions: [],
135 | });
136 |
137 | expect(AddLSpy).toHaveBeenCalledTimes(1);
138 | hook.unmount();
139 | expect(RmLSpy).toHaveBeenCalledTimes(1);
140 | });
141 |
142 | test("do not process if action does not match", () => {
143 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
144 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
145 | const resetSegmentsSpy = jest.spyOn(animationItem, "resetSegments");
146 |
147 | renderUseInitInteractivity({
148 | wrapperRef: wrapperRef as any,
149 | animationItem: animationItem as any,
150 | mode: "scroll",
151 | actions: [{ visibility: [0, 0.4], frames: [5, 10], type: "seek" }],
152 | });
153 |
154 | act(() => {
155 | fireEvent.scroll(document);
156 | });
157 |
158 | expect(goToAndStopSpy).toHaveBeenCalledTimes(0);
159 | expect(playSegmentsSpy).toHaveBeenCalledTimes(0);
160 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
161 | });
162 |
163 | test("handles `seek` type correctly", () => {
164 | // frameToGo = 10
165 |
166 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
167 | const goToAndStopArgMock = 10 - animationItem.firstFrame - 1;
168 |
169 | renderUseInitInteractivity({
170 | wrapperRef: wrapperRef as any,
171 | animationItem: animationItem as any,
172 | mode: "scroll",
173 | actions: [{ visibility: [0, 1], frames: [5, 10], type: "seek" }],
174 | });
175 |
176 | act(() => {
177 | fireEvent.scroll(document);
178 | fireEvent.scroll(document);
179 | });
180 |
181 | expect(goToAndStopSpy).toHaveBeenCalledTimes(2);
182 | expect(goToAndStopSpy).toHaveBeenCalledWith(goToAndStopArgMock, true);
183 | });
184 |
185 | test("handles `loop` type correctly", () => {
186 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
187 |
188 | renderUseInitInteractivity({
189 | wrapperRef: wrapperRef as any,
190 | animationItem: animationItem as any,
191 | mode: "scroll",
192 | actions: [
193 | { visibility: [0, 0.4], frames: [10, 15], type: "loop" },
194 | { visibility: [0.4, 1], frames: [5, 10], type: "loop" },
195 | ],
196 | });
197 |
198 | act(() => {
199 | fireEvent.scroll(document);
200 | });
201 |
202 | expect(playSegmentsSpy).toHaveBeenCalledTimes(1);
203 | expect(playSegmentsSpy).toHaveBeenCalledWith([5, 10], true);
204 |
205 | act(() => {
206 | fireEvent.scroll(document);
207 | });
208 |
209 | expect(playSegmentsSpy).toHaveBeenCalledTimes(1);
210 |
211 | // assignedSegment === action.frames
212 | animationItem.isPaused = true;
213 |
214 | act(() => {
215 | fireEvent.scroll(document);
216 | });
217 |
218 | expect(playSegmentsSpy).toHaveBeenCalledTimes(2);
219 |
220 | // container visibility => 0.2
221 | (wrapperRef.current.getBoundingClientRect as jest.Mock<
222 | any,
223 | any
224 | >).mockReturnValue({
225 | top: 0.6,
226 | left: 0,
227 | width: 0,
228 | height: 1,
229 | });
230 |
231 | act(() => {
232 | fireEvent.scroll(document);
233 | });
234 |
235 | expect(playSegmentsSpy).toHaveBeenCalledTimes(3);
236 | });
237 |
238 | test("handles `play` type correctly", () => {
239 | const playSpy = jest.spyOn(animationItem, "play");
240 | const resetSegmentsSpy = jest.spyOn(animationItem, "resetSegments");
241 |
242 | renderUseInitInteractivity({
243 | wrapperRef: wrapperRef as any,
244 | animationItem: animationItem as any,
245 | mode: "scroll",
246 | actions: [{ visibility: [0, 1], frames: [5, 10], type: "play" }],
247 | });
248 |
249 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
250 | expect(playSpy).toHaveBeenCalledTimes(0);
251 |
252 | act(() => {
253 | fireEvent.scroll(document);
254 | });
255 |
256 | animationItem.isPaused = true;
257 |
258 | act(() => {
259 | fireEvent.scroll(document);
260 | });
261 |
262 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(1);
263 | expect(resetSegmentsSpy).toBeCalledWith(true);
264 | expect(playSpy).toHaveBeenCalledTimes(1);
265 | });
266 |
267 | test("handles `stop` type correctly", () => {
268 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
269 | const goToAndStopArgMock = 5 - animationItem.firstFrame - 1;
270 |
271 | renderUseInitInteractivity({
272 | wrapperRef: wrapperRef as any,
273 | animationItem: animationItem as any,
274 | mode: "scroll",
275 | actions: [{ visibility: [0, 1], frames: [5, 10], type: "stop" }],
276 | });
277 |
278 | act(() => {
279 | fireEvent.scroll(document);
280 | });
281 |
282 | expect(goToAndStopSpy).toHaveBeenCalledTimes(1);
283 | expect(goToAndStopSpy).toHaveBeenCalledWith(goToAndStopArgMock, true);
284 | });
285 | });
286 |
287 | describe("cursor mode", () => {
288 | beforeAll(() => {
289 | (wrapperRef.current.getBoundingClientRect as jest.Mock<
290 | any,
291 | any
292 | >).mockReturnValue({
293 | left: -1,
294 | top: -1,
295 | width: 2,
296 | height: 2,
297 | });
298 | // x = 0.5; y = 0.5
299 | });
300 |
301 | test("attaches and detaches eventListeners", () => {
302 | const wrapperAddLSpy = jest.spyOn(wrapperRef.current, "addEventListener");
303 | const wrapperRmLSpy = jest.spyOn(
304 | wrapperRef.current,
305 | "removeEventListener",
306 | );
307 |
308 | const hook = renderUseInitInteractivity({
309 | wrapperRef: wrapperRef as any,
310 | animationItem: animationItem as any,
311 | mode: "cursor",
312 | actions: [],
313 | });
314 |
315 | expect(wrapperAddLSpy).toHaveBeenCalledTimes(2);
316 | hook.unmount();
317 | expect(wrapperRmLSpy).toHaveBeenCalledTimes(2);
318 | });
319 |
320 | test("handles mouseout event correctly", () => {
321 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
322 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
323 | const resetSegmentsSpy = jest.spyOn(animationItem, "resetSegments");
324 |
325 | renderUseInitInteractivity({
326 | wrapperRef: wrapperRef as any,
327 | animationItem: animationItem as any,
328 | mode: "cursor",
329 | actions: [
330 | { position: { x: [0, 1], y: [0, 1] }, frames: [5, 10], type: "loop" },
331 | ],
332 | });
333 |
334 | act(() => {
335 | fireEvent.mouseOut(wrapperRef.current);
336 | });
337 |
338 | expect(goToAndStopSpy).toHaveBeenCalledTimes(0);
339 | expect(playSegmentsSpy).toHaveBeenCalledTimes(0);
340 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
341 | });
342 |
343 | test("do not process lottie if action does not match", () => {
344 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
345 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
346 | const resetSegmentsSpy = jest.spyOn(animationItem, "resetSegments");
347 |
348 | const commonProps = {
349 | wrapperRef: wrapperRef as any,
350 | animationItem: animationItem as any,
351 | mode: "cursor" as "cursor",
352 | };
353 |
354 | renderUseInitInteractivity({
355 | ...commonProps,
356 | actions: [
357 | { position: { x: [0, 1], y: [1, 0] }, frames: [5, 10], type: "seek" },
358 | ],
359 | });
360 |
361 | act(() => {
362 | fireEvent.mouseMove(wrapperRef.current);
363 | });
364 |
365 | expect(goToAndStopSpy).toHaveBeenCalledTimes(0);
366 | expect(playSegmentsSpy).toHaveBeenCalledTimes(0);
367 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
368 |
369 | renderUseInitInteractivity({
370 | ...commonProps,
371 | actions: [
372 | { position: { x: 0.5, y: 0.8 }, frames: [5, 10], type: "seek" },
373 | ],
374 | });
375 |
376 | act(() => {
377 | fireEvent.mouseMove(wrapperRef.current);
378 | });
379 |
380 | expect(goToAndStopSpy).toHaveBeenCalledTimes(0);
381 | expect(playSegmentsSpy).toHaveBeenCalledTimes(0);
382 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
383 |
384 | renderUseInitInteractivity({
385 | ...commonProps,
386 | actions: [
387 | { position: { x: 0.5, y: NaN }, frames: [5, 10], type: "seek" },
388 | ],
389 | });
390 |
391 | act(() => {
392 | fireEvent.mouseMove(wrapperRef.current);
393 | });
394 |
395 | expect(goToAndStopSpy).toHaveBeenCalledTimes(0);
396 | expect(playSegmentsSpy).toHaveBeenCalledTimes(0);
397 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
398 | });
399 |
400 | test("handles `seek` type correctly", () => {
401 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
402 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
403 |
404 | renderUseInitInteractivity({
405 | wrapperRef: wrapperRef as any,
406 | animationItem: animationItem as any,
407 | mode: "cursor",
408 | actions: [
409 | { position: { x: [0, 1], y: [0, 1] }, frames: [5, 10], type: "seek" },
410 | ],
411 | });
412 |
413 | act(() => {
414 | fireEvent.mouseMove(wrapperRef.current);
415 | });
416 |
417 | expect(goToAndStopSpy).toHaveBeenCalledTimes(1);
418 | expect(goToAndStopSpy).toHaveBeenCalledWith(3, true);
419 |
420 | expect(playSegmentsSpy).toHaveBeenCalledTimes(1);
421 | expect(playSegmentsSpy).toHaveBeenCalledWith([5, 10], true);
422 | });
423 |
424 | test("handles `loop` type correctly", () => {
425 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
426 |
427 | renderUseInitInteractivity({
428 | wrapperRef: wrapperRef as any,
429 | animationItem: animationItem as any,
430 | mode: "cursor",
431 | actions: [
432 | { position: { x: [0, 1], y: [0, 1] }, frames: [5, 10], type: "loop" },
433 | ],
434 | });
435 |
436 | act(() => {
437 | fireEvent.mouseMove(wrapperRef.current);
438 | });
439 |
440 | expect(playSegmentsSpy).toHaveBeenCalledTimes(1);
441 | expect(playSegmentsSpy).toHaveBeenCalledWith([5, 10], true);
442 | });
443 |
444 | test("handles `play` type correctly", () => {
445 | const resetSegmentsSpy = jest.spyOn(animationItem, "resetSegments");
446 | const playSegmentsSpy = jest.spyOn(animationItem, "playSegments");
447 |
448 | renderUseInitInteractivity({
449 | wrapperRef: wrapperRef as any,
450 | animationItem: animationItem as any,
451 | mode: "cursor",
452 | actions: [
453 | { position: { x: [0, 1], y: [0, 1] }, frames: [5, 10], type: "play" },
454 | ],
455 | });
456 |
457 | act(() => {
458 | fireEvent.mouseMove(wrapperRef.current);
459 | });
460 |
461 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(0);
462 | expect(playSegmentsSpy).toHaveBeenCalledTimes(1);
463 |
464 | animationItem.isPaused = true;
465 |
466 | act(() => {
467 | fireEvent.mouseMove(wrapperRef.current);
468 | });
469 |
470 | expect(resetSegmentsSpy).toHaveBeenCalledTimes(1);
471 | expect(resetSegmentsSpy).toHaveBeenCalledWith(false);
472 |
473 | expect(playSegmentsSpy).toHaveBeenCalledTimes(2);
474 | expect(playSegmentsSpy).toHaveBeenCalledWith([5, 10]);
475 | });
476 |
477 | test("handles `stop` type correctly", () => {
478 | const goToAndStopSpy = jest.spyOn(animationItem, "goToAndStop");
479 |
480 | renderUseInitInteractivity({
481 | wrapperRef: wrapperRef as any,
482 | animationItem: animationItem as any,
483 | mode: "cursor",
484 | actions: [
485 | { position: { x: [0, 1], y: [0, 1] }, frames: [5, 10], type: "stop" },
486 | ],
487 | });
488 |
489 | act(() => {
490 | fireEvent.mouseMove(wrapperRef.current);
491 | });
492 |
493 | expect(goToAndStopSpy).toHaveBeenCalledTimes(1);
494 | expect(goToAndStopSpy).toHaveBeenCalledWith(5, true);
495 | });
496 | });
497 |
498 | describe("helpers", () => {
499 | test("getContainerVisbility does correct calculations", () => {
500 | const values = {
501 | top: 5,
502 | height: -10,
503 | innerHeight: 15,
504 | result: 2,
505 | };
506 |
507 | const wrapper = wrapperRef.current;
508 |
509 | (wrapper.getBoundingClientRect as jest.Mock
).mockReturnValue({
510 | top: values.top,
511 | height: values.height,
512 | });
513 | window = Object.assign(window, { innerHeight: values.innerHeight });
514 |
515 | const result = getContainerVisibility(wrapper as any);
516 |
517 | expect(wrapper.getBoundingClientRect).toHaveBeenCalledTimes(1);
518 | expect(result).toEqual(values.result);
519 | });
520 |
521 | test("getContainerCursorPosition does correct calculations", () => {
522 | const values = {
523 | left: 5,
524 | top: 5,
525 | width: 2,
526 | height: 2,
527 | cursorX: 15,
528 | cursorY: 15,
529 | result: { x: 5, y: 5 },
530 | };
531 |
532 | const wrapper = wrapperRef.current;
533 |
534 | (wrapper.getBoundingClientRect as jest.Mock).mockReturnValue({
535 | top: values.top,
536 | left: values.left,
537 | width: values.width,
538 | height: values.height,
539 | });
540 |
541 | const result = getContainerCursorPosition(
542 | wrapper as any,
543 | values.cursorX,
544 | values.cursorY,
545 | );
546 |
547 | expect(wrapper.getBoundingClientRect).toHaveBeenCalledTimes(1);
548 | expect(result).toEqual(values.result);
549 | });
550 | });
551 | });
552 |
--------------------------------------------------------------------------------
/src/components/Lottie.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import useLottie from "../hooks/useLottie";
3 | import useLottieInteractivity from "../hooks/useLottieInteractivity";
4 | import { LottieComponentProps } from "../types";
5 |
6 | const Lottie = (props: LottieComponentProps) => {
7 | const { style, interactivity, ...lottieProps } = props;
8 |
9 | /**
10 | * Initialize the 'useLottie' hook
11 | */
12 | const {
13 | View,
14 | play,
15 | stop,
16 | pause,
17 | setSpeed,
18 | goToAndStop,
19 | goToAndPlay,
20 | setDirection,
21 | playSegments,
22 | setSubframe,
23 | getDuration,
24 | destroy,
25 | animationContainerRef,
26 | animationLoaded,
27 | animationItem,
28 | } = useLottie(lottieProps, style);
29 |
30 | /**
31 | * Make the hook variables/methods available through the provided 'lottieRef'
32 | */
33 | useEffect(() => {
34 | if (props.lottieRef) {
35 | props.lottieRef.current = {
36 | play,
37 | stop,
38 | pause,
39 | setSpeed,
40 | goToAndPlay,
41 | goToAndStop,
42 | setDirection,
43 | playSegments,
44 | setSubframe,
45 | getDuration,
46 | destroy,
47 | animationContainerRef,
48 | animationLoaded,
49 | animationItem,
50 | };
51 | }
52 | // eslint-disable-next-line react-hooks/exhaustive-deps
53 | }, [props.lottieRef?.current]);
54 |
55 | return useLottieInteractivity({
56 | lottieObj: {
57 | View,
58 | play,
59 | stop,
60 | pause,
61 | setSpeed,
62 | goToAndStop,
63 | goToAndPlay,
64 | setDirection,
65 | playSegments,
66 | setSubframe,
67 | getDuration,
68 | destroy,
69 | animationContainerRef,
70 | animationLoaded,
71 | animationItem,
72 | },
73 | actions: interactivity?.actions ?? [],
74 | mode: interactivity?.mode ?? "scroll",
75 | });
76 | };
77 |
78 | export default Lottie;
79 |
--------------------------------------------------------------------------------
/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Ensure the additional Jest matchers are available for all test files
3 | */
4 | import "@testing-library/jest-dom/extend-expect";
5 |
--------------------------------------------------------------------------------
/src/hooks/useLottie.tsx:
--------------------------------------------------------------------------------
1 | import lottie, {
2 | AnimationConfigWithData,
3 | AnimationItem,
4 | AnimationDirection,
5 | AnimationSegment,
6 | RendererType,
7 | } from "lottie-web";
8 | import React, {
9 | CSSProperties,
10 | useEffect,
11 | useRef,
12 | ReactElement,
13 | useState,
14 | } from "react";
15 | import {
16 | Listener,
17 | LottieOptions,
18 | LottieRefCurrentProps,
19 | PartialListener,
20 | } from "../types";
21 |
22 | const useLottie = (
23 | props: LottieOptions,
24 | style?: CSSProperties,
25 | ): { View: ReactElement } & LottieRefCurrentProps => {
26 | const {
27 | animationData,
28 | loop,
29 | autoplay,
30 | initialSegment,
31 |
32 | onComplete,
33 | onLoopComplete,
34 | onEnterFrame,
35 | onSegmentStart,
36 | onConfigReady,
37 | onDataReady,
38 | onDataFailed,
39 | onLoadedImages,
40 | onDOMLoaded,
41 | onDestroy,
42 |
43 | // Specified here to take them out from the 'rest'
44 | lottieRef,
45 | renderer,
46 | name,
47 | assetsPath,
48 | rendererSettings,
49 |
50 | // TODO: find a better way to extract the html props to avoid specifying
51 | // all the props that we want to exclude (as you can see above)
52 | ...rest
53 | } = props;
54 |
55 | const [animationLoaded, setAnimationLoaded] = useState(false);
56 | const animationInstanceRef = useRef();
57 | const animationContainer = useRef(null);
58 |
59 | /*
60 | ======================================
61 | INTERACTION METHODS
62 | ======================================
63 | */
64 |
65 | /**
66 | * Play
67 | */
68 | const play = (): void => {
69 | animationInstanceRef.current?.play();
70 | };
71 |
72 | /**
73 | * Stop
74 | */
75 | const stop = (): void => {
76 | animationInstanceRef.current?.stop();
77 | };
78 |
79 | /**
80 | * Pause
81 | */
82 | const pause = (): void => {
83 | animationInstanceRef.current?.pause();
84 | };
85 |
86 | /**
87 | * Set animation speed
88 | * @param speed
89 | */
90 | const setSpeed = (speed: number): void => {
91 | animationInstanceRef.current?.setSpeed(speed);
92 | };
93 |
94 | /**
95 | * Got to frame and play
96 | * @param value
97 | * @param isFrame
98 | */
99 | const goToAndPlay = (value: number, isFrame?: boolean): void => {
100 | animationInstanceRef.current?.goToAndPlay(value, isFrame);
101 | };
102 |
103 | /**
104 | * Got to frame and stop
105 | * @param value
106 | * @param isFrame
107 | */
108 | const goToAndStop = (value: number, isFrame?: boolean): void => {
109 | animationInstanceRef.current?.goToAndStop(value, isFrame);
110 | };
111 |
112 | /**
113 | * Set animation direction
114 | * @param direction
115 | */
116 | const setDirection = (direction: AnimationDirection): void => {
117 | animationInstanceRef.current?.setDirection(direction);
118 | };
119 |
120 | /**
121 | * Play animation segments
122 | * @param segments
123 | * @param forceFlag
124 | */
125 | const playSegments = (
126 | segments: AnimationSegment | AnimationSegment[],
127 | forceFlag?: boolean,
128 | ): void => {
129 | animationInstanceRef.current?.playSegments(segments, forceFlag);
130 | };
131 |
132 | /**
133 | * Set sub frames
134 | * @param useSubFrames
135 | */
136 | const setSubframe = (useSubFrames: boolean): void => {
137 | animationInstanceRef.current?.setSubframe(useSubFrames);
138 | };
139 |
140 | /**
141 | * Get animation duration
142 | * @param inFrames
143 | */
144 | const getDuration = (inFrames?: boolean): number | undefined =>
145 | animationInstanceRef.current?.getDuration(inFrames);
146 |
147 | /**
148 | * Destroy animation
149 | */
150 | const destroy = (): void => {
151 | animationInstanceRef.current?.destroy();
152 |
153 | // Removing the reference to the animation so separate cleanups are skipped.
154 | // Without it the internal `lottie-react` instance throws exceptions as it already cleared itself on destroy.
155 | animationInstanceRef.current = undefined;
156 | };
157 |
158 | /*
159 | ======================================
160 | LOTTIE
161 | ======================================
162 | */
163 |
164 | /**
165 | * Load a new animation, and if it's the case, destroy the previous one
166 | * @param {Object} forcedConfigs
167 | */
168 | const loadAnimation = (forcedConfigs = {}) => {
169 | // Return if the container ref is null
170 | if (!animationContainer.current) {
171 | return;
172 | }
173 |
174 | // Destroy any previous instance
175 | animationInstanceRef.current?.destroy();
176 |
177 | // Build the animation configuration
178 | const config: AnimationConfigWithData = {
179 | ...props,
180 | ...forcedConfigs,
181 | container: animationContainer.current,
182 | };
183 |
184 | // Save the animation instance
185 | animationInstanceRef.current = lottie.loadAnimation(config);
186 |
187 | setAnimationLoaded(!!animationInstanceRef.current);
188 |
189 | // Return a function that will clean up
190 | return () => {
191 | animationInstanceRef.current?.destroy();
192 | animationInstanceRef.current = undefined;
193 | };
194 | };
195 |
196 | /**
197 | * (Re)Initialize when animation data changed
198 | */
199 | useEffect(() => {
200 | const onUnmount = loadAnimation();
201 |
202 | // Clean up on unmount
203 | return () => onUnmount?.();
204 | // eslint-disable-next-line react-hooks/exhaustive-deps
205 | }, [animationData, loop]);
206 |
207 | // Update the autoplay state
208 | useEffect(() => {
209 | if (!animationInstanceRef.current) {
210 | return;
211 | }
212 |
213 | animationInstanceRef.current.autoplay = !!autoplay;
214 | }, [autoplay]);
215 |
216 | // Update the initial segment state
217 | useEffect(() => {
218 | if (!animationInstanceRef.current) {
219 | return;
220 | }
221 |
222 | // When null should reset to default animation length
223 | if (!initialSegment) {
224 | animationInstanceRef.current.resetSegments(true);
225 | return;
226 | }
227 |
228 | // If it's not a valid segment, do nothing
229 | if (!Array.isArray(initialSegment) || !initialSegment.length) {
230 | return;
231 | }
232 |
233 | // If the current position it's not in the new segment
234 | // set the current position to start
235 | if (
236 | animationInstanceRef.current.currentRawFrame < initialSegment[0] ||
237 | animationInstanceRef.current.currentRawFrame > initialSegment[1]
238 | ) {
239 | animationInstanceRef.current.currentRawFrame = initialSegment[0];
240 | }
241 |
242 | // Update the segment
243 | animationInstanceRef.current.setSegment(
244 | initialSegment[0],
245 | initialSegment[1],
246 | );
247 | }, [initialSegment]);
248 |
249 | /*
250 | ======================================
251 | EVENTS
252 | ======================================
253 | */
254 |
255 | /**
256 | * Reinitialize listener on change
257 | */
258 | useEffect(() => {
259 | const partialListeners: PartialListener[] = [
260 | { name: "complete", handler: onComplete },
261 | { name: "loopComplete", handler: onLoopComplete },
262 | { name: "enterFrame", handler: onEnterFrame },
263 | { name: "segmentStart", handler: onSegmentStart },
264 | { name: "config_ready", handler: onConfigReady },
265 | { name: "data_ready", handler: onDataReady },
266 | { name: "data_failed", handler: onDataFailed },
267 | { name: "loaded_images", handler: onLoadedImages },
268 | { name: "DOMLoaded", handler: onDOMLoaded },
269 | { name: "destroy", handler: onDestroy },
270 | ];
271 |
272 | const listeners = partialListeners.filter(
273 | (listener: PartialListener): listener is Listener =>
274 | listener.handler != null,
275 | );
276 |
277 | if (!listeners.length) {
278 | return;
279 | }
280 |
281 | const deregisterList = listeners.map(
282 | /**
283 | * Handle the process of adding an event listener
284 | * @param {Listener} listener
285 | * @return {Function} Function that deregister the listener
286 | */
287 | (listener) => {
288 | animationInstanceRef.current?.addEventListener(
289 | listener.name,
290 | listener.handler,
291 | );
292 |
293 | // Return a function to deregister this listener
294 | return () => {
295 | animationInstanceRef.current?.removeEventListener(
296 | listener.name,
297 | listener.handler,
298 | );
299 | };
300 | },
301 | );
302 |
303 | // Deregister listeners on unmount
304 | return () => {
305 | deregisterList.forEach((deregister) => deregister());
306 | };
307 | }, [
308 | onComplete,
309 | onLoopComplete,
310 | onEnterFrame,
311 | onSegmentStart,
312 | onConfigReady,
313 | onDataReady,
314 | onDataFailed,
315 | onLoadedImages,
316 | onDOMLoaded,
317 | onDestroy,
318 | ]);
319 |
320 | /**
321 | * Build the animation view
322 | */
323 | const View = ;
324 |
325 | return {
326 | View,
327 | play,
328 | stop,
329 | pause,
330 | setSpeed,
331 | goToAndStop,
332 | goToAndPlay,
333 | setDirection,
334 | playSegments,
335 | setSubframe,
336 | getDuration,
337 | destroy,
338 | animationContainerRef: animationContainer,
339 | animationLoaded,
340 | animationItem: animationInstanceRef.current,
341 | };
342 | };
343 |
344 | export default useLottie;
345 |
--------------------------------------------------------------------------------
/src/hooks/useLottieInteractivity.tsx:
--------------------------------------------------------------------------------
1 | import { AnimationSegment } from "lottie-web";
2 | import React, { useEffect, ReactElement } from "react";
3 | import { InteractivityProps } from "../types";
4 |
5 | // helpers
6 | export function getContainerVisibility(container: Element): number {
7 | const { top, height } = container.getBoundingClientRect();
8 |
9 | const current = window.innerHeight - top;
10 | const max = window.innerHeight + height;
11 | return current / max;
12 | }
13 |
14 | export function getContainerCursorPosition(
15 | container: Element,
16 | cursorX: number,
17 | cursorY: number,
18 | ): { x: number; y: number } {
19 | const { top, left, width, height } = container.getBoundingClientRect();
20 |
21 | const x = (cursorX - left) / width;
22 | const y = (cursorY - top) / height;
23 |
24 | return { x, y };
25 | }
26 |
27 | export type InitInteractivity = {
28 | wrapperRef: React.RefObject;
29 | animationItem: InteractivityProps["lottieObj"]["animationItem"];
30 | actions: InteractivityProps["actions"];
31 | mode: InteractivityProps["mode"];
32 | };
33 |
34 | export const useInitInteractivity = ({
35 | wrapperRef,
36 | animationItem,
37 | mode,
38 | actions,
39 | }: InitInteractivity) => {
40 | useEffect(() => {
41 | const wrapper = wrapperRef.current;
42 |
43 | if (!wrapper || !animationItem || !actions.length) {
44 | return;
45 | }
46 |
47 | animationItem.stop();
48 |
49 | const scrollModeHandler = () => {
50 | let assignedSegment: number[] | null = null;
51 |
52 | const scrollHandler = () => {
53 | const currentPercent = getContainerVisibility(wrapper);
54 | // Find the first action that satisfies the current position conditions
55 | const action = actions.find(
56 | ({ visibility }) =>
57 | visibility &&
58 | currentPercent >= visibility[0] &&
59 | currentPercent <= visibility[1],
60 | );
61 |
62 | // Skip if no matching action was found!
63 | if (!action) {
64 | return;
65 | }
66 |
67 | if (
68 | action.type === "seek" &&
69 | action.visibility &&
70 | action.frames.length === 2
71 | ) {
72 | // Seek: Go to a frame based on player scroll position action
73 | const frameToGo =
74 | action.frames[0] +
75 | Math.ceil(
76 | ((currentPercent - action.visibility[0]) /
77 | (action.visibility[1] - action.visibility[0])) *
78 | action.frames[1],
79 | );
80 |
81 | //! goToAndStop must be relative to the start of the current segment
82 | animationItem.goToAndStop(
83 | frameToGo - animationItem.firstFrame - 1,
84 | true,
85 | );
86 | }
87 |
88 | if (action.type === "loop") {
89 | // Loop: Loop a given frames
90 | if (assignedSegment === null) {
91 | // if not playing any segments currently. play those segments and save to state
92 | animationItem.playSegments(action.frames as AnimationSegment, true);
93 | assignedSegment = action.frames;
94 | } else {
95 | // if playing any segments currently.
96 | //check if segments in state are equal to the frames selected by action
97 | if (assignedSegment !== action.frames) {
98 | // if they are not equal. new segments are to be loaded
99 | animationItem.playSegments(
100 | action.frames as AnimationSegment,
101 | true,
102 | );
103 | assignedSegment = action.frames;
104 | } else if (animationItem.isPaused) {
105 | // if they are equal the play method must be called only if lottie is paused
106 | animationItem.playSegments(
107 | action.frames as AnimationSegment,
108 | true,
109 | );
110 | assignedSegment = action.frames;
111 | }
112 | }
113 | }
114 |
115 | if (action.type === "play" && animationItem.isPaused) {
116 | // Play: Reset segments and continue playing full animation from current position
117 | animationItem.resetSegments(true);
118 | animationItem.play();
119 | }
120 |
121 | if (action.type === "stop") {
122 | // Stop: Stop playback
123 | animationItem.goToAndStop(
124 | action.frames[0] - animationItem.firstFrame - 1,
125 | true,
126 | );
127 | }
128 | };
129 |
130 | document.addEventListener("scroll", scrollHandler);
131 |
132 | return () => {
133 | document.removeEventListener("scroll", scrollHandler);
134 | };
135 | };
136 |
137 | const cursorModeHandler = () => {
138 | const handleCursor = (_x: number, _y: number) => {
139 | let x = _x;
140 | let y = _y;
141 |
142 | // Resolve cursor position if cursor is inside container
143 | if (x !== -1 && y !== -1) {
144 | // Get container cursor position
145 | const pos = getContainerCursorPosition(wrapper, x, y);
146 |
147 | // Use the resolved position
148 | x = pos.x;
149 | y = pos.y;
150 | }
151 |
152 | // Find the first action that satisfies the current position conditions
153 | const action = actions.find(({ position }) => {
154 | if (
155 | position &&
156 | Array.isArray(position.x) &&
157 | Array.isArray(position.y)
158 | ) {
159 | return (
160 | x >= position.x[0] &&
161 | x <= position.x[1] &&
162 | y >= position.y[0] &&
163 | y <= position.y[1]
164 | );
165 | }
166 |
167 | if (
168 | position &&
169 | !Number.isNaN(position.x) &&
170 | !Number.isNaN(position.y)
171 | ) {
172 | return x === position.x && y === position.y;
173 | }
174 |
175 | return false;
176 | });
177 |
178 | // Skip if no matching action was found!
179 | if (!action) {
180 | return;
181 | }
182 |
183 | // Process action types:
184 | if (
185 | action.type === "seek" &&
186 | action.position &&
187 | Array.isArray(action.position.x) &&
188 | Array.isArray(action.position.y) &&
189 | action.frames.length === 2
190 | ) {
191 | // Seek: Go to a frame based on player scroll position action
192 | const xPercent =
193 | (x - action.position.x[0]) /
194 | (action.position.x[1] - action.position.x[0]);
195 | const yPercent =
196 | (y - action.position.y[0]) /
197 | (action.position.y[1] - action.position.y[0]);
198 |
199 | animationItem.playSegments(action.frames as AnimationSegment, true);
200 | animationItem.goToAndStop(
201 | Math.ceil(
202 | ((xPercent + yPercent) / 2) *
203 | (action.frames[1] - action.frames[0]),
204 | ),
205 | true,
206 | );
207 | }
208 |
209 | if (action.type === "loop") {
210 | animationItem.playSegments(action.frames as AnimationSegment, true);
211 | }
212 |
213 | if (action.type === "play") {
214 | // Play: Reset segments and continue playing full animation from current position
215 | if (animationItem.isPaused) {
216 | animationItem.resetSegments(false);
217 | }
218 | animationItem.playSegments(action.frames as AnimationSegment);
219 | }
220 |
221 | if (action.type === "stop") {
222 | animationItem.goToAndStop(action.frames[0], true);
223 | }
224 | };
225 |
226 | const mouseMoveHandler = (ev: MouseEvent) => {
227 | handleCursor(ev.clientX, ev.clientY);
228 | };
229 |
230 | const mouseOutHandler = () => {
231 | handleCursor(-1, -1);
232 | };
233 |
234 | wrapper.addEventListener("mousemove", mouseMoveHandler);
235 | wrapper.addEventListener("mouseout", mouseOutHandler);
236 |
237 | return () => {
238 | wrapper.removeEventListener("mousemove", mouseMoveHandler);
239 | wrapper.removeEventListener("mouseout", mouseOutHandler);
240 | };
241 | };
242 |
243 | switch (mode) {
244 | case "scroll":
245 | return scrollModeHandler();
246 | case "cursor":
247 | return cursorModeHandler();
248 | }
249 | // eslint-disable-next-line react-hooks/exhaustive-deps
250 | }, [mode, animationItem]);
251 | };
252 |
253 | const useLottieInteractivity = ({
254 | actions,
255 | mode,
256 | lottieObj,
257 | }: InteractivityProps): ReactElement => {
258 | const { animationItem, View, animationContainerRef } = lottieObj;
259 |
260 | useInitInteractivity({
261 | actions,
262 | animationItem,
263 | mode,
264 | wrapperRef: animationContainerRef,
265 | });
266 |
267 | return View;
268 | };
269 |
270 | export default useLottieInteractivity;
271 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import LottiePlayer from "lottie-web";
2 | import Lottie from "./components/Lottie";
3 | import useLottie from "./hooks/useLottie";
4 | import useLottieInteractivity from "./hooks/useLottieInteractivity";
5 |
6 | export { LottiePlayer, useLottie, useLottieInteractivity };
7 |
8 | export default Lottie;
9 | export * from "./types";
10 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnimationConfigWithData,
3 | AnimationDirection,
4 | AnimationEventCallback,
5 | AnimationEventName,
6 | AnimationEvents,
7 | AnimationItem,
8 | AnimationSegment,
9 | RendererType,
10 | } from "lottie-web";
11 | import React, { MutableRefObject, ReactElement, RefObject } from "react";
12 |
13 | export type LottieRefCurrentProps = {
14 | play: () => void;
15 | stop: () => void;
16 | pause: () => void;
17 | setSpeed: (speed: number) => void;
18 | goToAndStop: (value: number, isFrame?: boolean) => void;
19 | goToAndPlay: (value: number, isFrame?: boolean) => void;
20 | setDirection: (direction: AnimationDirection) => void;
21 | playSegments: (
22 | segments: AnimationSegment | AnimationSegment[],
23 | forceFlag?: boolean,
24 | ) => void;
25 | setSubframe: (useSubFrames: boolean) => void;
26 | getDuration: (inFrames?: boolean) => number | undefined;
27 | destroy: () => void;
28 | animationContainerRef: RefObject;
29 | animationLoaded: boolean;
30 | animationItem: AnimationItem | undefined;
31 | };
32 |
33 | export type LottieRef = MutableRefObject;
34 |
35 | export type LottieOptions = Omit<
36 | AnimationConfigWithData,
37 | "container" | "animationData"
38 | > & {
39 | animationData: unknown;
40 | lottieRef?: LottieRef;
41 | onComplete?: AnimationEventCallback<
42 | AnimationEvents[AnimationEventName]
43 | > | null;
44 | onLoopComplete?: AnimationEventCallback<
45 | AnimationEvents[AnimationEventName]
46 | > | null;
47 | onEnterFrame?: AnimationEventCallback<
48 | AnimationEvents[AnimationEventName]
49 | > | null;
50 | onSegmentStart?: AnimationEventCallback<
51 | AnimationEvents[AnimationEventName]
52 | > | null;
53 | onConfigReady?: AnimationEventCallback<
54 | AnimationEvents[AnimationEventName]
55 | > | null;
56 | onDataReady?: AnimationEventCallback<
57 | AnimationEvents[AnimationEventName]
58 | > | null;
59 | onDataFailed?: AnimationEventCallback<
60 | AnimationEvents[AnimationEventName]
61 | > | null;
62 | onLoadedImages?: AnimationEventCallback<
63 | AnimationEvents[AnimationEventName]
64 | > | null;
65 | onDOMLoaded?: AnimationEventCallback<
66 | AnimationEvents[AnimationEventName]
67 | > | null;
68 | onDestroy?: AnimationEventCallback<
69 | AnimationEvents[AnimationEventName]
70 | > | null;
71 | } & Omit, "loop">;
72 |
73 | export type PartialLottieOptions = Omit & {
74 | animationData?: LottieOptions["animationData"];
75 | };
76 |
77 | // Interactivity
78 | export type Axis = "x" | "y";
79 | export type Position = { [key in Axis]: number | [number, number] };
80 |
81 | export type Action = {
82 | type: "seek" | "play" | "stop" | "loop";
83 |
84 | frames: [number] | [number, number];
85 | visibility?: [number, number];
86 | position?: Position;
87 | };
88 |
89 | export type InteractivityProps = {
90 | lottieObj: { View: ReactElement } & LottieRefCurrentProps;
91 | actions: Action[];
92 | mode: "scroll" | "cursor";
93 | };
94 |
95 | export type LottieComponentProps = LottieOptions & {
96 | interactivity?: Omit;
97 | };
98 |
99 | export type PartialLottieComponentProps = Omit<
100 | LottieComponentProps,
101 | "animationData"
102 | > & {
103 | animationData?: LottieOptions["animationData"];
104 | };
105 |
106 | export type Listener = {
107 | name: AnimationEventName;
108 | handler: AnimationEventCallback;
109 | };
110 | export type PartialListener = Omit & {
111 | handler?: Listener["handler"] | null;
112 | };
113 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "./src/**/*"
5 | ],
6 | "exclude": [
7 | "./src/__tests__"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "ES2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
5 | "allowJs": true /* Allow javascript files to be compiled. */,
6 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
7 | "declaration": true /* Generates corresponding '.d.ts' file. */,
8 | "outDir": "./compiled" /* Redirect output structure to the directory. */,
9 |
10 | /* Strict Type-Checking Options */
11 | "strict": true /* Enable all strict type-checking options. */,
12 |
13 | /* Module Resolution Options */
14 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
15 | "allowSyntheticDefaultImports": true,
16 | "esModuleInterop": true /* Enabled for compatibility with Jest (and Babel) */,
17 | "skipLibCheck": true,
18 |
19 | /* Advanced Options */
20 | "resolveJsonModule": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
21 | },
22 | "include": ["./src/**/*"],
23 | "exclude": ["./src/__tests__"]
24 | }
25 |
--------------------------------------------------------------------------------