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