├── .browserslist ├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── ENHANCEMENT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yml │ ├── stale.yml │ └── tests.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .storybook └── main.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── cypress.json ├── cypress ├── integration │ ├── load_animation.spec.js │ ├── player_controls.spec.js │ ├── player_methods.spec.js │ └── player_properties.spec.js ├── plugins │ └── index.js ├── react-testing-pages │ ├── .eslintignore │ ├── .gitignore │ ├── README.md │ ├── craco.config.js │ ├── package.json │ ├── public │ │ ├── animation.lottie.json │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── App.css │ │ │ ├── Controls.js │ │ │ ├── Home.js │ │ │ ├── Load.js │ │ │ ├── Methods.js │ │ │ └── Properties.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ ├── webpack.config.js │ └── yarn.lock └── support │ ├── commands.js │ └── index.js ├── package.json ├── release.config.js ├── rollup.config.js ├── src ├── ColorPicker.tsx ├── Controls.tsx ├── Player.tsx ├── Popover.tsx ├── Seeker.tsx ├── Styles.css ├── index.ts └── versions.ts ├── stories └── index.stories.tsx ├── tsconfig.json └── yarn.lock /.browserslist: -------------------------------------------------------------------------------- 1 | > 2% 2 | not dead 3 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.7.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Third party 2 | **/node_modules 3 | 4 | # Build products 5 | docs/ 6 | dist/ 7 | 8 | # Testing 9 | coverage/ 10 | example/ 11 | cypress/ 12 | cypress/react-testing-pages/src/components/lottie-react.js 13 | 14 | stories/ 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true }, 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 6 | 'plugin:prettier/recommended', // Enables eslint-config-prettier and eslint-plugin-prettier 7 | 'plugin:react/recommended', // Uses the recommended rules from eslint-plugin-react 8 | 'plugin:react-hooks/recommended', // Uses the recommended rules from eslint-plugin-react-hooks 9 | 'plugin:jsx-a11y/recommended', // Uses the recommended rules from eslint-plugin-jsx-a11y 10 | 'prettier/react', // Disable rules that would conflict with prettier 11 | ], 12 | plugins: ['@typescript-eslint', 'simple-import-sort'], 13 | rules: { 14 | // Disable sorting rules to not conflict with simple-import-sort 15 | 'sort-imports': 'off', 16 | 17 | // Enable the simple-import-sort rule 18 | 'simple-import-sort/sort': 'error', 19 | 20 | // Disable some preference rules 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/interface-name-prefix': 'off', 23 | '@typescript-eslint/explicit-function-return-type': 'off', 24 | 'react/prop-types': 'off', 25 | }, 26 | overrides: [ 27 | { 28 | files: ['*.js'], 29 | rules: { 30 | '@typescript-eslint/no-var-requires': 'off', 31 | }, 32 | }, 33 | ], 34 | parserOptions: { 35 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 36 | sourceType: 'module', // Allows for the use of imports 37 | ecmaFeatures: { 38 | jsx: true, 39 | }, 40 | 41 | // typescript-eslint specific options 42 | warnOnUnsupportedTypeScriptVersion: true, 43 | }, 44 | settings: { 45 | react: { 46 | version: 'detect', // React version. "detect" automatically picks the version you have installed. 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐛 Bug Report' 3 | about: Something isn't working 4 | --- 5 | 6 | ## Overview 7 | 8 | ... 9 | 10 | ## Consuming repo 11 | 12 | > What repo were you working in when this issue occurred? 13 | 14 | ... 15 | 16 | ## Labels 17 | 18 | - [ ] Add the `Type: Bug` label to this issue. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ENHANCEMENT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '📈 Enhancement' 3 | about: Enhancement to our codebase that isn't a adding or changing a feature 4 | --- 5 | 6 | ## Overview 7 | 8 | ... 9 | 10 | ## Motivation 11 | 12 | > What inspired this enhancement? What makes you think this should be included? 13 | 14 | ... 15 | 16 | ## Labels 17 | 18 | - [ ] Add the `Type: Enhancement` label to this issue. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🙌 Feature Request' 3 | about: Suggest a new feature, or changes to an existing one 4 | --- 5 | 6 | ## Overview 7 | 8 | ... 9 | 10 | ## Type 11 | 12 | - [ ] New feature 13 | - [ ] Changes to existing features 14 | 15 | ## Motivation 16 | 17 | > What inspired this feature request? What problems were you facing, or what else makes you think this should be 18 | > included? 19 | 20 | ... 21 | 22 | ## Labels 23 | 24 | - [ ] Add the `Type: Feature Request` label to this issue. 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 8 | 9 | ## Type of change 10 | 11 | 14 | 15 | - [ ] Patch: Bug (non-breaking change which fixes an issue) 16 | - [ ] Minor: New feature (non-breaking change which adds functionality) 17 | - [ ] Major: Breaking change (fix or feature that would cause existing functionality to not work as expected) 18 | 19 | ## Checklist 20 | 21 | - [ ] This is something we need to do. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master, beta] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 15 11 | 12 | strategy: 13 | matrix: 14 | node-version: [14.x] 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Cache NPM 22 | uses: actions/cache@v4 23 | env: 24 | cache-name: cache-npm 25 | with: 26 | path: ~/.npm 27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-build-${{ env.cache-name }}- 30 | - name: Setup Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | registry-url: 'https://npm.pkg.github.com' 35 | - name: Install dependencies 36 | run: yarn install | cd ./cypress/react-testing-pages/ && yarn 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 39 | 40 | # - name: Run tests 41 | # continue-on-error: false 42 | # run: yarn run start-and-run-tests 43 | # env: 44 | # NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 45 | - name: Run linting 46 | run: yarn lint 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 49 | - name: Bump version 50 | run: yarn run version 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | - name: Build project 54 | run: yarn build 55 | 56 | release-npm: 57 | needs: build 58 | name: Release npm 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: actions/setup-node@v4 63 | with: 64 | node-version: ${{ matrix.node-version }} 65 | registry-url: https://registry.npmjs.org/ 66 | - run: yarn install --frozen-lockfile 67 | - run: npm publish --access public 68 | env: 69 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | NPMJS_TOKEN: ${{ secrets.NPMJS_TOKEN }} 72 | GIT_COMMIT: ${{ github.sha }} 73 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 74 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 75 | GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} 76 | GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} 77 | 78 | release-gpr: 79 | needs: build 80 | name: Release gpr 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | - uses: actions/setup-node@v4 85 | with: 86 | node-version: ${{ matrix.node-version }} 87 | registry-url: https://npm.pkg.github.com/ 88 | scope: '@lottiefiles' 89 | - run: yarn install --frozen-lockfile 90 | - run: echo "//npm.pkg.github.com:_authToken=${{ secrets.GITHUB_TOKEN }}" > ~/.npmrc 91 | - run: npm publish --@lottiefiles:registry=https://npm.pkg.github.com/ 92 | env: 93 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | NPMJS_TOKEN: ${{ secrets.NPMJS_TOKEN }} 96 | GIT_COMMIT: ${{ github.sha }} 97 | GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} 98 | GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} 99 | GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} 100 | GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} 101 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | # Idle number of days before marking an issue/pr as stale. 14 | days-before-stale: 60 15 | # Idle number of days before closing an stale issue/pr. 16 | # Set to -1 to never automatically close stale issues. 17 | days-before-close: -1 18 | # Message to post on the stale issue. 19 | stale-issue-message: 20 | 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed 21 | in 7 days if no further activity occurs.' 22 | # Label to apply on the stale issue 23 | stale-issue-label: 'stale' 24 | # Labels on an issue exempted from being marked as stale. 25 | # Set to #wip for work-in-progress: 26 | exempt-issue-labels: 'wip' 27 | # Message to post on the stale pr. 28 | stale-pr-message: 29 | 'This rull request has been automatically marked as stale because it has not had recent activity. It will be 30 | closed in 7 days if no further activity occurs.' 31 | # Label to apply on the stale pr. 32 | stale-pr-label: 'stale' 33 | # Labels on a pr exempted from being marked as stale. 34 | # Set to #wip for work-in-progress: 35 | exempt-pr-labels: 'wip' 36 | # Whether to remove stale label from issue/pr on updates or comments. 37 | remove-stale-when-updated: true 38 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | pull_request: 5 | branches: [master, main, beta] 6 | 7 | jobs: 8 | tests: 9 | name: Tests 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Cache NPM 24 | uses: actions/cache@v4 25 | env: 26 | cache-name: cache-npm 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-build-${{ env.cache-name }}- 32 | - name: Setup Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | registry-url: 'https://npm.pkg.github.com' 37 | 38 | - name: Install dependencies 39 | run: yarn install 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 42 | 43 | - name: Run linting 44 | run: yarn lint 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # node-waf configuration 15 | .lock-wscript 16 | 17 | # Dependency directories 18 | **/node_modules/ 19 | 20 | # Force Karam to not add package-lock.json 21 | package-lock.json 22 | 23 | # Optional npm cache directory 24 | .npm 25 | 26 | # Optional eslint cache 27 | .eslintcache 28 | 29 | # Yarn Integrity file 30 | .yarn-integrity 31 | 32 | # dotenv environment variables file 33 | .env 34 | *.env 35 | 36 | # IDEs 37 | **/.idea 38 | **/.vscode 39 | **/.history 40 | 41 | # Operating system 42 | .DS_Store 43 | .tmp 44 | 45 | # Dist artifacts 46 | dist/ 47 | 48 | # Tests 49 | coverage/ 50 | cypress/react-testing-pages/src/components/lottie-react.js 51 | .nyc_output -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode 3 | .idea 4 | .history 5 | *.log 6 | 7 | .storybook 8 | stories 9 | 10 | # CI/CD 11 | .travis.yml 12 | 13 | # Configs 14 | jest.config.js 15 | rollup.config.js 16 | tsconfig.json 17 | 18 | # Docs 19 | docs/ 20 | examples/ 21 | cypress/ 22 | cypress/react-testing-pages/src/components/lottie-react.js 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @lottiefiles:registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | package.json 3 | package-lock.json 4 | yarn.lock 5 | cypress/ 6 | cypress/react-testing-pages/src/components/lottie-react.js 7 | stories/ 8 | src/versions.d.ts 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSpacing: true, 4 | endOfLine: 'lf', 5 | insertPragma: false, 6 | jsxBracketSameLine: false, 7 | printWidth: 120, 8 | proseWrap: 'always', 9 | requirePragma: false, 10 | semi: true, 11 | singleQuote: true, 12 | tabWidth: 2, 13 | trailingComma: 'all', 14 | useTabs: false, 15 | overrides: [ 16 | { 17 | files: '*.json', 18 | options: { 19 | tabWidth: 2, 20 | singleQuote: false 21 | } 22 | } 23 | ] 24 | }; -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | webpackFinal: async config => { 4 | config.module.rules.push({ 5 | test: /\.(ts|tsx)$/, 6 | use: [ 7 | { 8 | loader: require.resolve('ts-loader'), 9 | }, 10 | // Optional 11 | { 12 | loader: require.resolve('react-docgen-typescript-loader'), 13 | }, 14 | ], 15 | }); 16 | config.module.rules.push({ 17 | test: /\.scss$/, 18 | use: ['style-loader', 'css-loader', 'sass-loader'], 19 | include: path.resolve(__dirname, '../'), 20 | }); 21 | 22 | config.resolve.extensions.push('.ts', '.tsx'); 23 | return config; 24 | }, 25 | stories: ['../**/*.stories.tsx'], 26 | addons: ['@storybook/preset-typescript', '@storybook/addon-actions', '@storybook/addon-links'], 27 | }; 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.6.0 4 | 5 | ### Minor Changes 6 | 7 | - 0589acd: chore: 🤖 upgrade React to v19.0.0 8 | 9 | ## 3.5.4 10 | 11 | ### Patch Changes 12 | 13 | - b40ce3d: chore: 🤖 upgrade lottie-web v5.12.2 14 | 15 | ## 3.5.3 16 | 17 | ### Patch Changes 18 | 19 | - bumped lottie-web 20 | 21 | ## 3.5.2 22 | 23 | ### Patch Changes 24 | 25 | - d71a178: added instanceloaded event 26 | 27 | ## 3.5.1 28 | 29 | ### Patch Changes 30 | 31 | - f807fde: re-arranged events to fire when internal state is set 32 | 33 | ## 3.5.0 34 | 35 | ### Minor Changes 36 | 37 | - 978552b: added getVersions(), updated lottie-web 38 | 39 | ## 3.4.9 40 | 41 | ### Patch Changes 42 | 43 | - 730c3b2: add aria-label to Controls 44 | - 32e0728: fix double animation with react 18 strict mode 45 | - cd94951: chore: version bump 46 | 47 | ## 3.4.6 48 | 49 | ### Patch Changes 50 | 51 | - 8d24ecd: changes type for the renderer variable 52 | - fixed source-maps 53 | 54 | ## 3.4.3 55 | 56 | ### Patch Changes 57 | 58 | - bumping package version to correct number 59 | 60 | ## 3.3.1 61 | 62 | ### Patch Changes 63 | 64 | - added changesets All notable changes to this project will be documented in this file. The format is based on 65 | [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to 66 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html), enforced with 67 | [semantic-release](https://github.com/semantic-release/semantic-release). 68 | 69 | ## [3.4.2](https://github.com/LottieFiles/lottie-react/compare/v3.4.1...v3.4.2) (2022-02-22) 70 | 71 | ### Bug Fixes 72 | 73 | - fixed speed affecting all lotties on a page 74 | ([92cec07](https://github.com/LottieFiles/lottie-react/commit/92cec07ce0fb3239883179c8a4fd344806cc29ba)) 75 | 76 | ## [3.4.1](https://github.com/LottieFiles/lottie-react/compare/v3.4.0...v3.4.1) (2021-08-27) 77 | 78 | ### Bug Fixes 79 | 80 | - **player:** added null check on window 81 | ([b1498ae](https://github.com/LottieFiles/lottie-react/commit/b1498ae73aa56d5ff392120e9935b09cdad3df56)) 82 | 83 | # [3.4.0](https://github.com/LottieFiles/lottie-react/compare/v3.3.1...v3.4.0) (2021-08-27) 84 | 85 | ### Features 86 | 87 | - **window:** added lottie to the window object 88 | ([f587f7f](https://github.com/LottieFiles/lottie-react/commit/f587f7f2ad16aa8b58119aaac94e7c777354edf9)) 89 | 90 | ## [3.3.1](https://github.com/LottieFiles/lottie-react/compare/v3.3.0...v3.3.1) (2021-05-24) 91 | 92 | ### Bug Fixes 93 | 94 | - **onhover bugfix:** onhover bug fixz 95 | ([2bfa12b](https://github.com/LottieFiles/lottie-react/commit/2bfa12b38482c9dd992bfcf738c2a7a684fd1aca)) 96 | 97 | # [3.3.0](https://github.com/LottieFiles/lottie-react/compare/v3.2.0...v3.3.0) (2021-05-18) 98 | 99 | ### Features 100 | 101 | - **player core:** added onhover function 102 | ([a2dc06c](https://github.com/LottieFiles/lottie-react/commit/a2dc06c1d60535fcd834da45c0611dbdece147ed)) 103 | 104 | # [3.2.0](https://github.com/LottieFiles/lottie-react/compare/v3.1.4...v3.2.0) (2021-05-10) 105 | 106 | ### Features 107 | 108 | - **packages update:** testing build pipeline. CI/CD 109 | ([5cb47ff](https://github.com/LottieFiles/lottie-react/commit/5cb47ff9f6f02873afcbeaf004dbf23ef556ad2c)) 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LottieFiles.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## LottiePlayer React Component 2 | 3 | This is a React component for the Lottie Web Player 4 | 5 | ## Demo 6 | 7 | ![screencast](https://i.imgur.com/miLzIkJ.gif) 8 | 9 | ## Documentation 10 | 11 | - [View documentation](https://docs.lottiefiles.com/lottie-player/components/lottie-react) 12 | 13 | #### In Javascript or TypeScript: 14 | 15 | 1. Install package using npm or yarn. 16 | 17 | ```shell 18 | npm install --save @lottiefiles/react-lottie-player 19 | ``` 20 | 21 | 2. Import package in your code. 22 | 23 | ```javascript 24 | import { Player, Controls } from '@lottiefiles/react-lottie-player'; 25 | ``` 26 | 27 | ## Example/Development 28 | 29 | 1. Clone repo 30 | 31 | 2. run yarn install 32 | 33 | 3. run yarn storybook 34 | 35 | ```shell 36 | yarn storybook 37 | ``` 38 | 39 | ## Usage 40 | 41 | ### Player component 42 | 43 | Add the element `Player` and set the `src` prop to a URL pointing to a valid Lottie JSON. 44 | 45 | ```javascript 46 | 52 | 53 | 54 | ``` 55 | 56 | ## Props 57 | 58 | | Prop | Description | Type | Default | 59 | | -------------------- | ---------------------------------------------------------------------- | ------------------ | ----------- | 60 | | `lottieRef` | Get lottie animation object | `function` | `undefined` | 61 | | `onEvent` | Listen for events | `function` | `undefined` | 62 | | `onStateChange` | Play state changes | `function` | `undefined` | 63 | | `onBackgroundChange` | Listen for bg changes | `function` | `undefined` | 64 | | `autoplay` | Autoplay animation on load. | `boolean` | `false` | 65 | | `background` | Background color. | `string` | `undefined` | 66 | | `controls` | Show controls. | `boolean` | `false` | 67 | | `direction` | Direction of animation. | `number` | `1` | 68 | | `hover` | Whether to play on mouse hover. | `boolean` | `false` | 69 | | `keepLastFrame` | Stop animation on the last frame.
Has no effect if `loop` is true. | `boolean` | `false` | 70 | | `loop` | Whether to loop animation. | `boolean` | `false` | 71 | | `renderer` | Renderer to use. | `"svg" | "canvas"` | `'svg'` | 72 | | `speed` | Animation speed. | `number` | `1` | 73 | | `style` | The style for the container. | `object` | `undefined` | 74 | | `src` _(required)_ | Bodymovin JSON data or URL to JSON. | `object` | `string`| `undefined` | 75 | 76 | ## Get Player instance 77 | 78 | To call methods on the instance of the Player component. you may get a reference to the component and call the methods 79 | on ref.current. This is esentially reacts way of doing a document.getElementById(); You may then use this ref ie: player 80 | in the example below to call methods that are described in this documentation. See ref in 81 | [react documentation](https://reactjs.org/docs/refs-and-the-dom.html) 82 | 83 | ```javascript 84 | import React from 'react'; 85 | import { Player } from '@lottiefiles/react-lottie-player'; 86 | 87 | class App extends React.Component { 88 | constructor(props) { 89 | super(props); 90 | this.player = React.createRef(); // initialize your ref 91 | } 92 | render() { 93 | return ( 94 | 102 | ); 103 | } 104 | } 105 | 106 | export default App; 107 | ``` 108 | 109 | ## Get Lottie instance 110 | 111 | The lottieRef prop returns the Lottie instance which you can use to set data and call methods as described in the 112 | [bodymovin documentation](https://github.com/airbnb/lottie-web). 113 | 114 | ```javascript 115 | import React from 'react'; 116 | import { Player } from '@lottiefiles/react-lottie-player'; 117 | 118 | class App extends React.Component { 119 | constructor(props) { 120 | super(props); 121 | this.state = { lottie: null }; // initialize your state 122 | } 123 | 124 | render() { 125 | return ( 126 | { 128 | this.setState({ lottie: instance }); // the lottie instance is returned in the argument of this prop. set it to your local state 129 | }} 130 | autoplay={false} 131 | loop={true} 132 | controls={true} 133 | src="https://assets3.lottiefiles.com/packages/lf20_XZ3pkn.json" 134 | style={{ height: '300px', width: '300px' }} 135 | > 136 | ); 137 | } 138 | } 139 | 140 | export default App; 141 | ``` 142 | 143 | ## Listening for events 144 | 145 | ```javascript 146 | import React from 'react'; 147 | import { Player } from '@lottiefiles/react-lottie-player'; 148 | 149 | class App extends React.Component { 150 | constructor(props) { 151 | super(props); 152 | this.player = React.createRef(); 153 | } 154 | 155 | doSomething() { 156 | this.player.current.play(); // make use of the player and call methods 157 | } 158 | 159 | render() { 160 | return ( 161 | { 163 | if (event === 'load') this.doSomething(); // check event type and do something 164 | }} 165 | ref={this.player} 166 | autoplay={false} 167 | loop={true} 168 | controls={true} 169 | src="https://assets3.lottiefiles.com/packages/lf20_XZ3pkn.json" 170 | style={{ height: '300px', width: '300px' }} 171 | > 172 | ); 173 | } 174 | } 175 | 176 | export default App; 177 | ``` 178 | 179 | ## Events 180 | 181 | The following events are exposed and can be listened to via `addEventListener` calls. 182 | 183 | | Name | Description | 184 | | ---------- | ------------------------------------------------------------------------- | 185 | | `load` | Animation data is loaded. | 186 | | `error` | An animation source cannot be parsed, fails to load or has format errors. | 187 | | `ready` | Animation data is loaded and player is ready. | 188 | | `play` | Animation starts playing. | 189 | | `pause` | Animation is paused. | 190 | | `stop` | Animation is stopped. | 191 | | `freeze` | Animation is paused due to player being invisible. | 192 | | `loop` | An animation loop is completed. | 193 | | `complete` | Animation is complete (all loops completed). | 194 | | `frame` | A new frame is entered. | 195 | 196 | ## Methods 197 | 198 | ### `pause() => void` 199 | 200 | Pause animation play. 201 | 202 | #### Returns 203 | 204 | Type: `void` 205 | 206 | ### `play() => void` 207 | 208 | Start playing animation. 209 | 210 | #### Returns 211 | 212 | Type: `void` 213 | 214 | ### `setPlayerDirection(direction: 1 | -1 ) => void` 215 | 216 | Animation play direction. 217 | 218 | #### Parameters 219 | 220 | | Name | Type | Description | 221 | | ------- | -------- | ----------------- | 222 | | `value` | `number` | Direction values. | 223 | 224 | #### Returns 225 | 226 | Type: `void` 227 | 228 | ### `setPlayerSpeed(speed?: number) => void` 229 | 230 | Sets animation play speed. 231 | 232 | #### Parameters 233 | 234 | | Name | Type | Description | 235 | | ------- | -------- | --------------- | 236 | | `value` | `number` | Playback speed. | 237 | 238 | #### Returns 239 | 240 | Type: `void` 241 | 242 | ### `stop() => void` 243 | 244 | Stops animation play. 245 | 246 | #### Returns 247 | 248 | Type: `void` 249 | 250 | ### `setSeeker(frame: number, play: boolean) => void` 251 | 252 | Seek to a given frame. 253 | 254 | #### Returns 255 | 256 | Type: `void` 257 | 258 | ## Contributing 259 | 260 | We use changesets to maintain a changelog for this repository. When making any change to the codebase that impacts functionality or performance we require a changeset to be present. 261 | 262 | To add a changeset run: 263 | 264 | ``` 265 | yarn run changeset 266 | ``` 267 | 268 | And select the type of version bump you'd like (major, minor, path). 269 | 270 | You can document the change in detail and format it properly using Markdown by opening the ".md" file that the "yarn changeset" command created in the ".changeset" folder. Open the file, it should look something like this: 271 | 272 | ``` 273 | --- 274 | "@lottiefiles/pkg1": minor 275 | "@lottiefiles/pkg2": major 276 | --- 277 | 278 | This is where you document your **changes** using Markdown. 279 | 280 | - You can write 281 | - However you'd like 282 | - In as much detail as you'd like 283 | 284 | Aim to provide enough details so that team mates and future you can understand the changes and the context of the change. 285 | ``` 286 | 287 | You can commit your changes and the changeset to your branch and then create a pull request on the develop branch. 288 | 289 | ## License 290 | 291 | MIT License © LottieFiles.com 292 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const plugins = []; 2 | if (process.env.NODE_ENV === 'test') plugins.push('istanbul'); 3 | 4 | module.exports = function(api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | '@babel/preset-typescript', 10 | [ 11 | '@babel/preset-env', 12 | { 13 | targets: { 14 | edge: '17', 15 | firefox: '60', 16 | chrome: '67', 17 | safari: '11.1', 18 | }, 19 | }, 20 | ], 21 | ], 22 | plugins, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Design Barn Inc. 3 | */ 4 | 5 | module.exports = { extends: ['@commitlint/config-conventional'] }; 6 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "screenshotOnRunFailure": false 4 | } -------------------------------------------------------------------------------- /cypress/integration/load_animation.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Design Barn Inc. 3 | */ 4 | 5 | context("Player component DOM check", () => { 6 | beforeEach(() => { 7 | cy.visit("http://localhost:8080/load"); 8 | }); 9 | 10 | it('Loads an animation on page.', () => { 11 | cy.get("#player-one").should("have.length", 1); 12 | }); 13 | 14 | it('Checks that loading with an empty URL displays an error.', function () { 15 | cy.get("#container-two .lf-error") 16 | .should("have.class", "lf-error"); 17 | }); 18 | 19 | it('Loads an animation with invalid url.', function () { 20 | cy.get("#container-three .lf-error") 21 | .should("have.class", "lf-error"); 22 | }); 23 | 24 | it("Looks for lf-player-container class", () => { 25 | cy.get("#container-one .lf-player-container") 26 | .should("have.class", "lf-player-container"); 27 | }); 28 | 29 | it("Looks lf-player-controls class", () => { 30 | cy.get("#container-one .lf-player-controls") 31 | .should("have.class", "lf-player-controls"); 32 | }); 33 | 34 | it.skip("Looks inside shadow-dom for aria-label", () => { 35 | cy.get("#player-one lottie-player") 36 | .shadow() 37 | .find("#animation-container") 38 | .should("have.attr", "aria-label"); 39 | }); 40 | 41 | it("Verifies controls", () => { 42 | cy.get("#container-one") 43 | .find(".lf-player-controls") 44 | cy.get("#container-one") 45 | .find(".lf-player-btn").should('have.length', 3) 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /cypress/integration/player_controls.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Design Barn Inc. 3 | */ 4 | 5 | import Lottie from "lottie-web"; 6 | 7 | context("Player controls", () => { 8 | beforeEach(() => { 9 | cy.visit("http://localhost:8080/controls"); 10 | }); 11 | 12 | it("clicks on play button and verifies animation is playing", function (done) { 13 | cy.wait(3000); 14 | cy.get("#container-one .lf-player-btn").eq(0).click({force: true}); 15 | 16 | cy.window().then( (win) => { 17 | if (!win.lottie.getRegisteredAnimations()[0].isPaused) 18 | done(); 19 | }); 20 | }); 21 | 22 | it("clicks on pause button and verifies animation is paused", function (done) { 23 | cy.wait(3000); 24 | cy.get("#container-two .lf-player-btn").eq(0).click({force: true}); 25 | 26 | cy.window().then( (win) => { 27 | if (win.lottie.getRegisteredAnimations()[1].isPaused) 28 | done(); 29 | }); 30 | }); 31 | 32 | it("clicks on stop button and verififes animation is stopped and at frame 0", function (done) { 33 | cy.wait(3000); 34 | cy.get("#container-three .lf-player-btn").eq(1).click({force: true}); 35 | 36 | cy.window().then( (win) => { 37 | if (win.lottie.getRegisteredAnimations()[2].isPaused && 38 | win.lottie.getRegisteredAnimations()[2].currentFrame === 0) 39 | done(); 40 | }); 41 | }); 42 | 43 | it("clicks on loop button and verififes animation loops", function (done) { 44 | cy.get("#container-four .lf-player-btn").eq(2).click(); 45 | 46 | cy.wait(3000); 47 | cy.window().then( (win) => { 48 | if (!win.lottie.getRegisteredAnimations()[3].loop || win.lottie.getRegisteredAnimations()[3].playCount > 1) 49 | done(); 50 | }); 51 | }); 52 | 53 | it.skip("clicks on color background choice and verifies background color", function () { 54 | cy.get("#player-five #lottie-controls").find("#lottie-bg-picker-button").click(); 55 | cy.get("#player-five #lottie-controls").find("#lottie-color-choice-4").click(); 56 | cy.get("#player-five lottie-player") 57 | .should("have.css", "background-color") 58 | .and("eq", "rgb(58, 146, 218)"); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /cypress/integration/player_methods.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Design Barn Inc. 3 | */ 4 | 5 | context("Player methods", () => { 6 | beforeEach(() => { 7 | cy.visit("http://localhost:8080/methods"); 8 | }); 9 | 10 | it("Player-one should have play toggled.", function (done) { 11 | cy.wait(3000); 12 | cy.window().then( (win) => { 13 | if (!win.lottie.getRegisteredAnimations()[0].isPaused) 14 | done(); 15 | }); 16 | }); 17 | 18 | it.skip("Player-two should have loop toggled.", function (done) { 19 | cy.wait(3000); 20 | cy.window().then( (win) => { 21 | if (!win.lottie.getRegisteredAnimations()[1].loop) 22 | done(); 23 | }); 24 | }); 25 | 26 | it("Player-three should play at x5 the speed", function(done) { 27 | cy.wait(3000); 28 | cy.window().then( (win) => { 29 | if (win.lottie.getRegisteredAnimations()[2].playSpeed === 5) 30 | done(); 31 | }); 32 | }); 33 | 34 | it.skip("Player-four Should have a green background.", () => { 35 | cy.get("#player-four lottie-player") 36 | .should("have.css", "background-color") 37 | .and("eq", "rgb(0, 255, 107)"); 38 | }); 39 | 40 | it("Player-five should be paused.", function (done) { 41 | cy.wait(3000); 42 | cy.window().then( (win) => { 43 | if (win.lottie.getRegisteredAnimations()[4].isPaused) 44 | done(); 45 | }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /cypress/integration/player_properties.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Design Barn Inc. 3 | */ 4 | 5 | context("Player properties", () => { 6 | beforeEach(() => { 7 | cy.visit("http://localhost:8080/properties"); 8 | }); 9 | 10 | it("Player-one Should have a green background.", () => { 11 | cy.get("#player-one") 12 | .should("have.css", "background-color") 13 | .and("eq", "rgb(0, 255, 107)"); 14 | }); 15 | 16 | it("Player-two should play at x5 the speed", () => { 17 | cy.wait(3000); 18 | cy.window().then( (win) => { 19 | if (!win.lottie.getRegisteredAnimations()[1].playSpeed === 5) 20 | done(); 21 | }); 22 | }); 23 | 24 | it("Player-three should autoplay and loop", function (done) { 25 | cy.wait(3000); 26 | cy.window().then( (win) => { 27 | if (!win.lottie.getRegisteredAnimations()[2].isPaused 28 | && win.lottie.getRegisteredAnimations()[2].loop) 29 | done(); 30 | }); 31 | 32 | }); 33 | 34 | it("Player-four should play on hover", function (done) { 35 | cy.wait(3000); 36 | cy.get("#player-four").trigger('mouseenter'); 37 | cy.window().then( (win) => { 38 | if (!win.lottie.getRegisteredAnimations()[3].isPaused) 39 | done(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | require('@cypress/code-coverage/task')(on, config) 23 | // include any other plugin code... 24 | 25 | // It's IMPORTANT to return the config object 26 | // with any changed environment variables 27 | return config 28 | } 29 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/.eslintignore: -------------------------------------------------------------------------------- 1 | ./src/components/lottie-react.js -------------------------------------------------------------------------------- /cypress/react-testing-pages/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/craco.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CracoAlias = require("craco-alias"); 3 | 4 | module.exports = { 5 | plugins: [ 6 | { 7 | plugin: CracoAlias, 8 | options: { 9 | unsafeAllowModulesOutsideOfSrc : true, 10 | aliases: { 11 | '@lottiefiles/react-lottie-player': path.resolve('../../dist/lottie-react.esm.js'), 12 | } 13 | } 14 | } 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-testing-pages", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.4.3", 7 | "@testing-library/jest-dom": "^5.16.2", 8 | "@testing-library/react": "^12.1.3", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-router": "^6.2.2", 13 | "react-router-dom": "^6.2.2", 14 | "react-scripts": "5.0.1", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "scripts": { 18 | "start": "PORT=8080 craco start", 19 | "build": "craco build", 20 | "test": "craco test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "craco-alias": "^3.0.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/App.js: -------------------------------------------------------------------------------- 1 | import LoadPage from './components/Load'; 2 | import PlayerControls from './components/Controls'; 3 | import Properties from './components/Properties'; 4 | import Methods from './components/Methods'; 5 | import Home from './components/Home'; 6 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 7 | import './App.css'; 8 | 9 | function App() { 10 | return ( 11 |
12 | 13 | 14 | } /> 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | 20 | 21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/components/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/components/Controls.js: -------------------------------------------------------------------------------- 1 | import { Player, Controls } from '@lottiefiles/react-lottie-player'; 2 | import './App.css'; 3 | 4 | function PlayerControls() { 5 | return ( 6 |
7 |
8 | 13 | 14 | 15 | 16 |
17 |
18 | 24 | 25 | 26 |
27 | 28 |
29 | 35 | 36 | 37 |
38 | 39 |
40 | 45 | 46 | 47 |
48 | 49 | {/*
50 | 56 | 57 | 58 |
*/} 59 |
60 | ); 61 | } 62 | 63 | export default PlayerControls; 64 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import { Player, Controls } from '@lottiefiles/react-lottie-player'; 2 | import './App.css'; 3 | 4 | function Home() { 5 | return ( 6 |
7 | 14 | 15 | 16 | 22 | 23 | 24 | 30 | 31 | 32 | 36 | 37 | 38 |
39 | ); 40 | } 41 | 42 | export default Home; 43 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/components/Load.js: -------------------------------------------------------------------------------- 1 | import { Player, Controls } from '@lottiefiles/react-lottie-player'; 2 | import './App.css'; 3 | 4 | function Load() { 5 | return ( 6 |
7 |
8 | 14 | 15 | 16 |
17 | 18 |
19 | 25 | 26 |
27 | 28 |
29 | 35 | 36 |
37 | 38 |
39 | 43 | 44 |
45 |
46 | ); 47 | } 48 | 49 | export default Load; 50 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/components/Methods.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Player, Controls } from '@lottiefiles/react-lottie-player'; 3 | import './App.css'; 4 | 5 | class Methods extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.pOneRef = React.createRef(null); 10 | this.pTwoRef = React.createRef(null); 11 | this.pThreeRef = React.createRef(null); 12 | this.pFourRef = React.createRef(null); 13 | this.pFiveRef = React.createRef(null); 14 | } 15 | 16 | playFirstAnimation() { 17 | this.pOneRef.current.play(); 18 | // Method doesn't exist 19 | // this.pTwoRef.current.loop(); 20 | } 21 | 22 | speedUpAnimation() { 23 | this.pThreeRef.current.setPlayerSpeed(5); 24 | } 25 | 26 | pauseAnimation() { 27 | this.pFiveRef.current.pause(); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 |
34 | { 37 | if (event === 'load') this.playFirstAnimation(); 38 | }} 39 | id="player-one" 40 | loop 41 | src={process.env.PUBLIC_URL + '/animation.lottie.json'} 42 | style={{ height: '300px', width: '300px' }}> 43 | 44 | 45 |
46 | 47 |
48 | 55 | 56 | 57 |
58 | 59 |
60 | { 63 | if (event === 'load') this.speedUpAnimation(); 64 | }} 65 | autoplay 66 | id="player-three" 67 | loop 68 | src={process.env.PUBLIC_URL + '/animation.lottie.json'} 69 | style={{ height: '300px', width: '300px' }}> 70 | 71 | 72 |
73 | 74 |
75 | 82 | 83 | 84 |
85 | 86 |
87 | { 90 | if (event === 'load') this.pauseAnimation(); 91 | }} 92 | autoplay 93 | id="player-five" 94 | loop 95 | src={process.env.PUBLIC_URL + '/animation.lottie.json'} 96 | style={{ height: '300px', width: '300px' }}> 97 | 98 | 99 |
100 |
101 | ); 102 | } 103 | } 104 | 105 | export default Methods; 106 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/components/Properties.js: -------------------------------------------------------------------------------- 1 | import { Player, Controls } from '@lottiefiles/react-lottie-player'; 2 | import './App.css'; 3 | 4 | function Properties() { 5 | return ( 6 |
7 | 14 | 15 | 16 | 23 | 24 | 25 | 31 | 32 | 33 | 39 | 40 | 41 |
42 | ); 43 | } 44 | 45 | export default Properties; 46 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = createRoot(document.getElementById('root')); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ) 14 | 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /cypress/react-testing-pages/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (config, ...rest) => { 2 | return { 3 | ...config, 4 | resolve: { 5 | modules: [ 6 | path.resolve('../../dist/lottie-react.js'), 7 | { 8 | ...config.resolve, 9 | symlinks: false 10 | } 11 | ], 12 | } 13 | } 14 | }; -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | import '@cypress/code-coverage/support' 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lottiefiles/react-lottie-player", 3 | "version": "3.6.0", 4 | "description": "Lottie web player wrapper for React", 5 | "main": "dist/lottie-react.js", 6 | "module": "dist/lottie-react.esm.js", 7 | "types": "dist/src/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "src" 11 | ], 12 | "devDependencies": { 13 | "@babel/core": "^7.9.6", 14 | "@babel/preset-env": "^7.16.11", 15 | "@changesets/cli": "^2.21.1", 16 | "@commitlint/cli": "^12.1.1", 17 | "@commitlint/config-conventional": "^12.1.1", 18 | "@cypress/code-coverage": "^3.9.12", 19 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 20 | "@rollup/plugin-babel": "^5.3.1", 21 | "@rollup/plugin-commonjs": "^11.1.0", 22 | "@rollup/plugin-node-resolve": "^7.1.3", 23 | "@rollup/plugin-strip": "^1.3.2", 24 | "@rollup/plugin-typescript": "^8.1.0", 25 | "@semantic-release/changelog": "^5.0.1", 26 | "@semantic-release/commit-analyzer": "^8.0.1", 27 | "@semantic-release/git": "^9.0.0", 28 | "@semantic-release/github": "^7.2.1", 29 | "@semantic-release/npm": "^7.1.1", 30 | "@semantic-release/release-notes-generator": "^9.0.2", 31 | "@storybook/addon-actions": "^5.3.18", 32 | "@storybook/addon-console": "^1.2.2", 33 | "@storybook/addon-links": "^5.3.18", 34 | "@storybook/addons": "^5.3.18", 35 | "@storybook/preset-create-react-app": "^3.0.0", 36 | "@storybook/preset-typescript": "^3.0.0", 37 | "@storybook/react": "^5.3.18", 38 | "@types/jest": "^25.2.3", 39 | "@types/react": "^18.0.9", 40 | "@types/styled-components": "^5.1.0", 41 | "@typescript-eslint/eslint-plugin": "^2.24.0", 42 | "@typescript-eslint/parser": "^2.24.0", 43 | "babel-loader": "^8.1.0", 44 | "babel-plugin-istanbul": "^6.1.1", 45 | "cross-env": "^7.0.3", 46 | "css-loader": "^3.5.3", 47 | "csstype": "^2.6.10", 48 | "cypress": "^9.5.1", 49 | "eslint": "^6.8.0", 50 | "eslint-config-prettier": "^6.10.0", 51 | "eslint-loader": "^3.0.3", 52 | "eslint-plugin-jsx-a11y": "^6.2.3", 53 | "eslint-plugin-prettier": "^3.1.2", 54 | "eslint-plugin-react": "^7.19.0", 55 | "eslint-plugin-react-hooks": "^3.0.0", 56 | "eslint-plugin-simple-import-sort": "^5.0.2", 57 | "husky": ">=4", 58 | "jest": "^26.0.1", 59 | "lint-staged": ">=10", 60 | "prettier": "^1.19.1", 61 | "react": "^19.0.0", 62 | "react-docgen-typescript-loader": "^3.7.2", 63 | "react-dom": "^19.0.0", 64 | "react-scripts": "^3.4.1", 65 | "rollup": "^2.9.1", 66 | "rollup-plugin-filesize": "^9.0.0", 67 | "rollup-plugin-peer-deps-external": "^2.2.2", 68 | "rollup-plugin-postcss": "^3.1.1", 69 | "rollup-plugin-terser": "^5.3.0", 70 | "rollup-plugin-typescript2": "^0.27.0", 71 | "semantic-release": "^17.4.2", 72 | "source-map-support": "^0.5.21", 73 | "style-loader": "^1.2.1", 74 | "ts-jest": "^26.0.0", 75 | "ts-loader": "^7.0.4", 76 | "ts-node": "^10.7.0", 77 | "typescript": "^3.5.3", 78 | "typings-for-css-modules-loader": "^1.7.0", 79 | "webpack": "^4.43.0" 80 | }, 81 | "peerDependencies": { 82 | "react": "16 - 19" 83 | }, 84 | "dependencies": { 85 | "lottie-web": "^5.12.2" 86 | }, 87 | "scripts": { 88 | "predev": "node -p \"'export const REACT_LOTTIE_PLAYER_VERSION = ' + JSON.stringify(require('./package.json').version) + '; \\n' + 'export const LOTTIE_WEB_VERSION = ' + JSON.stringify(require('./package.json').dependencies['lottie-web']) + ';'\" > src/versions.ts", 89 | "dev": "rollup -c -w", 90 | "prebuild": "node -p \"'export const REACT_LOTTIE_PLAYER_VERSION = ' + JSON.stringify(require('./package.json').version) + '; \\n' + 'export const LOTTIE_WEB_VERSION = ' + JSON.stringify(require('./package.json').dependencies['lottie-web']) + ';'\" > src/versions.ts", 91 | "build": "rollup -c", 92 | "prepublishOnly": "yarn build", 93 | "run-tests": "while ! nc -z localhost 8080 ; do sleep 1 ; done && yarn run start-cypress && npx nyc report --reporter=text-summary", 94 | "postrun-tests": "kill $(lsof -t -i:8080)", 95 | "start-and-run-tests": "cross-env NODE_ENV=test yarn run build && yarn run serve-app && yarn run-tests", 96 | "serve-app": "cd ./cypress/react-testing-pages && yarn && yarn run start &", 97 | "start-cypress": "yarn run cypress run", 98 | "lint": "tsc --noEmit && eslint . --ext .ts,.tsx,.js", 99 | "lint:fix": "eslint . --ext .ts,.tsx,.js --fix", 100 | "storybook": "start-storybook -p 9009", 101 | "build-storybook": "build-storybook", 102 | "version": "yarn changeset version" 103 | }, 104 | "repository": { 105 | "type": "git", 106 | "url": "git+ssh://git@github.com/LottieFiles/lottie-react.git" 107 | }, 108 | "keywords": [ 109 | "lottie", 110 | "lottiefiles", 111 | "react" 112 | ], 113 | "author": "Jawish Hameed ", 114 | "contributors": [ 115 | "Karam Ali " 116 | ], 117 | "license": "MIT", 118 | "bugs": { 119 | "url": "https://github.com/LottieFiles/lottie-react/issues" 120 | }, 121 | "homepage": "https://github.com/LottieFiles/lottie-react#readme", 122 | "husky": { 123 | "hooks": { 124 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 125 | "pre-commit": "lint-staged" 126 | } 127 | }, 128 | "lint-staged": { 129 | "src/**/*.{css,scss,md}": [ 130 | "prettier --write" 131 | ], 132 | "src/**/*.{js,jsx,ts,tsx,json}": [ 133 | "eslint . --ext .ts,.tsx,.js --fix" 134 | ] 135 | }, 136 | "nyc": { 137 | "extends": "@istanbuljs/nyc-config-typescript", 138 | "all": true 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Design Barn Inc. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const pkg = require('./package.json'); 8 | 9 | const CHANGELOG_HEADER = `# Changelog 10 | All notable changes to this project will be documented in this file. 11 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), enforced with [semantic-release](https://github.com/semantic-release/semantic-release). 12 | `; 13 | 14 | const SUCCESS_COMMENT = `:tada: This \${issue.pull_request ? 'pull request is included' : 'issue is fixed'} in v\${nextRelease.version}, available on npm: [${pkg.name}@\${nextRelease.version}](https://www.npmjs.com/package/${pkg.name}).`; 15 | 16 | /** 17 | * See: 18 | * https://semantic-release.gitbook.io/semantic-release/ 19 | * https://github.com/semantic-release/npm 20 | * https://github.com/semantic-release/github 21 | * https://github.com/semantic-release/git 22 | * https://github.com/semantic-release/release-notes-generator 23 | * https://github.com/semantic-release/commit-analyzer 24 | * https://github.com/semantic-release/changelog 25 | */ 26 | module.exports = { 27 | branches: ['master', { name: 'beta', prerelease: true }], 28 | tagFormat: 'v${version}', 29 | ci: true, 30 | plugins: [ 31 | [ 32 | '@semantic-release/commit-analyzer', 33 | { 34 | preset: 'conventionalcommits', 35 | }, 36 | ], 37 | '@semantic-release/release-notes-generator', 38 | [ 39 | '@semantic-release/changelog', 40 | { 41 | changelogFile: 'CHANGELOG.md', 42 | changelogTitle: CHANGELOG_HEADER, 43 | }, 44 | ], 45 | '@semantic-release/git', 46 | [ 47 | '@semantic-release/github', 48 | { 49 | message: 'chore(release): v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 50 | assets: 'dist/*.tgz', 51 | successComment: SUCCESS_COMMENT, 52 | addReleases: 'bottom', 53 | }, 54 | ], 55 | [ 56 | '@semantic-release/npm', 57 | { 58 | npmPublish: true, 59 | tarballDir: 'dist', 60 | }, 61 | ], 62 | ], 63 | }; 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import strip from '@rollup/plugin-strip'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import filesize from 'rollup-plugin-filesize'; 7 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 8 | import postcss from 'rollup-plugin-postcss'; 9 | import { terser } from 'rollup-plugin-terser'; 10 | 11 | const pkg = require('./package.json'); 12 | 13 | const isProduction = !process.env.ROLLUP_WATCH; 14 | const name = 'ReactLottiePlayer'; 15 | 16 | export default { 17 | input: './src/index.ts', 18 | treeshake: false, 19 | output: [ 20 | { 21 | file: pkg.main, 22 | format: 'cjs', 23 | sourcemap: true, 24 | name, 25 | }, 26 | { 27 | file: pkg.module, 28 | format: 'es', 29 | sourcemap: true, 30 | name, 31 | }, 32 | ], 33 | plugins: [ 34 | // Remove debugger statements and console.log calls 35 | isProduction && strip(), 36 | 37 | // Externalize peerDependencies 38 | peerDepsExternal(), 39 | 40 | // Resolve packages from node_modules 41 | resolve({ jsnext: true, extensions: ['.ts', '.js', '.tsx'] }), 42 | 43 | // Convert commonjs modules to ES modules 44 | commonjs(), 45 | 46 | babel({ 47 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs'], 48 | exclude: ['./node_modules/**'], 49 | }), 50 | 51 | // Use Typescript to transpile code 52 | // typescript({ lib: ['es5', 'es6', 'dom'], target: 'es5' }), 53 | typescript({ tsconfig: './tsconfig.json' }), 54 | 55 | // In production mode, minify 56 | isProduction && terser(), 57 | 58 | // Show output filesize 59 | filesize(), 60 | 61 | // bundle with css files 62 | postcss({ 63 | extensions: ['.css'], 64 | }), 65 | ], 66 | }; 67 | -------------------------------------------------------------------------------- /src/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface ColorPickerProps { 4 | colorChangedEvent?: (hex: string) => void; 5 | } 6 | export class ColorPicker extends React.Component { 7 | state = { 8 | red: 0, 9 | green: 0, 10 | blue: 0, 11 | rgba: null, 12 | hex: '#000000', 13 | colorComponents: [], 14 | }; 15 | 16 | handleChange = (rgb: string, value: any) => { 17 | if (rgb === 'r') { 18 | const hex = 19 | '#' + 20 | (value | (1 << 8)).toString(16).slice(1) + 21 | (this.state.green | (1 << 8)).toString(16).slice(1) + 22 | (this.state.blue | (1 << 8)).toString(16).slice(1); 23 | this.setState({ hex: hex }); 24 | } else if (rgb === 'g') { 25 | const hex = 26 | '#' + 27 | (this.state.red | (1 << 8)).toString(16).slice(1) + 28 | (value | (1 << 8)).toString(16).slice(1) + 29 | (this.state.blue | (1 << 8)).toString(16).slice(1); 30 | this.setState({ hex: hex }); 31 | } else if (rgb === 'b') { 32 | const hex = 33 | '#' + 34 | (this.state.red | (1 << 8)).toString(16).slice(1) + 35 | (this.state.green | (1 << 8)).toString(16).slice(1) + 36 | (value | (1 << 8)).toString(16).slice(1); 37 | this.setState({ hex: hex }); 38 | } 39 | }; 40 | parseColor = (input: string) => { 41 | if (typeof input !== 'string') { 42 | return; 43 | } 44 | if (input[0] === '#') { 45 | const _colorComponents = 46 | input.length === 4 47 | ? [input.slice(1, 2), input.slice(2, 3), input.slice(3, 4)].map(n => parseInt(`${n}${n}`, 16)) 48 | : [input.slice(1, 3), input.slice(3, 5), input.slice(5, 7)].map(n => parseInt(n, 16)); 49 | this.setState({ colorComponents: _colorComponents }); 50 | } else if (input.startsWith('rgb')) { 51 | const _colorComponents = input.match(/\d+/g)?.map(n => parseInt(n)); 52 | if (_colorComponents !== undefined) { 53 | this.setState({ colorComponents: _colorComponents }); 54 | } 55 | } 56 | 57 | if (this.state.colorComponents.length) { 58 | this.setState({ red: this.state.colorComponents[0] }); 59 | this.setState({ green: this.state.colorComponents[1] }); 60 | this.setState({ blue: this.state.colorComponents[2] }); 61 | } 62 | }; 63 | 64 | componentDidUpdate(_prevProps: any, prevState: any) { 65 | if (this.props.colorChangedEvent) { 66 | if (this.state.hex !== prevState.hex) { 67 | this.props.colorChangedEvent(this.state.hex); 68 | } 69 | } 70 | return true; 71 | } 72 | public render() { 73 | return ( 74 |
75 |
76 |
77 | Red 78 | { 84 | this.setState({ red: event.target.value }); 85 | this.handleChange('r', event.target.value); 86 | }} 87 | /> 88 | { 95 | this.setState({ red: event.target.value }); 96 | this.handleChange('r', event.target.value); 97 | }} 98 | /> 99 |
100 |
101 | Green 102 | { 108 | this.setState({ green: event.target.value }); 109 | this.handleChange('g', event.target.value); 110 | }} 111 | /> 112 | { 119 | this.setState({ green: event.target.value }); 120 | this.handleChange('g', event.target.value); 121 | }} 122 | /> 123 |
124 |
125 | Blue 126 | { 132 | this.setState({ blue: event.target.value }); 133 | this.handleChange('b', event.target.value); 134 | }} 135 | /> 136 | { 143 | this.setState({ blue: event.target.value }); 144 | this.handleChange('b', event.target.value); 145 | }} 146 | /> 147 |
148 |
149 |
150 |
154 |
155 | { 160 | this.setState({ hex: e.target.value }); 161 | this.parseColor(e.target.value); 162 | }} 163 | /> 164 |
165 |
166 |
167 | ); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Controls.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 3 | import './Styles.css'; 4 | 5 | import * as React from 'react'; 6 | 7 | import { ColorPicker } from './ColorPicker'; 8 | import { PlayerEvent, PlayerState } from './Player'; 9 | import { Popover } from './Popover'; 10 | import { Seeker } from './Seeker'; 11 | 12 | const ControlButtonStyle = { 13 | display: 'inline-flex', 14 | cursor: 'pointer', 15 | }; 16 | 17 | interface IControlProps { 18 | instance?: any; 19 | loop?: boolean; 20 | pause?: () => void; 21 | play?: () => void; 22 | playerState?: PlayerState; 23 | seeker?: number; 24 | setLoop?: (value: boolean) => void; 25 | setSeeker?: (seek: number, play: boolean) => void; 26 | stop?: () => void; 27 | visible?: boolean; 28 | buttons?: string[]; 29 | debug?: boolean; 30 | toggleDebug?: () => void; 31 | showLabels?: boolean; 32 | darkTheme?: boolean; 33 | transparentTheme?: boolean; 34 | colorChangedEvent?: () => void; 35 | snapshot?: () => void; 36 | } 37 | 38 | export class Controls extends React.Component { 39 | public constructor(props: IControlProps) { 40 | super(props); 41 | 42 | this.state = { 43 | activeFrame: 0, 44 | mouseDown: false, 45 | }; 46 | } 47 | 48 | public render() { 49 | const { instance, playerState, seeker, setLoop, setSeeker, play, pause, stop, visible, buttons } = this.props; 50 | 51 | // Render nothing if lottie instance is not available 52 | if (!instance) { 53 | return null; 54 | } 55 | 56 | // Hide controls if not set to visible 57 | if (!visible) { 58 | return null; 59 | } 60 | 61 | const showPlayButton = !buttons || buttons.includes('play'); 62 | const showStopButton = !buttons || buttons.includes('stop'); 63 | const showRepeatButton = !buttons || buttons.includes('repeat'); 64 | const showFrameInput = !buttons || buttons.includes('frame'); 65 | const showBackgroundChange = !buttons || buttons.includes('background'); 66 | const showSnapshot = !buttons || buttons.includes('snapshot'); 67 | const ICON_SIZE = { width: 14, height: 14, viewBox: '0 0 24 24' }; 68 | const currentFrame = Math.round(instance.currentFrame); 69 | 70 | return ( 71 |
83 | {showPlayButton && ( 84 |
{ 89 | if (playerState === PlayerState.Playing) { 90 | if (typeof pause === 'function') { 91 | pause(); 92 | } 93 | } else { 94 | if (typeof play === 'function') { 95 | play(); 96 | } 97 | } 98 | }} 99 | onKeyDown={() => { 100 | if (playerState === PlayerState.Playing) { 101 | if (typeof pause === 'function') { 102 | pause(); 103 | } 104 | } else { 105 | if (typeof play === 'function') { 106 | play(); 107 | } 108 | } 109 | }} 110 | className="lf-player-btn" 111 | style={ControlButtonStyle} 112 | > 113 | {playerState === PlayerState.Playing ? ( 114 | 115 | 116 | 117 | 118 | ) : ( 119 | 120 | 121 | 122 | )} 123 |
124 | )} 125 | {showStopButton && ( 126 |
stop && stop()} 131 | onKeyDown={() => stop && stop()} 132 | className={playerState === PlayerState.Stopped ? 'lf-player-btn active' : 'lf-player-btn'} 133 | style={ControlButtonStyle} 134 | > 135 | 136 | 140 | 141 |
142 | )} 143 | { 149 | if (setSeeker) { 150 | this.setState({ activeFrame: newFrame }, () => { 151 | setSeeker(newFrame, false); 152 | }); 153 | } 154 | }} 155 | onChangeEnd={(newFrame: any) => { 156 | if (setSeeker) { 157 | this.setState({ activeFrame: newFrame }, () => { 158 | setSeeker(newFrame, false); 159 | }); 160 | } 161 | }} 162 | showLabels={this.props.showLabels} 163 | darkTheme={this.props.darkTheme} 164 | /> 165 | {showFrameInput && ( 166 |
167 | 184 |
185 | )} 186 | {showRepeatButton && ( 187 |
{ 192 | if (instance && setLoop) { 193 | setLoop(!instance.loop); 194 | } 195 | }} 196 | onKeyDown={() => { 197 | if (instance && setLoop) { 198 | setLoop(!instance.loop); 199 | } 200 | }} 201 | className={instance.loop ? 'lf-player-btn active' : 'lf-player-btn'} 202 | style={ControlButtonStyle} 203 | > 204 | 205 | 215 | 229 | 230 |
231 | )} 232 | {showBackgroundChange && ( 233 | 236 | 243 | 244 | } 245 | > 246 |
247 | 248 |
249 |
250 | )} 251 | {showSnapshot && ( 252 | 255 | 262 | 263 | 264 | 270 | 271 | } 272 | > 273 |
{ 277 | if (setSeeker) setSeeker(currentFrame + (e.deltaY > 0 ? -1 : 1), false); 278 | }} 279 | > 280 |
Frame {currentFrame}
281 |
282 | Download SVG 283 |
284 |
285 | Download PNG 286 |
287 | Scroll with mousewheel to find exact frame 288 |
289 |
290 | )} 291 |
292 | ); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/Player.tsx: -------------------------------------------------------------------------------- 1 | import lottie, { AnimationItem } from 'lottie-web'; 2 | import * as React from 'react'; 3 | 4 | import { LOTTIE_WEB_VERSION, REACT_LOTTIE_PLAYER_VERSION } from './versions'; 5 | 6 | /** 7 | * Parse a resource into a JSON object or a URL string 8 | */ 9 | export function parseSrc(src: string | object): string | object { 10 | if (typeof src === 'object') { 11 | return src; 12 | } 13 | 14 | try { 15 | return JSON.parse(src); 16 | } catch (e) { 17 | // Do nothing... 18 | } 19 | 20 | // Try construct an absolute URL from the src URL 21 | try { 22 | return new URL(src).toString(); 23 | } catch (e) { 24 | // Do nothing... 25 | } 26 | 27 | return src; 28 | } 29 | 30 | // Necessary so that we can add Lottie to the window afterwards 31 | declare global { 32 | interface Window { 33 | lottie: any; 34 | } 35 | } 36 | 37 | // Define valid player states 38 | export enum PlayerState { 39 | Loading = 'loading', 40 | Playing = 'playing', 41 | Paused = 'paused', 42 | Stopped = 'stopped', 43 | Frozen = 'frozen', 44 | Error = 'error', 45 | } 46 | 47 | // Define player events 48 | export enum PlayerEvent { 49 | Load = 'load', 50 | InstanceSaved = 'instanceSaved', 51 | Error = 'error', 52 | Ready = 'ready', 53 | Play = 'play', 54 | Pause = 'pause', 55 | Stop = 'stop', 56 | Freeze = 'freeze', 57 | Loop = 'loop', 58 | Complete = 'complete', 59 | Frame = 'frame', 60 | } 61 | 62 | export type Versions = { 63 | lottieWebVersion: string; 64 | lottiePlayerVersion: string; 65 | }; 66 | 67 | export type PlayerDirection = -1 | 1; 68 | 69 | export interface IPlayerProps { 70 | id?: string; 71 | lottieRef?: (ref: AnimationItem) => void; 72 | onEvent?: (event: PlayerEvent) => any; 73 | onStateChange?: (state: PlayerState) => any; 74 | onBackgroundChange?: (color: string) => void; 75 | autoplay?: boolean; 76 | background?: string; 77 | children?: React.ReactNode | React.ReactNode[]; 78 | controls?: boolean; 79 | direction?: PlayerDirection; 80 | hover?: boolean; 81 | loop?: boolean | number; 82 | renderer?: any; 83 | speed?: number; 84 | src: object | string; 85 | style?: React.CSSProperties; 86 | rendererSettings?: object; 87 | keepLastFrame?: boolean; 88 | className?: string; 89 | } 90 | 91 | interface IPlayerState { 92 | animationData: any; 93 | background: string; 94 | containerRef: React.Ref | null; 95 | debug?: boolean; 96 | instance: AnimationItem | null; 97 | seeker: number; 98 | playerState: PlayerState; 99 | } 100 | 101 | // Build default config for lottie-web player 102 | const defaultOptions = { 103 | clearCanvas: false, 104 | hideOnTransparent: true, 105 | progressiveLoad: true, 106 | }; 107 | 108 | export class Player extends React.Component { 109 | public static async getDerivedStateFromProps(nextProps: any, prevState: any) { 110 | if (nextProps.background !== prevState.background) { 111 | return { background: nextProps.background }; 112 | } else { 113 | return null; 114 | } 115 | } 116 | 117 | public container: Element | null = null; 118 | public unmounted = false; 119 | 120 | constructor(props: IPlayerProps) { 121 | super(props); 122 | 123 | if (typeof window !== 'undefined') { 124 | window.lottie = lottie; 125 | } 126 | this.state = { 127 | animationData: null, 128 | background: 'transparent', 129 | containerRef: React.createRef(), 130 | debug: true, 131 | instance: null, 132 | playerState: PlayerState.Loading, 133 | seeker: 0, 134 | }; 135 | } 136 | 137 | /** 138 | * Returns the lottie-web version and this player's version 139 | */ 140 | public getVersions(): Versions { 141 | return { 142 | lottieWebVersion: LOTTIE_WEB_VERSION, 143 | lottiePlayerVersion: REACT_LOTTIE_PLAYER_VERSION, 144 | }; 145 | } 146 | 147 | static defaultProps = { 148 | loop: false, 149 | }; 150 | 151 | public async componentDidMount() { 152 | if (!this.unmounted) { 153 | await this.createLottie(); 154 | } 155 | } 156 | 157 | public componentWillUnmount() { 158 | this.unmounted = true; 159 | if (this.state.instance) { 160 | this.state.instance.destroy(); 161 | } 162 | } 163 | 164 | public async componentDidUpdate(prevProps: any) { 165 | if (this.props.src !== prevProps.src) { 166 | if (this.state.instance) { 167 | this.state.instance.destroy(); 168 | } 169 | await this.createLottie(); 170 | } 171 | } 172 | handleBgChange = (childData: any) => { 173 | this.setState({ background: childData }); 174 | }; 175 | triggerDownload = (dataUri: any, filename: any) => { 176 | const element = document.createElement('a'); 177 | 178 | element.href = dataUri; 179 | element.download = filename; 180 | document.body.appendChild(element); 181 | 182 | element.click(); 183 | 184 | document.body.removeChild(element); 185 | }; 186 | snapshot = (download = true) => { 187 | let data; 188 | const id = this.props.id ? this.props.id : 'lottie'; 189 | const lottieElement = document.getElementById(id); 190 | if (this.props.renderer === 'svg') { 191 | // Get SVG element and serialize markup 192 | if (lottieElement) { 193 | const svgElement = lottieElement.querySelector('svg'); 194 | 195 | if (svgElement) { 196 | const serializedSvg = new XMLSerializer().serializeToString(svgElement); 197 | data = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(serializedSvg); 198 | } 199 | } 200 | 201 | // Trigger file download if needed 202 | if (download) { 203 | // this.triggerDownload(data, `snapshot_${progress}.svg`); 204 | this.triggerDownload(data, `snapshot.svg`); 205 | } 206 | } else if (this.props.renderer === 'canvas') { 207 | if (lottieElement) { 208 | const canvas = lottieElement.querySelector('canvas'); 209 | if (canvas) { 210 | data = canvas.toDataURL('image/png'); 211 | } 212 | } 213 | // Trigger file download if needed 214 | if (download) { 215 | // this.triggerDownload(data, `snapshot_${progress}.png`); 216 | this.triggerDownload(data, `snapshot.png`); 217 | } 218 | } 219 | 220 | return data; 221 | }; 222 | 223 | public render() { 224 | const { children, loop, style, onBackgroundChange, className } = this.props; 225 | const { animationData, instance, playerState, seeker, debug, background } = this.state; 226 | 227 | return ( 228 |
229 | {this.state.playerState === PlayerState.Error ? ( 230 |
231 | 232 | ⚠️ 233 | 234 |
235 | ) : ( 236 |
(this.container = el)} 239 | style={{ 240 | background, 241 | margin: '0 auto', 242 | outline: 'none', 243 | overflow: 'hidden', 244 | ...style, 245 | }} 246 | className={className} 247 | >
248 | )} 249 | {React.Children.map(children, child => { 250 | if (React.isValidElement(child)) { 251 | return React.cloneElement(child, { 252 | animationData, 253 | background, 254 | debug, 255 | instance, 256 | loop, 257 | pause: () => this.pause(), 258 | play: () => this.play(), 259 | playerState, 260 | seeker, 261 | setBackground: (value: string) => { 262 | this.setState({ background: value }); 263 | 264 | if (typeof onBackgroundChange === 'function') { 265 | onBackgroundChange(value); 266 | } 267 | }, 268 | setSeeker: (f: number, p: boolean) => this.setSeeker(f, p), 269 | stop: () => this.stop(), 270 | toggleDebug: () => this.toggleDebug(), 271 | setLoop: (loop: boolean) => this.setLoop(loop), 272 | colorChangedEvent: (hex: string) => { 273 | this.handleBgChange(hex); 274 | }, 275 | snapshot: () => { 276 | this.snapshot(); 277 | }, 278 | }); 279 | } 280 | return null; 281 | })} 282 |
283 | ); 284 | } 285 | 286 | private toggleDebug() { 287 | this.setState({ debug: !this.state.debug }); 288 | } 289 | 290 | private async createLottie() { 291 | const { 292 | autoplay, 293 | direction, 294 | loop, 295 | lottieRef, 296 | renderer, 297 | speed, 298 | src, 299 | background, 300 | rendererSettings, 301 | hover, 302 | } = this.props; 303 | const { instance } = this.state; 304 | 305 | if (!src || !this.container) { 306 | return; 307 | } 308 | 309 | // Load the resource information 310 | try { 311 | // Parse the src to see if it is a URL or Lottie JSON data 312 | let animationData = parseSrc(src); 313 | 314 | if (typeof animationData === 'string') { 315 | const fetchResult = await fetch(animationData as string).catch(() => { 316 | this.setState({ playerState: PlayerState.Error }); 317 | this.triggerEvent(PlayerEvent.Error); 318 | throw new Error('@LottieFiles/lottie-react: Animation data could not be fetched.'); 319 | }); 320 | 321 | animationData = await fetchResult.json().catch(() => { 322 | this.setState({ playerState: PlayerState.Error }); 323 | this.triggerEvent(PlayerEvent.Error); 324 | throw new Error('@LottieFiles/lottie-react: Animation data could not be fetched.'); 325 | }); 326 | } 327 | 328 | // Clear previous animation, if any 329 | if (instance) { 330 | instance.destroy(); 331 | } 332 | 333 | // Initialize lottie player and load animation 334 | const newInstance = lottie.loadAnimation({ 335 | rendererSettings: rendererSettings || defaultOptions, 336 | animationData, 337 | autoplay: autoplay || false, 338 | container: this.container as Element, 339 | loop: loop || false, 340 | renderer, 341 | }); 342 | if (speed) { 343 | newInstance.setSpeed(speed); 344 | } 345 | this.setState({ animationData }); 346 | 347 | this.setState({ instance: newInstance }, () => { 348 | this.triggerEvent(PlayerEvent.InstanceSaved); 349 | 350 | if (typeof lottieRef === 'function') { 351 | lottieRef(newInstance); 352 | } 353 | if (autoplay) { 354 | this.play(); 355 | } 356 | }); 357 | 358 | // Handle new frame event 359 | newInstance.addEventListener('enterFrame', () => { 360 | this.triggerEvent(PlayerEvent.Frame); 361 | 362 | this.setState({ 363 | seeker: Math.floor((newInstance as any).currentFrame), 364 | }); 365 | }); 366 | 367 | // Handle lottie-web ready event 368 | newInstance.addEventListener('DOMLoaded', () => { 369 | this.triggerEvent(PlayerEvent.Load); 370 | }); 371 | 372 | // Handle animation data load complete 373 | newInstance.addEventListener('data_ready', () => { 374 | this.triggerEvent(PlayerEvent.Ready); 375 | }); 376 | 377 | // Set error state when animation load fail event triggers 378 | newInstance.addEventListener('data_failed', () => { 379 | this.setState({ playerState: PlayerState.Error }); 380 | this.triggerEvent(PlayerEvent.Error); 381 | }); 382 | 383 | // Handle new loop event 384 | newInstance.addEventListener('loopComplete', () => { 385 | this.triggerEvent(PlayerEvent.Loop); 386 | }); 387 | 388 | // Set state to paused if loop is off and anim has completed 389 | newInstance.addEventListener('complete', () => { 390 | this.triggerEvent(PlayerEvent.Complete); 391 | this.setState({ playerState: PlayerState.Paused }); 392 | 393 | if (!this.props.keepLastFrame || this.props.loop) { 394 | this.setSeeker(0); 395 | } 396 | }); 397 | 398 | // Set handlers to auto play animation on hover if enabled 399 | if (this.container) { 400 | this.container.addEventListener('mouseenter', () => { 401 | if (hover && this.state.playerState !== PlayerState.Playing) { 402 | if (this.props.keepLastFrame) { 403 | this.stop(); 404 | } 405 | this.play(); 406 | } 407 | }); 408 | this.container.addEventListener('mouseleave', () => { 409 | if (hover && this.state.playerState === PlayerState.Playing) { 410 | this.stop(); 411 | } 412 | }); 413 | } 414 | 415 | // Set initial playback speed and direction 416 | if (speed) { 417 | this.setPlayerSpeed(speed); 418 | } 419 | 420 | if (direction) { 421 | this.setPlayerDirection(direction); 422 | } 423 | 424 | if (background) { 425 | this.setState({ background }); 426 | } 427 | } catch (e) { 428 | this.setState({ playerState: PlayerState.Error }); 429 | this.triggerEvent(PlayerEvent.Error); 430 | } 431 | } 432 | 433 | public play() { 434 | const { instance } = this.state; 435 | 436 | if (instance) { 437 | this.triggerEvent(PlayerEvent.Play); 438 | 439 | instance.play(); 440 | 441 | this.setState({ playerState: PlayerState.Playing }); 442 | } 443 | } 444 | 445 | public pause() { 446 | const { instance } = this.state; 447 | 448 | if (instance) { 449 | this.triggerEvent(PlayerEvent.Pause); 450 | 451 | instance.pause(); 452 | 453 | this.setState({ playerState: PlayerState.Paused }); 454 | } 455 | } 456 | 457 | public stop() { 458 | const { instance } = this.state; 459 | 460 | if (instance) { 461 | this.triggerEvent(PlayerEvent.Stop); 462 | 463 | instance.stop(); 464 | 465 | this.setState({ playerState: PlayerState.Stopped }); 466 | } 467 | } 468 | 469 | public setPlayerSpeed(speed: number) { 470 | const { instance } = this.state; 471 | 472 | if (instance) { 473 | instance.setSpeed(speed); 474 | } 475 | } 476 | 477 | public setPlayerDirection(direction: PlayerDirection) { 478 | const { instance } = this.state; 479 | 480 | if (instance) { 481 | instance.setDirection(direction); 482 | } 483 | } 484 | 485 | public setSeeker(seek: number, play = false) { 486 | const { instance, playerState } = this.state; 487 | 488 | if (instance) { 489 | if (!play || playerState !== PlayerState.Playing) { 490 | instance.goToAndStop(seek, true); 491 | this.triggerEvent(PlayerEvent.Pause); 492 | this.setState({ playerState: PlayerState.Paused }); 493 | } else { 494 | instance.goToAndPlay(seek, true); 495 | } 496 | } 497 | } 498 | 499 | public setLoop(loop: boolean) { 500 | const { instance } = this.state; 501 | 502 | if (instance) { 503 | instance.loop = loop; 504 | this.setState({ instance: instance }); 505 | } 506 | } 507 | private triggerEvent(event: PlayerEvent) { 508 | const { onEvent } = this.props; 509 | 510 | if (onEvent) { 511 | onEvent(event); 512 | } 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/Popover.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/mouse-events-have-key-events */ 2 | /* eslint-disable react/jsx-no-comment-textnodes */ 3 | import * as React from 'react'; 4 | import { useEffect, useState } from 'react'; 5 | import { ReactNode } from 'react'; 6 | 7 | interface IPopoverProps { 8 | children: ReactNode; 9 | icon: ReactNode; 10 | } 11 | 12 | export const Popover: React.FC = (props: IPopoverProps) => { 13 | const { children, icon } = props; 14 | const [_triggerRef, setTriggerRef] = useState(null); 15 | const [_contentRef, setContentRef] = useState(null); 16 | const [_alignment, setAlignment] = useState(null); 17 | const [_open, setOpen] = useState(false); 18 | 19 | useEffect(() => { 20 | if (_triggerRef && _contentRef) { 21 | const triggerBounds = _triggerRef.getBoundingClientRect(); 22 | const contentBounds = _contentRef.getBoundingClientRect(); 23 | 24 | const alignment = triggerBounds.left + contentBounds.width > window.innerWidth ? -1 : 0; 25 | 26 | setAlignment(alignment); 27 | } 28 | }, [_alignment, _contentRef, _triggerRef]); 29 | 30 | // /** 31 | // * Show content box 32 | // */ 33 | const show = () => { 34 | setOpen(true); 35 | }; 36 | 37 | /** 38 | * Hide content box 39 | */ 40 | const hide = () => { 41 | setOpen(false); 42 | }; 43 | 44 | return ( 45 |
{ 48 | show(); 49 | }} 50 | onMouseLeave={() => { 51 | hide(); 52 | }} 53 | ref={triggerRef => { 54 | setTriggerRef(triggerRef); 55 | }} 56 | > 57 |
{icon}
58 | 59 |
{ 62 | setContentRef(contentRef); 63 | }} 64 | style={{ 65 | bottom: '22px', 66 | right: '0px', 67 | zIndex: 2, 68 | visibility: _open ? 'visible' : 'hidden', 69 | }} 70 | > 71 | {children} 72 |
73 |
74 | ); 75 | }; 76 | export default Popover; 77 | -------------------------------------------------------------------------------- /src/Seeker.tsx: -------------------------------------------------------------------------------- 1 | import './Styles.css'; 2 | 3 | import * as React from 'react'; 4 | 5 | // const active = 'rgba(15, 204, 206, 0.4)'; 6 | // const inactive = '#dbdbdb'; 7 | 8 | interface ISeekerProps { 9 | className?: string; 10 | disabled?: boolean; 11 | max: number; 12 | min: number; 13 | onChange: (e: any) => void; 14 | onChangeStart?: (v: number) => void; 15 | onChangeEnd?: (v: number) => void; 16 | showLabels?: boolean; 17 | step: number; 18 | value: number; 19 | darkTheme?: boolean; 20 | } 21 | 22 | export class Seeker extends React.Component { 23 | inputRef: any = React.createRef(); 24 | 25 | constructor(props: ISeekerProps) { 26 | super(props); 27 | this.state = { value: 0 }; 28 | } 29 | 30 | handleChange = () => (event: any) => { 31 | const value = event.target.value; 32 | const frame = Math.floor((value / 100) * this.props.max); 33 | this.props.onChange(frame); 34 | }; 35 | 36 | render() { 37 | const progress = (this.props.value / this.props.max) * 100; 38 | const seekerStyle = { 39 | backgroundImage: `-webkit-gradient(linear, left top, right top, color-stop(${progress}%, rgba(15, 204, 206, 0.4)), color-stop(${progress}%, #DAE1E7))`, 40 | }; 41 | const seekerContainerStyle = { 42 | display: 'flex', 43 | flexDirection: 'column', 44 | alignItems: 'center', 45 | width: '100%', 46 | marginRight: '5px', 47 | marginLeft: '5px', 48 | position: 'relative', 49 | } as React.CSSProperties; 50 | 51 | const minLabelStyle = { 52 | position: 'absolute', 53 | left: 0, 54 | marginTop: '8px', 55 | width: '20px', 56 | display: 'block', 57 | border: '0px', 58 | backgroundColor: this.props.darkTheme ? '#505050' : 'rgb(218, 225, 231)', 59 | color: this.props.darkTheme ? '#B9B9B9' : '#555', 60 | padding: '2px', 61 | textAlign: 'center', 62 | borderRadius: '3px', 63 | fontSize: '8px', 64 | fontWeight: 'bold', 65 | } as React.CSSProperties; 66 | const maxLabelStyle = { 67 | position: 'absolute', 68 | right: 0, 69 | marginTop: '8px', 70 | width: '20px', 71 | display: 'block', 72 | border: '0px', 73 | backgroundColor: this.props.darkTheme ? '#505050' : 'rgb(218, 225, 231)', 74 | color: this.props.darkTheme ? '#B9B9B9' : '#555', 75 | padding: '2px', 76 | textAlign: 'center', 77 | borderRadius: '3px', 78 | fontSize: '8px', 79 | fontWeight: 'bold', 80 | } as React.CSSProperties; 81 | return ( 82 |
83 | 98 | {this.props.showLabels && ( 99 |
100 |
{this.props.min}
101 |
{this.props.max}
102 |
103 | )} 104 |
105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Styles.css: -------------------------------------------------------------------------------- 1 | .lf-progress { 2 | -webkit-appearance: none; 3 | -moz-apperance: none; 4 | width: 100%; 5 | /* margin: 0 10px; */ 6 | height: 4px; 7 | border-radius: 3px; 8 | cursor: pointer; 9 | } 10 | .lf-progress:focus { 11 | outline: none; 12 | border: none; 13 | } 14 | .lf-progress::-moz-range-track { 15 | cursor: pointer; 16 | background: none; 17 | border: none; 18 | outline: none; 19 | } 20 | .lf-progress::-webkit-slider-thumb { 21 | -webkit-appearance: none !important; 22 | height: 13px; 23 | width: 13px; 24 | border: 0; 25 | border-radius: 50%; 26 | background: #0fccce; 27 | cursor: pointer; 28 | } 29 | .lf-progress::-moz-range-thumb { 30 | -moz-appearance: none !important; 31 | height: 13px; 32 | width: 13px; 33 | border: 0; 34 | border-radius: 50%; 35 | background: #0fccce; 36 | cursor: pointer; 37 | } 38 | .lf-progress::-ms-track { 39 | width: 100%; 40 | height: 3px; 41 | cursor: pointer; 42 | background: transparent; 43 | border-color: transparent; 44 | color: transparent; 45 | } 46 | .lf-progress::-ms-fill-lower { 47 | background: #ccc; 48 | border-radius: 3px; 49 | } 50 | .lf-progress::-ms-fill-upper { 51 | background: #ccc; 52 | border-radius: 3px; 53 | } 54 | .lf-progress::-ms-thumb { 55 | border: 0; 56 | height: 15px; 57 | width: 15px; 58 | border-radius: 50%; 59 | background: #0fccce; 60 | cursor: pointer; 61 | } 62 | .lf-progress:focus::-ms-fill-lower { 63 | background: #ccc; 64 | } 65 | .lf-progress:focus::-ms-fill-upper { 66 | background: #ccc; 67 | } 68 | .lf-player-container :focus { 69 | outline: 0; 70 | } 71 | .lf-popover { 72 | position: relative; 73 | } 74 | 75 | .lf-popover-content { 76 | display: inline-block; 77 | position: absolute; 78 | opacity: 1; 79 | visibility: visible; 80 | transform: translate(0, -10px); 81 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); 82 | transition: all 0.3s cubic-bezier(0.75, -0.02, 0.2, 0.97); 83 | } 84 | 85 | .lf-popover-content.hidden { 86 | opacity: 0; 87 | visibility: hidden; 88 | transform: translate(0, 0px); 89 | } 90 | 91 | .lf-player-btn-container { 92 | display: flex; 93 | align-items: center; 94 | } 95 | .lf-player-btn { 96 | cursor: pointer; 97 | fill: #999; 98 | width: 14px; 99 | } 100 | 101 | .lf-player-btn.active { 102 | fill: #555; 103 | } 104 | 105 | .lf-popover { 106 | position: relative; 107 | } 108 | 109 | .lf-popover-content { 110 | display: inline-block; 111 | position: absolute; 112 | background-color: #ffffff; 113 | opacity: 1; 114 | 115 | transform: translate(0, -10px); 116 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); 117 | transition: all 0.3s cubic-bezier(0.75, -0.02, 0.2, 0.97); 118 | padding: 10px; 119 | } 120 | 121 | .lf-popover-content.hidden { 122 | opacity: 0; 123 | visibility: hidden; 124 | transform: translate(0, 0px); 125 | } 126 | 127 | .lf-arrow { 128 | position: absolute; 129 | z-index: -1; 130 | content: ''; 131 | bottom: -9px; 132 | border-style: solid; 133 | border-width: 10px 10px 0px 10px; 134 | } 135 | 136 | .lf-left-align, 137 | .lf-left-align .lfarrow { 138 | left: 0; 139 | right: unset; 140 | } 141 | 142 | .lf-right-align, 143 | .lf-right-align .lf-arrow { 144 | right: 0; 145 | left: unset; 146 | } 147 | 148 | .lf-text-input { 149 | border: 1px #ccc solid; 150 | border-radius: 5px; 151 | padding: 3px; 152 | width: 60px; 153 | margin: 0; 154 | } 155 | 156 | .lf-color-picker { 157 | display: flex; 158 | flex-direction: row; 159 | justify-content: space-between; 160 | height: 90px; 161 | } 162 | 163 | .lf-color-selectors { 164 | display: flex; 165 | flex-direction: column; 166 | justify-content: space-between; 167 | } 168 | 169 | .lf-color-component { 170 | display: flex; 171 | flex-direction: row; 172 | font-size: 12px; 173 | align-items: center; 174 | justify-content: center; 175 | } 176 | 177 | .lf-color-component strong { 178 | width: 40px; 179 | } 180 | 181 | .lf-color-component input[type='range'] { 182 | margin: 0 0 0 10px; 183 | } 184 | 185 | .lf-color-component input[type='number'] { 186 | width: 50px; 187 | margin: 0 0 0 10px; 188 | } 189 | 190 | .lf-color-preview { 191 | font-size: 12px; 192 | display: flex; 193 | flex-direction: column; 194 | align-items: center; 195 | justify-content: space-between; 196 | padding-left: 5px; 197 | } 198 | 199 | .lf-preview { 200 | height: 60px; 201 | width: 60px; 202 | } 203 | 204 | .lf-popover-snapshot { 205 | width: 150px; 206 | } 207 | .lf-popover-snapshot h5 { 208 | margin: 5px 0 10px 0; 209 | font-size: 0.75rem; 210 | } 211 | .lf-popover-snapshot a { 212 | display: block; 213 | text-decoration: none; 214 | } 215 | .lf-popover-snapshot a:before { 216 | content: '⥼'; 217 | margin-right: 5px; 218 | } 219 | .lf-popover-snapshot .lf-note { 220 | display: block; 221 | margin-top: 10px; 222 | color: #999; 223 | } 224 | .lf-player-controls > div { 225 | margin-right: 5px; 226 | margin-left: 5px; 227 | } 228 | .lf-player-controls > div:first-child { 229 | margin-left: 0px; 230 | } 231 | .lf-player-controls > div:last-child { 232 | margin-right: 0px; 233 | } 234 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Player'; 2 | export * from './Controls'; 3 | export * from './Seeker'; 4 | -------------------------------------------------------------------------------- /src/versions.ts: -------------------------------------------------------------------------------- 1 | export const REACT_LOTTIE_PLAYER_VERSION = '3.5.4'; 2 | export const LOTTIE_WEB_VERSION = '^5.12.2'; 3 | -------------------------------------------------------------------------------- /stories/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-console'; 2 | 3 | import * as React from 'react'; 4 | import { useCallback, useRef, useEffect } from 'react'; 5 | 6 | import { Controls } from '../src/Controls'; 7 | import { Player } from '../src/Player'; 8 | 9 | export default { 10 | title: 'Components', 11 | parameters: { 12 | info: { inline: true }, 13 | }, 14 | }; 15 | 16 | export const LottiePlayer = (): any => { 17 | const playerRef = useRef(null); 18 | 19 | const onEvent = useCallback((event: any) => { 20 | console.log('event', event); 21 | }, []); 22 | 23 | useEffect(() => { 24 | if (playerRef !== null && playerRef.current !== null) { 25 | // @ts-ignore: Object is possibly 'null'. 26 | console.log(playerRef.current.getVersions()); 27 | console.log('Calling versions()'); 28 | } 29 | }, [playerRef]); 30 | 31 | return ( 32 |
33 | 45 | 51 | 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", // Specify ECMAScript target version 4 | "module": "esnext", // Specify module code generation 5 | "moduleResolution": "node", // Resolve modules using Node.js style 6 | // "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export 7 | "resolveJsonModule": true, // Include modules imported with .json extension 8 | "jsx": "react", // Support JSX in .tsx files 9 | "lib": ["es5", "es6", "dom"], 10 | // "allowJs": true, // Allow JavaScript 11 | "skipLibCheck": true, 12 | // "esModuleInterop": true, 13 | "sourceMap": true, // Generate corresponding .map file 14 | "declaration": true, // Do not generate corresponding .d.ts file 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "strictNullChecks": true, 18 | "noImplicitReturns": true, // Report errors on implicit return types 19 | "noImplicitThis": true, // Report errors on implicit this type 20 | "noImplicitAny": true, // Report errors on implicit any types 21 | "noUnusedLocals": true, // Report errors on unused locals 22 | "noUnusedParameters": true, // Report errors on unused parameters 23 | "noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement 24 | "experimentalDecorators": true, // Enables experimental support for ES decorators 25 | "emitDecoratorMetadata": true, // Emit decorator metadata 26 | "importHelpers": false, // Import helper functions from tslib 27 | "preserveConstEnums": true, 28 | "noEmit": false, 29 | "plugins": [], 30 | "declarationDir": "" 31 | }, 32 | "include": ["src", "stories"], 33 | "exclude": ["node_modules"] 34 | } 35 | --------------------------------------------------------------------------------