├── .nvmrc
├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── custom.md
│ ├── feature_request.md
│ └── bug_report.md
├── .prettierrc.json
├── .babelrc
├── src
├── globals.d.ts
├── index.tsx
├── components
│ └── Lottie.ts
├── __tests__
│ ├── Lottie.test.tsx
│ ├── useLottie.test.tsx
│ └── useLottieInteractivity.test.tsx
├── types.ts
└── hooks
│ ├── useLottieInteractivity.tsx
│ └── useLottie.tsx
├── tsconfig.eslint.json
├── CONTRIBUTING.md
├── .gitignore
├── doczrc.js
├── .travis.yml
├── docs
├── hooks
│ ├── useLottieInteractivity
│ │ ├── UseInteractivityBasic.js
│ │ ├── CursorDiagonalSync.js
│ │ ├── ScrollWithOffset.js
│ │ ├── CursorHorizontalSync.js
│ │ ├── PlaySegmentsOnHover.js
│ │ ├── ScrollWithOffsetAndLoop.js
│ │ └── README.mdx
│ └── useLottie
│ │ ├── UseLottieExamples.js
│ │ └── README.mdx
├── components
│ └── Lottie
│ │ ├── LottieWithInteractivity.js
│ │ ├── LottieExamples.js
│ │ └── README.mdx
└── assets
│ ├── likeButton.json
│ └── hamster.json
├── tsconfig.json
├── LICENSE
├── CODE_OF_CONDUCT.md
├── package.json
├── rollup.config.js
├── .eslintrc.js
├── README.md
└── jest.config.ts
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.13.0
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [Gamote]
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "./src/**/*"
5 | ],
6 | "exclude": [
7 | "./src/__tests__"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTING
2 |
3 | Let us know if you have any suggestions or contributions. This package has the mission to help developers, so if you have any features that you think we should prioritize, reach out to us.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Coverage directory used by tools like istanbul
2 | coverage
3 | .coveralls.yml
4 |
5 | # Dependency directories
6 | node_modules
7 |
8 | # IDE directories
9 | .idea
10 |
11 | /compiled
12 | /.docz
13 | /docs-dist
14 | build
15 |
16 | .DS_Store
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - lts/*
4 |
5 | cache:
6 | directories:
7 | - node_modules
8 |
9 | #script:
10 | # - yarn test
11 | #
12 | #after_success:
13 | # - yarn coverage
14 |
15 | before_deploy:
16 | - "yarn docz:build"
17 |
18 | deploy:
19 | provider: pages
20 | skip_cleanup: true
21 | github_token: $GITHUB_TOKEN
22 | local_dir: docs-dist
23 | on:
24 | branch: main
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/components/Lottie/LottieWithInteractivity.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Lottie from "../../../src/components/Lottie";
3 | import robotAnimation from "../../assets/robotAnimation.json";
4 |
5 | const style = {
6 | height: 500,
7 | };
8 |
9 | const interactivity = {
10 | mode: "scroll",
11 | actions: [
12 | {
13 | visibility: [0, 0.2],
14 | type: "stop",
15 | frames: [0],
16 | },
17 | {
18 | visibility: [0.2, 0.45],
19 | type: "seek",
20 | frames: [0, 45],
21 | },
22 | {
23 | visibility: [0.45, 1.0],
24 | type: "loop",
25 | frames: [45, 60],
26 | },
27 | ],
28 | };
29 |
30 | const Example = () => {
31 | return (
32 |
37 | );
38 | };
39 |
40 | export default Example;
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/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/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright David Gamote and other contributors.
4 |
5 | This software consists of voluntary contributions made by many
6 | individuals. For exact contribution history, see the revision history
7 | available on GitHub.
8 |
9 | The following license applies to all parts of this software except as
10 | documented below:
11 |
12 | ====
13 |
14 | Permission is hereby granted, free of charge, to any person obtaining
15 | a copy of this software and associated documentation files (the
16 | "Software"), to deal in the Software without restriction, including
17 | without limitation the rights to use, copy, modify, merge, publish,
18 | distribute, sublicense, and/or sell copies of the Software, and to
19 | permit persons to whom the Software is furnished to do so, subject to
20 | the following conditions:
21 |
22 | The above copyright notice and this permission notice shall be
23 | included in all copies or substantial portions of the Software.
24 |
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 |
33 | ====
34 |
35 | Copyright and related rights for sample code are waived via CC0. Sample
36 | code is defined as all source code displayed within the prose of the
37 | documentation.
38 |
39 | CC0: http://creativecommons.org/publicdomain/zero/1.0/
40 |
41 | ====
42 |
43 | Files located in the node_modules and vendor directories are externally
44 | maintained libraries used by this software which have their own
45 | licenses; we recommend you read them, as their terms may differ from the
46 | terms above.
47 |
--------------------------------------------------------------------------------
/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/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contact@gamote.ro. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/components/Lottie/LottieExamples.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import Lottie from "../../../src/components/Lottie";
3 | import groovyWalkAnimation from "../../assets/groovyWalk.json";
4 | // import likeButtonAnimation from "../../assets/likeButton.json";
5 |
6 | const styles = {
7 | animation: {
8 | height: 300,
9 | border: 3,
10 | borderStyle: "solid",
11 | borderRadius: 7,
12 | },
13 | };
14 |
15 | const LottieExamples = () => {
16 | const ref = useRef();
17 | const [animationData, setAnimationData] = useState(groovyWalkAnimation);
18 | const [loop, setLoop] = useState(true);
19 | const [autoplay, setAutoplay] = useState(true);
20 | const [initialSegment, setInitialSegment] = useState(null);
21 | const [style, setStyle] = useState(styles.animation);
22 | const [show, setShow] = useState(true);
23 |
24 | useEffect(() => {
25 | // Test the unmount hooks
26 | // setTimeout(() => {
27 | // setShow(false);
28 | // }, 4000);
29 | //
30 | // setTimeout(() => {
31 | // setShow(true);
32 | // }, 8000);
33 | //
34 | // // Test ref
35 | // setTimeout(() => {
36 | // console.log("Info: ref current value", ref.current);
37 | // ref.current.pause();
38 | // }, 4000);
39 | //
40 | // Test animationData
41 | // setTimeout(() => {
42 | // console.log("Info: animationData changed in", likeButtonAnimation);
43 | // setAnimationData(likeButtonAnimation);
44 | // }, 2000);
45 | //
46 | // Test loop
47 | // setTimeout(() => {
48 | // console.log("Info: loop changed in", false);
49 | // setLoop(false);
50 | // }, 2000);
51 | // setTimeout(() => {
52 | // console.log("Info: loop changed in", true);
53 | // setLoop(true);
54 | // }, 20000);
55 | // setTimeout(() => {
56 | // console.log("Info: loop changed in", 3);
57 | // setLoop(6);
58 | // }, 3000);
59 | //
60 | // Test autoplay
61 | // setTimeout(() => {
62 | // console.log("Info: autoplay changed in", true);
63 | // setAutoplay(true);
64 | // }, 4000);
65 | //
66 | // Test initialSegment
67 | // setTimeout(() => {
68 | // console.log("Info: initialSegment changed in", [0, 10]);
69 | // setInitialSegment([0, 10]);
70 | // }, 4000);
71 | //
72 | // Test styles
73 | // setTimeout(() => {
74 | // console.log("Info: styles changed in", {
75 | // ...styles.animation,
76 | // backgroundColor: "blue",
77 | // });
78 | // setStyle({
79 | // ...styles.animation,
80 | // backgroundColor: "blue",
81 | // });
82 | // }, 4000);
83 | }, []);
84 |
85 | const animation = (
86 | {
93 | // console.log("Info: onComplete called");
94 | // }}
95 | // onLoopComplete={() => {
96 | // console.log("Info: onLoopComplete called");
97 | // }}
98 | // onEnterFrame={() => {
99 | // console.log("Info: onEnterFrame called");
100 | // }}
101 | // onSegmentStart={() => {
102 | // console.log("Info: onSegmentStart called");
103 | // }}
104 | // onConfigReady={() => {
105 | // console.log("Info: onConfigReady called");
106 | // }}
107 | // onDataReady={() => {
108 | // console.log("Info: onDataReady called");
109 | // }}
110 | // onDataFailed={() => {
111 | // console.log("Info: onDataFailed called");
112 | // }}
113 | // onLoadedImages={() => {
114 | // console.log("Info: onLoadedImages called");
115 | // }}
116 | // onDOMLoaded={() => {
117 | // console.log("Info: onDOMLoaded called");
118 | // }}
119 | // onDestroy={() => {
120 | // console.log("Info: onDestroy called");
121 | // }}
122 | style={style}
123 | />
124 | );
125 |
126 | return show ? animation : "Animation is hidden";
127 | };
128 |
129 | export default LottieExamples;
130 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const { peerDependencies } = require("./package.json");
2 |
3 | module.exports = {
4 | env: {
5 | /* (i) An environment provides predefined global variables */
6 | browser: true, // Browser global variables
7 | node: true, // Node.js global variables and Node.js scoping
8 | es2021: true, // Adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12
9 | },
10 | parserOptions: {
11 | ecmaVersion: 2021, // Allows for the parsing of modern ECMAScript features
12 | sourceType: "module", // Allow imports of code placed in ECMAScript modules
13 | ecmaFeatures: {
14 | /* (i) Which additional language features you'd like to use */
15 | jsx: true, // Enable JSX
16 | },
17 | },
18 | plugins: [
19 | /* (i) Place to define plugins, normally there is no need for this as "extends" will automatically import the plugin */
20 | ],
21 | extends: [
22 | "eslint:recommended", // Rules recommended by ESLint (eslint)
23 | "plugin:react/recommended", // React rules (eslint-plugin-react)
24 | "plugin:react-hooks/recommended", // React Hooks rules (eslint-plugin-react-hooks)
25 | "plugin:jsx-a11y/recommended", // Accessibility rules (eslint-plugin-jsx-a11y)
26 | "plugin:import/errors", // Recommended errors for import (eslint-plugin-import)
27 | "plugin:import/warnings", // Recommended warnings for import (eslint-plugin-import)
28 | "plugin:import/typescript", // Typescript support for the import rules (eslint-plugin-import)
29 | "plugin:promise/recommended", // Enforce best practices for JavaScript promises (eslint-plugin-promise)
30 | "plugin:prettier/recommended", // This will display Prettier errors as ESLint errors. (!) Make sure this is always the last configuration in the extends array. (eslint-plugin-prettier & eslint-config-prettier)
31 | ],
32 | /* (i) Apply TypeScript rules just to TypeScript files */
33 | overrides: [
34 | {
35 | files: ["*.ts", "*.tsx"],
36 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser
37 | parserOptions: {
38 | tsconfigRootDir: __dirname, // Required by `@typescript-eslint/recommended-requiring-type-checking`
39 | project: ["./tsconfig.eslint.json"], // Required by `@typescript-eslint/recommended-requiring-type-checking`
40 | },
41 | extends: [
42 | "plugin:@typescript-eslint/recommended", // TypeScript rules (@typescript-eslint/eslint-plugin)
43 | "plugin:@typescript-eslint/recommended-requiring-type-checking", // Linting with Type Information. More info: https://git.io/JEDmJ (@typescript-eslint/eslint-plugin)
44 | ],
45 | },
46 | ],
47 | globals: {
48 | Atomics: "readonly",
49 | SharedArrayBuffer: "readonly",
50 | },
51 | rules: {
52 | /* (i) Place to specify ESLint rules. Can be used to overwrite rules specified by the extended configs */
53 |
54 | // Define extensions that shouldn't be specified on import
55 | "import/extensions": [
56 | "error",
57 | "ignorePackages",
58 | {
59 | ts: "never",
60 | tsx: "never",
61 | },
62 | ],
63 |
64 | // Enforce a convention in module import order
65 | "import/order": [
66 | "error",
67 | {
68 | alphabetize: {
69 | order: "asc",
70 | },
71 | // this is the default order except for added `internal` in the middle
72 | groups: [
73 | "builtin",
74 | "external",
75 | "internal",
76 | "parent",
77 | "sibling",
78 | "index",
79 | ],
80 | "newlines-between": "never",
81 | },
82 | ],
83 |
84 | "no-console": "warn", // Warning for console logging
85 | "arrow-body-style": ["error", "as-needed"], // Disallow the use of braces around arrow function body when is not needed
86 | "prefer-arrow-callback": "error", // Produce error anywhere an arrow function can be used instead of a function expression
87 |
88 | // React rules
89 | "react/prop-types": 0, // Disable the requirement for prop types definitions, we will use TypeScript's types for component props instead
90 | "react/jsx-filename-extension": [2, { extensions: [".tsx"] }], // Allow JSX only in `.tsx` files
91 | "react/react-in-jsx-scope": 0, // `React` doesn't need to be imported in React 17
92 | "react/destructuring-assignment": 2, // Always destructure component `props`
93 |
94 | // React Hooks rules
95 | "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
96 | "react-hooks/exhaustive-deps": "warn", // Checks hook dependencies
97 |
98 | // Disable the `import/no-unresolved` rule for peer dependencies
99 | // This is useful when you develop a React library and `react` it's not present in `dependencies`
100 | // nor in `devDependencies` but it is specified in the `peerDependencies`
101 | // More info: https://github.com/import-js/eslint-plugin-import/issues/825#issuecomment-542618188
102 | "import/no-unresolved": [
103 | "error",
104 | { ignore: Object.keys(peerDependencies) },
105 | ],
106 | },
107 | settings: {
108 | react: {
109 | version: "detect", // Tells `eslint-plugin-react` to automatically detect the version of React to use
110 | },
111 | },
112 | };
113 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lottie-react
2 |
3 | [](https://www.npmjs.com/package/lottie-react) [](https://www.npmjs.com/package/lottie-react) [](https://snyk.io/test/github/Gamote/lottie-react?targetFile=package.json) [](https://coveralls.io/github/Gamote/lottie-react?branch=master) [](https://github.com/facebook/jest) [](https://github.com/Gamote/lottie-react/blob/master/LICENSE)
4 |
5 | This project is meant to give developers full control over **[Lottie](https://airbnb.design/lottie/)** instance with minimal implementation by wrapping **[lottie-web](https://github.com/airbnb/lottie-web)** in a Component or Hook that can be easily used in **React** applications.
6 |
7 | ## Installation
8 |
9 | 1. Make sure you have the peer-dependencies installed: `react` and `react-dom`.
10 |
11 | > _**Note:** This library is using React Hooks so the **minimum** version required for both **react** and **react-dom** is **v16.8.0**._
12 |
13 | 2. Install `lottie-react` using **yarn**
14 |
15 | ```shell
16 | yarn add lottie-react
17 | ```
18 |
19 | or **npm**
20 |
21 | ```shell
22 | npm i lottie-react
23 | ```
24 |
25 | ## Usage
26 |
27 | ### Using the component ([try it](https://codesandbox.io/s/lottie-react-component-2k13t))
28 |
29 | ```tsx
30 | import React from "react";
31 | import Lottie from "lottie-react";
32 | import groovyWalkAnimation from "./groovyWalk.json";
33 |
34 | const App = () => ;
35 |
36 | export default App;
37 | ```
38 |
39 | ### Using the Hook ([try it](https://codesandbox.io/s/lottie-react-hook-13nio))
40 |
41 | ```tsx
42 | import React from "react";
43 | import { useLottie } from "lottie-react";
44 | import groovyWalkAnimation from "./groovyWalk.json";
45 |
46 | const App = () => {
47 | const options = {
48 | animationData: groovyWalkAnimation,
49 | loop: true
50 | };
51 |
52 | const { View } = useLottie(options);
53 |
54 | return <>{View}>;
55 | };
56 |
57 | export default App;
58 | ```
59 |
60 | ### 📄 Documentation
61 |
62 | Checkout the [**documentation**](https://lottiereact.com) at [**https://lottiereact.com**](https://lottiereact.com) for more information and examples.
63 |
64 | ## Tests
65 |
66 | Run the tests using the `yarn test` command.
67 |
68 | ### Coverage report
69 | ```text
70 | -----------------------------|---------|----------|---------|---------|-------------------
71 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
72 | -----------------------------|---------|----------|---------|---------|-------------------
73 | All files | 100 | 100 | 100 | 100 |
74 | components | 100 | 100 | 100 | 100 |
75 | Lottie.ts | 100 | 100 | 100 | 100 |
76 | hooks | 100 | 100 | 100 | 100 |
77 | useLottie.tsx | 100 | 100 | 100 | 100 |
78 | useLottieInteractivity.tsx | 100 | 100 | 100 | 100 |
79 | -----------------------------|---------|----------|---------|---------|-------------------
80 | ```
81 |
82 | ## Contribution
83 |
84 | Any **questions** or **suggestions**? Use the [**Discussions**](https://github.com/Gamote/lottie-react/discussions) tab. Any **issues**? Don't hesitate to document it in the [**Issues**](https://github.com/Gamote/lottie-react/issues) tab, and we will do our best to investigate it and fix it. Any **solutions**? You are very welcomed to open a [**pull request**](https://github.com/Gamote/lottie-react/pulls).
85 |
86 | > 👩💻 `v3` is under development and is planning to bring a lot of features and improvements. But unfortunately, at the moment all the maintainers are super busy with work related projects. You can check out the progress under the `v3` branch. And of course, you are encouraged to contribute. :)
87 |
88 | Thank you for investing your time in contributing to our project! ✨
89 |
90 | ## Projects to check out
91 |
92 | - [lottie-web](https://github.com/airbnb/lottie-web) - Lottie implementation for Web. Our project is based on it, and you might want to check it out in order to have a better understanding on what's behind this package or what features could you expect to have in the future.
93 | - [lottie-android](https://github.com/airbnb/lottie-android) - Lottie implementation for Android
94 | - [lottie-ios](https://github.com/airbnb/lottie-ios) - Lottie implementation for iOS
95 | - [lottie-react-native](https://github.com/react-native-community/lottie-react-native) - Lottie implementation for React Native
96 | - [LottieFiles](https://lottiefiles.com/) - Are you looking for animations files? LottieFiles has a lot of them!
97 |
98 | ## License
99 |
100 | **lottie-react** is available under the [MIT license](https://github.com/Gamote/lottie-react/blob/main/LICENSE).
101 |
102 | Thanks to [David Probst Jr](https://lottiefiles.com/davidprobstjr) for the animations used in the examples.
103 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/components/Lottie/README.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Lottie
3 | menu: Components
4 | route: /components/Lottie
5 | ---
6 |
7 | import { Playground } from "docz";
8 | import LottieExamples from "./LottieExamples";
9 | import LottieWithInteractivity from "./LottieWithInteractivity.js";
10 |
11 | # Lottie
12 |
13 | ## Getting Started
14 |
15 |
16 |
17 | ```jsx
18 | import Lottie from "lottie-react";
19 | import groovyWalkAnimation from "./groovyWalk.json";
20 |
21 | const Example = () => {
22 | return ;
23 | };
24 |
25 | export default Example;
26 | ```
27 |
28 | ## Props
29 |
30 | ### `animationData`
31 |
32 | A JSON Object with the exported animation data
33 |
34 | ```yaml
35 | Type: Object
36 | Default: none
37 | Required: ☑
38 | ```
39 |
40 | ---
41 |
42 | ### `loop`
43 |
44 | 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 |
46 | ```yaml
47 | Type: Boolean | Number
48 | Default: true
49 | Required: ☐
50 | ```
51 |
52 | ---
53 |
54 | ### `autoplay`
55 |
56 | If set to true, animation will play as soon as it's loaded
57 |
58 | ```yaml
59 | Type: Boolean
60 | Default: true
61 | Required: ☐
62 | ```
63 |
64 | ---
65 |
66 | ### `initialSegment`
67 |
68 | 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
69 |
70 | **Gotcha**: The animation will re-run every time the segment array changes. Therefore, to ensure that the animation behaves as expected, you must provide a stable array.
71 |
72 | ```yaml
73 | Type: Array
74 | Default: none
75 | Required: ☐
76 | ```
77 |
78 | ---
79 |
80 | ### `onComplete`
81 |
82 | ---
83 |
84 | ### `onLoopComplete`
85 |
86 | ---
87 |
88 | ### `onEnterFrame`
89 |
90 | ---
91 |
92 | ### `onSegmentStart`
93 |
94 | ---
95 |
96 | ### `onConfigReady`
97 |
98 | ---
99 |
100 | ### `onDataReady`
101 |
102 | ---
103 |
104 | ### `onDataFailed`
105 |
106 | ---
107 |
108 | ### `onLoadedImages`
109 |
110 | ---
111 |
112 | ### `onDOMLoaded`
113 |
114 | ---
115 |
116 | ### `onDestroy`
117 |
118 | ---
119 |
120 | ### `style`
121 |
122 | Style object that applies to the animation wrapper (which is a div)
123 |
124 | ```yaml
125 | Type: Object
126 | Default: none
127 | Required: ☐
128 | ```
129 |
130 | ---
131 |
132 | ### `lottieRef`
133 |
134 | Expects a React ref object in which interaction methods will be stored
135 |
136 | ```yaml
137 | Type: React.RefObject
138 | Default: none
139 | Required: ☐
140 | ```
141 |
142 | ---
143 |
144 | ### `interactivity`
145 |
146 | Interactivity params to sync animation with either scroll or cursor.
147 |
148 | ```yaml
149 | Type: Object
150 | Default: none
151 | Required: ☐
152 | ```
153 |
154 | ---
155 |
156 | ### `React.HTMLProps`
157 |
158 | Alongside the props listed above, the `` component also extends `React.HTMLProps`. This allows you to pass props to the container `` 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/assets/likeButton.json:
--------------------------------------------------------------------------------
1 | {"v":"4.6.9","fr":30,"ip":0,"op":38,"w":100,"h":100,"nm":"heart","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"飛沫_08","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":18,"s":[100],"e":[0]},{"t":32}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[50,50,0],"e":[87,28.25,0],"to":[6.16666650772095,-3.625,0],"ti":[-4.75,2.75000834465027,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[87,28.25,0],"e":[78.5,33.5,0],"to":[4.75,-2.75000834465027,0],"ti":[1.41666662693024,-0.87499165534973,0]},{"t":32}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[100,100,100],"e":[250,250,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[250,250,100],"e":[50,50,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3]},"p":{"a":0,"k":[0,0]},"nm":"楕円形パス 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.6078431,0.9215686,0.8745098,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"楕円形 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"飛沫_07","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":18,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":22,"s":[100],"e":[0]},{"t":32}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[50,50,0],"e":[13.5,28.75,0],"to":[-6.08333349227905,-3.54166674613953,0],"ti":[5.22916650772095,2.765625,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":22,"s":[13.5,28.75,0],"e":[18.625,33.406,0],"to":[-5.22916650772095,-2.765625,0],"ti":[-0.85416668653488,-0.77604168653488,0]},{"t":32}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[100,100,100],"e":[250,250,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":22,"s":[250,250,100],"e":[50,50,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3]},"p":{"a":0,"k":[0,0]},"nm":"楕円形パス 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.9568627,0.9058824,0.5372549,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"楕円形 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"飛沫_06","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":18,"s":[100],"e":[0]},{"t":32}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[50,50,0],"e":[14,70.5,0],"to":[-6,3.41666674613953,0],"ti":[5.16666650772095,-2.64583325386047,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[14,70.5,0],"e":[19,65.875,0],"to":[-5.16666650772095,2.64583325386047,0],"ti":[-0.83333331346512,0.77083331346512,0]},{"t":32}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[100,100,100],"e":[250,250,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[250,250,100],"e":[50,50,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3]},"p":{"a":0,"k":[0,0]},"nm":"楕円形パス 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.6078431,0.9215686,0.8745098,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"楕円形 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"飛沫_05","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":18,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":22,"s":[100],"e":[0]},{"t":32}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":18,"s":[50,50,0],"e":[85.5,70.5,0],"to":[5.91666650772095,3.41666674613953,0],"ti":[-5.04166650772095,-2.64583325386047,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":22,"s":[85.5,70.5,0],"e":[80.25,65.875,0],"to":[5.04166650772095,2.64583325386047,0],"ti":[0.875,0.77083331346512,0]},{"t":32}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[100,100,100],"e":[250,250,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":22,"s":[250,250,100],"e":[50,50,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3]},"p":{"a":0,"k":[0,0]},"nm":"楕円形パス 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.9568627,0.9058824,0.5372549,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"楕円形 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"飛沫_02","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":10,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[100],"e":[0]},{"t":32}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[50,50,0],"e":[50,92,0],"to":[0,7,0],"ti":[0,-5.33333349227905,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[50,92,0],"e":[50,82,0],"to":[0,5.33333349227905,0],"ti":[0,1.66666662693024,0]},{"t":32}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":10,"s":[100,100,100],"e":[180,180,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[180,180,100],"e":[50,50,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3]},"p":{"a":0,"k":[0,0]},"nm":"楕円形パス 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.8784314,0.5882353,0.8509804,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"楕円形 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"飛沫_01","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":10,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[100],"e":[0]},{"t":32}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[50,50,0],"e":[50,8,0],"to":[0,-7,0],"ti":[0,5.33333349227905,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[50,8,0],"e":[50,18,0],"to":[0,-5.33333349227905,0],"ti":[0,-1.66666662693024,0]},{"t":32}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":10,"s":[100,100,100],"e":[180,180,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[180,180,100],"e":[50,50,100]},{"t":32}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3]},"p":{"a":0,"k":[0,0]},"nm":"楕円形パス 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.8784314,0.5882353,0.8509804,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"楕円形 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"heart_03","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[50,50,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.589,0.589,0.685],"y":[1,1,0.685]},"o":{"x":[0.183,0.183,0.173],"y":[0.041,0.041,0.173]},"n":["0p589_1_0p183_0p041","0p589_1_0p183_0p041","0p685_0p685_0p173_0p173"],"t":11,"s":[0,0,100],"e":[300,300,100]},{"i":{"x":[0.418,0.418,0.833],"y":[1.005,1.005,0.833]},"o":{"x":[0.483,0.483,0.333],"y":[0,0,0.333]},"n":["0p418_1p005_0p483_0","0p418_1p005_0p483_0","0p833_0p833_0p333_0p333"],"t":16,"s":[300,300,100],"e":[200,200,100]},{"i":{"x":[0.639,0.639,0.778],"y":[1,1,0.778]},"o":{"x":[0.412,0.412,0.157],"y":[0,0,0.157]},"n":["0p639_1_0p412_0","0p639_1_0p412_0","0p778_0p778_0p157_0p157"],"t":21,"s":[200,200,100],"e":[240,240,100]},{"i":{"x":[0.625,0.625,0.833],"y":[1,1,0.833]},"o":{"x":[0.359,0.359,0.167],"y":[0,0,0.167]},"n":["0p625_1_0p359_0","0p625_1_0p359_0","0p833_0p833_0p167_0p167"],"t":25,"s":[240,240,100],"e":[200,200,100]},{"i":{"x":[0.593,0.593,0.833],"y":[1,1,0.833]},"o":{"x":[0.395,0.395,0.167],"y":[0,0,0.167]},"n":["0p593_1_0p395_0","0p593_1_0p395_0","0p833_0p833_0p167_0p167"],"t":29,"s":[200,200,100],"e":[215,215,100]},{"i":{"x":[0.533,0.533,0.833],"y":[1,1,0.833]},"o":{"x":[0.579,0.579,0.167],"y":[0,0,0.167]},"n":["0p533_1_0p579_0","0p533_1_0p579_0","0p833_0p833_0p167_0p167"],"t":32,"s":[215,215,100],"e":[200,200,100]},{"t":35}]}},"ao":0,"ef":[{"ty":5,"nm":"グラデーション","mn":"ADBE Ramp","ix":1,"en":1,"ef":[{"ty":3,"nm":"グラデーションの開始","mn":"ADBE Ramp-0001","ix":1,"v":{"a":0,"k":[30.5,67.25]}},{"ty":2,"nm":"開始色","mn":"ADBE Ramp-0002","ix":2,"v":{"a":0,"k":[0.9764706,0.2588235,0.2431373,1]}},{"ty":3,"nm":"グラデーションの終了","mn":"ADBE Ramp-0003","ix":3,"v":{"a":0,"k":[70,33]}},{"ty":2,"nm":"終了色","mn":"ADBE Ramp-0004","ix":4,"v":{"a":0,"k":[0.9803922,0.2666667,0.5294118,1]}},{"ty":7,"nm":"グラデーションのシェイプ","mn":"ADBE Ramp-0005","ix":5,"v":{"a":0,"k":1}},{"ty":0,"nm":"グラデーションの拡散","mn":"ADBE Ramp-0006","ix":6,"v":{"a":0,"k":0}},{"ty":0,"nm":"元の画像とブレンド","mn":"ADBE Ramp-0007","ix":7,"v":{"a":0,"k":0}},{"ty":6,"nm":"","mn":"ADBE Ramp-0008","ix":8,"v":0}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.339,1.458],[1.963,-0.285],[0.72,-0.686],[1.543,0.226],[1.224,-1.331],[-0.71,-2.118],[-1.501,-0.867],[-0.207,-0.025],[-0.082,0.05],[-1.083,3.23]],"o":[[-1.226,-1.329],[-1.544,0.226],[-0.72,-0.686],[-1.958,-0.285],[-1.338,1.457],[1.085,3.233],[0.072,0.046],[0.248,0],[1.509,-0.873],[0.712,-2.117]],"v":[[8.503,-6.715],[3.569,-8.338],[0,-6.268],[-3.57,-8.338],[-8.504,-6.715],[-9.523,-0.92],[-0.521,8.451],[-0.009,8.623],[0.519,8.451],[9.521,-0.92]],"c":true}},"nm":"パス 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.9764706,0.2588235,0.2431373,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"シェイプ 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"heart_02","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[50,50,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.339,1.458],[1.963,-0.285],[0.72,-0.686],[1.543,0.226],[1.224,-1.331],[-0.71,-2.118],[-1.501,-0.867],[-0.207,-0.025],[-0.082,0.05],[-1.083,3.23]],"o":[[-1.226,-1.329],[-1.544,0.226],[-0.72,-0.686],[-1.958,-0.285],[-1.338,1.457],[1.085,3.233],[0.072,0.046],[0.248,0],[1.509,-0.873],[0.712,-2.117]],"v":[[8.503,-6.715],[3.569,-8.338],[0,-6.268],[-3.57,-8.338],[-8.504,-6.715],[-9.523,-0.92],[-0.521,8.451],[-0.009,8.623],[0.519,8.451],[9.521,-0.92]],"c":true}},"nm":"パス 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":4,"s":[0,0],"e":[300,300]},{"t":15}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":4,"s":[100],"e":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":10,"s":[70],"e":[0]},{"t":15}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"シェイプ 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"heart_01","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[50,49.893,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[0,0,100],"e":[300,300,100]},{"t":13}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.339,1.458],[1.963,-0.285],[0.72,-0.686],[1.543,0.226],[1.224,-1.331],[-0.71,-2.118],[-1.501,-0.867],[-0.207,-0.025],[-0.082,0.05],[-1.083,3.23]],"o":[[-1.226,-1.329],[-1.544,0.226],[-0.72,-0.686],[-1.958,-0.285],[-1.338,1.457],[1.085,3.233],[0.072,0.046],[0.248,0],[1.509,-0.873],[0.712,-2.117]],"v":[[8.503,-6.715],[3.569,-8.338],[0,-6.268],[-3.57,-8.338],[-8.504,-6.715],[-9.523,-0.92],[-0.521,8.451],[-0.009,8.623],[0.519,8.451],[9.521,-0.92]],"c":true}},"nm":"パス 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"線 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0.8784314,0.5882353,0.8509804,1],"e":[0.9568627,0.9058824,0.5372549,1]},{"t":17}]},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[0]},{"t":17}]},"r":1,"nm":"塗り 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"トランスフォーム"}],"nm":"シェイプ 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":150,"st":0,"bm":0,"sr":1}]}
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/assets/hamster.json:
--------------------------------------------------------------------------------
1 | {"v":"5.5.2","fr":60,"ip":0,"op":179,"w":124,"h":124,"nm":"1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"leg1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-5,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":35,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":55,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":65,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":85,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":95,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":115,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":125,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":135,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":145,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":155,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":165,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":175,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":185,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":195,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":205,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":215,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":225,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":235,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":245,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":255,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":265,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":275,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":285,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":295,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":305,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":315,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":325,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":335,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":345,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":355,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":365,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":375,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":385,"s":[-30]},{"t":395,"s":[0]}],"ix":10},"p":{"a":0,"k":[74.339,90.231,0],"ix":2},"a":{"a":0,"k":[9.643,5.297,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.554,0],[0,2.553],[0,0]],"o":[[0,0],[0,2.553],[-2.553,0],[0,0],[0,0]],"v":[[4.643,-5.172],[4.643,0.529],[-0.001,5.172],[-4.643,0.529],[-4.643,-5.172]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.670999983245,0.620000023935,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.643,10.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":-5,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"leg2 ","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-2,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":18,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":38,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":48,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":58,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":68,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":78,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":88,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":98,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":108,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":118,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":128,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":138,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":148,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":158,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":168,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":178,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":188,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":198,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":208,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":218,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":228,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":238,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":248,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":258,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":268,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":278,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":288,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":298,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":308,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":318,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":328,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":338,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":348,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":358,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":368,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":378,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":388,"s":[30]},{"t":398,"s":[0]}],"ix":10},"p":{"a":0,"k":[47.81,89.981,0],"ix":2},"a":{"a":0,"k":[9.643,5.047,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.554,0],[0,2.553],[0,0]],"o":[[0,0],[0,2.553],[-2.553,0],[0,0],[0,0]],"v":[[4.643,-5.172],[4.643,0.529],[-0.001,5.172],[-4.643,0.529],[-4.643,-5.172]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.642,10.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":-2,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"body ","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[63.026,94.993,0],"ix":2},"a":{"a":0,"k":[33.101,40.006,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":50,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":70,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":80,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":90,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":100,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":110,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":120,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":130,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":140,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":150,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":160,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":170,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":180,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":190,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":200,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":210,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":220,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":230,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":240,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":250,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":260,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":270,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":280,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":290,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":300,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":310,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":320,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":330,"s":[102,98,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":340,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":350,"s":[102,98,100]},{"t":360,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,3.614],[2.318,1.386],[0,0],[7.585,0],[2.516,-2.633],[0.343,0],[0,0],[0.951,-7.922],[-9.228,0],[0,0],[0,0],[-2.396,4.083]],"o":[[0,-2.883],[0,0],[0,-7.585],[-3.907,0],[-0.337,-0.022],[0,0],[-7.979,0],[-1.134,9.443],[0,0],[0,0],[5.075,0],[3.324,-0.94]],"v":[[29.601,3.306],[25.717,-3.491],[25.717,-3.964],[11.926,-17.756],[1.971,-13.468],[0.956,-13.52],[-12.529,-13.52],[-28.467,0.211],[-12.943,17.756],[0.956,17.756],[11.926,17.756],[23.82,10.923]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[34.601,22.756],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-0.906,-1.632],[0.906,1.632]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[55.184,25.129],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.906,-1.632],[-0.906,1.632]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[53.062,25.129],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.901],[0.901,0],[0,0.901],[-0.901,0]],"o":[[0,0.901],[-0.901,0],[0,-0.901],[0.901,0]],"v":[[1.632,0],[0.001,1.632],[-1.632,0],[0.001,-1.632]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[54.278,22.756],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.901],[0.901,0],[0,0.901],[-0.901,0]],"o":[[0,0.901],[-0.901,0],[0,-0.901],[0.901,0]],"v":[[1.632,0],[0,1.632],[-1.632,0],[0,-1.632]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[56.089,15.942],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-0.901],[0.902,0],[0,0.901],[-0.9,0]],"o":[[0,0.901],[-0.9,0],[0,-0.901],[0.902,0]],"v":[[1.632,0],[0,1.632],[-1.632,0],[0,-1.632]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[45.137,15.942],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.078,0],[0,0],[0,5.756],[-5.756,0],[0,0],[0,0]],"o":[[0,0],[-5.756,0],[0,-5.756],[0,0],[0,0],[-2.614,5.053]],"v":[[-4.298,10.465],[-4.298,10.465],[-14.762,-0.001],[-4.298,-10.465],[10.445,-10.465],[14.762,-3.286]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.670999983245,0.620000023935,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[49.873,30.047],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[7.585,0],[2.516,-2.633],[0.343,0],[0,0],[0.952,-7.922],[-9.229,0],[0,0],[0,0],[0,7.617]],"o":[[0,0],[0,-7.585],[-3.907,0],[-0.337,-0.021],[0,0],[-7.979,0],[-1.134,9.442],[0,0],[0,0],[7.617,0],[0,0]],"v":[[29.601,1.127],[25.717,-3.964],[11.926,-17.756],[1.971,-13.469],[0.956,-13.52],[-12.529,-13.52],[-28.467,0.211],[-12.943,17.756],[0.956,17.756],[11.926,17.756],[25.717,3.964]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[34.601,22.756],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"tail ","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[8.629,22.756,0],"ix":2},"a":{"a":0,"k":[14.115,9.615,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-2.549],[2.55,0],[0,2.549],[-2.549,0]],"o":[[0,2.549],[-2.549,0],[0,-2.549],[2.55,0]],"v":[[4.615,0],[-0.001,4.615],[-4.615,0],[-0.001,-4.615]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.615,9.615],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"ears","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[46.632,5.001,0],"ix":2},"a":{"a":0,"k":[18.582,9.615,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-2.549],[2.549,0],[0,2.549],[-2.55,0]],"o":[[0,2.549],[-2.55,0],[0,-2.549],[2.549,0]],"v":[[4.616,0],[0.001,4.615],[-4.616,0],[0.001,-4.615]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.549,9.615],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-2.549],[2.549,0],[0,2.549],[-2.549,0]],"o":[[0,2.549],[-2.549,0],[0,-2.549],[2.549,0]],"v":[[4.616,0],[0.001,4.615],[-4.616,0],[0.001,-4.615]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.616,9.615],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"leg4 ","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":50,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":70,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":80,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":100,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":110,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":120,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":130,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":140,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":150,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":160,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":170,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":180,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":190,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":200,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":210,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":220,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":230,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":240,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":250,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":260,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":270,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":280,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":290,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":300,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":310,"s":[30]},{"t":320,"s":[0]}],"ix":10},"p":{"a":0,"k":[43.432,89.981,0],"ix":2},"a":{"a":0,"k":[9.768,6.047,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.554,0],[0,2.553],[0,0]],"o":[[0,0],[0,2.553],[-2.553,0],[0,0],[0,0]],"v":[[4.643,-5.172],[4.643,0.529],[-0.001,5.172],[-4.643,0.529],[-4.643,-5.172]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.501999978458,0.501999978458,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.642,10.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"leg3 ","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-3,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":7,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":27,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":37,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":47,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":57,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":67,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":87,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":97,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":107,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":117,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":127,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":137,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":147,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":157,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":167,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":177,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":187,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":197,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":207,"s":[-30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":217,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":227,"s":[-30]},{"t":237,"s":[0]}],"ix":10},"p":{"a":0,"k":[70.2,90.231,0],"ix":2},"a":{"a":0,"k":[9.768,6.297,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.554,0],[0,2.553],[0,0]],"o":[[0,0],[0,2.553],[-2.553,0],[0,0],[0,0]],"v":[[4.643,-5.172],[4.643,0.529],[-0.001,5.172],[-4.643,0.529],[-4.643,-5.172]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.670999983245,0.620000023935,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.642,10.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":-3,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"wheel ","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":241,"s":[360]}],"ix":10},"p":{"a":0,"k":[62,59.477,0],"ix":2},"a":{"a":0,"k":[54.035,54.035,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.329,-3.346],[-3.329,3.346]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[85.285,22.616],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[5,54.035],[14.441,54.035]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4.398,-1.732],[4.398,1.732]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[12.798,37.792],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4.339,1.873],[4.339,-1.873]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[13.344,71.599],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-3.292,3.382],[3.292,-3.382]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[23.124,85.787],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-1.765,4.411],[1.765,-4.411]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[37.562,95.207],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.051,4.72],[-0.051,-4.72]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[54.511,98.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.85,4.35],[-1.85,-4.35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[71.38,94.821],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.276,3.255],[-3.276,-3.255]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[85.399,85.194],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4.279,1.739],[-4.279,-1.739]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[94.994,70.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4.721,0.107],[-4.721,-0.107]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[98.35,53.929],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4.36,-1.827],[-4.359,1.827]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[94.913,36.909],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-3.323,-3.361],[3.323,3.361]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[22.872,22.521],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-1.896,-4.329],[1.896,4.329]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[36.253,13.439],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.803,-4.368],[-1.803,4.368]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[70.94,13.066],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[54.035,5],[54.035,14.441]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-21.867],[21.867,0],[0,21.867],[-21.867,0]],"o":[[0,21.867],[-21.867,0],[0,-21.867],[21.867,0]],"v":[[39.594,0.001],[0,39.594],[-39.594,0.001],[0,-39.594]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[54.035,54.035],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-27.082],[27.081,0],[0,27.081],[-27.081,0]],"o":[[0,27.081],[-27.081,0],[0,-27.082],[27.081,0]],"v":[[49.035,0],[0,49.035],[-49.035,0],[0,-49.035]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[54.035,54.035],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"base","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[61.999,69.181,0],"ix":2},"a":{"a":0,"k":[59.081,49.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[37.702,-37.702],[44.378,-44.378],[-44.378,44.378]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[49.378,49.378],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-37.701,-37.702],[-44.377,-44.378],[44.377,44.378]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.071000005685,0.075,0.19199999641,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[68.786,49.378],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
--------------------------------------------------------------------------------