├── .changeset ├── README.md └── config.json ├── .codesandbox └── ci.json ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── babel.config.js ├── demo ├── index.html ├── package.json ├── serve.json ├── src │ ├── App.jsx │ ├── index.css │ ├── index.jsx │ ├── sandboxes │ │ ├── animini-config-anime │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── anime.js │ │ │ │ └── styles.module.css │ │ ├── animini-config-fat │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── fat.js │ │ │ │ └── styles.module.css │ │ ├── animini-config │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── tsconfig.json │ │ ├── animini-drag │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── tsconfig.json │ │ ├── animini-inertia │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── tsconfig.json │ │ ├── animini-perf │ │ │ ├── package.json │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── Boxes │ │ │ │ ├── AnimeBox.jsx │ │ │ │ ├── AniminiBox.jsx │ │ │ │ ├── AniminiVanillaBox.jsx │ │ │ │ ├── FatBox.jsx │ │ │ │ ├── FramerMotionBox.jsx │ │ │ │ ├── GsapBox.jsx │ │ │ │ ├── MotionBox.jsx │ │ │ │ ├── SpringBox.jsx │ │ │ │ └── fat.js │ │ │ │ ├── index.css │ │ │ │ ├── index.jsx │ │ │ │ └── styles.module.css │ │ ├── animini-scroll │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── tsconfig.json │ │ ├── animini-three-perf │ │ │ ├── package.json │ │ │ └── src │ │ │ │ ├── App.jsx │ │ │ │ ├── index.css │ │ │ │ └── index.jsx │ │ └── animini-three │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ └── styles.module.css ├── tsconfig.json ├── vercel.json └── vite.config.js ├── package.json ├── packages ├── core-react │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── buildReactHook.ts │ │ └── index.ts ├── core │ ├── CHANGELOG.md │ ├── algorithms │ │ └── package.json │ ├── package.json │ └── src │ │ ├── FrameLoop.ts │ │ ├── algorithms │ │ ├── ease.ts │ │ ├── index.ts │ │ ├── inertia.ts │ │ ├── lerp.ts │ │ └── spring.ts │ │ ├── animated │ │ ├── Animated.ts │ │ └── AnimatedValue.ts │ │ ├── buildAnimate.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ ├── color.ts │ │ ├── index.ts │ │ ├── interpolate.ts │ │ ├── math.ts │ │ ├── object.ts │ │ └── string.ts ├── dom │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ └── index.ts ├── react-dom │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── useAnimateDom.ts ├── react-three │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── useAnimateThree.ts └── three │ ├── CHANGELOG.md │ ├── package.json │ └── src │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── targets ├── target-dom │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── adapters │ │ ├── color.ts │ │ ├── generic.ts │ │ ├── index.ts │ │ ├── string.ts │ │ └── transform.ts │ │ ├── index.ts │ │ ├── targetDom.ts │ │ ├── types.ts │ │ └── utils.ts └── target-three │ ├── CHANGELOG.md │ ├── package.json │ └── src │ ├── adapters │ ├── color.ts │ ├── euler.ts │ └── index.ts │ ├── index.ts │ ├── targetThree.ts │ └── types.ts ├── test-perf ├── .gitignore ├── package.json └── tests │ ├── bench.test.js │ ├── perf.test.js │ └── utils.js └── tsconfig.json /.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 | "changelog": "@changesets/cli/changelog", 3 | "commit": false, 4 | "linked": [["@animini/*"]], 5 | "access": "public", 6 | "baseBranch": "main", 7 | "updateInternalDependencies": "patch", 8 | "ignore": ["demo", "test-perf"] 9 | } 10 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "sandboxes": [ 4 | "/demo/src/sandboxes/animini-drag", 5 | "/demo/src/sandboxes/animini-inertia", 6 | "/demo/src/sandboxes/animini-config", 7 | "/demo/src/sandboxes/animini-scroll", 8 | "/demo/src/sandboxes/animini-perf", 9 | "/demo/src/sandboxes/animini-three", 10 | "/demo/src/sandboxes/animini-three-perf" 11 | ], 12 | "node": "16" 13 | } 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .yarn/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "no-cond-assign": "off", 5 | "no-console": "warn" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - '**/package.json' 8 | - '.changeset/**' 9 | - '.github/workflows/release.yml' 10 | env: 11 | HUSKY: 0 # Bypass husky commit hook for CI 12 | jobs: 13 | version: 14 | timeout-minutes: 8 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout code repository 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Use Node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '16' 26 | 27 | - name: Cache pnpm modules 28 | uses: actions/cache@v3 29 | with: 30 | path: ~/.pnpm-store 31 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 32 | restore-keys: | 33 | ${{ runner.os }}- 34 | 35 | - uses: pnpm/action-setup@v2.1.0 36 | with: 37 | version: 6.32.11 38 | run_install: true 39 | 40 | - name: Copy README file to React Dom package 41 | uses: canastro/copy-file-action@master 42 | with: 43 | source: 'README.md' 44 | target: 'packages/react-dom/README.md' 45 | 46 | - name: Copy README file to React Three package 47 | uses: canastro/copy-file-action@master 48 | with: 49 | source: 'README.md' 50 | target: 'packages/react-three/README.md' 51 | 52 | - name: Set up NPM credentials 53 | run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 54 | env: 55 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | 57 | - name: Create versions PR & prepare publish 58 | id: changesets 59 | uses: changesets/action@v1 60 | # if: github.ref == 'refs/heads/master' 61 | with: 62 | version: pnpm ci:version 63 | publish: pnpm ci:publish 64 | # Messages 65 | commit: 'chore(deploy): Release' 66 | title: 'chore(deploy): Release' 67 | env: 68 | # npm publish token required for publishing. Set this in secrets 69 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 70 | # automatically available in actions 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | .yarn/* 6 | !.yarn/releases 7 | !.yarn/plugins 8 | !.yarn/sdks 9 | !.yarn/versions 10 | .pnp.* 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | build/ 17 | dist/ 18 | .cache/ 19 | .parcel-cache/ 20 | 21 | # misc 22 | .DS_Store 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | storybook-static/ 33 | .idea 34 | cypress/screenshots 35 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "fluid": false 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm (tag)](https://img.shields.io/npm/v/@animini/dom?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@animini/dom) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@animini/dom?style=flat&colorA=000000&colorB=000000&label=gzipped)](https://bundlephobia.com/result?p=@animini/dom) 2 | 3 | ## Demo 4 | 5 | https://animini.vercel.app/ 6 | 7 | ## Installation 8 | 9 | ### For the React DOM 10 | 11 | ```bash 12 | yarn add @animini/react-dom 13 | ``` 14 | 15 | ### For React Three Fiber 16 | 17 | ```bash 18 | yarn add @animini/react-three 19 | ``` 20 | 21 | ### Instructions 22 | 23 | ```js 24 | import { useDrag } from '@use-gesture/react' 25 | import { useAnimate, spring } from '@animini/react-dom' 26 | 27 | const easing = spring() 28 | 29 | export default function App() { 30 | const [ref, api] = useAnimate() 31 | 32 | useDrag( 33 | ({ active, movement: [x, y] }) => { 34 | api.start({ scale: active ? 1.2 : 1, x: active ? x : 0, y: active ? y : 0 }, (k) => ({ 35 | immediate: k !== 'scale' && active, 36 | easing 37 | })) 38 | }, 39 | { target: ref } 40 | ) 41 | 42 | return
43 | } 44 | ``` 45 | 46 | ## Easings 47 | 48 | ### Lerp 49 | 50 | Lerp is the lightest, fastest and default easing algorithm for Animini. It supports a `factor` attribute that will change the momentum of the lerp. 51 | 52 | ```js 53 | import { useAnimate, lerp } from '@animini/react-dom' 54 | 55 | const easing = lerp({ factor: 0.05 }) 56 | api.start({ x: 100 }, { easing }) 57 | ``` 58 | 59 | ### Spring 60 | 61 | ```js 62 | import { useAnimate, spring } from '@animini/react-dom' 63 | 64 | const easing = spring({ 65 | tension: 170, // spring tension 66 | friction: 26, // spring friction 67 | mass: 1, // target mass 68 | velocity // initial velocity 69 | }) 70 | 71 | api.start({ x: 100 }, { easing }) 72 | ``` 73 | 74 | ### Ease (Bezier) 75 | 76 | ```js 77 | import { useAnimate, ease } from '@animini/react-dom' 78 | 79 | const easing = ease( 80 | 300, // duration of the ease in ms 81 | [0.25, 0.1, 0.25, 1] // coordinates of the bezier curve 82 | ) 83 | 84 | api.start({ x: 100 }, { easing }) 85 | ``` 86 | 87 | ### Inertia 88 | 89 | Inertia aims at emulating a thrown object. Inertia will not reach its destination and only works if the value is already moving or if the easing is given an initial velocity. 90 | 91 | Inertia supports `min` and `max` bounds which the element will bounce against as a rubberband bouncing on a wall. 92 | 93 | ```js 94 | import { useAnimate, inertia } from '@animini/react-dom' 95 | 96 | const easing = inertia({ 97 | momentum: 0.998, // momentum of the inertia 98 | velocity: undefined, // initial velocity (leave it undefined to use the current velocity of the value) 99 | min: -100, // min bound 100 | max: 100, // max bound 101 | rubberband = 0.15 // elasticity factor when reaching bounds defined by min / max 102 | }) 103 | 104 | api.start({ x: 100 }, { easing }) 105 | ``` 106 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | comments: false, 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | bugfixes: true, 8 | targets: { 9 | esmodules: true 10 | } 11 | } 12 | ], 13 | '@babel/preset-react', 14 | '@babel/preset-typescript' 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sandboxes Animini 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --port 3400 --host", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@animini/dom": "*", 12 | "@animini/react-dom": "*", 13 | "@animini/react-three": "*", 14 | "@leva-ui/plugin-bezier": "*", 15 | "@leva-ui/plugin-spring": "*", 16 | "@react-spring/web": "^9.4.5", 17 | "@react-three/drei": "9.13.0", 18 | "@react-three/fiber": "^8.0.24", 19 | "@use-gesture/react": "*", 20 | "animejs": "^3.2.1", 21 | "framer-motion": "^6.3.11", 22 | "gsap": "^3.10.4", 23 | "leva": "*", 24 | "motion": "^10.10.0", 25 | "react": "^18.1.0", 26 | "react-dom": "^18.1.0", 27 | "three": "^0.141.0", 28 | "tinycolor2": "^1.4.2", 29 | "wouter": "^2.8.0-alpha.2" 30 | }, 31 | "devDependencies": { 32 | "@types/three": "^0.141.0", 33 | "@vitejs/plugin-react": "^1.3.2", 34 | "typescript": "^4.7.3", 35 | "typescript-plugin-css-modules": "^3.4.0", 36 | "vite": "2.9.12" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/*", "destination": "/index.html" } 4 | ] 5 | } -------------------------------------------------------------------------------- /demo/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, Route } from 'wouter' 3 | 4 | import styles from './styles.module.css' 5 | 6 | import Drag from './sandboxes/animini-drag/src/App' 7 | // import ConfigFat from './sandboxes/animini-config-fat/src/App' 8 | import ConfigAnime from './sandboxes/animini-config-anime/src/App' 9 | import Inertia from './sandboxes/animini-inertia/src/App' 10 | import Scroll from './sandboxes/animini-scroll/src/App' 11 | import Config from './sandboxes/animini-config/src/App' 12 | import Perf from './sandboxes/animini-perf/src/App' 13 | import Three from './sandboxes/animini-three/src/App' 14 | import ThreePerf from './sandboxes/animini-three-perf/src/App' 15 | 16 | const links = { 17 | 'animini-drag': Drag, 18 | 'animini-inertia': Inertia, 19 | 'animini-scroll': Scroll, 20 | 'animini-config': Config, 21 | 'animini-perf': Perf, 22 | 'animini-three': Three, 23 | 'animini-three-perf': ThreePerf, 24 | 'animini-config-anime': ConfigAnime 25 | // 'animini-config-fat': ConfigFat, 26 | } 27 | 28 | const Example = ({ link }) => { 29 | const Component = links[link] 30 | 31 | return ( 32 | <> 33 | 34 | {/*eslint-disable-next-line jsx-a11y/anchor-is-valid */} 35 | ← Back 36 | 37 | 43 | Codesandbox 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default function App() { 51 | return ( 52 | <> 53 | 54 |
55 |
56 | 68 | Github repo → 69 | 70 |
71 |

Animini demos

72 |

Sandboxes

73 |
74 | {Object.keys(links).map((link) => ( 75 | 76 | {/*eslint-disable-next-line jsx-a11y/anchor-is-valid */} 77 | {link} 78 | 79 | ))} 80 |
81 |
82 |
83 | {(params) => } 84 | 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | margin: 0; 11 | } 12 | 13 | *, 14 | *:after, 15 | *:before { 16 | box-sizing: border-box; 17 | } 18 | 19 | .flex { 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .flex.fill { 25 | height: 100%; 26 | } 27 | 28 | .flex.center { 29 | justify-content: center; 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config-anime/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { useControls, button } from 'leva' 3 | import anime from './anime' 4 | 5 | import styles from './styles.module.css' 6 | 7 | export default function App() { 8 | const ref = useRef(null) 9 | 10 | useControls({ 11 | animate: button(async (get) => { 12 | anime({ 13 | targets: ref.current, 14 | translateX: '-10%', 15 | backgroundColor: '#00FF00' 16 | }) 17 | }) 18 | }) 19 | 20 | return ( 21 |
22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config-anime/src/anime.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Defaults 3 | 4 | const defaultInstanceSettings = { 5 | update: null, 6 | begin: null, 7 | loopBegin: null, 8 | changeBegin: null, 9 | change: null, 10 | changeComplete: null, 11 | loopComplete: null, 12 | complete: null, 13 | loop: 1, 14 | direction: 'normal', 15 | autoplay: true, 16 | timelineOffset: 0 17 | } 18 | 19 | const defaultTweenSettings = { 20 | duration: 1000, 21 | delay: 0, 22 | endDelay: 0, 23 | easing: 'easeOutElastic(1, .5)', 24 | round: 0 25 | } 26 | 27 | const validTransforms = [ 28 | 'translateX', 29 | 'translateY', 30 | 'translateZ', 31 | 'rotate', 32 | 'rotateX', 33 | 'rotateY', 34 | 'rotateZ', 35 | 'scale', 36 | 'scaleX', 37 | 'scaleY', 38 | 'scaleZ', 39 | 'skew', 40 | 'skewX', 41 | 'skewY', 42 | 'perspective', 43 | 'matrix', 44 | 'matrix3d' 45 | ] 46 | 47 | // Caching 48 | 49 | const cache = { 50 | CSS: {}, 51 | springs: {} 52 | } 53 | 54 | // Utils 55 | 56 | function minMax(val, min, max) { 57 | return Math.min(Math.max(val, min), max) 58 | } 59 | 60 | function stringContains(str, text) { 61 | return str.indexOf(text) > -1 62 | } 63 | 64 | function applyArguments(func, args) { 65 | return func.apply(null, args) 66 | } 67 | 68 | const is = { 69 | arr: (a) => Array.isArray(a), 70 | obj: (a) => stringContains(Object.prototype.toString.call(a), 'Object'), 71 | pth: (a) => is.obj(a) && a.hasOwnProperty('totalLength'), 72 | svg: (a) => a instanceof SVGElement, 73 | inp: (a) => a instanceof HTMLInputElement, 74 | dom: (a) => a.nodeType || is.svg(a), 75 | str: (a) => typeof a === 'string', 76 | fnc: (a) => typeof a === 'function', 77 | und: (a) => typeof a === 'undefined', 78 | nil: (a) => is.und(a) || a === null, 79 | hex: (a) => /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(a), 80 | rgb: (a) => /^rgb/.test(a), 81 | hsl: (a) => /^hsl/.test(a), 82 | col: (a) => is.hex(a) || is.rgb(a) || is.hsl(a), 83 | key: (a) => 84 | !defaultInstanceSettings.hasOwnProperty(a) && 85 | !defaultTweenSettings.hasOwnProperty(a) && 86 | a !== 'targets' && 87 | a !== 'keyframes' 88 | } 89 | 90 | // Easings 91 | 92 | function parseEasingParameters(string) { 93 | const match = /\(([^)]+)\)/.exec(string) 94 | return match ? match[1].split(',').map((p) => parseFloat(p)) : [] 95 | } 96 | 97 | // Spring solver inspired by Webkit Copyright © 2016 Apple Inc. All rights reserved. https://webkit.org/demos/spring/spring.js 98 | 99 | function spring(string, duration) { 100 | const params = parseEasingParameters(string) 101 | const mass = minMax(is.und(params[0]) ? 1 : params[0], 0.1, 100) 102 | const stiffness = minMax(is.und(params[1]) ? 100 : params[1], 0.1, 100) 103 | const damping = minMax(is.und(params[2]) ? 10 : params[2], 0.1, 100) 104 | const velocity = minMax(is.und(params[3]) ? 0 : params[3], 0.1, 100) 105 | const w0 = Math.sqrt(stiffness / mass) 106 | const zeta = damping / (2 * Math.sqrt(stiffness * mass)) 107 | const wd = zeta < 1 ? w0 * Math.sqrt(1 - zeta * zeta) : 0 108 | const a = 1 109 | const b = zeta < 1 ? (zeta * w0 + -velocity) / wd : -velocity + w0 110 | 111 | function solver(t) { 112 | let progress = duration ? (duration * t) / 1000 : t 113 | if (zeta < 1) { 114 | progress = Math.exp(-progress * zeta * w0) * (a * Math.cos(wd * progress) + b * Math.sin(wd * progress)) 115 | } else { 116 | progress = (a + b * progress) * Math.exp(-progress * w0) 117 | } 118 | if (t === 0 || t === 1) return t 119 | return 1 - progress 120 | } 121 | 122 | function getDuration() { 123 | const cached = cache.springs[string] 124 | if (cached) return cached 125 | const frame = 1 / 6 126 | let elapsed = 0 127 | let rest = 0 128 | while (true) { 129 | elapsed += frame 130 | if (solver(elapsed) === 1) { 131 | rest++ 132 | if (rest >= 16) break 133 | } else { 134 | rest = 0 135 | } 136 | } 137 | const duration = elapsed * frame * 1000 138 | cache.springs[string] = duration 139 | return duration 140 | } 141 | 142 | return duration ? solver : getDuration 143 | } 144 | 145 | // Basic steps easing implementation https://developer.mozilla.org/fr/docs/Web/CSS/transition-timing-function 146 | 147 | function steps(steps = 10) { 148 | return (t) => Math.ceil(minMax(t, 0.000001, 1) * steps) * (1 / steps) 149 | } 150 | 151 | // BezierEasing https://github.com/gre/bezier-easing 152 | 153 | const bezier = (() => { 154 | const kSplineTableSize = 11 155 | const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0) 156 | 157 | function A(aA1, aA2) { 158 | return 1.0 - 3.0 * aA2 + 3.0 * aA1 159 | } 160 | function B(aA1, aA2) { 161 | return 3.0 * aA2 - 6.0 * aA1 162 | } 163 | function C(aA1) { 164 | return 3.0 * aA1 165 | } 166 | 167 | function calcBezier(aT, aA1, aA2) { 168 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT 169 | } 170 | function getSlope(aT, aA1, aA2) { 171 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1) 172 | } 173 | 174 | function binarySubdivide(aX, aA, aB, mX1, mX2) { 175 | let currentX, 176 | currentT, 177 | i = 0 178 | do { 179 | currentT = aA + (aB - aA) / 2.0 180 | currentX = calcBezier(currentT, mX1, mX2) - aX 181 | if (currentX > 0.0) { 182 | aB = currentT 183 | } else { 184 | aA = currentT 185 | } 186 | } while (Math.abs(currentX) > 0.0000001 && ++i < 10) 187 | return currentT 188 | } 189 | 190 | function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { 191 | for (let i = 0; i < 4; ++i) { 192 | const currentSlope = getSlope(aGuessT, mX1, mX2) 193 | if (currentSlope === 0.0) return aGuessT 194 | const currentX = calcBezier(aGuessT, mX1, mX2) - aX 195 | aGuessT -= currentX / currentSlope 196 | } 197 | return aGuessT 198 | } 199 | 200 | function bezier(mX1, mY1, mX2, mY2) { 201 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) return 202 | let sampleValues = new Float32Array(kSplineTableSize) 203 | 204 | if (mX1 !== mY1 || mX2 !== mY2) { 205 | for (let i = 0; i < kSplineTableSize; ++i) { 206 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2) 207 | } 208 | } 209 | 210 | function getTForX(aX) { 211 | let intervalStart = 0 212 | let currentSample = 1 213 | const lastSample = kSplineTableSize - 1 214 | 215 | for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { 216 | intervalStart += kSampleStepSize 217 | } 218 | 219 | --currentSample 220 | 221 | const dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]) 222 | const guessForT = intervalStart + dist * kSampleStepSize 223 | const initialSlope = getSlope(guessForT, mX1, mX2) 224 | 225 | if (initialSlope >= 0.001) { 226 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2) 227 | } else if (initialSlope === 0.0) { 228 | return guessForT 229 | } else { 230 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2) 231 | } 232 | } 233 | 234 | return (x) => { 235 | if (mX1 === mY1 && mX2 === mY2) return x 236 | if (x === 0 || x === 1) return x 237 | return calcBezier(getTForX(x), mY1, mY2) 238 | } 239 | } 240 | 241 | return bezier 242 | })() 243 | 244 | const penner = (() => { 245 | // Based on jQuery UI's implemenation of easing equations from Robert Penner (http://www.robertpenner.com/easing) 246 | 247 | const eases = { linear: () => (t) => t } 248 | 249 | const functionEasings = { 250 | Sine: () => (t) => 1 - Math.cos((t * Math.PI) / 2), 251 | Circ: () => (t) => 1 - Math.sqrt(1 - t * t), 252 | Back: () => (t) => t * t * (3 * t - 2), 253 | Bounce: () => (t) => { 254 | let pow2, 255 | b = 4 256 | while (t < ((pow2 = Math.pow(2, --b)) - 1) / 11) {} 257 | return 1 / Math.pow(4, 3 - b) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - t, 2) 258 | }, 259 | Elastic: (amplitude = 1, period = 0.5) => { 260 | const a = minMax(amplitude, 1, 10) 261 | const p = minMax(period, 0.1, 2) 262 | return (t) => { 263 | return t === 0 || t === 1 264 | ? t 265 | : -a * 266 | Math.pow(2, 10 * (t - 1)) * 267 | Math.sin(((t - 1 - (p / (Math.PI * 2)) * Math.asin(1 / a)) * (Math.PI * 2)) / p) 268 | } 269 | } 270 | } 271 | 272 | const baseEasings = ['Quad', 'Cubic', 'Quart', 'Quint', 'Expo'] 273 | 274 | baseEasings.forEach((name, i) => { 275 | functionEasings[name] = () => (t) => Math.pow(t, i + 2) 276 | }) 277 | 278 | Object.keys(functionEasings).forEach((name) => { 279 | const easeIn = functionEasings[name] 280 | eases['easeIn' + name] = easeIn 281 | eases['easeOut' + name] = (a, b) => (t) => 1 - easeIn(a, b)(1 - t) 282 | eases['easeInOut' + name] = (a, b) => (t) => t < 0.5 ? easeIn(a, b)(t * 2) / 2 : 1 - easeIn(a, b)(t * -2 + 2) / 2 283 | eases['easeOutIn' + name] = (a, b) => (t) => 284 | t < 0.5 ? (1 - easeIn(a, b)(1 - t * 2)) / 2 : (easeIn(a, b)(t * 2 - 1) + 1) / 2 285 | }) 286 | 287 | return eases 288 | })() 289 | 290 | function parseEasings(easing, duration) { 291 | if (is.fnc(easing)) return easing 292 | const name = easing.split('(')[0] 293 | const ease = penner[name] 294 | const args = parseEasingParameters(easing) 295 | switch (name) { 296 | case 'spring': 297 | return spring(easing, duration) 298 | case 'cubicBezier': 299 | return applyArguments(bezier, args) 300 | case 'steps': 301 | return applyArguments(steps, args) 302 | default: 303 | return applyArguments(ease, args) 304 | } 305 | } 306 | 307 | // Strings 308 | 309 | function selectString(str) { 310 | try { 311 | let nodes = document.querySelectorAll(str) 312 | return nodes 313 | } catch (e) { 314 | return 315 | } 316 | } 317 | 318 | // Arrays 319 | 320 | function filterArray(arr, callback) { 321 | const len = arr.length 322 | const thisArg = arguments.length >= 2 ? arguments[1] : void 0 323 | const result = [] 324 | for (let i = 0; i < len; i++) { 325 | if (i in arr) { 326 | const val = arr[i] 327 | if (callback.call(thisArg, val, i, arr)) { 328 | result.push(val) 329 | } 330 | } 331 | } 332 | return result 333 | } 334 | 335 | function flattenArray(arr) { 336 | return arr.reduce((a, b) => a.concat(is.arr(b) ? flattenArray(b) : b), []) 337 | } 338 | 339 | function toArray(o) { 340 | if (is.arr(o)) return o 341 | if (is.str(o)) o = selectString(o) || o 342 | if (o instanceof NodeList || o instanceof HTMLCollection) return [].slice.call(o) 343 | return [o] 344 | } 345 | 346 | function arrayContains(arr, val) { 347 | return arr.some((a) => a === val) 348 | } 349 | 350 | // Objects 351 | 352 | function cloneObject(o) { 353 | const clone = {} 354 | for (let p in o) clone[p] = o[p] 355 | return clone 356 | } 357 | 358 | function replaceObjectProps(o1, o2) { 359 | const o = cloneObject(o1) 360 | for (let p in o1) o[p] = o2.hasOwnProperty(p) ? o2[p] : o1[p] 361 | return o 362 | } 363 | 364 | function mergeObjects(o1, o2) { 365 | const o = cloneObject(o1) 366 | for (let p in o2) o[p] = is.und(o1[p]) ? o2[p] : o1[p] 367 | return o 368 | } 369 | 370 | // Colors 371 | 372 | function rgbToRgba(rgbValue) { 373 | const rgb = /rgb\((\d+,\s*[\d]+,\s*[\d]+)\)/g.exec(rgbValue) 374 | return rgb ? `rgba(${rgb[1]},1)` : rgbValue 375 | } 376 | 377 | function hexToRgba(hexValue) { 378 | const rgx = /^#?([a-f\d])([a-f\d])([a-f\d])$/i 379 | const hex = hexValue.replace(rgx, (m, r, g, b) => r + r + g + g + b + b) 380 | const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 381 | const r = parseInt(rgb[1], 16) 382 | const g = parseInt(rgb[2], 16) 383 | const b = parseInt(rgb[3], 16) 384 | return `rgba(${r},${g},${b},1)` 385 | } 386 | 387 | function hslToRgba(hslValue) { 388 | const hsl = 389 | /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(hslValue) || 390 | /hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(hslValue) 391 | const h = parseInt(hsl[1], 10) / 360 392 | const s = parseInt(hsl[2], 10) / 100 393 | const l = parseInt(hsl[3], 10) / 100 394 | const a = hsl[4] || 1 395 | function hue2rgb(p, q, t) { 396 | if (t < 0) t += 1 397 | if (t > 1) t -= 1 398 | if (t < 1 / 6) return p + (q - p) * 6 * t 399 | if (t < 1 / 2) return q 400 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6 401 | return p 402 | } 403 | let r, g, b 404 | if (s == 0) { 405 | r = g = b = l 406 | } else { 407 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s 408 | const p = 2 * l - q 409 | r = hue2rgb(p, q, h + 1 / 3) 410 | g = hue2rgb(p, q, h) 411 | b = hue2rgb(p, q, h - 1 / 3) 412 | } 413 | return `rgba(${r * 255},${g * 255},${b * 255},${a})` 414 | } 415 | 416 | function colorToRgb(val) { 417 | if (is.rgb(val)) return rgbToRgba(val) 418 | if (is.hex(val)) return hexToRgba(val) 419 | if (is.hsl(val)) return hslToRgba(val) 420 | } 421 | 422 | // Units 423 | 424 | function getUnit(val) { 425 | const split = 426 | /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec( 427 | val 428 | ) 429 | if (split) return split[1] 430 | } 431 | 432 | function getTransformUnit(propName) { 433 | if (stringContains(propName, 'translate') || propName === 'perspective') return 'px' 434 | if (stringContains(propName, 'rotate') || stringContains(propName, 'skew')) return 'deg' 435 | } 436 | 437 | // Values 438 | 439 | function getFunctionValue(val, animatable) { 440 | if (!is.fnc(val)) return val 441 | return val(animatable.target, animatable.id, animatable.total) 442 | } 443 | 444 | function getAttribute(el, prop) { 445 | return el.getAttribute(prop) 446 | } 447 | 448 | function convertPxToUnit(el, value, unit) { 449 | const valueUnit = getUnit(value) 450 | if (arrayContains([unit, 'deg', 'rad', 'turn'], valueUnit)) return value 451 | const cached = cache.CSS[value + unit] 452 | if (!is.und(cached)) return cached 453 | const baseline = 100 454 | const tempEl = document.createElement(el.tagName) 455 | const parentEl = el.parentNode && el.parentNode !== document ? el.parentNode : document.body 456 | parentEl.appendChild(tempEl) 457 | tempEl.style.position = 'absolute' 458 | tempEl.style.width = baseline + unit 459 | const factor = baseline / tempEl.offsetWidth 460 | parentEl.removeChild(tempEl) 461 | const convertedUnit = factor * parseFloat(value) 462 | cache.CSS[value + unit] = convertedUnit 463 | return convertedUnit 464 | } 465 | 466 | function getCSSValue(el, prop, unit) { 467 | if (prop in el.style) { 468 | const uppercasePropName = prop.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() 469 | const value = el.style[prop] || getComputedStyle(el).getPropertyValue(uppercasePropName) || '0' 470 | return unit ? convertPxToUnit(el, value, unit) : value 471 | } 472 | } 473 | 474 | function getAnimationType(el, prop) { 475 | if (is.dom(el) && !is.inp(el) && (!is.nil(getAttribute(el, prop)) || (is.svg(el) && el[prop]))) return 'attribute' 476 | if (is.dom(el) && arrayContains(validTransforms, prop)) return 'transform' 477 | if (is.dom(el) && prop !== 'transform' && getCSSValue(el, prop)) return 'css' 478 | if (el[prop] != null) return 'object' 479 | } 480 | 481 | function getElementTransforms(el) { 482 | if (!is.dom(el)) return 483 | const str = el.style.transform || '' 484 | const reg = /(\w+)\(([^)]*)\)/g 485 | const transforms = new Map() 486 | let m 487 | while ((m = reg.exec(str))) transforms.set(m[1], m[2]) 488 | return transforms 489 | } 490 | 491 | function getTransformValue(el, propName, animatable, unit) { 492 | const defaultVal = stringContains(propName, 'scale') ? 1 : 0 + getTransformUnit(propName) 493 | const value = getElementTransforms(el).get(propName) || defaultVal 494 | if (animatable) { 495 | animatable.transforms.list.set(propName, value) 496 | animatable.transforms['last'] = propName 497 | } 498 | return unit ? convertPxToUnit(el, value, unit) : value 499 | } 500 | 501 | function getOriginalTargetValue(target, propName, unit, animatable) { 502 | switch (getAnimationType(target, propName)) { 503 | case 'transform': 504 | return getTransformValue(target, propName, animatable, unit) 505 | case 'css': 506 | return getCSSValue(target, propName, unit) 507 | case 'attribute': 508 | return getAttribute(target, propName) 509 | default: 510 | return target[propName] || 0 511 | } 512 | } 513 | 514 | function getRelativeValue(to, from) { 515 | const operator = /^(\*=|\+=|-=)/.exec(to) 516 | if (!operator) return to 517 | const u = getUnit(to) || 0 518 | const x = parseFloat(from) 519 | const y = parseFloat(to.replace(operator[0], '')) 520 | switch (operator[0][0]) { 521 | case '+': 522 | return x + y + u 523 | case '-': 524 | return x - y + u 525 | case '*': 526 | return x * y + u 527 | } 528 | } 529 | 530 | function validateValue(val, unit) { 531 | if (is.col(val)) return colorToRgb(val) 532 | if (/\s/g.test(val)) return val 533 | const originalUnit = getUnit(val) 534 | const unitLess = originalUnit ? val.substr(0, val.length - originalUnit.length) : val 535 | if (unit) return unitLess + unit 536 | return unitLess 537 | } 538 | 539 | // getTotalLength() equivalent for circle, rect, polyline, polygon and line shapes 540 | // adapted from https://gist.github.com/SebLambla/3e0550c496c236709744 541 | 542 | function getDistance(p1, p2) { 543 | return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)) 544 | } 545 | 546 | function getCircleLength(el) { 547 | return Math.PI * 2 * getAttribute(el, 'r') 548 | } 549 | 550 | function getRectLength(el) { 551 | return getAttribute(el, 'width') * 2 + getAttribute(el, 'height') * 2 552 | } 553 | 554 | function getLineLength(el) { 555 | return getDistance( 556 | { x: getAttribute(el, 'x1'), y: getAttribute(el, 'y1') }, 557 | { x: getAttribute(el, 'x2'), y: getAttribute(el, 'y2') } 558 | ) 559 | } 560 | 561 | function getPolylineLength(el) { 562 | const points = el.points 563 | let totalLength = 0 564 | let previousPos 565 | for (let i = 0; i < points.numberOfItems; i++) { 566 | const currentPos = points.getItem(i) 567 | if (i > 0) totalLength += getDistance(previousPos, currentPos) 568 | previousPos = currentPos 569 | } 570 | return totalLength 571 | } 572 | 573 | function getPolygonLength(el) { 574 | const points = el.points 575 | return getPolylineLength(el) + getDistance(points.getItem(points.numberOfItems - 1), points.getItem(0)) 576 | } 577 | 578 | // Path animation 579 | 580 | function getTotalLength(el) { 581 | if (el.getTotalLength) return el.getTotalLength() 582 | switch (el.tagName.toLowerCase()) { 583 | case 'circle': 584 | return getCircleLength(el) 585 | case 'rect': 586 | return getRectLength(el) 587 | case 'line': 588 | return getLineLength(el) 589 | case 'polyline': 590 | return getPolylineLength(el) 591 | case 'polygon': 592 | return getPolygonLength(el) 593 | } 594 | } 595 | 596 | function setDashoffset(el) { 597 | const pathLength = getTotalLength(el) 598 | el.setAttribute('stroke-dasharray', pathLength) 599 | return pathLength 600 | } 601 | 602 | // Motion path 603 | 604 | function getParentSvgEl(el) { 605 | let parentEl = el.parentNode 606 | while (is.svg(parentEl)) { 607 | if (!is.svg(parentEl.parentNode)) break 608 | parentEl = parentEl.parentNode 609 | } 610 | return parentEl 611 | } 612 | 613 | function getParentSvg(pathEl, svgData) { 614 | const svg = svgData || {} 615 | const parentSvgEl = svg.el || getParentSvgEl(pathEl) 616 | const rect = parentSvgEl.getBoundingClientRect() 617 | const viewBoxAttr = getAttribute(parentSvgEl, 'viewBox') 618 | const width = rect.width 619 | const height = rect.height 620 | const viewBox = svg.viewBox || (viewBoxAttr ? viewBoxAttr.split(' ') : [0, 0, width, height]) 621 | return { 622 | el: parentSvgEl, 623 | viewBox: viewBox, 624 | x: viewBox[0] / 1, 625 | y: viewBox[1] / 1, 626 | w: width, 627 | h: height, 628 | vW: viewBox[2], 629 | vH: viewBox[3] 630 | } 631 | } 632 | 633 | function getPath(path, percent) { 634 | const pathEl = is.str(path) ? selectString(path)[0] : path 635 | const p = percent || 100 636 | return function (property) { 637 | return { 638 | property, 639 | el: pathEl, 640 | svg: getParentSvg(pathEl), 641 | totalLength: getTotalLength(pathEl) * (p / 100) 642 | } 643 | } 644 | } 645 | 646 | function getPathProgress(path, progress, isPathTargetInsideSVG) { 647 | function point(offset = 0) { 648 | const l = progress + offset >= 1 ? progress + offset : 0 649 | return path.el.getPointAtLength(l) 650 | } 651 | const svg = getParentSvg(path.el, path.svg) 652 | const p = point() 653 | const p0 = point(-1) 654 | const p1 = point(+1) 655 | const scaleX = isPathTargetInsideSVG ? 1 : svg.w / svg.vW 656 | const scaleY = isPathTargetInsideSVG ? 1 : svg.h / svg.vH 657 | switch (path.property) { 658 | case 'x': 659 | return (p.x - svg.x) * scaleX 660 | case 'y': 661 | return (p.y - svg.y) * scaleY 662 | case 'angle': 663 | return (Math.atan2(p1.y - p0.y, p1.x - p0.x) * 180) / Math.PI 664 | } 665 | } 666 | 667 | // Decompose value 668 | 669 | function decomposeValue(val, unit) { 670 | // const rgx = /-?\d*\.?\d+/g; // handles basic numbers 671 | // const rgx = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g; // handles exponents notation 672 | const rgx = /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g // handles exponents notation 673 | const value = validateValue(is.pth(val) ? val.totalLength : val, unit) + '' 674 | return { 675 | original: value, 676 | numbers: value.match(rgx) ? value.match(rgx).map(Number) : [0], 677 | strings: is.str(val) || unit ? value.split(rgx) : [] 678 | } 679 | } 680 | 681 | // Animatables 682 | 683 | function parseTargets(targets) { 684 | const targetsArray = targets ? flattenArray(is.arr(targets) ? targets.map(toArray) : toArray(targets)) : [] 685 | return filterArray(targetsArray, (item, pos, self) => self.indexOf(item) === pos) 686 | } 687 | 688 | function getAnimatables(targets) { 689 | const parsed = parseTargets(targets) 690 | return parsed.map((t, i) => { 691 | return { target: t, id: i, total: parsed.length, transforms: { list: getElementTransforms(t) } } 692 | }) 693 | } 694 | 695 | // Properties 696 | 697 | function normalizePropertyTweens(prop, tweenSettings) { 698 | let settings = cloneObject(tweenSettings) 699 | // Override duration if easing is a spring 700 | if (/^spring/.test(settings.easing)) settings.duration = spring(settings.easing) 701 | if (is.arr(prop)) { 702 | const l = prop.length 703 | const isFromTo = l === 2 && !is.obj(prop[0]) 704 | if (!isFromTo) { 705 | // Duration divided by the number of tweens 706 | if (!is.fnc(tweenSettings.duration)) settings.duration = tweenSettings.duration / l 707 | } else { 708 | // Transform [from, to] values shorthand to a valid tween value 709 | prop = { value: prop } 710 | } 711 | } 712 | const propArray = is.arr(prop) ? prop : [prop] 713 | return propArray 714 | .map((v, i) => { 715 | const obj = is.obj(v) && !is.pth(v) ? v : { value: v } 716 | // Default delay value should only be applied to the first tween 717 | if (is.und(obj.delay)) obj.delay = !i ? tweenSettings.delay : 0 718 | // Default endDelay value should only be applied to the last tween 719 | if (is.und(obj.endDelay)) obj.endDelay = i === propArray.length - 1 ? tweenSettings.endDelay : 0 720 | return obj 721 | }) 722 | .map((k) => mergeObjects(k, settings)) 723 | } 724 | 725 | function flattenKeyframes(keyframes) { 726 | const propertyNames = filterArray(flattenArray(keyframes.map((key) => Object.keys(key))), (p) => is.key(p)).reduce( 727 | (a, b) => { 728 | if (a.indexOf(b) < 0) a.push(b) 729 | return a 730 | }, 731 | [] 732 | ) 733 | const properties = {} 734 | for (let i = 0; i < propertyNames.length; i++) { 735 | const propName = propertyNames[i] 736 | properties[propName] = keyframes.map((key) => { 737 | const newKey = {} 738 | for (let p in key) { 739 | if (is.key(p)) { 740 | if (p == propName) newKey.value = key[p] 741 | } else { 742 | newKey[p] = key[p] 743 | } 744 | } 745 | return newKey 746 | }) 747 | } 748 | return properties 749 | } 750 | 751 | function getProperties(tweenSettings, params) { 752 | const properties = [] 753 | const keyframes = params.keyframes 754 | if (keyframes) params = mergeObjects(flattenKeyframes(keyframes), params) 755 | for (let p in params) { 756 | if (is.key(p)) { 757 | properties.push({ 758 | name: p, 759 | tweens: normalizePropertyTweens(params[p], tweenSettings) 760 | }) 761 | } 762 | } 763 | return properties 764 | } 765 | 766 | // Tweens 767 | 768 | function normalizeTweenValues(tween, animatable) { 769 | const t = {} 770 | for (let p in tween) { 771 | let value = getFunctionValue(tween[p], animatable) 772 | if (is.arr(value)) { 773 | value = value.map((v) => getFunctionValue(v, animatable)) 774 | if (value.length === 1) value = value[0] 775 | } 776 | t[p] = value 777 | } 778 | t.duration = parseFloat(t.duration) 779 | t.delay = parseFloat(t.delay) 780 | return t 781 | } 782 | 783 | function normalizeTweens(prop, animatable) { 784 | let previousTween 785 | return prop.tweens.map((t) => { 786 | const tween = normalizeTweenValues(t, animatable) 787 | const tweenValue = tween.value 788 | let to = is.arr(tweenValue) ? tweenValue[1] : tweenValue 789 | const toUnit = getUnit(to) 790 | const originalValue = getOriginalTargetValue(animatable.target, prop.name, toUnit, animatable) 791 | const previousValue = previousTween ? previousTween.to.original : originalValue 792 | const from = is.arr(tweenValue) ? tweenValue[0] : previousValue 793 | const fromUnit = getUnit(from) || getUnit(originalValue) 794 | const unit = toUnit || fromUnit 795 | if (is.und(to)) to = previousValue 796 | tween.from = decomposeValue(from, unit) 797 | tween.to = decomposeValue(getRelativeValue(to, from), unit) 798 | tween.start = previousTween ? previousTween.end : 0 799 | tween.end = tween.start + tween.delay + tween.duration + tween.endDelay 800 | tween.easing = parseEasings(tween.easing, tween.duration) 801 | tween.isPath = is.pth(tweenValue) 802 | tween.isPathTargetInsideSVG = tween.isPath && is.svg(animatable.target) 803 | tween.isColor = is.col(tween.from.original) 804 | if (tween.isColor) tween.round = 1 805 | previousTween = tween 806 | return tween 807 | }) 808 | } 809 | 810 | // Tween progress 811 | 812 | const setProgressValue = { 813 | css: (t, p, v) => (t.style[p] = v), 814 | attribute: (t, p, v) => t.setAttribute(p, v), 815 | object: (t, p, v) => (t[p] = v), 816 | transform: (t, p, v, transforms, manual) => { 817 | transforms.list.set(p, v) 818 | if (p === transforms.last || manual) { 819 | let str = '' 820 | transforms.list.forEach((value, prop) => { 821 | str += `${prop}(${value}) ` 822 | }) 823 | t.style.transform = str 824 | } 825 | } 826 | } 827 | 828 | // Set Value helper 829 | 830 | function setTargetsValue(targets, properties) { 831 | const animatables = getAnimatables(targets) 832 | animatables.forEach((animatable) => { 833 | for (let property in properties) { 834 | const value = getFunctionValue(properties[property], animatable) 835 | const target = animatable.target 836 | const valueUnit = getUnit(value) 837 | const originalValue = getOriginalTargetValue(target, property, valueUnit, animatable) 838 | const unit = valueUnit || getUnit(originalValue) 839 | const to = getRelativeValue(validateValue(value, unit), originalValue) 840 | const animType = getAnimationType(target, property) 841 | setProgressValue[animType](target, property, to, animatable.transforms, true) 842 | } 843 | }) 844 | } 845 | 846 | // Animations 847 | 848 | function createAnimation(animatable, prop) { 849 | const animType = getAnimationType(animatable.target, prop.name) 850 | if (animType) { 851 | const tweens = normalizeTweens(prop, animatable) 852 | const lastTween = tweens[tweens.length - 1] 853 | return { 854 | type: animType, 855 | property: prop.name, 856 | animatable: animatable, 857 | tweens: tweens, 858 | duration: lastTween.end, 859 | delay: tweens[0].delay, 860 | endDelay: lastTween.endDelay 861 | } 862 | } 863 | } 864 | 865 | function getAnimations(animatables, properties) { 866 | return filterArray( 867 | flattenArray( 868 | animatables.map((animatable) => { 869 | return properties.map((prop) => { 870 | return createAnimation(animatable, prop) 871 | }) 872 | }) 873 | ), 874 | (a) => !is.und(a) 875 | ) 876 | } 877 | 878 | // Create Instance 879 | 880 | function getInstanceTimings(animations, tweenSettings) { 881 | const animLength = animations.length 882 | const getTlOffset = (anim) => (anim.timelineOffset ? anim.timelineOffset : 0) 883 | const timings = {} 884 | timings.duration = animLength 885 | ? Math.max.apply( 886 | Math, 887 | animations.map((anim) => getTlOffset(anim) + anim.duration) 888 | ) 889 | : tweenSettings.duration 890 | timings.delay = animLength 891 | ? Math.min.apply( 892 | Math, 893 | animations.map((anim) => getTlOffset(anim) + anim.delay) 894 | ) 895 | : tweenSettings.delay 896 | timings.endDelay = animLength 897 | ? timings.duration - 898 | Math.max.apply( 899 | Math, 900 | animations.map((anim) => getTlOffset(anim) + anim.duration - anim.endDelay) 901 | ) 902 | : tweenSettings.endDelay 903 | return timings 904 | } 905 | 906 | let instanceID = 0 907 | 908 | function createNewInstance(params) { 909 | const instanceSettings = replaceObjectProps(defaultInstanceSettings, params) 910 | const tweenSettings = replaceObjectProps(defaultTweenSettings, params) 911 | const properties = getProperties(tweenSettings, params) 912 | const animatables = getAnimatables(params.targets) 913 | const animations = getAnimations(animatables, properties) 914 | const timings = getInstanceTimings(animations, tweenSettings) 915 | const id = instanceID 916 | instanceID++ 917 | return mergeObjects(instanceSettings, { 918 | id: id, 919 | children: [], 920 | animatables: animatables, 921 | animations: animations, 922 | duration: timings.duration, 923 | delay: timings.delay, 924 | endDelay: timings.endDelay 925 | }) 926 | } 927 | 928 | // Core 929 | 930 | let activeInstances = [] 931 | 932 | const engine = (() => { 933 | let raf 934 | 935 | function play() { 936 | if (!raf && (!isDocumentHidden() || !anime.suspendWhenDocumentHidden) && activeInstances.length > 0) { 937 | raf = requestAnimationFrame(step) 938 | } 939 | } 940 | function step(t) { 941 | // memo on algorithm issue: 942 | // dangerous iteration over mutable `activeInstances` 943 | // (that collection may be updated from within callbacks of `tick`-ed animation instances) 944 | let activeInstancesLength = activeInstances.length 945 | let i = 0 946 | while (i < activeInstancesLength) { 947 | const activeInstance = activeInstances[i] 948 | if (!activeInstance.paused) { 949 | activeInstance.tick(t) 950 | i++ 951 | } else { 952 | activeInstances.splice(i, 1) 953 | activeInstancesLength-- 954 | } 955 | } 956 | raf = i > 0 ? requestAnimationFrame(step) : undefined 957 | } 958 | 959 | function handleVisibilityChange() { 960 | if (!anime.suspendWhenDocumentHidden) return 961 | 962 | if (isDocumentHidden()) { 963 | // suspend ticks 964 | raf = cancelAnimationFrame(raf) 965 | } else { 966 | // is back to active tab 967 | // first adjust animations to consider the time that ticks were suspended 968 | activeInstances.forEach((instance) => instance._onDocumentVisibility()) 969 | engine() 970 | } 971 | } 972 | if (typeof document !== 'undefined') { 973 | document.addEventListener('visibilitychange', handleVisibilityChange) 974 | } 975 | 976 | return play 977 | })() 978 | 979 | function isDocumentHidden() { 980 | return !!document && document.hidden 981 | } 982 | 983 | // Public Instance 984 | 985 | function anime(params = {}) { 986 | let startTime = 0, 987 | lastTime = 0, 988 | now = 0 989 | let children, 990 | childrenLength = 0 991 | let resolve = null 992 | 993 | function makePromise(instance) { 994 | const promise = window.Promise && new Promise((_resolve) => (resolve = _resolve)) 995 | instance.finished = promise 996 | return promise 997 | } 998 | 999 | let instance = createNewInstance(params) 1000 | let promise = makePromise(instance) 1001 | 1002 | function toggleInstanceDirection() { 1003 | const direction = instance.direction 1004 | if (direction !== 'alternate') { 1005 | instance.direction = direction !== 'normal' ? 'normal' : 'reverse' 1006 | } 1007 | instance.reversed = !instance.reversed 1008 | children.forEach((child) => (child.reversed = instance.reversed)) 1009 | } 1010 | 1011 | function adjustTime(time) { 1012 | return instance.reversed ? instance.duration - time : time 1013 | } 1014 | 1015 | function resetTime() { 1016 | startTime = 0 1017 | lastTime = adjustTime(instance.currentTime) * (1 / anime.speed) 1018 | } 1019 | 1020 | function seekChild(time, child) { 1021 | if (child) child.seek(time - child.timelineOffset) 1022 | } 1023 | 1024 | function syncInstanceChildren(time) { 1025 | if (!instance.reversePlayback) { 1026 | for (let i = 0; i < childrenLength; i++) seekChild(time, children[i]) 1027 | } else { 1028 | for (let i = childrenLength; i--; ) seekChild(time, children[i]) 1029 | } 1030 | } 1031 | 1032 | function setAnimationsProgress(insTime) { 1033 | let i = 0 1034 | const animations = instance.animations 1035 | const animationsLength = animations.length 1036 | while (i < animationsLength) { 1037 | const anim = animations[i] 1038 | const animatable = anim.animatable 1039 | const tweens = anim.tweens 1040 | const tweenLength = tweens.length - 1 1041 | let tween = tweens[tweenLength] 1042 | // Only check for keyframes if there is more than one tween 1043 | if (tweenLength) tween = filterArray(tweens, (t) => insTime < t.end)[0] || tween 1044 | const elapsed = minMax(insTime - tween.start - tween.delay, 0, tween.duration) / tween.duration 1045 | const eased = isNaN(elapsed) ? 1 : tween.easing(elapsed) 1046 | const strings = tween.to.strings 1047 | const round = tween.round 1048 | const numbers = [] 1049 | const toNumbersLength = tween.to.numbers.length 1050 | let progress 1051 | for (let n = 0; n < toNumbersLength; n++) { 1052 | let value 1053 | const toNumber = tween.to.numbers[n] 1054 | const fromNumber = tween.from.numbers[n] || 0 1055 | if (!tween.isPath) { 1056 | value = fromNumber + eased * (toNumber - fromNumber) 1057 | } else { 1058 | value = getPathProgress(tween.value, eased * toNumber, tween.isPathTargetInsideSVG) 1059 | } 1060 | if (round) { 1061 | if (!(tween.isColor && n > 2)) { 1062 | value = Math.round(value * round) / round 1063 | } 1064 | } 1065 | numbers.push(value) 1066 | } 1067 | // Manual Array.reduce for better performances 1068 | const stringsLength = strings.length 1069 | if (!stringsLength) { 1070 | progress = numbers[0] 1071 | } else { 1072 | progress = strings[0] 1073 | for (let s = 0; s < stringsLength; s++) { 1074 | const a = strings[s] 1075 | const b = strings[s + 1] 1076 | const n = numbers[s] 1077 | if (!isNaN(n)) { 1078 | if (!b) { 1079 | progress += n + ' ' 1080 | } else { 1081 | progress += n + b 1082 | } 1083 | } 1084 | } 1085 | } 1086 | setProgressValue[anim.type](animatable.target, anim.property, progress, animatable.transforms) 1087 | anim.currentValue = progress 1088 | i++ 1089 | } 1090 | } 1091 | 1092 | function setCallback(cb) { 1093 | if (instance[cb] && !instance.passThrough) instance[cb](instance) 1094 | } 1095 | 1096 | function countIteration() { 1097 | if (instance.remaining && instance.remaining !== true) { 1098 | instance.remaining-- 1099 | } 1100 | } 1101 | 1102 | function setInstanceProgress(engineTime) { 1103 | const insDuration = instance.duration 1104 | const insDelay = instance.delay 1105 | const insEndDelay = insDuration - instance.endDelay 1106 | const insTime = adjustTime(engineTime) 1107 | instance.progress = minMax((insTime / insDuration) * 100, 0, 100) 1108 | instance.reversePlayback = insTime < instance.currentTime 1109 | if (children) { 1110 | syncInstanceChildren(insTime) 1111 | } 1112 | if (!instance.began && instance.currentTime > 0) { 1113 | instance.began = true 1114 | setCallback('begin') 1115 | } 1116 | if (!instance.loopBegan && instance.currentTime > 0) { 1117 | instance.loopBegan = true 1118 | setCallback('loopBegin') 1119 | } 1120 | if (insTime <= insDelay && instance.currentTime !== 0) { 1121 | setAnimationsProgress(0) 1122 | } 1123 | if ((insTime >= insEndDelay && instance.currentTime !== insDuration) || !insDuration) { 1124 | setAnimationsProgress(insDuration) 1125 | } 1126 | if (insTime > insDelay && insTime < insEndDelay) { 1127 | if (!instance.changeBegan) { 1128 | instance.changeBegan = true 1129 | instance.changeCompleted = false 1130 | setCallback('changeBegin') 1131 | } 1132 | setCallback('change') 1133 | setAnimationsProgress(insTime) 1134 | } else { 1135 | if (instance.changeBegan) { 1136 | instance.changeCompleted = true 1137 | instance.changeBegan = false 1138 | setCallback('changeComplete') 1139 | } 1140 | } 1141 | instance.currentTime = minMax(insTime, 0, insDuration) 1142 | if (instance.began) setCallback('update') 1143 | if (engineTime >= insDuration) { 1144 | lastTime = 0 1145 | countIteration() 1146 | if (!instance.remaining) { 1147 | instance.paused = true 1148 | if (!instance.completed) { 1149 | instance.completed = true 1150 | setCallback('loopComplete') 1151 | setCallback('complete') 1152 | if (!instance.passThrough && 'Promise' in window) { 1153 | resolve() 1154 | promise = makePromise(instance) 1155 | } 1156 | } 1157 | } else { 1158 | startTime = now 1159 | setCallback('loopComplete') 1160 | instance.loopBegan = false 1161 | if (instance.direction === 'alternate') { 1162 | toggleInstanceDirection() 1163 | } 1164 | } 1165 | } 1166 | } 1167 | 1168 | instance.reset = function () { 1169 | const direction = instance.direction 1170 | instance.passThrough = false 1171 | instance.currentTime = 0 1172 | instance.progress = 0 1173 | instance.paused = true 1174 | instance.began = false 1175 | instance.loopBegan = false 1176 | instance.changeBegan = false 1177 | instance.completed = false 1178 | instance.changeCompleted = false 1179 | instance.reversePlayback = false 1180 | instance.reversed = direction === 'reverse' 1181 | instance.remaining = instance.loop 1182 | children = instance.children 1183 | childrenLength = children.length 1184 | for (let i = childrenLength; i--; ) instance.children[i].reset() 1185 | if ((instance.reversed && instance.loop !== true) || (direction === 'alternate' && instance.loop === 1)) 1186 | instance.remaining++ 1187 | setAnimationsProgress(instance.reversed ? instance.duration : 0) 1188 | } 1189 | 1190 | // internal method (for engine) to adjust animation timings before restoring engine ticks (rAF) 1191 | instance._onDocumentVisibility = resetTime 1192 | 1193 | // Set Value helper 1194 | 1195 | instance.set = function (targets, properties) { 1196 | setTargetsValue(targets, properties) 1197 | return instance 1198 | } 1199 | 1200 | instance.tick = function (t) { 1201 | now = t 1202 | if (!startTime) startTime = now 1203 | setInstanceProgress((now + (lastTime - startTime)) * anime.speed) 1204 | } 1205 | 1206 | instance.seek = function (time) { 1207 | setInstanceProgress(adjustTime(time)) 1208 | } 1209 | 1210 | instance.pause = function () { 1211 | instance.paused = true 1212 | resetTime() 1213 | } 1214 | 1215 | instance.play = function () { 1216 | if (!instance.paused) return 1217 | if (instance.completed) instance.reset() 1218 | instance.paused = false 1219 | activeInstances.push(instance) 1220 | resetTime() 1221 | engine() 1222 | } 1223 | 1224 | instance.reverse = function () { 1225 | toggleInstanceDirection() 1226 | instance.completed = instance.reversed ? false : true 1227 | resetTime() 1228 | } 1229 | 1230 | instance.restart = function () { 1231 | instance.reset() 1232 | instance.play() 1233 | } 1234 | 1235 | instance.remove = function (targets) { 1236 | const targetsArray = parseTargets(targets) 1237 | removeTargetsFromInstance(targetsArray, instance) 1238 | } 1239 | 1240 | instance.reset() 1241 | 1242 | if (instance.autoplay) instance.play() 1243 | 1244 | return instance 1245 | } 1246 | 1247 | // Remove targets from animation 1248 | 1249 | function removeTargetsFromAnimations(targetsArray, animations) { 1250 | for (let a = animations.length; a--; ) { 1251 | if (arrayContains(targetsArray, animations[a].animatable.target)) { 1252 | animations.splice(a, 1) 1253 | } 1254 | } 1255 | } 1256 | 1257 | function removeTargetsFromInstance(targetsArray, instance) { 1258 | const animations = instance.animations 1259 | const children = instance.children 1260 | removeTargetsFromAnimations(targetsArray, animations) 1261 | for (let c = children.length; c--; ) { 1262 | const child = children[c] 1263 | const childAnimations = child.animations 1264 | removeTargetsFromAnimations(targetsArray, childAnimations) 1265 | if (!childAnimations.length && !child.children.length) children.splice(c, 1) 1266 | } 1267 | if (!animations.length && !children.length) instance.pause() 1268 | } 1269 | 1270 | function removeTargetsFromActiveInstances(targets) { 1271 | const targetsArray = parseTargets(targets) 1272 | for (let i = activeInstances.length; i--; ) { 1273 | const instance = activeInstances[i] 1274 | removeTargetsFromInstance(targetsArray, instance) 1275 | } 1276 | } 1277 | 1278 | // Stagger helpers 1279 | 1280 | function stagger(val, params = {}) { 1281 | const direction = params.direction || 'normal' 1282 | const easing = params.easing ? parseEasings(params.easing) : null 1283 | const grid = params.grid 1284 | const axis = params.axis 1285 | let fromIndex = params.from || 0 1286 | const fromFirst = fromIndex === 'first' 1287 | const fromCenter = fromIndex === 'center' 1288 | const fromLast = fromIndex === 'last' 1289 | const isRange = is.arr(val) 1290 | const val1 = isRange ? parseFloat(val[0]) : parseFloat(val) 1291 | const val2 = isRange ? parseFloat(val[1]) : 0 1292 | const unit = getUnit(isRange ? val[1] : val) || 0 1293 | const start = params.start || 0 + (isRange ? val1 : 0) 1294 | let values = [] 1295 | let maxValue = 0 1296 | return (el, i, t) => { 1297 | if (fromFirst) fromIndex = 0 1298 | if (fromCenter) fromIndex = (t - 1) / 2 1299 | if (fromLast) fromIndex = t - 1 1300 | if (!values.length) { 1301 | for (let index = 0; index < t; index++) { 1302 | if (!grid) { 1303 | values.push(Math.abs(fromIndex - index)) 1304 | } else { 1305 | const fromX = !fromCenter ? fromIndex % grid[0] : (grid[0] - 1) / 2 1306 | const fromY = !fromCenter ? Math.floor(fromIndex / grid[0]) : (grid[1] - 1) / 2 1307 | const toX = index % grid[0] 1308 | const toY = Math.floor(index / grid[0]) 1309 | const distanceX = fromX - toX 1310 | const distanceY = fromY - toY 1311 | let value = Math.sqrt(distanceX * distanceX + distanceY * distanceY) 1312 | if (axis === 'x') value = -distanceX 1313 | if (axis === 'y') value = -distanceY 1314 | values.push(value) 1315 | } 1316 | maxValue = Math.max(...values) 1317 | } 1318 | if (easing) values = values.map((val) => easing(val / maxValue) * maxValue) 1319 | if (direction === 'reverse') 1320 | values = values.map((val) => (axis ? (val < 0 ? val * -1 : -val) : Math.abs(maxValue - val))) 1321 | } 1322 | const spacing = isRange ? (val2 - val1) / maxValue : val1 1323 | return start + spacing * (Math.round(values[i] * 100) / 100) + unit 1324 | } 1325 | } 1326 | 1327 | // Timeline 1328 | 1329 | function timeline(params = {}) { 1330 | let tl = anime(params) 1331 | tl.duration = 0 1332 | tl.add = function (instanceParams, timelineOffset) { 1333 | const tlIndex = activeInstances.indexOf(tl) 1334 | const children = tl.children 1335 | if (tlIndex > -1) activeInstances.splice(tlIndex, 1) 1336 | function passThrough(ins) { 1337 | ins.passThrough = true 1338 | } 1339 | for (let i = 0; i < children.length; i++) passThrough(children[i]) 1340 | let insParams = mergeObjects(instanceParams, replaceObjectProps(defaultTweenSettings, params)) 1341 | insParams.targets = insParams.targets || params.targets 1342 | const tlDuration = tl.duration 1343 | insParams.autoplay = false 1344 | insParams.direction = tl.direction 1345 | insParams.timelineOffset = is.und(timelineOffset) ? tlDuration : getRelativeValue(timelineOffset, tlDuration) 1346 | passThrough(tl) 1347 | tl.seek(insParams.timelineOffset) 1348 | const ins = anime(insParams) 1349 | passThrough(ins) 1350 | const totalDuration = ins.duration + insParams.timelineOffset 1351 | children.push(ins) 1352 | const timings = getInstanceTimings(children, params) 1353 | tl.delay = timings.delay 1354 | tl.endDelay = timings.endDelay 1355 | tl.duration = timings.duration 1356 | tl.seek(0) 1357 | tl.reset() 1358 | if (tl.autoplay) tl.play() 1359 | return tl 1360 | } 1361 | return tl 1362 | } 1363 | 1364 | anime.version = '3.2.1' 1365 | anime.speed = 1 1366 | // TODO:#review: naming, documentation 1367 | anime.suspendWhenDocumentHidden = true 1368 | anime.running = activeInstances 1369 | anime.remove = removeTargetsFromActiveInstances 1370 | anime.get = getOriginalTargetValue 1371 | anime.set = setTargetsValue 1372 | anime.convertPx = convertPxToUnit 1373 | anime.path = getPath 1374 | anime.setDashoffset = setDashoffset 1375 | anime.stagger = stagger 1376 | anime.timeline = timeline 1377 | anime.easing = parseEasings 1378 | anime.penner = penner 1379 | anime.random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min 1380 | 1381 | export default anime 1382 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config-anime/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .square { 2 | height: 200px; 3 | width: 200px; 4 | border-radius: 4px; 5 | background-color: #ec625c; 6 | touch-action: none; 7 | } 8 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config-fat/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { useControls, button } from 'leva' 3 | import fat from './fat' 4 | 5 | import styles from './styles.module.css' 6 | 7 | export default function App() { 8 | const ref = useRef(null) 9 | 10 | useControls({ 11 | animate: button(async (get) => { 12 | fat.animate(ref.current, { 13 | translateX: '-10%' 14 | // backgroundColor: '#00FF00' 15 | }) 16 | }) 17 | }) 18 | 19 | return ( 20 |
21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config-fat/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .square { 2 | height: 200px; 3 | width: 200px; 4 | border-radius: 4px; 5 | background-color: #ec625c; 6 | touch-action: none; 7 | transform: translateX(-50px); 8 | } 9 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-config", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "dependencies": { 6 | "@leva-ui/plugin-spring": "*", 7 | "@leva-ui/plugin-bezier": "*", 8 | "@animini/react-dom": "*", 9 | "leva": "*", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-scripts": "5.0.1" 13 | }, 14 | "devDependencies": { 15 | "typescript-plugin-css-modules": "^3.4.0", 16 | "@types/react": "^18.0.9", 17 | "@types/react-dom": "^18.0.3", 18 | "typescript": "^4.7.2" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useControls, button } from 'leva' 2 | import { spring as levaSpring } from '@leva-ui/plugin-spring' 3 | import { bezier } from '@leva-ui/plugin-bezier' 4 | import { useAnimate, spring, lerp, ease } from '@animini/react-dom' 5 | 6 | import styles from './styles.module.css' 7 | 8 | export default function App() { 9 | useControls({ 10 | easeMethod: { value: spring, options: { lerp, spring, ease } }, 11 | factor: { value: 0.05, min: 0, max: 1, optional: true, render: (get) => get('easeMethod') === lerp }, 12 | springConfig: levaSpring({ render: (get) => get('easeMethod') === spring }), 13 | easeConfig: bezier({ render: (get) => get('easeMethod') === ease }), 14 | duration: { value: 300, render: (get) => get('easeMethod') === ease }, 15 | animate: button(async (get) => { 16 | const method = get('easeMethod') 17 | let easing 18 | switch (method) { 19 | case lerp: 20 | easing = method({ factor: get('factor') }) 21 | break 22 | case spring: 23 | easing = method(get('springConfig')) 24 | break 25 | default: 26 | easing = method(get('duration'), get('easeConfig')) 27 | } 28 | try { 29 | await api.start({ scale: 1.5, rotate: 75 }, { easing }) 30 | await api.start({ scale: 1, rotate: 0, x: '50%' }, { easing }) 31 | await api.start({ clipPath: 'rect(0, 0px, 0px, 0px)', x: 0 }, { easing }) 32 | } catch {} 33 | }), 34 | stop: button(() => api.stop()) 35 | }) 36 | 37 | const [ref, api] = useAnimate() 38 | 39 | return ( 40 |
41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')!) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .square { 2 | height: 200px; 3 | width: 200px; 4 | border-radius: 4px; 5 | background-color: #ec625c; 6 | touch-action: none; 7 | transform: translate(10%) rotate(45deg); 8 | transform-origin: top; 9 | clip-path: inset(10px 20px 30px 40px); 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx", 8 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-drag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-drag", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "dependencies": { 6 | "@leva-ui/plugin-spring": "*", 7 | "@animini/dom": "*", 8 | "@animini/react-dom": "*", 9 | "@use-gesture/react": "*", 10 | "leva": "*", 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-scripts": "5.0.1" 14 | }, 15 | "devDependencies": { 16 | "typescript-plugin-css-modules": "^3.4.0", 17 | "@types/react": "^18.0.9", 18 | "@types/react-dom": "^18.0.3", 19 | "typescript": "^4.7.2" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | }, 27 | "browserslist": [ 28 | ">0.2%", 29 | "not dead", 30 | "not ie <= 11", 31 | "not op_mini all" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-drag/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useDrag } from '@use-gesture/react' 2 | import { useControls } from 'leva' 3 | import { spring as levaSpring } from '@leva-ui/plugin-spring' 4 | import { useAnimate, spring, lerp } from '@animini/react-dom' 5 | import styles from './styles.module.css' 6 | 7 | export default function App() { 8 | const { easeMethod, factor, springConfig, stickToDrag } = useControls({ 9 | stickToDrag: false, 10 | easeMethod: { value: lerp, options: { lerp, spring } }, 11 | factor: { value: 0.05, min: 0, max: 1, optional: true, render: (get) => get('easeMethod') === lerp }, 12 | springConfig: levaSpring({ render: (get) => get('easeMethod') === spring }) 13 | }) 14 | 15 | const [ref, api] = useAnimate() 16 | const easing = easeMethod(easeMethod === lerp ? { factor } : springConfig) 17 | 18 | useDrag( 19 | ({ active, movement: [x, y] }) => { 20 | api.start( 21 | { 22 | scale: active ? 1.2 : 1, 23 | x: active ? x : 0, 24 | y: active ? y : 0, 25 | backgroundColor: active ? '#5698cf50' : '#ec625c' 26 | }, 27 | (k) => ({ 28 | easing, 29 | immediate: k !== 'scale' && active && stickToDrag 30 | }) 31 | ) 32 | }, 33 | { target: ref } 34 | ) 35 | 36 | return ( 37 |
38 |
39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-drag/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-drag/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')!) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-drag/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .drag { 2 | height: 80px; 3 | width: 80px; 4 | background-color: #ec625c; 5 | touch-action: none; 6 | } 7 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-drag/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx", 8 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-inertia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-inertia", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "dependencies": { 6 | "@leva-ui/plugin-spring": "*", 7 | "@animini/react-dom": "*", 8 | "@use-gesture/react": "*", 9 | "leva": "*", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-scripts": "5.0.1" 13 | }, 14 | "devDependencies": { 15 | "typescript-plugin-css-modules": "^3.4.0", 16 | "@types/react": "^18.0.9", 17 | "@types/react-dom": "^18.0.3", 18 | "typescript": "^4.7.2" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-inertia/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useDrag } from '@use-gesture/react' 2 | import { useAnimate, inertia } from '@animini/react-dom' 3 | import { useControls } from 'leva' 4 | import styles from './styles.module.css' 5 | 6 | export default function App() { 7 | const [ref, api] = useAnimate() 8 | const { limitXY } = useControls({ 9 | limitXY: { 10 | value: { x: 200, y: 200 }, 11 | transient: false, 12 | onChange: (value) => { 13 | document.documentElement.style.setProperty('--limitX', value.x + 'px') 14 | document.documentElement.style.setProperty('--limitY', value.y + 'px') 15 | } 16 | } 17 | }) 18 | 19 | useDrag( 20 | ({ active, offset: [x, y] }) => { 21 | api.start({ x, y }, (key) => ({ 22 | //@ts-ignore 23 | easing: inertia({ min: -limitXY[key], max: limitXY[key] }), 24 | immediate: active 25 | })) 26 | }, 27 | { 28 | target: ref, 29 | from: () => [api.get('x') || 0, api.get('y') || 0] as [number, number], 30 | bounds: { left: -limitXY.x, right: limitXY.x, top: -limitXY.y, bottom: limitXY.y }, 31 | rubberband: true 32 | } 33 | ) 34 | 35 | return ( 36 |
37 |
38 |
39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-inertia/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-inertia/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')!) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-inertia/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .drag { 2 | height: 80px; 3 | width: 80px; 4 | background-color: #ec625c; 5 | touch-action: none; 6 | } 7 | 8 | .bounds { 9 | --border: 10px; 10 | --drag: 80px; 11 | position: absolute; 12 | pointer-events: none; 13 | border: var(--border) solid cornsilk; 14 | width: calc((var(--limitX, 300px) + var(--border)) * 2 + var(--drag)); 15 | height: calc((var(--limitY, 300px) + var(--border)) * 2 + var(--drag)); 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-inertia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx", 8 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-perf", 3 | "main": "src/index.jsx", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "@animini/dom": "*", 7 | "@animini/react-dom": "*", 8 | "@react-spring/web": "^9.4.5", 9 | "animejs": "^3.2.1", 10 | "framer-motion": "^6.3.11", 11 | "gsap": "^3.10.4", 12 | "leva": "0.9.25", 13 | "motion": "^10.10.0", 14 | "react": "^18.1.0", 15 | "react-dom": "^18.1.0", 16 | "tinycolor2": "^1.4.2", 17 | "react-scripts": "5.1.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useControls, button } from 'leva' 3 | import tinycolor from 'tinycolor2' 4 | import Animini from './Boxes/AniminiBox' 5 | import AniminiVanilla from './Boxes/AniminiVanillaBox' 6 | import ReactSpring from './Boxes/SpringBox' 7 | import Motion from './Boxes/MotionBox' 8 | import Gsap from './Boxes/GsapBox' 9 | import FramerMotion from './Boxes/FramerMotionBox' 10 | import FatJS from './Boxes/FatBox' 11 | import AnimeJS from './Boxes/AnimeBox' 12 | 13 | const COUNT = 4000 14 | 15 | const styles = Array(COUNT) 16 | .fill(1) 17 | .map(() => ({ 18 | position: 'absolute', 19 | top: Math.random() * 100 + 'vh', 20 | left: Math.random() * 100 + 'vw', 21 | width: 10, 22 | height: 10, 23 | backgroundColor: tinycolor.random().toHexString() 24 | })) 25 | 26 | const stillStyles = Array(COUNT).fill({ 27 | x: 0, 28 | y: 0, 29 | scale: 1, 30 | backgroundColor: tinycolor.random().toHexString() 31 | }) 32 | const moveStyles = stillStyles.map(() => ({ 33 | x: Math.random() * 500 - 250, 34 | y: Math.random() * 500 - 250, 35 | scale: 1 + Math.random(), 36 | backgroundColor: tinycolor.random().toHexString() 37 | })) 38 | 39 | export default function Perf() { 40 | const [move, setMove] = useState(false) 41 | const [clicked, setClicked] = useState(false) 42 | const { count, Model: Box } = useControls({ 43 | count: { value: 1000, min: 100, max: 4000 }, 44 | Model: { 45 | options: { Animini, AniminiVanilla, AnimeJS, ReactSpring, Motion, FramerMotion, Gsap, FatJS } 46 | }, 47 | Shuffle: button(() => { 48 | const ts = performance.now() 49 | setClicked(true) 50 | setMove((m) => !m) 51 | const raf = window.requestIdleCallback || window.requestAnimationFrame 52 | raf(() => { 53 | setClicked(false) 54 | // eslint-disable-next-line no-console 55 | console.log('TIME:', performance.now() - ts) 56 | }) 57 | }) 58 | }) 59 | 60 | const motionStyles = move ? moveStyles : stillStyles 61 | 62 | return ( 63 |
64 | 73 | {clicked && 'CLICKED'} 74 | 75 | {styles.slice(0, count).map((style, i) => ( 76 | 77 | ))} 78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/AnimeBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react' 2 | import anime from 'animejs' 3 | 4 | export default function Box({ x, y, backgroundColor, scale, style }) { 5 | const ref = useRef() 6 | 7 | useEffect(() => { 8 | anime({ 9 | targets: ref.current, 10 | translateX: x, 11 | translateY: y, 12 | scale, 13 | background: backgroundColor, 14 | easing: 'spring(1, 170, 26, 0)' 15 | }) 16 | }, [x, y, backgroundColor, scale]) 17 | 18 | return
19 | } 20 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/AniminiBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useAnimate, spring } from '@animini/react-dom' 3 | 4 | const config = { easing: spring() } 5 | 6 | export default function Box({ x, y, backgroundColor, scale, style }) { 7 | const [ref, api] = useAnimate() 8 | 9 | useEffect(() => { 10 | api.start({ x, y, backgroundColor, scale }, config) 11 | }, [x, y, backgroundColor, scale, api]) 12 | 13 | return
14 | } 15 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/AniminiVanillaBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import { animate, spring } from '@animini/dom' 3 | 4 | const config = { easing: spring() } 5 | 6 | export default function Box({ x, y, backgroundColor, scale, style }) { 7 | const ref = useRef() 8 | 9 | useEffect(() => { 10 | animate({ el: ref.current, ...config }, { x, y, backgroundColor, scale }) 11 | }, [x, y, backgroundColor, scale]) 12 | 13 | return
14 | } 15 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/FatBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react' 2 | import fat from './fat' 3 | 4 | export default function Box({ x, y, backgroundColor, scale, style }) { 5 | const ref = useRef() 6 | 7 | useEffect(() => { 8 | fat.animate( 9 | ref.current, 10 | { 11 | translateX: x + 'px', 12 | translateY: y + 'px', 13 | scaleX: scale, 14 | scaleY: scale, 15 | backgroundColor 16 | }, 17 | { ease: 'backInOut' } 18 | ) 19 | }, [x, y, backgroundColor, scale]) 20 | 21 | return
22 | } 23 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/FramerMotionBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { motion } from 'framer-motion' 3 | 4 | const spring = { 5 | type: 'spring', 6 | damping: 26, 7 | stiffness: 170 8 | } 9 | 10 | export default function Box({ x, y, backgroundColor, scale, style }) { 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/GsapBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react' 2 | import gsap from 'gsap' 3 | 4 | export default function Box({ x, y, backgroundColor, scale, style }) { 5 | const ref = useRef() 6 | 7 | useEffect(() => { 8 | gsap.to(ref.current, { 9 | x, 10 | y, 11 | scale, 12 | backgroundColor 13 | }) 14 | }, [x, y, backgroundColor, scale]) 15 | 16 | return
17 | } 18 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/MotionBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react' 2 | import { animate, spring } from 'motion' 3 | 4 | const easing = spring({ damping: 26, stiffness: 170 }) 5 | 6 | export default function Box({ x, y, backgroundColor, scale, style }) { 7 | const ref = useRef() 8 | 9 | useEffect(() => { 10 | animate( 11 | ref.current, 12 | { 13 | x, 14 | y, 15 | scale, 16 | backgroundColor 17 | }, 18 | { easing } 19 | ) 20 | }, [x, y, backgroundColor, scale]) 21 | 22 | return
23 | } 24 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/Boxes/SpringBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { a, useSpring } from '@react-spring/web' 3 | 4 | export default function Box({ x, y, backgroundColor, scale, style }) { 5 | const spring = useSpring({ x, y, backgroundColor, scale }) 6 | 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-perf/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | font-size: 14px; 4 | padding: 10px; 5 | align-items: center; 6 | } 7 | 8 | .buttons > * { 9 | margin-left: 4px; 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-scroll/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-scroll", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "dependencies": { 6 | "@animini/react-dom": "*", 7 | "leva": "*", 8 | "react": "^18.1.0", 9 | "react-dom": "^18.1.0", 10 | "react-scripts": "5.0.1" 11 | }, 12 | "devDependencies": { 13 | "typescript-plugin-css-modules": "^3.4.0", 14 | "@types/react": "^18.0.9", 15 | "@types/react-dom": "^18.0.3", 16 | "typescript": "^4.7.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-scroll/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useControls, button } from 'leva' 2 | import { useAnimate } from '@animini/react-dom' 3 | 4 | import styles from './styles.module.css' 5 | 6 | export default function App() { 7 | useControls({ 8 | duration: { value: 300 }, 9 | 'scroll to bottom': button(() => api.start({ scrollTop: '300vh' })), 10 | 'scroll to top': button(() => api.start({ scrollTop: 0 })) 11 | }) 12 | 13 | const api = useAnimate({ el: window }) 14 | 15 | return ( 16 |
17 |
18 |
19 |
20 | Introduction Any Fraoch Heather Ale can find lice on some sudsy dude, but it takes a real Full Sail IPA to 21 | assimilate a Sam Adams. If a blue moon over a bar stool buries another corona light, then a Corona near the 22 | bullfrog brew procrastinates. A Heineken falls in love with a Hops Alligator Ale beyond a Pilsner. A malt 23 | from a chain saw goes to sleep, and another Keystone knowingly tries to seduce the dude over a Keystone 24 | light. Another hairy PBR When you see a broken bottle, it means that a tornado brew gets stinking drunk. 25 | Most people believe that a power drill drink accidentally has a change of heart about the sake bomb, but 26 | they need to remember how carelessly an incinerated beer procrastinates. When a broken bottle meditates, the 27 | change around a Pilsner Urquell meditates. Now and then, a thoroughly drunk Guiness slyly befriends the 28 | dumbly so-called power drill drink. When a Jamaica Red Ale is feline, a girl scout derives perverse 29 | satisfaction from the miller inside a St. Pauli Girl. The Miller beyond a Guiness The St. Pauli Girl over a 30 | bottle starts reminiscing about a lost buzz, but a stein can be kind to a Dixie Beer beyond a girl scout. 31 | Sometimes a Wolverine Beer dies, but another ravishing Busch always has a change of heart about a bullfrog 32 | brew for some blood clot! A moldy Kashmir IPA caricatures a Busch from the ESB. The Pilsner tries to seduce 33 | another blotched blue moon. The Yuengling is frozen. A power drill drink An Alaskan bill takes a coffee 34 | break, but the bottle from another Sam Adams carelessly takes a peek at a thoroughly boiled Guiness. 35 | Sometimes a mug returns home, but another IPA defined by the Miller always plays pinochle with a skinny 36 | Fraoch Heather Ale! When a pathetic Hazed and Infused hibernates, a Coors goes to sleep. The coors light 37 | recognizes an Imperial Stout defined by a Luna Sea ESB. Conclusions A Hefeweizen assimilates the linguistic 38 | Hommel Bier. A Yuengling sanitizes a colt 45 inside a Budweiser. Now and then, an Amarillo Pale Ale borrows 39 | money from the resplendent miller. A Sierra Nevada shares a shower with an annoying girl scout. A Bacardi 40 | Silver for the monkey bite figures out a dreamlike PBR. 41 |
42 |
43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-scroll/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-scroll/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')!) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-scroll/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .scroller { 2 | width: 100vw; 3 | /* height: 100vh; */ 4 | /* overflow: scroll; */ 5 | line-height: 1.6; 6 | font-size: 2.2em; 7 | font-weight: 600; 8 | } 9 | 10 | .scroller > div { 11 | padding: 10vh 0; 12 | height: 400vh; 13 | width: 100vw; 14 | background: linear-gradient(0deg, rgba(131, 58, 180, 1) 0%, rgba(253, 29, 29, 1) 50%, rgba(252, 176, 69, 1) 100%); 15 | } 16 | 17 | .scroller > div > div { 18 | max-width: 720px; 19 | margin: 0 auto; 20 | mix-blend-mode: color-dodge; 21 | color: indianred; 22 | } 23 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-scroll/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx", 8 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three-perf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-three-perf", 3 | "version": "1.0.0", 4 | "main": "src/index.jsx", 5 | "dependencies": { 6 | "@animini/react-three": "*", 7 | "@leva-ui/plugin-spring": "*", 8 | "@react-three/drei": "^9.11.3", 9 | "@react-three/fiber": "^8.0.18", 10 | "leva": "*", 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-scripts": "5.0.1", 14 | "three": "*" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three-perf/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useLayoutEffect } from 'react' 2 | import * as THREE from 'three' 3 | import { Canvas } from '@react-three/fiber' 4 | import { useAnimate, spring } from '@animini/react-three' 5 | import { useControls } from 'leva' 6 | import { spring as levaSpring } from '@leva-ui/plugin-spring' 7 | 8 | const colors = ['#A2CCB6', '#FCEEB5', '#EE786E', '#e0feff'] 9 | 10 | function Box({ position, scale, rotation, color }) { 11 | const [mesh, setMesh] = useAnimate() 12 | const [material, setMaterial] = useAnimate() 13 | const { springConfig } = useControls({ springConfig: levaSpring() }) 14 | 15 | useLayoutEffect(() => { 16 | const config = { easing: spring(springConfig) } 17 | setMesh.start({ position, scale, rotation }, config) 18 | setMaterial.start({ color }, config) 19 | }, [color, position, scale, rotation, springConfig, setMesh, setMaterial]) 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | function Content() { 30 | const [, set] = useState(0) 31 | const { number } = useControls({ number: { value: 50, step: 1, min: 10, max: 1000 } }) 32 | const data = new Array(number).fill().map((_, i) => { 33 | const r = Math.random() 34 | return { 35 | position: { x: 250 - Math.random() * 500, y: 250 - Math.random() * 500, z: i * 3 }, 36 | color: colors[Math.round(Math.random() * (colors.length - 1))], 37 | scale: { x: 1 + r * 200, y: 1 + r * 100, z: 10 }, 38 | rotation: { x: 0, y: 0, z: THREE.MathUtils.degToRad(Math.round(Math.random()) * 45) } 39 | } 40 | }) 41 | 42 | useEffect(() => void setInterval(() => set((i) => i + 1), 2000), []) 43 | return data.map((props, index) => ) 44 | } 45 | 46 | function Lights() { 47 | return ( 48 | 49 | 50 | 51 | 59 | 60 | ) 61 | } 62 | 63 | export default function App() { 64 | return ( 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three-perf/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three-perf/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animini-three", 3 | "version": "1.0.0", 4 | "main": "src/index.tsx", 5 | "dependencies": { 6 | "@animini/react-three": "*", 7 | "@react-three/drei": "^9.11.3", 8 | "@react-three/fiber": "^8.0.18", 9 | "leva": "*", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-scripts": "5.0.1", 13 | "three": "*" 14 | }, 15 | "devDependencies": { 16 | "typescript-plugin-css-modules": "^3.4.0", 17 | "@types/react": "^18.0.9", 18 | "@types/react-dom": "^18.0.3", 19 | "@types/three": "^0.140.0", 20 | "typescript": "^4.7.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useAnimate } from '@animini/react-three' 2 | import { useControls, button } from 'leva' 3 | import { Canvas } from '@react-three/fiber' 4 | import { OrbitControls } from '@react-three/drei' 5 | import * as THREE from 'three' 6 | 7 | const torusknot = new THREE.TorusKnotBufferGeometry(3, 0.8, 256, 16) 8 | 9 | const Mesh = () => { 10 | const [mat, apiMat] = useAnimate() 11 | 12 | const [ref, api] = useAnimate() 13 | useControls({ 14 | changeColor: button(() => apiMat.start({ color: 'rgb(255,0,0)' })), 15 | changeScale: button(() => api.start({ scale: { x: 2, y: 1, z: 2 } })), 16 | changeRotation: button(() => api.start({ rotation: { x: 2, y: 1, z: 2 } })) 17 | }) 18 | 19 | return ( 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | export default function App() { 27 | return ( 28 | 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | font-family: system-ui; 10 | min-height: 100vh; 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *:after, 16 | *:before { 17 | box-sizing: border-box; 18 | } 19 | 20 | .flex { 21 | display: flex; 22 | align-items: center; 23 | } 24 | 25 | .flex.fill { 26 | height: 100%; 27 | } 28 | 29 | .flex.center { 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | import './index.css' 6 | 7 | const root = createRoot(document.getElementById('root')!) 8 | root.render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /demo/src/sandboxes/animini-three/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "esModuleInterop": true, 6 | "lib": ["dom", "es2015"], 7 | "jsx": "react-jsx", 8 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/styles.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | margin: 0 auto; 3 | max-width: 720px; 4 | padding: 20vh 16px 0; 5 | min-height: 100vh; 6 | } 7 | 8 | .btn { 9 | padding: 10px; 10 | font-weight: 500; 11 | font-size: 14px; 12 | text-decoration: none; 13 | border-radius: 2px; 14 | border: 1px solid transparent; 15 | } 16 | 17 | .back { 18 | position: fixed; 19 | left: 10px; 20 | bottom: 10px; 21 | z-index: 100; 22 | background-color: #000; 23 | border-color: #333; 24 | color: #fff; 25 | } 26 | 27 | .source { 28 | position: fixed; 29 | right: 10px; 30 | bottom: 10px; 31 | z-index: 100; 32 | background-color: #000; 33 | border-color: #333; 34 | color: #fff; 35 | } 36 | 37 | .link { 38 | color: inherit; 39 | } 40 | 41 | .linkList { 42 | display: inline-flex; 43 | flex-direction: column; 44 | gap: 10px; 45 | } 46 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": ["vite/client"], 6 | "allowJs": true, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react", 18 | "plugins": [{ "name": "typescript-plugin-css-modules" }] 19 | }, 20 | "include": ["./src"] 21 | } 22 | -------------------------------------------------------------------------------- /demo/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | // build: { 8 | // minify: false 9 | // } 10 | }) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/root", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "private": true, 6 | "preconstruct": { 7 | "packages": [ 8 | "packages/*", 9 | "targets/*" 10 | ] 11 | }, 12 | "scripts": { 13 | "build": "preconstruct build", 14 | "watch": "preconstruct watch", 15 | "dev": "preconstruct dev", 16 | "validate": "preconstruct validate", 17 | "lint": "pretty-quick --staged", 18 | "release": "pnpm build && pnpm changeset publish", 19 | "tsc": "tsc --noEmit", 20 | "size": "pnpm size-limit", 21 | "test:size": "pnpm build && pnpm size", 22 | "prepare": "husky install", 23 | "demo:dev": "pnpm --filter demo run dev", 24 | "demo:build": "pnpm --filter demo run build", 25 | "demo:serve": "pnpm --filter demo run build && pnpm --filter demo run serve", 26 | "test:perf": "jest ./test-perf/tests/perf.*.js", 27 | "test:bench": "jest ./test-perf/tests/bench.*.js", 28 | "ci:version": "pnpm changeset version && pnpm --filter \"@animini/*\" install --lockfile-only", 29 | "ci:publish": "pnpm build && pnpm changeset publish" 30 | }, 31 | "size-limit": [ 32 | { 33 | "name": "react-dom", 34 | "import": "{ useAnimate }", 35 | "limit": "5 KB", 36 | "path": "packages/react-dom/dist/*.esm.js" 37 | }, 38 | { 39 | "name": "react-three", 40 | "import": "{ useAnimate }", 41 | "limit": "3 KB", 42 | "path": "packages/react-three/dist/*.esm.js", 43 | "ignore": [ 44 | "three", 45 | "@react-three/fiber" 46 | ] 47 | } 48 | ], 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/dbismut/animini.git" 52 | }, 53 | "bugs": "https://github.com/dbismut/animini/issues", 54 | "peerDependencies": { 55 | "react": ">=16.8.0", 56 | "react-dom": ">=16.8.0" 57 | }, 58 | "devDependencies": { 59 | "@babel/core": "^7.18.5", 60 | "@babel/eslint-parser": "^7.18.2", 61 | "@babel/preset-env": "^7.18.2", 62 | "@babel/preset-react": "^7.17.12", 63 | "@babel/preset-typescript": "^7.17.12", 64 | "@changesets/cli": "^2.23.0", 65 | "@preconstruct/cli": "^2.1.5", 66 | "@react-three/fiber": "^8.0.24", 67 | "@size-limit/preset-small-lib": "^7.0.8", 68 | "@types/react": "^18.0.12", 69 | "@types/react-dom": "^18.0.4", 70 | "@types/three": "^0.141.0", 71 | "@typescript-eslint/eslint-plugin": "^5.28.0", 72 | "@typescript-eslint/parser": "^5.28.0", 73 | "all-contributors-cli": "^6.20.0", 74 | "eslint": "^8.17.0", 75 | "eslint-config-react-app": "^7.0.1", 76 | "husky": "^8.0.1", 77 | "jest": "^28.1.1", 78 | "pnpm": "^7.2.1", 79 | "prettier": "^2.7.0", 80 | "pretty-quick": "^3.1.3", 81 | "react": "^18.1.0", 82 | "react-dom": "^18.1.0", 83 | "size-limit": "^7.0.8", 84 | "three": "^0.141.0", 85 | "ts-jest": "^28.0.5", 86 | "tsd": "^0.21.0", 87 | "typescript": "^4.7.3" 88 | }, 89 | "prettier": { 90 | "printWidth": 120, 91 | "tabWidth": 2, 92 | "useTabs": false, 93 | "semi": false, 94 | "singleQuote": true, 95 | "trailingComma": "none", 96 | "bracketSpacing": true, 97 | "bracketSameLine": false, 98 | "fluid": false 99 | }, 100 | "jest": { 101 | "transformIgnorePatterns": [ 102 | "/node_modules/(?!(@animini/.*-latest)/)" 103 | ] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/core-react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/core-react 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [2d867a9] 12 | - @animini/core@0.3.0 13 | 14 | ## 0.2.6 15 | 16 | ### Patch Changes 17 | 18 | - Updated dependencies [c838bf8] 19 | - @animini/core@0.2.6 20 | 21 | ## 0.2.5 22 | 23 | ### Patch Changes 24 | 25 | - Updated dependencies [0885116] 26 | - @animini/core@0.2.5 27 | 28 | ## 0.2.4 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies [a0f7cdb] 33 | - @animini/core@0.2.4 34 | 35 | ## 0.2.3 36 | 37 | ### Patch Changes 38 | 39 | - fd31840: Remove cached values 40 | - Updated dependencies [fd31840] 41 | - @animini/core@0.2.3 42 | 43 | ## 0.2.2 44 | 45 | ### Patch Changes 46 | 47 | - 6c367d3: add syncCachedValues param to buildAnimate 48 | - Updated dependencies [6c367d3] 49 | - @animini/core@0.2.2 50 | 51 | ## 0.2.1 52 | 53 | ### Patch Changes 54 | 55 | - bf2fd1b: Internal refactoring 56 | - Updated dependencies [bf2fd1b] 57 | - @animini/core@0.2.1 58 | 59 | ## 0.2.0 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [10ba638] 64 | - @animini/core@0.2.0 65 | -------------------------------------------------------------------------------- /packages/core-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/core-react", 3 | "version": "0.3.0", 4 | "description": "React core for animini", 5 | "keywords": [], 6 | "main": "dist/animini-core-react.cjs.js", 7 | "module": "dist/animini-core-react.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/core-react" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "peerDependencies": { 16 | "react": ">=16.8.0" 17 | }, 18 | "dependencies": { 19 | "@animini/core": "0.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core-react/src/buildReactHook.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useState } from 'react' 2 | import { ApiType, buildAnimate, ConfigWithOptionalEl, Payload, Target } from '@animini/core' 3 | 4 | type HookReturnType> = C['el'] extends ElementType 5 | ? ApiType 6 | : [React.RefObject, ApiType] 7 | 8 | export function buildReactHook(target: Target) { 9 | const animate = buildAnimate(target) 10 | 11 | return function useAnimate< 12 | ElementType, 13 | C extends ConfigWithOptionalEl, 14 | Values extends BuildValues = BuildValues 15 | >(masterConfig?: C): HookReturnType { 16 | const el = useRef(null) 17 | 18 | const [[_el, api]] = useState(() => { 19 | let _el 20 | let _config = masterConfig 21 | if (_config && 'el' in _config) { 22 | _el = _config.el 23 | } else { 24 | // @ts-ignore 25 | _config = { ...masterConfig, el } 26 | } 27 | return [_el, animate(_config as any)] as [ElementType | undefined, ApiType] 28 | }) 29 | 30 | useEffect(() => { 31 | return () => api.clean() 32 | }, [api]) 33 | 34 | if (_el) return api as any 35 | return [el, api] as any 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { buildReactHook } from './buildReactHook' 2 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/core 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ## 0.2.6 10 | 11 | ### Patch Changes 12 | 13 | - c838bf8: Support for scrollTop / scrollLeft 14 | 15 | ## 0.2.5 16 | 17 | ### Patch Changes 18 | 19 | - 0885116: Add string interpolation 20 | 21 | ## 0.2.4 22 | 23 | ### Patch Changes 24 | 25 | - a0f7cdb: Adapter inside Animated 26 | 27 | ## 0.2.3 28 | 29 | ### Patch Changes 30 | 31 | - fd31840: Remove cached values 32 | 33 | ## 0.2.2 34 | 35 | ### Patch Changes 36 | 37 | - 6c367d3: add syncCachedValues param to buildAnimate 38 | 39 | ## 0.2.1 40 | 41 | ### Patch Changes 42 | 43 | - bf2fd1b: Internal refactoring 44 | 45 | ## 0.2.0 46 | 47 | ### Minor Changes 48 | 49 | - 10ba638: Refactor package 50 | 51 | ## 0.1.4 52 | 53 | ### Patch Changes 54 | 55 | - a7b464c: dom: immediate transition to NaN strings 56 | 57 | ## 0.1.3 58 | 59 | ### Patch Changes 60 | 61 | - bb94d14: fix timestamp of first frame 62 | 63 | ## 0.1.2 64 | 65 | ### Patch Changes 66 | 67 | - 6b9d542: fix global loop in three 68 | 69 | ## 0.1.1 70 | 71 | ### Patch Changes 72 | 73 | - 944cfc7: improve color algorithm 74 | 75 | ## 0.1.0 76 | 77 | ### Minor Changes 78 | 79 | - b442912: - typescript 80 | - promise-based 81 | - added api.stop 82 | - ability to change algorithm per animation call 83 | - added ease algorithm 84 | - added inertia algorithm 85 | - improved color parsing for dom 86 | - improved color parsing for three 87 | - unit support for dom 88 | 89 | ## 0.0.6 90 | 91 | ### Patch Changes 92 | 93 | - 891dbf7: revert mechanics 94 | 95 | ## 0.0.5 96 | 97 | ### Patch Changes 98 | 99 | - 098cac2: feat: one controller for all simultaneous values 100 | 101 | ## 0.0.4 102 | 103 | ### Patch Changes 104 | 105 | - 748a414: perf: use prototype getter and setter which leads to massive perf increase 106 | 107 | ## 0.0.3 108 | 109 | ### Patch Changes 110 | 111 | - 9c2cd35: fix: time delta can't be null 112 | 113 | ## 0.0.2 114 | 115 | ### Patch Changes 116 | 117 | - a8301ed: feat: hook r3f raf 118 | -------------------------------------------------------------------------------- /packages/core/algorithms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "dist/animini-core-algorithms.cjs.js", 3 | "module": "dist/animini-core-algorithms.esm.js" 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/core", 3 | "version": "0.3.0", 4 | "description": "Core of animini", 5 | "keywords": [], 6 | "main": "dist/animini-core.cjs.js", 7 | "module": "dist/animini-core.esm.js", 8 | "sideEffects": false, 9 | "preconstruct": { 10 | "entrypoints": [ 11 | "./index.ts", 12 | "./algorithms/index.ts" 13 | ] 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/dbismut/animini.git", 18 | "directory": "packages/core" 19 | }, 20 | "bugs": "https://github.com/dbismut/animini/issues" 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/FrameLoop.ts: -------------------------------------------------------------------------------- 1 | import { clamp } from './utils' 2 | 3 | function now() { 4 | return typeof performance != 'undefined' ? performance.now() : Date.now() 5 | } 6 | 7 | const FPS = 100 / 6 8 | 9 | type Time = { 10 | _elapsed: number 11 | elapsed: number 12 | start: number 13 | delta: number 14 | } 15 | 16 | export class FrameLoop { 17 | private rafId = 0 18 | private queue = new Set() 19 | private running = false 20 | public time = {} as Time 21 | onDemand = false 22 | 23 | tick() { 24 | if (!this.running) return 25 | this.update() 26 | this.rafId = window.requestAnimationFrame(this.tick.bind(this)) 27 | } 28 | 29 | update() { 30 | if (!this.running) return 31 | this.updateTime() 32 | this.queue.forEach((cb) => cb()) 33 | } 34 | 35 | run() { 36 | if (!this.running) { 37 | this.time = { start: now(), elapsed: 0, delta: 0, _elapsed: 0 } 38 | this.running = true 39 | if (!this.onDemand) { 40 | // we need elapsed time to be > 0 on the first frame 41 | this.rafId = window.requestAnimationFrame(this.tick.bind(this)) 42 | } 43 | } 44 | } 45 | 46 | start(cb: Function) { 47 | this.queue.add(cb) 48 | this.run() 49 | } 50 | 51 | stop(cb: Function) { 52 | if (!cb) return 53 | this.queue.delete(cb) 54 | if (!this.queue.size) this.stopAll() 55 | } 56 | 57 | stopAll() { 58 | this.rafId && window.cancelAnimationFrame(this.rafId) 59 | this.running = false 60 | } 61 | 62 | updateTime() { 63 | const ts = now() 64 | const _elapsed = ts - this.time.start 65 | this.time.delta = clamp(_elapsed - this.time._elapsed, FPS, 64) 66 | this.time._elapsed = _elapsed 67 | this.time.elapsed += this.time.delta 68 | } 69 | } 70 | 71 | export const GlobalLoop = new FrameLoop() 72 | -------------------------------------------------------------------------------- /packages/core/src/algorithms/ease.ts: -------------------------------------------------------------------------------- 1 | import type { AnimatedValue } from '../animated/AnimatedValue' 2 | import { Algorithm } from '../types' 3 | import { lerp } from '../utils/math' 4 | 5 | /** 6 | * https://github.com/gre/bezier-easing 7 | * BezierEasing - use bezier curve for transition easing function 8 | * by Gaëtan Renaudeau 2014 - 2015 – MIT License 9 | */ 10 | 11 | const NEWTON_ITERATIONS = 4 12 | const NEWTON_MIN_SLOPE = 0.001 13 | const SUBDIVISION_PRECISION = 0.0000001 14 | const SUBDIVISION_MAX_ITERATIONS = 10 15 | const kSplineTableSize = 11 16 | const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0) 17 | 18 | const A = (aA1: number, aA2: number) => 1.0 - 3.0 * aA2 + 3.0 * aA1 19 | const B = (aA1: number, aA2: number) => 3.0 * aA2 - 6.0 * aA1 20 | const C = (aA1: number) => 3.0 * aA1 21 | 22 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. 23 | const calcBezier = (aT: number, aA1: number, aA2: number) => { 24 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT 25 | } 26 | 27 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. 28 | const getSlope = (aT: number, aA1: number, aA2: number) => { 29 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1) 30 | } 31 | 32 | const binarySubdivide = (aX: number, aA: number, aB: number, mX1: number, mX2: number) => { 33 | let currentX, 34 | currentT, 35 | i = 0 36 | do { 37 | currentT = aA + (aB - aA) / 2.0 38 | currentX = calcBezier(currentT, mX1, mX2) - aX 39 | if (currentX > 0.0) { 40 | aB = currentT 41 | } else { 42 | aA = currentT 43 | } 44 | } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS) 45 | return currentT 46 | } 47 | 48 | const newtonRaphsonIterate = (aX: number, aGuessT: number, mX1: number, mX2: number) => { 49 | for (let i = 0; i < NEWTON_ITERATIONS; ++i) { 50 | const currentSlope = getSlope(aGuessT, mX1, mX2) 51 | if (currentSlope === 0.0) { 52 | return aGuessT 53 | } 54 | const currentX = calcBezier(aGuessT, mX1, mX2) - aX 55 | aGuessT -= currentX / currentSlope 56 | } 57 | return aGuessT 58 | } 59 | 60 | const LinearEasing = (x: number) => { 61 | return x 62 | } 63 | 64 | export const bezier = (mX1: number, mY1: number, mX2: number, mY2: number) => { 65 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { 66 | throw new Error('bezier x values must be in [0, 1] range') 67 | } 68 | 69 | if (mX1 === mY1 && mX2 === mY2) { 70 | return LinearEasing 71 | } 72 | 73 | // Precompute samples table 74 | const sampleValues = new Float32Array(kSplineTableSize) 75 | for (let i = 0; i < kSplineTableSize; ++i) { 76 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2) 77 | } 78 | 79 | const getTForX = (aX: number) => { 80 | let intervalStart = 0.0 81 | let currentSample = 1 82 | let lastSample = kSplineTableSize - 1 83 | 84 | for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { 85 | intervalStart += kSampleStepSize 86 | } 87 | --currentSample 88 | 89 | // Interpolate to provide an initial guess for t 90 | const dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]) 91 | const guessForT = intervalStart + dist * kSampleStepSize 92 | 93 | const initialSlope = getSlope(guessForT, mX1, mX2) 94 | if (initialSlope >= NEWTON_MIN_SLOPE) { 95 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2) 96 | } else if (initialSlope === 0.0) { 97 | return guessForT 98 | } else { 99 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2) 100 | } 101 | } 102 | 103 | return (x: number) => { 104 | // Because JavaScript number are imprecise, we should guarantee the extremes are right. 105 | if (x === 0 || x === 1) { 106 | return x 107 | } 108 | return calcBezier(getTForX(x), mY1, mY2) 109 | } 110 | } 111 | 112 | type EaseConfig = [m1X: number, m1Y: number, m2X: number, m2Y: number] 113 | 114 | export function ease(duration: number, [m1X, m1Y, m2X, m2Y]: EaseConfig = [0.25, 0.1, 0.25, 1]): Algorithm { 115 | const bezierCurve = bezier(m1X, m1Y, m2X, m2Y) 116 | return { 117 | update(a: AnimatedValue) { 118 | const t = Math.min(1, a.time.elapsed / duration) 119 | const p = bezierCurve(t) 120 | return lerp(a.from, a.to, p) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /packages/core/src/algorithms/index.ts: -------------------------------------------------------------------------------- 1 | export { spring } from './spring' 2 | export { lerp } from './lerp' 3 | export { ease } from './ease' 4 | export { inertia } from './inertia' 5 | -------------------------------------------------------------------------------- /packages/core/src/algorithms/inertia.ts: -------------------------------------------------------------------------------- 1 | import type { AnimatedValue } from '../animated/AnimatedValue' 2 | import { Algorithm } from '../types' 3 | import { rubberbandIfOutOfBounds } from '../utils/math' 4 | import { spring } from './spring' 5 | 6 | type InertiaConfig = { momentum?: number; min?: number; max?: number; rubberband?: number; velocity?: number } 7 | 8 | export function inertia({ 9 | momentum = 0.998, 10 | velocity, 11 | min = -Infinity, 12 | max = Infinity, 13 | rubberband = 0.15 14 | }: InertiaConfig = {}): Algorithm { 15 | const springEase = spring() 16 | 17 | return { 18 | wanders: true, 19 | update(a: AnimatedValue) { 20 | const v0 = velocity ?? a.startVelocity 21 | 22 | if (!v0) return a.value 23 | 24 | if (a.value < min || a.value > max) { 25 | a.start(a.value < min ? min : max, { immediate: false, easing: springEase }) 26 | return a.update() 27 | } 28 | 29 | const e = Math.exp(-(1 - momentum) * a.time.elapsed) 30 | const value = a.from + (v0 / (1 - momentum)) * (1 - e) 31 | 32 | return rubberbandIfOutOfBounds(value, min, max, rubberband) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/algorithms/lerp.ts: -------------------------------------------------------------------------------- 1 | import { lerp as lerpFn } from '../utils/math' 2 | import type { AnimatedValue } from '../animated/AnimatedValue' 3 | import { Algorithm } from '../types' 4 | 5 | type LerpConfig = { factor?: number } 6 | 7 | export function lerp({ factor = 0.05 }: LerpConfig = {}): Algorithm { 8 | return { 9 | update(a: AnimatedValue) { 10 | return lerpFn(a.value, a.to, factor) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/algorithms/spring.ts: -------------------------------------------------------------------------------- 1 | import type { AnimatedValue } from '../animated/AnimatedValue' 2 | import { Algorithm } from '../types' 3 | 4 | type SpringConfig = { 5 | tension?: number 6 | friction?: number 7 | mass?: number 8 | velocity?: number 9 | } 10 | 11 | export function spring({ tension: k = 170, friction: c = 26, mass: m = 1, velocity }: SpringConfig = {}): Algorithm { 12 | const zeta = c / (2 * Math.sqrt(k * m)) 13 | const w0 = Math.sqrt(k / m) * 0.001 14 | const w1 = w0 * Math.sqrt(1.0 - zeta * zeta) 15 | 16 | return { 17 | update(a: AnimatedValue) { 18 | const t = a.time.elapsed 19 | const v0 = velocity ?? a.startVelocity 20 | const { to, distance: x0 } = a 21 | 22 | let value 23 | 24 | if (zeta < 1) { 25 | const envelope = Math.exp(-zeta * w0 * t) 26 | value = to - envelope * (((-v0 + zeta * w0 * x0) / w1) * Math.sin(w1 * t) + x0 * Math.cos(w1 * t)) 27 | } else { 28 | const envelope = Math.exp(-w0 * t) 29 | value = to - envelope * (x0 + (-v0 + w0 * x0) * t) 30 | } 31 | 32 | return value 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/animated/Animated.ts: -------------------------------------------------------------------------------- 1 | import type { FrameLoop } from '../FrameLoop' 2 | import { AnimatedValue } from './AnimatedValue' 3 | import { each, map } from '../utils/object' 4 | import { lerp } from '../algorithms' 5 | import { ConfigValue, Adapter, ParsedValue } from '../types' 6 | import { GlobalLoop } from '../FrameLoop' 7 | 8 | const defaultLerp = lerp() 9 | 10 | type Time = { 11 | elapsed: number 12 | delta: number 13 | } 14 | 15 | type Props = { 16 | value: any 17 | key?: string 18 | adapter?: Adapter 19 | el?: ElementType 20 | } 21 | 22 | export class Animated { 23 | public value: any 24 | public parsedValue: ParsedValue 25 | public to: any 26 | public time = {} as Time 27 | public key?: string 28 | public el?: ElementType 29 | private adapter?: Adapter 30 | private movingChildren = 0 31 | private children: AnimatedValue[] 32 | 33 | constructor({ value, adapter, el, key }: Props, private loop: FrameLoop = GlobalLoop) { 34 | this.el = el 35 | this.adapter = adapter 36 | this.key = key 37 | this.value = value 38 | 39 | this.onInit() 40 | 41 | this.parsedValue = adapter?.parseInitial ? adapter.parseInitial(value, this) : value 42 | this.children = map(this.parsedValue, (_v, i) => { 43 | return new AnimatedValue(this, i) 44 | }) 45 | } 46 | 47 | private onInit() { 48 | let fn 49 | if ((fn = this.adapter?.onInit)) fn(this) 50 | } 51 | 52 | private onStart() { 53 | let fn 54 | if ((fn = this.adapter?.onStart)) fn(this) 55 | } 56 | 57 | private parse(v: any) { 58 | let fn 59 | return (fn = this.adapter?.parse) ? fn(v, this) : (v as ParsedValue) 60 | } 61 | 62 | private formatValue() { 63 | let fn 64 | this.value = (fn = this.adapter?.format) ? fn(this.parsedValue, this) : this.parsedValue 65 | } 66 | 67 | private onUpdate() { 68 | let fn 69 | this.formatValue() 70 | if ((fn = this.adapter?.onUpdate)) fn(this) 71 | } 72 | 73 | get idle() { 74 | return this.movingChildren <= 0 75 | } 76 | 77 | start(to: any, { immediate = false, easing = defaultLerp }: ConfigValue = {}) { 78 | this.to = to 79 | this.time.elapsed = 0 80 | this.movingChildren = 0 81 | 82 | this.onStart() 83 | const _to = this.parse(to) 84 | 85 | each(this.children, (child) => { 86 | child.start(_to, { immediate, easing }) 87 | if (!child.idle) this.movingChildren++ 88 | }) 89 | } 90 | 91 | update() { 92 | this.time.elapsed += this.loop.time.delta 93 | this.time.delta = this.loop.time.delta 94 | 95 | each(this.children, (child) => { 96 | if (!child.idle) { 97 | child.update() 98 | if (child.idle) this.movingChildren-- 99 | } 100 | }) 101 | 102 | this.onUpdate() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/core/src/animated/AnimatedValue.ts: -------------------------------------------------------------------------------- 1 | import { ConfigValue, ParsedValue } from '../types' 2 | import type { Animated } from './Animated' 3 | 4 | export class AnimatedValue { 5 | private previousValue!: number 6 | public startVelocity!: number 7 | public from!: number 8 | public to!: number 9 | private na: boolean // non applicable transition 10 | private velocityPrecision: number = 1 11 | private distancePrecision: number = 1 12 | private config!: Required 13 | public idle = true 14 | public distance = 0 15 | public velocity = 0 16 | 17 | constructor(public parent: Animated, private key: number | string) { 18 | this.na = typeof this.value === 'string' 19 | } 20 | get time() { 21 | return this.parent.time 22 | } 23 | get value() { 24 | return this.key !== -1 ? (this.parent.parsedValue as any)[this.key] : this.parent.parsedValue 25 | } 26 | set value(value) { 27 | this.key !== -1 ? ((this.parent.parsedValue as any)[this.key] = value) : (this.parent.parsedValue = value) 28 | } 29 | 30 | start(to: string | ParsedValue, config: Required) { 31 | this.to = this.key === -1 ? to : (to as any)[this.key] 32 | this.config = config 33 | this.from = this.value 34 | if (!this.na) { 35 | this.distance = this.to - this.from 36 | this.startVelocity = this.velocity 37 | this.distancePrecision = config.easing.wanders ? 0.01 : Math.min(Math.abs(this.distance) * 1e-3, 1) 38 | this.velocityPrecision = this.distancePrecision ** 2 39 | } 40 | this.idle = config.immediate && this.to === this.value 41 | } 42 | 43 | update() { 44 | if (this.na) { 45 | this.value = this.to 46 | this.idle = true 47 | } 48 | if (this.idle) return this.value 49 | 50 | this.previousValue = this.value 51 | this.value = this.config.immediate ? this.to : this.config.easing.update(this) 52 | this.velocity = (this.value - this.previousValue) / this.time.delta 53 | 54 | if (this.to === this.value) { 55 | this.idle = true 56 | return this.value 57 | } 58 | 59 | const isMoving = Math.abs(this.velocity) > this.velocityPrecision 60 | if (!isMoving) { 61 | if (!this.config.easing.wanders) { 62 | const isTravelling = Math.abs(this.to - this.value) > this.distancePrecision 63 | if (!isTravelling) { 64 | this.idle = true 65 | this.value = this.to 66 | } 67 | } else { 68 | this.idle = true 69 | } 70 | } 71 | 72 | return this.value 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/core/src/buildAnimate.ts: -------------------------------------------------------------------------------- 1 | import { Animated } from './animated/Animated' 2 | import { GlobalLoop } from './FrameLoop' 3 | import { ConfigWithEl, Payload, Target } from './types' 4 | 5 | // TODO timeline (hard) 6 | // TODO from (easy) 7 | // TODO staggering (hard) 8 | // TODO extend target (easy) 9 | // TODO scroll (medium) 10 | // TODO delay (medium) 11 | 12 | type AnimationMap = Map> 13 | const elementAnimationsMap = new Map() 14 | 15 | export function buildAnimate(buildTarget: Target) { 16 | return function animate( 17 | masterConfigWithEl: ConfigWithEl, 18 | globalTo?: Partial 19 | ) { 20 | const target = buildTarget as Target 21 | const loop = target.loop || GlobalLoop 22 | const initial = {} 23 | 24 | let resolveRef: (value?: unknown) => void 25 | let rejectRef: (value?: unknown) => void 26 | 27 | const { el: element, ...masterConfig } = masterConfigWithEl 28 | 29 | const tElement = target.getElement?.(element) || element 30 | const el = typeof tElement === 'object' && 'current' in tElement ? tElement : { current: tElement } 31 | 32 | let animations: AnimationMap = elementAnimationsMap.get(element) 33 | if (!animations) { 34 | animations = new Map() 35 | elementAnimationsMap.set(element, animations) 36 | } 37 | 38 | const update = () => { 39 | const currentValues: any = {} 40 | if (!el.current) return 41 | 42 | let idle = true 43 | animations.forEach((animated, key) => { 44 | animated.update() 45 | currentValues[key] = animated.value 46 | idle &&= animated.idle 47 | }) 48 | target.setValues?.(currentValues, el.current, initial, idle) 49 | if (idle) { 50 | loop.stop(update) 51 | resolveRef() 52 | } 53 | } 54 | 55 | const start = (to: Partial, config = masterConfig) => { 56 | return new Promise((resolve, reject) => { 57 | resolveRef = resolve 58 | rejectRef = reject 59 | let idle = true 60 | for (let key in to) { 61 | let animated = animations.get(key) 62 | 63 | if (!animated) { 64 | const [value, adapter] = target.getInitialValueAndAdapter(el.current, key, initial) 65 | animated = new Animated({ value, adapter, key, el: el.current }, loop) 66 | animations.set(key, animated) 67 | } 68 | animated.start(to[key], typeof config === 'function' ? config(key) : config) 69 | idle &&= animated.idle 70 | } 71 | if (!idle) loop.start(update) 72 | // if animation is already idle resolve promise right away 73 | else resolveRef() 74 | }) 75 | } 76 | 77 | const stop = () => { 78 | loop.stop(update) 79 | rejectRef() 80 | } 81 | 82 | const clean = () => { 83 | loop.stop(update) 84 | resolveRef?.() 85 | elementAnimationsMap.delete(element) 86 | } 87 | 88 | const get = (key: keyof Values) => animations.get(key)?.value 89 | 90 | // TODO discuss this 👇 91 | // when `to` is passed to the function then promise-based functionality 92 | // wouldn't work. 93 | if (globalTo) start(globalTo) 94 | 95 | const api = { get, start, stop, clean } 96 | return api 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './buildAnimate' 2 | export * from './utils' 3 | export * from './FrameLoop' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Animated } from './animated/Animated' 2 | import { AnimatedValue } from './animated/AnimatedValue' 3 | import { FrameLoop } from './FrameLoop' 4 | 5 | export type { Animated } 6 | 7 | export type ParsedValue = number | number[] | Record 8 | 9 | export type AdapterFn = (value: any, animated: Animated) => R 10 | 11 | export type Adapter = { 12 | parse?(value: any, animated: Animated): ParsedValue 13 | parseInitial?(value: any, animated: Animated): ParsedValue 14 | format?(value: any, animated: Animated): any 15 | onInit?(animated: Animated): void 16 | onStart?(animated: Animated): void 17 | onUpdate?(animated: Animated): void 18 | } 19 | 20 | export type Algorithm = { 21 | /** 22 | * When true, the algorithm doesn't always reach its destination (ie inertia). 23 | * @note should probably be renamed. 24 | */ 25 | wanders?: boolean 26 | update: (a: AnimatedValue) => number 27 | } 28 | 29 | export type Payload = Record 30 | 31 | export type Target = { 32 | getElement?(element: any): ElementType 33 | loop?: FrameLoop 34 | setValues?(rawValues: Values, element: ElementType, initial?: any, idle?: boolean): void 35 | getInitialValueAndAdapter( 36 | element: ElementType, 37 | key: K, 38 | initial?: any 39 | ): [Values[K], Adapter | undefined] 40 | } 41 | 42 | export type ConfigValue = { 43 | immediate?: boolean 44 | easing?: Algorithm 45 | } 46 | 47 | export type ElementRef = { current: ElementType } 48 | export type Config = ConfigValue | ((key: string) => ConfigValue) 49 | export type ConfigWithEl = Config & { el: ElementType | ElementRef } 50 | export type ConfigWithOptionalEl = Config & { el?: ElementType } 51 | 52 | export type ApiType = { 53 | get: (key: keyof Values) => any 54 | start: (to: Partial, config?: Config) => Promise 55 | stop: () => void 56 | clean: () => void 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/utils/color.ts: -------------------------------------------------------------------------------- 1 | // TODO hsl ? 2 | 3 | import { substringMatch } from './string' 4 | 5 | // benchmarks https://jsbench.me/cql3n9zjhp/1 6 | 7 | export function parseColor(str: string) { 8 | let r: number, g: number, b: number, a: number | undefined, tmp: number | string[] 9 | if (str[0] === '#') { 10 | if ((str = str.substring(1)).length === 3) { 11 | str = str[0] + str[0] + str[1] + str[1] + str[2] + str[2] + 'FF' 12 | } else if (str.length === 6) { 13 | str += 'FF' 14 | } 15 | tmp = parseInt(str, 16) 16 | 17 | r = (tmp & 0xff000000) >>> 24 18 | g = (tmp & 0x00ff0000) >>> 16 19 | b = (tmp & 0x0000ff00) >>> 8 20 | a = (tmp & 0x000000ff) / 255 21 | } else { 22 | tmp = substringMatch(str, '(', ')').split(',') 23 | r = parseInt(tmp[0], 10) 24 | g = parseInt(tmp[1], 10) 25 | b = parseInt(tmp[2], 10) 26 | 27 | a = tmp.length > 3 ? parseFloat(tmp[3]) : 1 28 | } 29 | 30 | return [r, g, b, a] 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './math' 2 | export * from './object' 3 | export * from './string' 4 | export * from './color' 5 | export * from './interpolate' 6 | -------------------------------------------------------------------------------- /packages/core/src/utils/interpolate.ts: -------------------------------------------------------------------------------- 1 | // mostly stolen from https://github.com/d3/d3-interpolate/blob/main/src/string.js 2 | 3 | const reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g 4 | 5 | export function parseNumbers(v: string) { 6 | return v.match(reA)!.map((k) => +k) 7 | } 8 | 9 | export function interpolate(b: string) { 10 | let bi = (reA.lastIndex = 0), // scan index for next number in b 11 | bm, // current match in b 12 | bs, // string preceding current number in b, if any 13 | i = -1, // index in s 14 | s: (string | null)[] = [], // string constants and placeholders 15 | q: number[] = [] // number interpolators 16 | 17 | // Coerce inputs to strings. 18 | b = b + '' 19 | 20 | // Interpolate pairs of numbers in a & b. 21 | while ((bm = reA.exec(b))) { 22 | if ((bs = bm.index) > bi) { 23 | // a string precedes the next number in b 24 | bs = b.slice(bi, bs) 25 | if (s[i]) s[i] += bs // coalesce with previous string 26 | else s[++i] = bs 27 | } 28 | // interpolate non-matching numbers 29 | s[++i] = null 30 | q.push(+bm) 31 | bi = reA.lastIndex 32 | } 33 | 34 | // Add remains of b. 35 | if (bi < b.length) { 36 | bs = b.slice(bi) 37 | if (s[i]) s[i] += bs // coalesce with previous string 38 | else s[++i] = bs 39 | } 40 | 41 | return { 42 | values: q, 43 | compute(values: number[]) { 44 | i = 0 45 | return s.map((k) => (k === null ? values[i++] : k)).join('') 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/utils/math.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mattdesl/lerp 2 | 3 | export function lerp(v0: number, v1: number, t: number) { 4 | return v0 * (1 - t) + v1 * t 5 | } 6 | 7 | export function clamp(value: number, min: number, max: number) { 8 | return Math.max(min, Math.min(max, value)) 9 | } 10 | 11 | function rubberband(distance: number, dimension: number, constant: number) { 12 | if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance, constant * 5) 13 | return (distance * dimension * constant) / (dimension + constant * distance) 14 | } 15 | 16 | export function rubberbandIfOutOfBounds(position: number, min: number, max: number, constant = 0.15) { 17 | if (constant === 0) return clamp(position, min, max) 18 | if (position < min) return -rubberband(min - position, max - min, constant) + min 19 | if (position > max) return +rubberband(position - max, max - min, constant) + max 20 | return position 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function equal

(v0: P, v1: P) { 2 | if (Array.isArray(v0)) { 3 | return v0.every((val, index) => val === (v1 as any)[index]) 4 | } 5 | return v0 === v1 6 | } 7 | 8 | export function each

(array: P[], iterator: (v: P, i: number) => void) { 9 | if (Array.isArray(array)) { 10 | for (let i = 0; i < array.length; i++) iterator(array[i], i) 11 | } else { 12 | iterator(array, -1) 13 | } 14 | } 15 | 16 | export function map(obj: P | P[] | Record, iterator: (v: P, key: string | number) => K) { 17 | if (typeof obj === 'object') { 18 | if (Array.isArray(obj)) { 19 | return obj.map(iterator) 20 | } 21 | return Object.entries(obj).map(([key, value]) => iterator(value, key)) 22 | } 23 | return iterator(obj, -1) as any 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function parseUnitValue(value: number | string): [number] | [number, string] { 2 | if (typeof value === 'number') return [value] 3 | const _value = parseFloat(value) 4 | const unit: string = value.substring(('' + _value).length) 5 | return [_value, unit] 6 | } 7 | 8 | export function substringMatch(str: string, from: string, to?: string) { 9 | const pos = str.indexOf(from) 10 | if (pos !== -1) { 11 | if (to) { 12 | return str.substring(pos + from.length, str.indexOf(to)) 13 | } 14 | return str.substring(0, pos) 15 | } 16 | 17 | return '' 18 | } 19 | -------------------------------------------------------------------------------- /packages/dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/dom 2 | 3 | ## 0.3.0 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [2d867a9] 8 | - @animini/core@0.3.0 9 | - @animini/target-dom@0.3.0 10 | 11 | ## 0.2.6 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [c838bf8] 16 | - @animini/core@0.2.6 17 | - @animini/target-dom@0.2.6 18 | 19 | ## 0.2.5 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [0885116] 24 | - @animini/core@0.2.5 25 | - @animini/target-dom@0.2.5 26 | 27 | ## 0.2.4 28 | 29 | ### Patch Changes 30 | 31 | - a0f7cdb: Adapter inside Animated 32 | - Updated dependencies [a0f7cdb] 33 | - @animini/core@0.2.4 34 | - @animini/target-dom@0.2.4 35 | 36 | ## 0.2.3 37 | 38 | ### Patch Changes 39 | 40 | - Updated dependencies [fd31840] 41 | - @animini/core@0.2.3 42 | - @animini/target-dom@0.2.3 43 | 44 | ## 0.2.2 45 | 46 | ### Patch Changes 47 | 48 | - 6c367d3: add syncCachedValues param to buildAnimate 49 | - Updated dependencies [6c367d3] 50 | - @animini/core@0.2.2 51 | - @animini/target-dom@0.2.2 52 | 53 | ## 0.2.1 54 | 55 | ### Patch Changes 56 | 57 | - Updated dependencies [bf2fd1b] 58 | - @animini/core@0.2.1 59 | - @animini/target-dom@0.2.1 60 | 61 | ## 0.2.0 62 | 63 | ### Minor Changes 64 | 65 | - 10ba638: Refactor package 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies [10ba638] 70 | - @animini/core@0.2.0 71 | - @animini/target-dom@0.2.0 72 | -------------------------------------------------------------------------------- /packages/dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/dom", 3 | "version": "0.3.0", 4 | "description": "animini for the dom", 5 | "keywords": [], 6 | "main": "dist/animini-dom.cjs.js", 7 | "module": "dist/animini-dom.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/dom" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "dependencies": { 16 | "@animini/core": "0.3.0", 17 | "@animini/target-dom": "0.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import { buildAnimate } from '@animini/core' 2 | import { dom } from '@animini/target-dom' 3 | export * from '@animini/core/algorithms' 4 | 5 | export const animate = buildAnimate(dom) 6 | -------------------------------------------------------------------------------- /packages/react-dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/react-dom 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [2d867a9] 12 | - @animini/core@0.3.0 13 | - @animini/core-react@0.3.0 14 | - @animini/target-dom@0.3.0 15 | 16 | ## 0.2.6 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [c838bf8] 21 | - @animini/core@0.2.6 22 | - @animini/target-dom@0.2.6 23 | - @animini/core-react@0.2.6 24 | 25 | ## 0.2.5 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies [0885116] 30 | - @animini/core@0.2.5 31 | - @animini/target-dom@0.2.5 32 | - @animini/core-react@0.2.5 33 | 34 | ## 0.2.4 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies [a0f7cdb] 39 | - @animini/core@0.2.4 40 | - @animini/target-dom@0.2.4 41 | - @animini/core-react@0.2.4 42 | 43 | ## 0.2.3 44 | 45 | ### Patch Changes 46 | 47 | - Updated dependencies [fd31840] 48 | - @animini/core@0.2.3 49 | - @animini/core-react@0.2.3 50 | - @animini/target-dom@0.2.3 51 | 52 | ## 0.2.2 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies [6c367d3] 57 | - @animini/core@0.2.2 58 | - @animini/core-react@0.2.2 59 | - @animini/target-dom@0.2.2 60 | 61 | ## 0.2.1 62 | 63 | ### Patch Changes 64 | 65 | - bf2fd1b: Internal refactoring 66 | - Updated dependencies [bf2fd1b] 67 | - @animini/core@0.2.1 68 | - @animini/core-react@0.2.1 69 | - @animini/target-dom@0.2.1 70 | 71 | ## 0.2.0 72 | 73 | ### Minor Changes 74 | 75 | - 10ba638: Refactor package 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies [10ba638] 80 | - @animini/core@0.2.0 81 | - @animini/target-dom@0.2.0 82 | - @animini/core-react@0.2.0 83 | 84 | ## 0.1.4 85 | 86 | ### Patch Changes 87 | 88 | - a7b464c: dom: immediate transition to NaN strings 89 | - Updated dependencies [a7b464c] 90 | - @animini/core@0.1.4 91 | 92 | ## 0.1.3 93 | 94 | ### Patch Changes 95 | 96 | - Updated dependencies [bb94d14] 97 | - @animini/core@0.1.3 98 | 99 | ## 0.1.2 100 | 101 | ### Patch Changes 102 | 103 | - Updated dependencies [6b9d542] 104 | - @animini/core@0.1.2 105 | 106 | ## 0.1.1 107 | 108 | ### Patch Changes 109 | 110 | - Updated dependencies [944cfc7] 111 | - @animini/core@0.1.1 112 | 113 | ## 0.1.0 114 | 115 | ### Minor Changes 116 | 117 | - b442912: - typescript 118 | - promise-based 119 | - added api.stop 120 | - ability to change algorithm per animation call 121 | - added ease algorithm 122 | - added inertia algorithm 123 | - improved color parsing for dom 124 | - improved color parsing for three 125 | - unit support for dom 126 | 127 | ### Patch Changes 128 | 129 | - Updated dependencies [b442912] 130 | - @animini/core@0.1.0 131 | 132 | ## 0.0.8 133 | 134 | ### Patch Changes 135 | 136 | - 15ad827: fix px 137 | 138 | ## 0.0.7 139 | 140 | ### Patch Changes 141 | 142 | - a1fccbc: fix opacity 143 | 144 | ## 0.0.6 145 | 146 | ### Patch Changes 147 | 148 | - Updated dependencies [891dbf7] 149 | - @animini/core@0.0.6 150 | 151 | ## 0.0.5 152 | 153 | ### Patch Changes 154 | 155 | - 098cac2: feat: one controller for all simultaneous values 156 | - Updated dependencies [098cac2] 157 | - @animini/core@0.0.5 158 | 159 | ## 0.0.4 160 | 161 | ### Patch Changes 162 | 163 | - Updated dependencies [748a414] 164 | - @animini/core@0.0.4 165 | 166 | ## 0.0.3 167 | 168 | ### Patch Changes 169 | 170 | - Updated dependencies [9c2cd35] 171 | - @animini/core@0.0.3 172 | 173 | ## 0.0.2 174 | 175 | ### Patch Changes 176 | 177 | - Updated dependencies [a8301ed] 178 | - @animini/core@0.0.2 179 | -------------------------------------------------------------------------------- /packages/react-dom/README.md: -------------------------------------------------------------------------------- 1 | [![npm (tag)](https://img.shields.io/npm/v/@animini/dom?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@animini/dom) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@animini/dom?style=flat&colorA=000000&colorB=000000&label=gzipped)](https://bundlephobia.com/result?p=@animini/dom) 2 | 3 | ## Demo 4 | 5 | https://animini.vercel.app/ 6 | 7 | ## Installation 8 | 9 | ### For the DOM 10 | 11 | ```bash 12 | yarn add @animini/dom 13 | ``` 14 | 15 | ### For Three 16 | 17 | ```bash 18 | yarn add @animini/three 19 | ``` 20 | 21 | ### Instructions 22 | 23 | ```js 24 | import { useDrag } from '@use-gesture/react' 25 | import { useAnimate, spring } from '@animini/dom' 26 | 27 | const easing = spring() 28 | 29 | export default function App() { 30 | const [ref, api] = useAnimate() 31 | 32 | useDrag( 33 | ({ active, movement: [x, y] }) => { 34 | api.start({ scale: active ? 1.2 : 1, x: active ? x : 0, y: active ? y : 0 }, (k) => ({ 35 | immediate: k !== 'scale' && active, 36 | easing 37 | })) 38 | }, 39 | { target: ref } 40 | ) 41 | 42 | return

43 | } 44 | ``` 45 | 46 | ## Easings 47 | 48 | ### Lerp 49 | 50 | Lerp is the lightest, fastest and default easing algorithm for Animini. It supports a `factor` attribute that will change the momentum of the lerp. 51 | 52 | ```js 53 | import { useAnimate, lerp } from '@animini/dom' 54 | 55 | const easing = lerp({ factor: 0.05 }) 56 | api.start({ x: 100 }, { easing }) 57 | ``` 58 | 59 | ### Spring 60 | 61 | ```js 62 | import { useAnimate, spring } from '@animini/dom' 63 | 64 | const easing = spring({ 65 | tension: 170, // spring tension 66 | friction: 26, // spring friction 67 | mass: 1, // target mass 68 | velocity // initial velocity 69 | }) 70 | 71 | api.start({ x: 100 }, { easing }) 72 | ``` 73 | 74 | ### Ease (Bezier) 75 | 76 | ```js 77 | import { useAnimate, ease } from '@animini/dom' 78 | 79 | const easing = ease( 80 | 300, // duration of the ease in ms 81 | [0.25, 0.1, 0.25, 1] // coordinates of the bezier curve 82 | ) 83 | 84 | api.start({ x: 100 }, { easing }) 85 | ``` 86 | 87 | ### Inertia 88 | 89 | Inertia aims at emulating a thrown object. Inertia will not reach its destination and only works if the value is already moving or if the easing is given an initial velocity. 90 | 91 | Inertia supports `min` and `max` bounds which the element will bounce against as a rubberband bouncing on a wall. 92 | 93 | ```js 94 | import { useAnimate, inertia } from '@animini/dom' 95 | 96 | const easing = inertia({ 97 | momentum: 0.998, // momentum of the inertia 98 | velocity: undefined, // initial velocity (leave it undefined to use the current velocity of the value) 99 | min: -100, // min bound 100 | max: 100, // max bound 101 | rubberband = 0.15 // elasticity factor when reaching bounds defined by min / max 102 | }) 103 | 104 | api.start({ x: 100 }, { easing }) 105 | ``` 106 | -------------------------------------------------------------------------------- /packages/react-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/react-dom", 3 | "version": "0.3.0", 4 | "description": "animini hook for React dom", 5 | "keywords": [], 6 | "main": "dist/animini-react-dom.cjs.js", 7 | "module": "dist/animini-react-dom.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/react-dom" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "peerDependencies": { 16 | "react": ">=16.8.0" 17 | }, 18 | "dependencies": { 19 | "@animini/core": "0.3.0", 20 | "@animini/core-react": "0.3.0", 21 | "@animini/target-dom": "0.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | export { useAnimate } from './useAnimateDom' 2 | export * from '@animini/core/algorithms' 3 | -------------------------------------------------------------------------------- /packages/react-dom/src/useAnimateDom.ts: -------------------------------------------------------------------------------- 1 | import { ConfigWithOptionalEl } from '@animini/core' 2 | import { buildReactHook } from '@animini/core-react' 3 | import { dom } from '@animini/target-dom' 4 | 5 | export const useAnimateDom = buildReactHook(dom) 6 | 7 | export function useAnimate< 8 | Element extends HTMLElement | Window, 9 | C extends ConfigWithOptionalEl = ConfigWithOptionalEl 10 | >(masterConfig?: C) { 11 | return useAnimateDom(masterConfig) 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-three/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/three 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [2d867a9] 12 | - @animini/core@0.3.0 13 | - @animini/core-react@0.3.0 14 | - @animini/target-three@0.3.0 15 | 16 | ## 0.2.6 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [c838bf8] 21 | - @animini/core@0.2.6 22 | - @animini/core-react@0.2.6 23 | - @animini/target-three@0.2.6 24 | 25 | ## 0.2.5 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies [0885116] 30 | - @animini/core@0.2.5 31 | - @animini/target-three@0.2.5 32 | - @animini/core-react@0.2.5 33 | 34 | ## 0.2.4 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies [a0f7cdb] 39 | - @animini/core@0.2.4 40 | - @animini/target-three@0.2.4 41 | - @animini/core-react@0.2.4 42 | 43 | ## 0.2.3 44 | 45 | ### Patch Changes 46 | 47 | - Updated dependencies [fd31840] 48 | - @animini/core@0.2.3 49 | - @animini/core-react@0.2.3 50 | - @animini/target-three@0.2.3 51 | 52 | ## 0.2.2 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies [6c367d3] 57 | - @animini/core@0.2.2 58 | - @animini/core-react@0.2.2 59 | - @animini/target-three@0.2.2 60 | 61 | ## 0.2.1 62 | 63 | ### Patch Changes 64 | 65 | - bf2fd1b: Internal refactoring 66 | - Updated dependencies [bf2fd1b] 67 | - @animini/core@0.2.1 68 | - @animini/core-react@0.2.1 69 | - @animini/target-three@0.2.1 70 | 71 | ## 0.2.0 72 | 73 | ### Minor Changes 74 | 75 | - 10ba638: Refactor package 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies [10ba638] 80 | - @animini/core@0.2.0 81 | - @animini/target-three@0.2.0 82 | - @animini/core-react@0.2.0 83 | 84 | ## 0.1.4 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies [a7b464c] 89 | - @animini/core@0.1.4 90 | 91 | ## 0.1.3 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [bb94d14] 96 | - @animini/core@0.1.3 97 | 98 | ## 0.1.2 99 | 100 | ### Patch Changes 101 | 102 | - 6b9d542: fix global loop in three 103 | - Updated dependencies [6b9d542] 104 | - @animini/core@0.1.2 105 | 106 | ## 0.1.1 107 | 108 | ### Patch Changes 109 | 110 | - Updated dependencies [944cfc7] 111 | - @animini/core@0.1.1 112 | 113 | ## 0.1.0 114 | 115 | ### Minor Changes 116 | 117 | - b442912: - typescript 118 | - promise-based 119 | - added api.stop 120 | - ability to change algorithm per animation call 121 | - added ease algorithm 122 | - added inertia algorithm 123 | - improved color parsing for dom 124 | - improved color parsing for three 125 | - unit support for dom 126 | 127 | ### Patch Changes 128 | 129 | - Updated dependencies [b442912] 130 | - @animini/core@0.1.0 131 | 132 | ## 0.0.6 133 | 134 | ### Patch Changes 135 | 136 | - Updated dependencies [891dbf7] 137 | - @animini/core@0.0.6 138 | 139 | ## 0.0.5 140 | 141 | ### Patch Changes 142 | 143 | - 098cac2: feat: one controller for all simultaneous values 144 | - Updated dependencies [098cac2] 145 | - @animini/core@0.0.5 146 | 147 | ## 0.0.4 148 | 149 | ### Patch Changes 150 | 151 | - Updated dependencies [748a414] 152 | - @animini/core@0.0.4 153 | 154 | ## 0.0.3 155 | 156 | ### Patch Changes 157 | 158 | - Updated dependencies [9c2cd35] 159 | - @animini/core@0.0.3 160 | 161 | ## 0.0.2 162 | 163 | ### Patch Changes 164 | 165 | - Updated dependencies [a8301ed] 166 | - @animini/core@0.0.2 167 | -------------------------------------------------------------------------------- /packages/react-three/README.md: -------------------------------------------------------------------------------- 1 | [![npm (tag)](https://img.shields.io/npm/v/@animini/dom?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/@animini/dom) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@animini/dom?style=flat&colorA=000000&colorB=000000&label=gzipped)](https://bundlephobia.com/result?p=@animini/dom) 2 | 3 | ## Demo 4 | 5 | https://animini.vercel.app/ 6 | 7 | ## Installation 8 | 9 | ### For the DOM 10 | 11 | ```bash 12 | yarn add @animini/dom 13 | ``` 14 | 15 | ### For Three 16 | 17 | ```bash 18 | yarn add @animini/three 19 | ``` 20 | 21 | ### Instructions 22 | 23 | ```js 24 | import { useDrag } from '@use-gesture/react' 25 | import { useAnimate, spring } from '@animini/dom' 26 | 27 | const easing = spring() 28 | 29 | export default function App() { 30 | const [ref, api] = useAnimate() 31 | 32 | useDrag( 33 | ({ active, movement: [x, y] }) => { 34 | api.start({ scale: active ? 1.2 : 1, x: active ? x : 0, y: active ? y : 0 }, (k) => ({ 35 | immediate: k !== 'scale' && active, 36 | easing 37 | })) 38 | }, 39 | { target: ref } 40 | ) 41 | 42 | return
43 | } 44 | ``` 45 | 46 | ## Easings 47 | 48 | ### Lerp 49 | 50 | Lerp is the lightest, fastest and default easing algorithm for Animini. It supports a `factor` attribute that will change the momentum of the lerp. 51 | 52 | ```js 53 | import { useAnimate, lerp } from '@animini/dom' 54 | 55 | const easing = lerp({ factor: 0.05 }) 56 | api.start({ x: 100 }, { easing }) 57 | ``` 58 | 59 | ### Spring 60 | 61 | ```js 62 | import { useAnimate, spring } from '@animini/dom' 63 | 64 | const easing = spring({ 65 | tension: 170, // spring tension 66 | friction: 26, // spring friction 67 | mass: 1, // target mass 68 | velocity // initial velocity 69 | }) 70 | 71 | api.start({ x: 100 }, { easing }) 72 | ``` 73 | 74 | ### Ease (Bezier) 75 | 76 | ```js 77 | import { useAnimate, ease } from '@animini/dom' 78 | 79 | const easing = ease( 80 | 300, // duration of the ease in ms 81 | [0.25, 0.1, 0.25, 1] // coordinates of the bezier curve 82 | ) 83 | 84 | api.start({ x: 100 }, { easing }) 85 | ``` 86 | 87 | ### Inertia 88 | 89 | Inertia aims at emulating a thrown object. Inertia will not reach its destination and only works if the value is already moving or if the easing is given an initial velocity. 90 | 91 | Inertia supports `min` and `max` bounds which the element will bounce against as a rubberband bouncing on a wall. 92 | 93 | ```js 94 | import { useAnimate, inertia } from '@animini/dom' 95 | 96 | const easing = inertia({ 97 | momentum: 0.998, // momentum of the inertia 98 | velocity: undefined, // initial velocity (leave it undefined to use the current velocity of the value) 99 | min: -100, // min bound 100 | max: 100, // max bound 101 | rubberband = 0.15 // elasticity factor when reaching bounds defined by min / max 102 | }) 103 | 104 | api.start({ x: 100 }, { easing }) 105 | ``` 106 | -------------------------------------------------------------------------------- /packages/react-three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/react-three", 3 | "version": "0.3.0", 4 | "description": "animini hook for React Three Fiber", 5 | "keywords": [], 6 | "main": "dist/animini-react-three.cjs.js", 7 | "module": "dist/animini-react-three.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/react-three" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "peerDependencies": { 16 | "@react-three/fiber": "^8.0.19", 17 | "react": ">=16.8.0", 18 | "three": ">=0.140.0" 19 | }, 20 | "dependencies": { 21 | "@animini/core": "0.3.0", 22 | "@animini/core-react": "0.3.0", 23 | "@animini/target-three": "0.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-three/src/index.ts: -------------------------------------------------------------------------------- 1 | export { useAnimate } from './useAnimateThree' 2 | export * from '@animini/core/algorithms' 3 | -------------------------------------------------------------------------------- /packages/react-three/src/useAnimateThree.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { ConfigWithOptionalEl, GlobalLoop } from '@animini/core' 3 | import { buildReactHook } from '@animini/core-react' 4 | import { three, ThreeElementType, ThreeValues } from '@animini/target-three' 5 | import { addEffect } from '@react-three/fiber' 6 | 7 | let count = 0 8 | 9 | function setGlobalLoopOnDemand() { 10 | let unsub: () => void | undefined 11 | if (count++ === 0) { 12 | GlobalLoop.onDemand = true 13 | unsub = addEffect(() => { 14 | GlobalLoop.update() 15 | }) 16 | } 17 | return () => { 18 | if (--count === 0) { 19 | GlobalLoop.onDemand = false 20 | unsub?.() 21 | } 22 | } 23 | } 24 | 25 | export const useAnimateThree = buildReactHook(three) 26 | 27 | export function useAnimate< 28 | Element extends ThreeElementType, 29 | C extends ConfigWithOptionalEl = ConfigWithOptionalEl 30 | >(masterConfig?: C) { 31 | useEffect(() => { 32 | return setGlobalLoopOnDemand() 33 | }, []) 34 | 35 | return useAnimateThree>(masterConfig) 36 | } 37 | -------------------------------------------------------------------------------- /packages/three/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/three 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [2d867a9] 12 | - @animini/core@0.3.0 13 | - @animini/target-three@0.3.0 14 | 15 | ## 0.2.6 16 | 17 | ### Patch Changes 18 | 19 | - Updated dependencies [c838bf8] 20 | - @animini/core@0.2.6 21 | - @animini/target-three@0.2.6 22 | 23 | ## 0.2.5 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies [0885116] 28 | - @animini/core@0.2.5 29 | - @animini/target-three@0.2.5 30 | 31 | ## 0.2.4 32 | 33 | ### Patch Changes 34 | 35 | - a0f7cdb: Adapter inside Animated 36 | - Updated dependencies [a0f7cdb] 37 | - @animini/core@0.2.4 38 | - @animini/target-three@0.2.4 39 | 40 | ## 0.2.3 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies [fd31840] 45 | - @animini/core@0.2.3 46 | - @animini/target-three@0.2.3 47 | 48 | ## 0.2.2 49 | 50 | ### Patch Changes 51 | 52 | - 6c367d3: add syncCachedValues param to buildAnimate 53 | - Updated dependencies [6c367d3] 54 | - @animini/core@0.2.2 55 | - @animini/target-three@0.2.2 56 | 57 | ## 0.2.1 58 | 59 | ### Patch Changes 60 | 61 | - Updated dependencies [bf2fd1b] 62 | - @animini/core@0.2.1 63 | - @animini/target-three@0.2.1 64 | 65 | ## 0.2.0 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies [10ba638] 70 | - @animini/core@0.2.0 71 | - @animini/target-three@0.2.0 72 | -------------------------------------------------------------------------------- /packages/three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/three", 3 | "version": "0.3.0", 4 | "description": "animini for Three", 5 | "keywords": [], 6 | "main": "dist/animini-three.cjs.js", 7 | "module": "dist/animini-three.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/dom" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "peerDependencies": { 16 | "@react-three/fiber": "^8.0.19", 17 | "react": ">=16.8.0", 18 | "three": ">=0.140.0" 19 | }, 20 | "dependencies": { 21 | "@animini/core": "0.3.0", 22 | "@animini/target-three": "0.3.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/three/src/index.ts: -------------------------------------------------------------------------------- 1 | import { buildAnimate, ConfigWithEl } from '@animini/core' 2 | import { three, ThreeElementType, ThreeValues } from '@animini/target-three' 3 | export * from '@animini/core/algorithms' 4 | 5 | const animateThree = buildAnimate(three) 6 | 7 | export function animate( 8 | masterConfigWithEl: ConfigWithEl, 9 | globalTo?: Partial> 10 | ) { 11 | return animateThree(masterConfigWithEl, globalTo) 12 | } 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ 3 | - 'packages/*' 4 | - 'targets/*' 5 | - 'demo' 6 | - 'test-perf' 7 | -------------------------------------------------------------------------------- /targets/target-dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/target-dom 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [2d867a9] 12 | - @animini/core@0.3.0 13 | 14 | ## 0.2.6 15 | 16 | ### Patch Changes 17 | 18 | - c838bf8: Support for scrollTop / scrollLeft 19 | - Updated dependencies [c838bf8] 20 | - @animini/core@0.2.6 21 | 22 | ## 0.2.5 23 | 24 | ### Patch Changes 25 | 26 | - 0885116: Add string interpolation 27 | - Updated dependencies [0885116] 28 | - @animini/core@0.2.5 29 | 30 | ## 0.2.4 31 | 32 | ### Patch Changes 33 | 34 | - a0f7cdb: Adapter inside Animated 35 | - Updated dependencies [a0f7cdb] 36 | - @animini/core@0.2.4 37 | 38 | ## 0.2.3 39 | 40 | ### Patch Changes 41 | 42 | - fd31840: Remove cached values 43 | - Updated dependencies [fd31840] 44 | - @animini/core@0.2.3 45 | 46 | ## 0.2.2 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies [6c367d3] 51 | - @animini/core@0.2.2 52 | 53 | ## 0.2.1 54 | 55 | ### Patch Changes 56 | 57 | - bf2fd1b: Internal refactoring 58 | - Updated dependencies [bf2fd1b] 59 | - @animini/core@0.2.1 60 | 61 | ## 0.2.0 62 | 63 | ### Minor Changes 64 | 65 | - 10ba638: Refactor package 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies [10ba638] 70 | - @animini/core@0.2.0 71 | -------------------------------------------------------------------------------- /targets/target-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/target-dom", 3 | "version": "0.3.0", 4 | "description": "animini target for the dom", 5 | "keywords": [], 6 | "main": "dist/animini-target-dom.cjs.js", 7 | "module": "dist/animini-target-dom.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/target-dom" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "dependencies": { 16 | "@animini/core": "0.3.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /targets/target-dom/src/adapters/color.ts: -------------------------------------------------------------------------------- 1 | import { parseColor } from '@animini/core' 2 | import { DomAdapter } from '../types' 3 | 4 | export const color: DomAdapter = { 5 | parse: parseColor, 6 | format(c: number[]) { 7 | return `rgba(${~~c[0]}, ${~~c[1]}, ${~~c[2]}, ${c[3]})` 8 | }, 9 | parseInitial: parseColor 10 | } 11 | -------------------------------------------------------------------------------- /targets/target-dom/src/adapters/generic.ts: -------------------------------------------------------------------------------- 1 | import { parseUnitValue } from '@animini/core' 2 | import { DomAdapter } from '../types' 3 | import { SCROLL_KEYS } from '../utils' 4 | 5 | const parse: DomAdapter['parse'] = (value, animated) => { 6 | let [_value, unit] = parseUnitValue(value) 7 | if (isNaN(_value)) return value 8 | switch (unit) { 9 | case '%': 10 | const parent = (animated.el as HTMLElement)?.offsetParent 11 | // @ts-expect-error 12 | const size = (key === 'top' ? parent?.offsetHeight : parent?.offsetWidth) || 0 13 | return (_value * size) / 100 14 | case 'vw': 15 | return (_value * window.innerWidth) / 100 16 | case 'vh': 17 | return (_value * window.innerHeight) / 100 18 | } 19 | return _value 20 | } 21 | 22 | export const generic: DomAdapter = { 23 | parse, 24 | format(value: number, animated) { 25 | if (!isNaN(value as any) && !SCROLL_KEYS.includes(animated.key!)) return value + 'px' 26 | return value 27 | }, 28 | parseInitial: parse 29 | } 30 | -------------------------------------------------------------------------------- /targets/target-dom/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color' 2 | export * from './generic' 3 | export * from './transform' 4 | export * from './string' 5 | -------------------------------------------------------------------------------- /targets/target-dom/src/adapters/string.ts: -------------------------------------------------------------------------------- 1 | import { interpolate, Animated, parseNumbers } from '@animini/core' 2 | import { DomAdapter } from '../types' 3 | import { getSidesValues, SIDES_KEYS } from '../utils' 4 | 5 | interface AnimatedWithInterpolator extends Animated { 6 | i: ReturnType 7 | } 8 | 9 | export const string: DomAdapter = { 10 | onInit(a: AnimatedWithInterpolator) { 11 | a.i = interpolate(a.value) 12 | }, 13 | parse(value, a) { 14 | if (SIDES_KEYS.includes(a.key!)) { 15 | value = getSidesValues(value) 16 | } 17 | return parseNumbers(value)! 18 | }, 19 | parseInitial(_value, a: AnimatedWithInterpolator) { 20 | return a.i.values 21 | }, 22 | format(value, a: AnimatedWithInterpolator) { 23 | return a.i.compute(value) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /targets/target-dom/src/adapters/transform.ts: -------------------------------------------------------------------------------- 1 | import { parseUnitValue } from '@animini/core' 2 | import { DomAdapter } from '../types' 3 | 4 | export const transform: DomAdapter = { 5 | parse(value, animated) { 6 | const [_value, unit] = parseUnitValue(value) 7 | switch (unit) { 8 | case '%': 9 | return ( 10 | (_value * 11 | parseFloat(getComputedStyle(animated.el as HTMLElement)[animated.key === 'x' ? 'width' : 'height'])) / 12 | 100 13 | ) 14 | case 'vw': 15 | return (_value * window.innerWidth) / 100 16 | case 'vh': 17 | return (_value * window.innerHeight) / 100 18 | } 19 | return _value 20 | } 21 | } 22 | 23 | export const rotate: DomAdapter = { 24 | parse(value) { 25 | const [_value, unit] = parseUnitValue(value) 26 | switch (unit) { 27 | case 'rad': 28 | return (_value / 180) * Math.PI 29 | case 'turn': 30 | return _value * 360 31 | } 32 | return _value 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /targets/target-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | export { dom } from './targetDom' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /targets/target-dom/src/targetDom.ts: -------------------------------------------------------------------------------- 1 | import { color, generic, transform, string, rotate } from './adapters' 2 | import { DomAdapter, Styles, Transform } from './types' 3 | import { Target } from '@animini/core' 4 | import { 5 | getSidesValues, 6 | getTransformStyle, 7 | getTransformValues, 8 | SCROLL_KEYS, 9 | SIDES_KEYS, 10 | someDefined, 11 | TRANSFORM_KEYS 12 | } from './utils' 13 | 14 | const ADAPTERS: Partial> = { 15 | color, 16 | backgroundColor: color, 17 | borderColor: color, 18 | borderTopColor: color, 19 | borderLeftColor: color, 20 | borderBottomColor: color, 21 | borderRightColor: color, 22 | fill: color, 23 | stroke: color, 24 | textDecorationColor: color, 25 | x: transform, 26 | y: transform, 27 | rotate, 28 | clipPath: string, 29 | boxShadow: string, 30 | padding: string, 31 | margin: string, 32 | inset: string, 33 | opacity: undefined, 34 | scale: undefined 35 | } 36 | 37 | const IDENTITY = 'matrix(1, 0, 0, 1, 0, 0)' 38 | 39 | export const dom: Target = { 40 | getElement(element) { 41 | if (typeof element !== string) return element 42 | return document.querySelector(element) 43 | }, 44 | setValues(values, el, initial, idle) { 45 | const _el = el as HTMLElement 46 | 47 | const { x, y, zIndex, scale, scaleX, scaleY, skew, skewX, skewY, rotate, scrollTop, scrollLeft, ...rest } = values 48 | 49 | if (scrollLeft !== void 0 || scrollTop !== void 0) { 50 | const fallbackLeft = el === window ? el.scrollX : _el.scrollLeft 51 | const fallbackTop = el === window ? el.scrollY : _el.scrollTop 52 | el.scrollTo((scrollLeft as number) ?? fallbackLeft, (scrollTop as number) ?? fallbackTop) 53 | } 54 | 55 | if (el !== window) { 56 | for (let key in rest) { 57 | // @ts-expect-error 58 | _el.style[key] = rest[key] 59 | } 60 | 61 | const t = { x, y, scale, scaleX, scaleY, skew, skewX, skewY, rotate } 62 | if (!someDefined(Object.values(t))) return 63 | _el.style.transform = getTransformStyle(t as Transform, initial.transform) 64 | 65 | // TODO can potentially be optimized 👇 66 | if (idle && getComputedStyle(_el).transform === IDENTITY) _el.style.transform = 'none' 67 | } 68 | }, 69 | 70 | getInitialValueAndAdapter(el, key, initial) { 71 | // element is the window 72 | if (el === window) { 73 | if (key === 'scrollTop') return [el.scrollY, generic] 74 | else if (key === 'scrollLeft') return [el.scrollX, generic] 75 | // TODO return type doesn't make any sense 76 | return [0, undefined] 77 | } 78 | 79 | // element is an HTMLElement 80 | const style = getComputedStyle(el as HTMLElement) 81 | const adapter = key in ADAPTERS ? ADAPTERS[key] : generic 82 | 83 | // @ts-expect-error 84 | if (SCROLL_KEYS.includes(key as string)) return [el[key], adapter] 85 | 86 | if (TRANSFORM_KEYS.includes(key as string)) { 87 | initial.transform ||= getTransformValues(style) 88 | return [initial.transform[key as string], adapter] 89 | } 90 | 91 | // @ts-expect-error 92 | if (SIDES_KEYS.includes(key as string)) return [getSidesValues(style[key]), adapter] 93 | 94 | // @ts-expect-error 95 | return [style[key], adapter] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /targets/target-dom/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Adapter } from '@animini/core' 2 | 3 | // this needs some love 4 | export type Styles = Record & { 5 | x: number | string 6 | y: number | string 7 | scale: number 8 | scaleX: number 9 | scaleY: number 10 | skewX: number 11 | skewY: number 12 | skew: number 13 | rotate: number 14 | scrollTop: number | string 15 | scrollLeft: number | string 16 | } 17 | 18 | export type Transform = { 19 | scale: number 20 | scaleX: number 21 | scaleY: number 22 | // skewX: number 23 | // skewY: number 24 | // skew: number 25 | rotate: number 26 | x: number 27 | y: number 28 | } 29 | 30 | export type DomAdapter = Adapter 31 | -------------------------------------------------------------------------------- /targets/target-dom/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from './types' 2 | 3 | export const TRANSFORM_KEYS = ['x', 'y', 'scale', 'rotate', 'scaleX', 'scaleY', 'skew', 'skewX', 'skewY'] 4 | export const SIDES_KEYS = ['inset', 'margin', 'padding'] 5 | export const SCROLL_KEYS = ['scrollLeft', 'scrollTop'] 6 | 7 | const RAD_TO_DEG = 180 / Math.PI 8 | 9 | function round(n: number, d = 0) { 10 | const e = 10 ** d 11 | return Math.round(n * 1 * e) / e 12 | } 13 | 14 | export function someDefined(...args: any[]) { 15 | return args.some((v) => v !== void 0) 16 | } 17 | 18 | export function getSidesValues(value: string) { 19 | const n = value.split(' ') 20 | switch (n.length) { 21 | case 3: 22 | return value + ' ' + n[2] 23 | case 2: 24 | return value + ' ' + value 25 | case 1: 26 | return value + ' ' + value + ' ' + value + ' ' + value 27 | } 28 | return value 29 | } 30 | 31 | export function getTransformValues(style: CSSStyleDeclaration): Transform { 32 | const matrix = new DOMMatrixReadOnly(style.transform) 33 | const scaleX = round(Math.sqrt(matrix.a ** 2 + matrix.b ** 2), 3) 34 | 35 | // const skewX = Math.atan2(matrix.d, matrix.c) * RAD_TO_DEG - 90 36 | // const skewY = Math.atan2(matrix.b, matrix.a) * RAD_TO_DEG 37 | 38 | // console.log( 39 | // { 40 | // x: matrix.e, 41 | // y: matrix.f, 42 | // scale: scaleX, 43 | // scaleX, 44 | // scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), 45 | // skew: skewX, 46 | // skewX, 47 | // skewY, 48 | // rotate: Math.atan2(matrix.b, matrix.a) * RAD_TO_DEG 49 | // }, 50 | // matrix 51 | // ) 52 | 53 | return { 54 | x: matrix.e, 55 | y: matrix.f, 56 | scale: scaleX, 57 | scaleX, 58 | scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), 59 | // skew: skewX, 60 | // skewX, 61 | // skewY, 62 | rotate: Math.atan2(matrix.b, matrix.a) * RAD_TO_DEG 63 | } 64 | } 65 | 66 | export function getTransformStyle(t: Transform, i: Transform) { 67 | let s = '' 68 | if (someDefined(t.x, t.y, i.x, i.y)) s += `translate(${t.x ?? i.x}px, ${t.y ?? i.y}px)` 69 | if (someDefined(t.scale)) s += ` scale(${t.scale})` 70 | else if (someDefined(t.scaleX, t.scaleY, i.scaleX, i.scaleY)) 71 | s += ` scale(${t.scaleX ?? i.scaleX}, ${t.scaleY ?? i.scaleY})` 72 | if (someDefined(t.rotate, i.rotate)) s += ` rotate(${t.rotate ?? i.rotate}deg)` 73 | // if (someDefined(t.skew, i.skew)) s += ` skew(${t.skew ?? i.skew}deg)` 74 | return s 75 | } 76 | -------------------------------------------------------------------------------- /targets/target-three/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @animini/target-three 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 2d867a9: Package refactor, introducing vanilla api 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [2d867a9] 12 | - @animini/core@0.3.0 13 | 14 | ## 0.2.6 15 | 16 | ### Patch Changes 17 | 18 | - Updated dependencies [c838bf8] 19 | - @animini/core@0.2.6 20 | 21 | ## 0.2.5 22 | 23 | ### Patch Changes 24 | 25 | - 0885116: Add string interpolation 26 | - Updated dependencies [0885116] 27 | - @animini/core@0.2.5 28 | 29 | ## 0.2.4 30 | 31 | ### Patch Changes 32 | 33 | - a0f7cdb: Adapter inside Animated 34 | - Updated dependencies [a0f7cdb] 35 | - @animini/core@0.2.4 36 | 37 | ## 0.2.3 38 | 39 | ### Patch Changes 40 | 41 | - Updated dependencies [fd31840] 42 | - @animini/core@0.2.3 43 | 44 | ## 0.2.2 45 | 46 | ### Patch Changes 47 | 48 | - Updated dependencies [6c367d3] 49 | - @animini/core@0.2.2 50 | 51 | ## 0.2.1 52 | 53 | ### Patch Changes 54 | 55 | - bf2fd1b: Internal refactoring 56 | - Updated dependencies [bf2fd1b] 57 | - @animini/core@0.2.1 58 | 59 | ## 0.2.0 60 | 61 | ### Minor Changes 62 | 63 | - 10ba638: Refactor package 64 | 65 | ### Patch Changes 66 | 67 | - Updated dependencies [10ba638] 68 | - @animini/core@0.2.0 69 | -------------------------------------------------------------------------------- /targets/target-three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@animini/target-three", 3 | "version": "0.3.0", 4 | "description": "animini target for Three", 5 | "keywords": [], 6 | "main": "dist/animini-target-three.cjs.js", 7 | "module": "dist/animini-target-three.esm.js", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dbismut/animini.git", 12 | "directory": "packages/target-three" 13 | }, 14 | "bugs": "https://github.com/dbismut/animini/issues", 15 | "peerDependencies": { 16 | "three": ">=0.140.0" 17 | }, 18 | "dependencies": { 19 | "@animini/core": "0.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /targets/target-three/src/adapters/color.ts: -------------------------------------------------------------------------------- 1 | import { ThreeAdapter } from '../types' 2 | import { Color } from 'three' 3 | 4 | const kk = new Color() 5 | 6 | export const color: ThreeAdapter = { 7 | parse(str: string) { 8 | return kk.set(str) as unknown as Record 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /targets/target-three/src/adapters/euler.ts: -------------------------------------------------------------------------------- 1 | import { Euler, Vector3 } from 'three' 2 | import { ThreeAdapter } from '../types' 3 | 4 | export const euler: ThreeAdapter = { 5 | parseInitial(euler: Euler) { 6 | const v = new Vector3() 7 | return v.setFromEuler(euler) as any as Record 8 | }, 9 | onUpdate(animated) { 10 | // @ts-ignore 11 | animated.el[animated.key].setFromVector3(animated.value) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /targets/target-three/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color' 2 | export * from './euler' 3 | -------------------------------------------------------------------------------- /targets/target-three/src/index.ts: -------------------------------------------------------------------------------- 1 | export { three } from './targetThree' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /targets/target-three/src/targetThree.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '@animini/core' 2 | import { Color, Euler } from 'three' 3 | import { color, euler } from './adapters' 4 | import { ThreeElementType, ThreeAdapter, ThreeValues } from './types' 5 | 6 | const ADAPTERS = new Map([ 7 | [Color, color], 8 | [Euler, euler] 9 | ]) 10 | 11 | export const three: Target> = { 12 | getInitialValueAndAdapter(element, key) { 13 | const value = element[key] 14 | const constructor = value.__proto__.constructor 15 | const adapter = ADAPTERS.get(constructor) 16 | return [value, adapter] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /targets/target-three/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Adapter } from '@animini/core' 2 | import type { Object3D, Material } from 'three' 3 | 4 | export type ThreeElementType = Object3D | Material 5 | 6 | // TODO this also needs some love 7 | export type ThreeValues = Record< 8 | keyof Element, 9 | string | number | Record | number[] 10 | > 11 | 12 | export type ThreeAdapter = Adapter 13 | -------------------------------------------------------------------------------- /test-perf/.gitignore: -------------------------------------------------------------------------------- 1 | *.log.* -------------------------------------------------------------------------------- /test-perf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-perf", 3 | "version": "0.0.0", 4 | "keywords": [], 5 | "private": true, 6 | "dependencies": { 7 | "@animini/core-latest": "https://gitpkg.now.sh/dbismut/animini/packages/core?main&v=0.2.6", 8 | "@animini/target-dom-latest": "https://gitpkg.now.sh/dbismut/animini/targets/target-dom?main&v=0.2.6", 9 | "benchmark": "^2.1.4", 10 | "console-table-printer": "^2.11.0", 11 | "fs-extra": "^10.0.1", 12 | "microtime": "^3.1.0", 13 | "systeminformation": "^5.11.20" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test-perf/tests/bench.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import fs from 'fs-extra' 6 | import path from 'path' 7 | import { Table } from 'console-table-printer' 8 | import Benchmark from 'benchmark' 9 | import si from 'systeminformation' 10 | import { round, animatedBench } from './utils' 11 | 12 | let fullResults = {} 13 | 14 | beforeAll(() => { 15 | fullResults = {} 16 | }) 17 | 18 | async function bench(label, cb) { 19 | await it(`${label} should run faster than $ms`, async () => { 20 | const results = await new Promise((resolve) => { 21 | const suite = new Benchmark.Suite() 22 | suite 23 | .add('source', () => void cb(true)) 24 | .add('latest', () => void cb(false)) 25 | .on('cycle', function (event) { 26 | // eslint-disable-next-line no-console 27 | console.log(String(event.target)) 28 | }) 29 | .on('complete', function () { 30 | resolve(this) 31 | }) 32 | .run() 33 | }) 34 | 35 | Object.assign(fullResults, { 36 | [label]: { 37 | latest: results['1'].hz, 38 | source: results['0'].hz, 39 | vs: 1 - results['0'].hz / results['1'].hz 40 | } 41 | }) 42 | expect(0).toBeLessThan(1000) 43 | }) 44 | } 45 | 46 | bench('lerp int (10 itr.)', (useSource) => animatedBench(useSource, { limit: 10 })) 47 | bench('spring int (10 itr.)', (useSource) => animatedBench(useSource, { motion: 'spring', limit: 10 })) 48 | bench('spring array (10 itr.)', (useSource) => 49 | animatedBench(useSource, { motion: 'spring', limit: 10, from: [0, 0, 0], to: [100, 200, 300] }) 50 | ) 51 | bench('spring color (10 itr.)', (useSource) => 52 | animatedBench(useSource, { motion: 'spring', limit: 10, from: '#ff0000', to: '#000eac', adapter: 'color' }) 53 | ) 54 | 55 | function formatResults(results) { 56 | const r = {} 57 | r['latest'] = Benchmark.formatNumber(~~results.latest) + 'ops/s' 58 | r['source'] = Benchmark.formatNumber(~~results.source) + 'ops/s' 59 | r['vs'] = round(results.vs * 100, 2) + '%' 60 | return r 61 | } 62 | 63 | afterAll(async () => { 64 | const perfPath = path.resolve(__dirname, `logs/bench-${new Date().toISOString()}.log.json`) 65 | const cpu = await si.cpu() 66 | const p = new Table() 67 | 68 | Object.entries(fullResults).forEach(([key, source]) => { 69 | const results = formatResults(source) 70 | p.addRow({ test: key, ...results }, { color: source.vs > 0.1 ? 'red' : source.vs < -0.1 ? 'green' : undefined }) 71 | }) 72 | 73 | p.printTable() 74 | fs.ensureFileSync(perfPath) 75 | fs.writeJSONSync(perfPath, { cpu, results: fullResults }, { spaces: ' ' }) 76 | }) 77 | -------------------------------------------------------------------------------- /test-perf/tests/perf.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import fs from 'fs-extra' 6 | import path from 'path' 7 | import microtime from 'microtime' 8 | import { Table } from 'console-table-printer' 9 | import si from 'systeminformation' 10 | import { animatedBench, round, kFormat } from './utils' 11 | 12 | let sourceResults = {} 13 | let latestResults = {} 14 | 15 | beforeAll(() => { 16 | sourceResults = {} 17 | latestResults = {} 18 | }) 19 | 20 | function run(label, cb, runs = 100) { 21 | const source = { time: 0, iterations: 0, runs } 22 | const latest = { time: 0, iterations: 0, runs } 23 | 24 | for (let i = 0; i < runs; i++) { 25 | const useSourceFirst = Math.random() < 0.5 26 | ;[useSourceFirst, !useSourceFirst].forEach((useSource) => { 27 | const start = microtime.now() 28 | const iterations = cb(useSource) 29 | const time = microtime.now() - start 30 | const obj = useSource ? source : latest 31 | obj.iterations += iterations 32 | obj.time += time 33 | }) 34 | } 35 | 36 | source.timePerItr = source.time / source.iterations 37 | latest.timePerItr = latest.time / latest.iterations 38 | 39 | const ratio = source.timePerItr / latest.timePerItr 40 | source.vs = ratio - 1 41 | 42 | Object.assign(sourceResults, { [label]: source }) 43 | Object.assign(latestResults, { [label]: latest }) 44 | return source.time 45 | } 46 | 47 | function bench(label, limit, runs, cb) { 48 | it(`${label} should run faster than ${limit}ms`, () => { 49 | expect(run(label, cb, runs)).toBeLessThan(limit * 1000) 50 | }) 51 | } 52 | 53 | bench('lerp int (10 itr.)', 600, 5000, (useSource) => animatedBench(useSource, { limit: 10 })) 54 | bench('spring int (10 itr.)', 600, 5000, (useSource) => animatedBench(useSource, { motion: 'spring', limit: 10 })) 55 | bench('spring array (10 itr.)', 600, 5000, (useSource) => 56 | animatedBench(useSource, { motion: 'spring', limit: 10, from: [0, 0, 0], to: [100, 200, 300] }) 57 | ) 58 | bench('spring color (10 itr.)', 600, 5000, (useSource) => 59 | animatedBench(useSource, { motion: 'spring', limit: 10, from: '#ff0000', to: '#000eac', adapter: 'color' }) 60 | ) 61 | bench('lerp int', 1500, 5000, (useSource) => animatedBench(useSource, { to: 10 })) 62 | bench('spring int', 600, 5000, (useSource) => animatedBench(useSource, { motion: 'spring', to: 10 })) 63 | bench('spring array', 1800, 5000, (useSource) => 64 | animatedBench(useSource, { motion: 'spring', from: [0, 0, 0], to: [100, 200, 300] }) 65 | ) 66 | bench('spring color', 1800, 5000, (useSource) => 67 | animatedBench(useSource, { motion: 'spring', from: '#ff0000', to: '#000eac', adapter: 'color' }) 68 | ) 69 | 70 | function formatResults(results) { 71 | const r = {} 72 | r['runs'] = kFormat(results.runs) 73 | r['iterations'] = kFormat(results.iterations) 74 | r['time (ms)'] = round(results.time / 1000, 1) 75 | r['t/itr (ns)'] = round(results.time / results.iterations) 76 | r['vs latest'] = round(results.vs * 100, 2) + (results.vs < 0 ? '% faster' : '% slower') 77 | return r 78 | } 79 | 80 | afterAll(async () => { 81 | const perfPath = path.resolve(__dirname, `logs/perf-${new Date().toISOString()}.log.json`) 82 | const cpu = await si.cpu() 83 | const p = new Table() 84 | 85 | Object.entries(sourceResults).forEach(([key, source]) => { 86 | const results = formatResults(source) 87 | 88 | p.addRow({ test: key, ...results }, { color: source.vs > 0.1 ? 'red' : source.vs < -0.1 ? 'green' : undefined }) 89 | }) 90 | 91 | p.printTable() 92 | fs.ensureFileSync(perfPath) 93 | fs.writeJSONSync(perfPath, { cpu, results: sourceResults }, { spaces: ' ' }) 94 | }) 95 | -------------------------------------------------------------------------------- /test-perf/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { Animated } from '../../packages/core/src/animated/Animated' 2 | import { spring, lerp } from '../../packages/core/src/algorithms' 3 | import { color } from '../../targets/target-dom/src/adapters' 4 | 5 | import { Animated as AnimatedLatest } from '@animini/core-latest/src/animated/Animated' 6 | import { lerp as lerpLatest, spring as springLatest } from '@animini/core-latest/src/algorithms' 7 | import { color as colorLatest } from '@animini/target-dom-latest/src/adapters' 8 | 9 | const AdaptersSource = { color } 10 | const AdaptersLatest = { color: colorLatest } 11 | 12 | export function animateLatest({ motion, limit, from, to, config, adapter }) { 13 | const loop = { time: { elapsed: 0, delta: 16 } } 14 | const _motion = { easing: motion === 'spring' ? springLatest(config) : lerpLatest(config) } 15 | const animated = new AnimatedLatest({ value: from, adapter: AdaptersLatest[adapter] }, loop) 16 | animated.start(to, _motion) 17 | let iterations = 0 18 | while (!animated.idle && iterations < limit) { 19 | iterations++ 20 | loop.time.elapsed += loop.time.delta 21 | animated.update() 22 | } 23 | 24 | // console.log('SOURCE', animated.value) 25 | 26 | return iterations 27 | } 28 | 29 | export function animateSource({ motion, limit, from, to, config, adapter }) { 30 | const loop = { time: { elapsed: 0, delta: 16 } } 31 | const _motion = { easing: motion === 'spring' ? spring(config) : lerp(config) } 32 | const animated = new Animated({ value: from, adapter: AdaptersSource[adapter] }, loop) 33 | animated.start(to, _motion) 34 | let iterations = 0 35 | while (!animated.idle && iterations < limit) { 36 | iterations++ 37 | loop.time.elapsed += loop.time.delta 38 | animated.update() 39 | } 40 | 41 | // console.log('SOURCE', animated.value) 42 | 43 | return iterations 44 | } 45 | 46 | export function animatedBench( 47 | useSource, 48 | { motion = 'lerp', limit = Infinity, from = 0, to = 1 + Math.random() * 1000, config = {}, adapter } = {} 49 | ) { 50 | if (useSource) return animateSource({ motion, limit, from, to, config, adapter }) 51 | return animateLatest({ motion, limit, from, to, config, adapter }) 52 | } 53 | 54 | export function round(number, dec = 3) { 55 | return Number(number.toFixed(dec)) 56 | } 57 | 58 | export function kFormat(num) { 59 | return Math.abs(num) > 999 ? Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + 'K' : Math.sign(num) * Math.abs(num) 60 | } 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | { 3 | "include": ["packages/*/src/**/*", "targets/*/src/**/*"], 4 | "compilerOptions": { 5 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": ["DOM", "ESNext"] /* Specify library files to be included in the compilation. */, 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "incremental": true, /* Enable incremental compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. We use it for the Bezier returned value. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true /* Do not resolve the real path of symlinks. */, 51 | "skipLibCheck": true, 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | /* Experimental Options */ 58 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 59 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 60 | // error out if import and file system have a casing mismatch. Recommended by TS 61 | "forceConsistentCasingInFileNames": true, 62 | } 63 | } 64 | --------------------------------------------------------------------------------