40 |
s.setValue(0)}
43 | onMouseDown={() => s.setValue(1)}
44 | style={boxStyles}
45 | />
46 |
47 |
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/defaults.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | type dummy = (inst: Object) => void
4 |
5 | type defaultInstanceParams = {
6 | onUpdate: dummy,
7 | onComplete: dummy,
8 | onStart: dummy,
9 |
10 | // Change this to iterations
11 | iterations: number | string,
12 | direction: string,
13 | autoplay: boolean,
14 | offset: number
15 | }
16 |
17 | type defaultTweensParams = {
18 | duration: number,
19 | delay: number,
20 | easing: string,
21 | elasticity: number,
22 | round: number
23 | }
24 |
25 | const noop = (inst: Object): void => {}
26 |
27 | export const getDefaultInstanceParams = (): defaultInstanceParams => ({
28 | onUpdate: noop,
29 | onComplete: noop,
30 | onStart: noop,
31 |
32 | iterations: 1,
33 | direction: 'normal',
34 | autoplay: false,
35 | offset: 0
36 | })
37 |
38 | export const getDefaultTweensParams = (): defaultTweensParams => ({
39 | duration: 1000,
40 | delay: 0,
41 | easing: 'linear',
42 | elasticity: 500,
43 | round: 0
44 | })
45 |
46 | export const validTransforms = [
47 | 'translateX',
48 | 'translateY',
49 | 'translateZ',
50 | 'rotate',
51 | 'rotateX',
52 | 'rotateY',
53 | 'rotateZ',
54 | 'scale',
55 | 'scaleX',
56 | 'scaleY',
57 | 'scaleZ',
58 | 'skewX',
59 | 'skewY',
60 | 'perspective'
61 | ]
62 |
--------------------------------------------------------------------------------
/examples/Extra/speed.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { boxStyles } from '../styles'
4 |
5 | import { createTimeline, helpers } from '../../build/animated-timeline.min.js'
6 |
7 | const t = createTimeline({
8 | direction: 'alternate',
9 | easing: 'easeInOutSine',
10 | iterations: Infinity,
11 | speed: 0.75
12 | })
13 |
14 | const animate = (one, two) => {
15 | t
16 | .sequence([
17 | t.animate({
18 | el: one,
19 | scale: helpers.transition({
20 | from: 2,
21 | to: 1
22 | })
23 | }),
24 |
25 | t.animate({
26 | el: two,
27 | rotate: '360deg',
28 | offset: helpers.startBefore(1200)
29 | })
30 | ])
31 | .start()
32 | }
33 |
34 | export class ChangeSpeed extends React.Component {
35 | timer = null
36 |
37 | componentDidMount() {
38 | animate('#speed-one', '#speed-two')
39 |
40 | // Change the speed after 3s
41 | setTimeout(() => {
42 | t.getAnimations().forEach(animation => {
43 | animation.setSpeed(0.65)
44 | })
45 | }, 3000)
46 | }
47 |
48 | render() {
49 | return (
50 |
51 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/Timeline/sequence.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { boxStyles } from '../styles'
4 |
5 | import { createTimeline, helpers } from '../../build/animated-timeline.min.js'
6 |
7 | const t = createTimeline({
8 | direction: 'alternate',
9 | speed: 0.7,
10 | easing: 'easeInOutSine',
11 | iterations: Infinity
12 | })
13 |
14 | const one = React.createRef()
15 |
16 | const two = React.createRef()
17 |
18 | const animate = () => {
19 | t.sequence([
20 | t.animate({
21 | el: one.current,
22 | translateX: helpers.transition({
23 | from: 20,
24 | to: 10
25 | }),
26 | rotate: {
27 | value: 720
28 | },
29 | scale: helpers.transition({
30 | from: 2,
31 | to: 1
32 | })
33 | }),
34 |
35 | t.animate({
36 | el: two.current,
37 | translateY: helpers.transition({
38 | from: 100,
39 | to: 50
40 | }),
41 | height: '30px'
42 | })
43 | ])
44 | }
45 |
46 | export class SequenceTimeline extends React.Component {
47 | componentDidMount() {
48 | animate()
49 | }
50 |
51 | render() {
52 | return (
53 |
54 |
55 |
56 |
57 | )
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/examples/Lifecycle/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { boxStyles } from '../styles'
4 |
5 | import { createTimeline, helpers } from '../../build/animated-timeline.min.js'
6 |
7 | const t = createTimeline({
8 | speed: 1,
9 | iterations: 1,
10 | direction: 'alternate',
11 | easing: 'easeInOutSine',
12 | speed: 0.25
13 | })
14 |
15 | export class Lifecycle extends React.Component {
16 | state = {
17 | value: 0
18 | }
19 |
20 | componentDidMount() {
21 | t
22 | .animate({
23 | scale: helpers.transition({
24 | from: 2,
25 | to: 0.4
26 | })
27 | })
28 | .start()
29 |
30 | t.onStart = ({ began }) => {
31 | if (began) {
32 | console.log('Started animation!')
33 | }
34 | }
35 |
36 | t.onComplete = ({ completed, controller: { reverse, restart } }) => {
37 | if (completed) {
38 | console.log('Completed... starting again!')
39 | reverse()
40 | restart()
41 | }
42 | }
43 |
44 | t.onUpdate = ({ progress }) => {
45 | this.setState({ value: Math.floor(Number(progress)) })
46 | }
47 | }
48 |
49 | componentWillUnmount() {
50 | t.cancel()
51 | }
52 |
53 | render() {
54 | return (
55 |
56 | {this.state.value}
57 |
58 | )
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/spring/Controls.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Spring } from '../../build/animated-timeline.min.js'
4 |
5 | import { boxStyles } from './../styles'
6 |
7 | const s = Spring({ friction: 4, tension: 2 })
8 |
9 | export class SpringControls extends React.Component {
10 | state = {
11 | value: 0
12 | }
13 |
14 | componentDidMount() {
15 | s.animate({
16 | property: 'translateX',
17 | map: {
18 | inputRange: [0, 1],
19 | outputRange: ['0px', '30px']
20 | }
21 | })
22 | }
23 |
24 | handleChange = (e) => {
25 | const value = e.target.value
26 | s.seek(value)
27 | this.setState({ value })
28 | }
29 |
30 | render() {
31 | return (
32 |
33 | s.setValue(0)}
35 | onMouseDown={() => s.setValue(1)}
36 | style={boxStyles}
37 | />
38 |
45 | s.reset()}>Reset
46 | s.start()}>Start
47 | s.stop()}>Stop
48 | s.reverse()}>Reverse
49 |
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/examples/Seeking/basic.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { boxStyles } from '../styles'
4 |
5 | import {
6 | createTimeline,
7 | helpers,
8 | createMover
9 | } from '../../build/animated-timeline.min.js'
10 |
11 | const t = createTimeline({
12 | speed: 1,
13 | iterations: 1,
14 | direction: 'alternate',
15 | easing: 'easeInOutSine'
16 | })
17 |
18 | const seekAnimation = createMover(t)
19 |
20 | export class SeekBasic extends React.Component {
21 | state = { value: 0 }
22 |
23 | componentDidMount() {
24 | t.animate({
25 | scale: helpers.transition({
26 | from: 4,
27 | to: 2
28 | })
29 | })
30 | }
31 |
32 | handleChange = e => {
33 | this.setState({
34 | value: e.target.value
35 | })
36 |
37 | seekAnimation(this.state.value)
38 |
39 | // or with a callback function
40 |
41 | // This will seek the animation from the reverse direction
42 | // seekAnimation(({ duration }) => duration - this.state.value * 10)
43 | }
44 |
45 | render() {
46 | return (
47 |
48 |
49 |
50 |
57 |
58 |
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
4 |
5 | const output = () => ({
6 | filename: 'animated-timeline.min.js',
7 | path: path.resolve(__dirname, './build'),
8 | publicPath: '/',
9 | libraryTarget: 'umd'
10 | })
11 |
12 | const externals = () => ({
13 | fastdom: 'fastdom',
14 | 'html-tags': 'html-tags',
15 | invariant: 'invariant',
16 | 'prop-types': 'prop-types',
17 | rebound: 'rebound',
18 | 'svg-tag-names': 'svg-tag-names',
19 | 'react': 'react',
20 | })
21 |
22 | const jsLoader = () => ({
23 | test: /\.js$/,
24 | include: path.resolve(__dirname, './src'),
25 | exclude: ['node_modules', 'public', 'demo', 'dist', '.cache', 'docs', 'examples', 'media', 'flow-typed'],
26 | use: 'babel-loader'
27 | })
28 |
29 | const plugins = () => [
30 | new webpack.LoaderOptionsPlugin({
31 | minimize: true,
32 | debug: false
33 | }),
34 | new webpack.DefinePlugin({
35 | 'process.env.NODE_ENV': JSON.stringify('production')
36 | }),
37 | new webpack.optimize.ModuleConcatenationPlugin(),
38 | new UglifyJSPlugin()
39 | ]
40 |
41 | module.exports = {
42 | entry: path.resolve(__dirname, './src/index.js'),
43 | mode: 'production',
44 | output: output(),
45 | target: 'web',
46 | externals: externals(),
47 | devtool: 'source-map',
48 | module: {
49 | rules: [jsLoader()]
50 | },
51 | plugins: plugins()
52 | }
53 |
--------------------------------------------------------------------------------
/examples/Animate-Component/Advance.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import { Animate, helpers } from '../../build/animated-timeline.min.js'
4 |
5 | import { boxStyles } from '../styles'
6 |
7 | const timingProps = {
8 | duration: 2000
9 | }
10 |
11 | const animationProps = {
12 | translateX: helpers.transition({ from: 0, to: 200 }),
13 | scale: helpers.transition({ from: 2, to: 4 }),
14 | rotate: helpers.transition({ from: '360deg', to: '180deg' }),
15 | opacity: helpers.transition({ from: 0.2, to: 0.8 })
16 | }
17 |
18 | export class AnimateAdvance extends Component {
19 | state = { value: 0 }
20 |
21 | handleChange = e => this.setState({ value: e.target.value })
22 |
23 | onUpdate = props => {
24 | this.state.value = props.progress
25 | }
26 |
27 | seekAnimation = props => props.duration - this.state.value * 20
28 |
29 | render() {
30 | return (
31 |
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/examples/spring/Bounciness.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Spring } from '../../build/animated-timeline.min.js'
4 |
5 | import { boxStyles } from './../styles'
6 |
7 | const s = Spring({ bounciness: 26, speed: 4 })
8 |
9 | export class SpringBounciness extends React.Component {
10 | state = {
11 | rotate: '0deg',
12 | translateX: ''
13 | }
14 |
15 | componentDidMount() {
16 | s.animate({
17 | property: 'scale',
18 | map: {
19 | inputRange: [0, 1],
20 | outputRange: [1, 1.5]
21 | },
22 | interpolation: (style, value, options) =>
23 | this.handleInterpolations(value, options),
24 | shouldOscillate: true
25 | })
26 | }
27 |
28 | handleInterpolations = (value, options) => {
29 | this.setState({
30 | translateX: options.em(options.mapValues(value, 1, 1.5, 0, 1)),
31 | rotate: options.deg(options.mapValues(value, 1, 1.5, 0, 360))
32 | })
33 | }
34 |
35 | componentWillUnmount() {
36 | s.remove()
37 | }
38 |
39 | render() {
40 | return (
41 |
42 | s.setValue(0)}
44 | onMouseDown={() => s.setValue(1)}
45 | style={{
46 | ...boxStyles,
47 | transform: `translateX(${this.state.translateX}) rotate(${
48 | this.state.rotate
49 | })`
50 | }}
51 | />
52 |
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/examples/spring/Interpolations.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Spring } from '../../build/animated-timeline.min.js'
4 |
5 | import { boxStyles } from './../styles'
6 |
7 | const s = Spring({ friction: 15, tension: 3 })
8 |
9 | export class SpringInterpolate extends React.Component {
10 | state = {
11 | translateX: '',
12 | backgroundColor: '#a8123a'
13 | }
14 |
15 | componentDidMount() {
16 | s.animate({
17 | property: 'border-radius',
18 | map: {
19 | inputRange: [0, 1],
20 | outputRange: ['1px', '40px']
21 | },
22 | interpolation: (style, value, options) =>
23 | this.handleInterpolations(value, options),
24 | shouldOscillate: true
25 | })
26 | }
27 |
28 | componentWillUnmount() {
29 | s.remove()
30 | }
31 |
32 | handleInterpolations = (value, options) => {
33 | this.setState({
34 | translateX: options.em(options.mapValues(value, 3, 40, 0, 1)),
35 | backgroundColor: options.interpolateColor(
36 | value,
37 | '#4a79c4',
38 | '#a8123a',
39 | 0,
40 | 60
41 | )
42 | })
43 | }
44 |
45 | render() {
46 | return (
47 |
48 | s.setValue(0)}
50 | onMouseDown={() => s.setValue(1)}
51 | style={{
52 | ...boxStyles,
53 | transform: `translateX(${this.state.translateX})`,
54 | backgroundColor: this.state.backgroundColor
55 | }}
56 | />
57 |
58 | )
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/core/transforms.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { filterArray, stringContains } from '../utils/engineUtils'
4 |
5 | // Get the transform unit
6 | export const getTransformUnit = (propName: string): void | string => {
7 | if (stringContains(propName, 'translate') || propName === 'perspective')
8 | return 'px'
9 | if (stringContains(propName, 'rotate') || stringContains(propName, 'skew'))
10 | return 'deg'
11 | }
12 |
13 | // Get the transform value (scale, transform, rotate)
14 | export const getTransformValue = (
15 | el: HTMLElement,
16 | propName: string
17 | ): number | string => {
18 | // Get the default unit for transform
19 | const defaultUnit = getTransformUnit(propName)
20 | // Get the default value for transform
21 | const defaultVal = stringContains(propName, 'scale') ? 1 : 0 + defaultUnit
22 | // CSS transform string `scale(1)`, `transform(200)`, etc
23 | const str = el.style.transform
24 | if (!str) return defaultVal
25 | let match = []
26 | let props = []
27 | let values = []
28 | const rgx = /(\w+)\((.+?)\)/g
29 | while ((match = rgx.exec(str))) {
30 | props.push(match[1]) // Prop name `scale`
31 | values.push(match[2]) // Prop value `1`
32 | }
33 | // Get the value for corresponding propName (scale, transform)
34 | const value = values.filter((val, i) => props[i] === propName)
35 | // Transform value or return the default value
36 | return value.length ? value[0] : defaultVal
37 | }
38 |
39 | export const validTransforms = [
40 | 'translateX',
41 | 'translateY',
42 | 'translateZ',
43 | 'rotate',
44 | 'rotateX',
45 | 'rotateY',
46 | 'rotateZ',
47 | 'scale',
48 | 'scaleX',
49 | 'scaleY',
50 | 'scaleZ',
51 | 'skewX',
52 | 'skewY',
53 | 'perspective'
54 | ]
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "animated-timeline",
3 | "version": "1.0.0",
4 | "description": "Timeline based animations in React",
5 | "main": "build/animated-timeline.min.js",
6 | "files": [
7 | "build"
8 | ],
9 | "scripts": {
10 | "start": "./node_modules/.bin/parcel demo/index.html",
11 | "flow": "./node_modules/.bin/flow",
12 | "test": "./node_modules/.bin/jest",
13 | "build": "NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack.config.js --progress",
14 | "build:watch": "NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack.config.js -w --progress",
15 | "format": "find src -name '*.js' | xargs ./node_modules/.bin/prettier --write --no-semi --single-quote"
16 | },
17 | "author": "Nitin Tulswani
",
18 | "license": "ISC",
19 | "devDependencies": {
20 | "babel-core": "^6.26.0",
21 | "babel-jest": "^23.0.0-alpha.0",
22 | "babel-loader": "^7.1.4",
23 | "babel-plugin-transform-class-properties": "^6.24.1",
24 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
25 | "babel-preset-env": "^1.6.1",
26 | "babel-preset-flow": "^6.23.0",
27 | "babel-preset-react": "^6.24.1",
28 | "flow-bin": "^0.69.0",
29 | "husky": "^0.14.3",
30 | "jest": "^22.4.2",
31 | "parcel-bundler": "^1.7.1",
32 | "prettier": "^1.12.0",
33 | "react": "^16.3.2",
34 | "react-dom": "^16.3.0",
35 | "react-test-renderer": "^16.2.0",
36 | "uglifyjs-webpack-plugin": "^1.2.5",
37 | "webpack": "^4.6.0",
38 | "webpack-cli": "^2.1.2"
39 | },
40 | "peerDependencies": {
41 | "react": "^16.3.2",
42 | "react-dom": "^16.3.0"
43 | },
44 | "dependencies": {
45 | "fastdom": "^1.0.8",
46 | "html-tags": "^2.0.0",
47 | "invariant": "^2.2.4",
48 | "prop-types": "^15.6.1",
49 | "rebound": "^0.1.0",
50 | "svg-tag-names": "^1.1.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 |
4 | import { boxStyles } from '../examples/styles'
5 |
6 | import { AnimateBasic } from '../examples/Animate-Component/Basic'
7 | import { AnimateAdvance } from '../examples/Animate-Component/Advance'
8 | import { AnimateControls } from '../examples/Animate-Component/Controls'
9 |
10 | import { BasicTimeline } from '../examples/Timeline/basic'
11 | import { SequenceTimeline } from '../examples/Timeline/sequence'
12 | import { Timing } from '../examples/Timeline/timing'
13 | import { MultipleElements } from '../examples/Timeline/Multiple'
14 | import { KeyframesExample } from '../examples/Keyframes'
15 | import { SeekBasic } from '../examples/Seeking/basic'
16 | import { Lifecycle } from '../examples/Lifecycle'
17 | import { PromiseAPI } from '../examples/Promise'
18 | import { Staggered } from '../examples/Timeline/Staggered'
19 | import { ChangeSpeed } from '../examples/Extra/speed'
20 | import { Finish } from '../examples/Extra/Finish'
21 | import { SpringSystem } from '../examples/spring/Spring'
22 | import { SpringInterpolate } from '../examples/spring/Interpolations'
23 | import { SpringCallback } from '../examples/spring/Callback'
24 | import { SpringControls } from '../examples/spring/Controls'
25 | import { SpringMultiple } from '../examples/spring/Multiple'
26 | import { SpringStart } from '../examples/spring/Start'
27 | import { SpringVelocity } from '../examples/spring/Velocity'
28 | import { SpringBounciness } from '../examples/spring/Bounciness'
29 | import { SpringBlend } from '../examples/spring/Blend'
30 | import { SpringPromise } from '../examples/spring/SpringPromise'
31 |
32 | class App extends React.Component {
33 | render() {
34 | return (
35 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | render( , document.getElementById('root'))
43 |
--------------------------------------------------------------------------------
/examples/Animate-Component/Controls.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import { Animate, helpers } from '../../build/animated-timeline.min.js'
4 |
5 | const styles = {
6 | width: '20px',
7 | height: '20px',
8 | backgroundColor: 'pink',
9 | marginTop: 30
10 | }
11 |
12 | const timingProps = {
13 | duration: 1000,
14 | direction: 'alternate',
15 | iterations: Infinity
16 | }
17 |
18 | const animationProps = {
19 | rotate: {
20 | value: helpers.transition({ from: 360, to: 180 })
21 | },
22 | scale: helpers.transition({ from: 1, to: 2 })
23 | }
24 |
25 | export class AnimateControls extends Component {
26 | state = {
27 | start: false,
28 | reset: false,
29 | reverse: false,
30 | restart: false,
31 | finish: false
32 | }
33 |
34 | handleStateUpdate = which => {
35 | this.setState(state => ({
36 | [which]: !state[which]
37 | }))
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
53 |
54 |
55 |
this.handleStateUpdate('start')}>Start
56 |
this.handleStateUpdate('reverse')}>
57 | Reverse
58 |
59 |
this.handleStateUpdate('reset')}>Reset
60 |
this.handleStateUpdate('restart')}>
61 | Restart
62 |
63 |
this.handleStateUpdate('finish')}>finish
64 |
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/flow-typed/npm/fastdom_vx.x.x.js:
--------------------------------------------------------------------------------
1 | // flow-typed signature: dcc605496252a35bd9095de50b55c953
2 | // flow-typed version: <>/fastdom_v1.0.8/flow_v0.69.0
3 |
4 | /**
5 | * This is an autogenerated libdef stub for:
6 | *
7 | * 'fastdom'
8 | *
9 | * Fill this stub out by replacing all the `any` types.
10 | *
11 | * Once filled out, we encourage you to share your work with the
12 | * community by sending a pull request to:
13 | * https://github.com/flowtype/flow-typed
14 | */
15 |
16 | declare module 'fastdom' {
17 | declare module.exports: any;
18 | }
19 |
20 | /**
21 | * We include stubs for each file inside this npm package in case you need to
22 | * require those files directly. Feel free to delete any files that aren't
23 | * needed.
24 | */
25 | declare module 'fastdom/extensions/fastdom-promised' {
26 | declare module.exports: any;
27 | }
28 |
29 | declare module 'fastdom/extensions/fastdom-sandbox' {
30 | declare module.exports: any;
31 | }
32 |
33 | declare module 'fastdom/fastdom-strict' {
34 | declare module.exports: any;
35 | }
36 |
37 | declare module 'fastdom/fastdom' {
38 | declare module.exports: any;
39 | }
40 |
41 | declare module 'fastdom/fastdom.min' {
42 | declare module.exports: any;
43 | }
44 |
45 | declare module 'fastdom/src/fastdom-strict' {
46 | declare module.exports: any;
47 | }
48 |
49 | // Filename aliases
50 | declare module 'fastdom/extensions/fastdom-promised.js' {
51 | declare module.exports: $Exports<'fastdom/extensions/fastdom-promised'>;
52 | }
53 | declare module 'fastdom/extensions/fastdom-sandbox.js' {
54 | declare module.exports: $Exports<'fastdom/extensions/fastdom-sandbox'>;
55 | }
56 | declare module 'fastdom/fastdom-strict.js' {
57 | declare module.exports: $Exports<'fastdom/fastdom-strict'>;
58 | }
59 | declare module 'fastdom/fastdom.js' {
60 | declare module.exports: $Exports<'fastdom/fastdom'>;
61 | }
62 | declare module 'fastdom/fastdom.min.js' {
63 | declare module.exports: $Exports<'fastdom/fastdom.min'>;
64 | }
65 | declare module 'fastdom/src/fastdom-strict.js' {
66 | declare module.exports: $Exports<'fastdom/src/fastdom-strict'>;
67 | }
68 |
--------------------------------------------------------------------------------
/src/core/createMover.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import invariant from 'invariant'
4 |
5 | type moveArgs = number | Function
6 |
7 | /**
8 | * Creates a function that moves/changes an animation position along its timeline.
9 | *
10 | * const seek = createMover(Animated: AnimationInstance)
11 | *
12 | * With a number value for animation time:
13 | * seek(120)
14 | *
15 | * With a callback function that returns an integer:
16 | * seek(({ progress }) => progress * 10)
17 | */
18 | export const createMover = (instance: Object): Function => {
19 | invariant(
20 | instance !== undefined || instance !== null || instance === 'object',
21 | `Invalid timeline instance passed to createMover().`
22 | )
23 |
24 | const config = {
25 | // By default we sync the animation progress value with the user defined value.
26 | // TODO: There is a bug when synchronizing the engine time with the speed coefficient. We loose the current animation progress and hence animation starts again from the start point. So 'seek' will work though with varying speed but it won't synchronize with the current animation progress when speed coefficient's value is less than 0.6.
27 | default: (value: number | string): void =>
28 | instance.seek(instance.duration * (Number(value) / 100)),
29 | custom: (callback: Function): void => {
30 | invariant(
31 | typeof callback === 'function',
32 | `Expected callback to be a function instead got a ${typeof callback}.`
33 | )
34 |
35 | instance.seek(
36 | callback({
37 | duration: instance.duration,
38 | iterations: instance.iterations,
39 | progress: instance.progress,
40 | offset: instance.offset,
41 | delay: instance.delay,
42 | currentTime: instance.currentTime
43 | })
44 | )
45 | }
46 | }
47 |
48 | const seek = (arg: moveArgs): void => {
49 | invariant(
50 | typeof arg === 'number' ||
51 | typeof arg === 'function' ||
52 | typeof arg === 'string',
53 | `seek() expected a number or a callback function but instead got a ${typeof arg}`
54 | )
55 |
56 | if (typeof arg === 'number' || typeof arg === 'string') {
57 | return config.default(arg)
58 | } else if (typeof arg === 'function') {
59 | return config.custom(arg)
60 | }
61 | }
62 |
63 | return seek
64 | }
65 |
--------------------------------------------------------------------------------
/src/core/easing.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { bezier } from './bezier'
4 | import { isFunc } from '../utils/engineUtils'
5 |
6 | const names = [
7 | 'Quad',
8 | 'Cubic',
9 | 'Quart',
10 | 'Quint',
11 | 'Sine',
12 | 'Expo',
13 | 'Circ',
14 | 'Back',
15 | 'Elastic'
16 | ]
17 |
18 | const elastic = (t: number, p: number): number => {
19 | return t === 0 || t === 1
20 | ? t
21 | : -Math.pow(2, 10 * (t - 1)) *
22 | Math.sin(
23 | (t - 1 - p / (Math.PI * 2.0) * Math.asin(1)) * (Math.PI * 2) / p
24 | )
25 | }
26 |
27 | const equations = {
28 | In: [
29 | [0.55, 0.085, 0.68, 0.53] /* InQuad */,
30 | [0.55, 0.055, 0.675, 0.19] /* InCubic */,
31 | [0.895, 0.03, 0.685, 0.22] /* InQuart */,
32 | [0.755, 0.05, 0.855, 0.06] /* InQuint */,
33 | [0.47, 0.0, 0.745, 0.715] /* InSine */,
34 | [0.95, 0.05, 0.795, 0.035] /* InExpo */,
35 | [0.6, 0.04, 0.98, 0.335] /* InCirc */,
36 | [0.6, -0.28, 0.735, 0.045] /* InBack */,
37 | elastic /* InElastic */
38 | ],
39 | Out: [
40 | [0.25, 0.46, 0.45, 0.94] /* OutQuad */,
41 | [0.215, 0.61, 0.355, 1.0] /* OutCubic */,
42 | [0.165, 0.84, 0.44, 1.0] /* OutQuart */,
43 | [0.23, 1.0, 0.32, 1.0] /* OutQuint */,
44 | [0.39, 0.575, 0.565, 1.0] /* OutSine */,
45 | [0.19, 1.0, 0.22, 1.0] /* OutExpo */,
46 | [0.075, 0.82, 0.165, 1.0] /* OutCirc */,
47 | [0.175, 0.885, 0.32, 1.275] /* OutBack */,
48 | (t, f) => 1 - elastic(1 - t, f) /* OutElastic */
49 | ],
50 | InOut: [
51 | [0.455, 0.03, 0.515, 0.955] /* InOutQuad */,
52 | [0.645, 0.045, 0.355, 1.0] /* InOutCubic */,
53 | [0.77, 0.0, 0.175, 1.0] /* InOutQuart */,
54 | [0.86, 0.0, 0.07, 1.0] /* InOutQuint */,
55 | [0.445, 0.05, 0.55, 0.95] /* InOutSine */,
56 | [1.0, 0.0, 0.0, 1.0] /* InOutExpo */,
57 | [0.785, 0.135, 0.15, 0.86] /* InOutCirc */,
58 | [0.68, -0.55, 0.265, 1.55] /* InOutBack */,
59 | (t, f) =>
60 | t < 0.5
61 | ? elastic(t * 2, f) / 2
62 | : 1 - elastic(t * -2 + 2, f) / 2 /* InOutElastic */
63 | ]
64 | }
65 |
66 | const createEasingsInst = (): Object => {
67 | let functions = {
68 | linear: bezier(0.25, 0.25, 0.75, 0.75)
69 | }
70 |
71 | for (let type in equations) {
72 | equations[type].forEach((f, i) => {
73 | functions['ease' + type + names[i]] = isFunc(f)
74 | ? f
75 | : bezier.apply(this, f)
76 | })
77 | }
78 |
79 | return functions
80 | }
81 |
82 | const easings = createEasingsInst()
83 |
84 | export { easings }
85 |
--------------------------------------------------------------------------------
/flow-typed/npm/prop-types_vx.x.x.js:
--------------------------------------------------------------------------------
1 | // flow-typed signature: a806c5124e5c97d0feedbec8d8f3534b
2 | // flow-typed version: <>/prop-types_v15.6.1/flow_v0.68.0
3 |
4 | /**
5 | * This is an autogenerated libdef stub for:
6 | *
7 | * 'prop-types'
8 | *
9 | * Fill this stub out by replacing all the `any` types.
10 | *
11 | * Once filled out, we encourage you to share your work with the
12 | * community by sending a pull request to:
13 | * https://github.com/flowtype/flow-typed
14 | */
15 |
16 | declare module 'prop-types' {
17 | declare module.exports: any;
18 | }
19 |
20 | /**
21 | * We include stubs for each file inside this npm package in case you need to
22 | * require those files directly. Feel free to delete any files that aren't
23 | * needed.
24 | */
25 | declare module 'prop-types/checkPropTypes' {
26 | declare module.exports: any;
27 | }
28 |
29 | declare module 'prop-types/factory' {
30 | declare module.exports: any;
31 | }
32 |
33 | declare module 'prop-types/factoryWithThrowingShims' {
34 | declare module.exports: any;
35 | }
36 |
37 | declare module 'prop-types/factoryWithTypeCheckers' {
38 | declare module.exports: any;
39 | }
40 |
41 | declare module 'prop-types/lib/ReactPropTypesSecret' {
42 | declare module.exports: any;
43 | }
44 |
45 | declare module 'prop-types/prop-types' {
46 | declare module.exports: any;
47 | }
48 |
49 | declare module 'prop-types/prop-types.min' {
50 | declare module.exports: any;
51 | }
52 |
53 | // Filename aliases
54 | declare module 'prop-types/checkPropTypes.js' {
55 | declare module.exports: $Exports<'prop-types/checkPropTypes'>;
56 | }
57 | declare module 'prop-types/factory.js' {
58 | declare module.exports: $Exports<'prop-types/factory'>;
59 | }
60 | declare module 'prop-types/factoryWithThrowingShims.js' {
61 | declare module.exports: $Exports<'prop-types/factoryWithThrowingShims'>;
62 | }
63 | declare module 'prop-types/factoryWithTypeCheckers.js' {
64 | declare module.exports: $Exports<'prop-types/factoryWithTypeCheckers'>;
65 | }
66 | declare module 'prop-types/index' {
67 | declare module.exports: $Exports<'prop-types'>;
68 | }
69 | declare module 'prop-types/index.js' {
70 | declare module.exports: $Exports<'prop-types'>;
71 | }
72 | declare module 'prop-types/lib/ReactPropTypesSecret.js' {
73 | declare module.exports: $Exports<'prop-types/lib/ReactPropTypesSecret'>;
74 | }
75 | declare module 'prop-types/prop-types.js' {
76 | declare module.exports: $Exports<'prop-types/prop-types'>;
77 | }
78 | declare module 'prop-types/prop-types.min.js' {
79 | declare module.exports: $Exports<'prop-types/prop-types.min'>;
80 | }
81 |
--------------------------------------------------------------------------------
/docs/properties.md:
--------------------------------------------------------------------------------
1 | # Properties
2 |
3 | ### Timing properties
4 |
5 | * `duration` - animation duration.
6 |
7 | ```js
8 | createTimeline({ duration: 2000 })
9 | ```
10 |
11 | * `delay` - animation delay
12 |
13 | ```js
14 | createTimeline({ delay: 200 })
15 | ```
16 |
17 | * `iterations` - A number value or `Infinity` for defining iterations of an animation.
18 |
19 | ```js
20 | createTimeline({ iterations: 2}) // or createTimeline({ iterations: Infinity })
21 | ```
22 |
23 | * `speed` - animation speed
24 |
25 | ```js
26 | createTimeline({ speed: 0.8 })
27 | ```
28 |
29 | Change the speed in-between the running animations using `setSpeed`.
30 |
31 | ```js
32 | const t = createTimeline({
33 | duration: 2000
34 | })
35 |
36 | t.animate({
37 | scale: 2
38 | })
39 |
40 | // Change the speed after 3s
41 | setTimeout(() => {
42 | t.getAnimations().forEach((animation) => {
43 | animation.setSpeed(0.2)
44 | })
45 | }, 3000)
46 | ```
47 |
48 | * `direction` - animation direction. It can be `reversed`, `alternate` or `normal` which is default.
49 |
50 | ```js
51 | createTimeline({ direction: 'alternate' })
52 | ```
53 |
54 | * `autoplay` - autoplay the animation
55 |
56 | ```js
57 | createTimeline({ autoplay: true })
58 | ```
59 |
60 | * `easing` - Easing curve name
61 |
62 | ```js
63 | createTimeline({ easing: 'easeInQuad' })
64 | ```
65 |
66 | or use a custom easing curve
67 |
68 | ```js
69 | import { createTimeline, createEasingCurve } from 'animated-timeline'
70 |
71 | const custom_curve = createEasingCurve('custom_curve', [1, 2, 3, 4])
72 |
73 | createTimeline({ easing: custom_curve })
74 | ```
75 |
76 | Use [`helpers.getAvailableEasings()`](./helpers#reading-information) to see the available easing curve names you can use.
77 |
78 | * `elasticity` - A number value for defining elasticity
79 |
80 | ```js
81 | createTimeline({ elasticity: 500 })
82 | ```
83 |
84 | ## Animation Properties
85 |
86 | **Transforms**
87 |
88 | Use [`helpers.getAvailableTransforms()`](./helpers.md#reading-information) to see the available transform properties you can use
89 |
90 |
91 | **CSS properties**
92 |
93 | Checkout [this](https://docs.google.com/spreadsheets/d/1Hvi0nu2wG3oQ51XRHtMv-A_ZlidnwUYwgQsPQUg1R2s/pub?single=true&gid=0&output=html) list of CSS properties by style operation.
94 |
95 | See next ▶️
96 |
97 | [Component API](./Component.md)
98 |
99 | [Timeline API](./Timeline.md)
100 |
101 | [helpers API](./Keyframes.md)
102 |
103 | [Spring API](./Spring.md)
104 |
105 | [Keyframes API](./Keyframes.md)
106 |
--------------------------------------------------------------------------------
/docs/Keyframes.md:
--------------------------------------------------------------------------------
1 | # Keyframes
2 |
3 | Define keyframes for a property (css or transform) using the constructor function `Keyframes`. The object created by `Keyframes` has a property called `frames`.
4 |
5 | ## Example
6 |
7 | ```js
8 | import { Animate, Keyframes } from 'animated-timeline'
9 |
10 | const x = new Keyframes()
11 | .add({
12 | value: 10,
13 | duration: 1000
14 | })
15 | .add({
16 | value: 50,
17 | duration: 2000,
18 | offset: 0.8
19 | })
20 | .add({
21 | value: 0,
22 | duration: 3000
23 | })
24 |
25 | function App() {
26 | return (
27 |
36 |
38 | )
39 | }
40 | ```
41 |
42 | ## Defining keyframes-selector
43 |
44 | To define keyframes-selector i.e percentage of the animation duration, use the property `offset`.
45 |
46 | For example -
47 |
48 | ```css
49 | @keyframes xyz {
50 | 45% {
51 | height: 30px;
52 | }
53 | }
54 | ```
55 |
56 | The above css snippet will be written as -
57 |
58 | ```js
59 | const t = createTimeline({ duration: 2000 })
60 |
61 | const xyz = new Keyframes().add({ offset: 0.45, value: 30 })
62 |
63 | t.animate({ height: xyz.frames })
64 | ```
65 |
66 | with `Keyframes` constructor.
67 |
68 | For multiple endpoints, chain the `.add({})` calls.
69 |
70 | ```css
71 | @keyframes xyz {
72 | 0% {
73 | top: 0px;
74 | }
75 | 25% {
76 | top: 0px;
77 | }
78 | 50% {
79 | top: 100px;
80 | }
81 | 75% {
82 | top: 100px;
83 | }
84 | 100% {
85 | top: 0px;
86 | }
87 | }
88 | ```
89 |
90 | The above css snippet will be written as -
91 |
92 | ```js
93 | import { Animate, Keyframes } from 'animated-timeline'
94 |
95 | const xyz = new Keyframes()
96 | .add({ value: 0 })
97 | .add({ value: 100, offset: 0.25 })
98 | .add({ offset: 0.5, value: 100 })
99 | .add({
100 | offset: 0.75,
101 | value: 100,
102 | })
103 | .add({
104 | offset: 1,
105 | value: 100,
106 | })
107 |
108 | function App() {
109 | return (
110 |
116 | {children}
117 |
118 | )
119 | }
120 | ```
121 |
122 | ## API
123 |
124 | ### `Keyframes`
125 |
126 | Creates an object that has a property called `frames`
127 |
128 | ```js
129 | const xyz = new Keyframes().add({ ...props })
130 |
131 | console.log(xyz.frames)
132 | ```
133 |
134 | ## Examples
135 |
136 | [Check out the examples for using `Keyframes`](../examples/Keyframes/index.js)
137 |
138 | See next ▶️
139 |
140 | [Component API](./Component.md)
141 |
142 | [Timeline API](./Timeline.md)
143 |
144 | [helpers API](./helpers.md)
145 |
146 | [Spring API](./Spring.md)
147 |
148 | [Animation properties](./properties.md)
149 |
--------------------------------------------------------------------------------
/examples/Timeline/timing.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { boxStyles } from '../styles'
4 |
5 | import { createTimeline, helpers } from '../../build/animated-timeline.min.js'
6 |
7 | const t = createTimeline({
8 | easing: 'easeInOutSine',
9 | iterations: 1,
10 | direction: 'normal',
11 | speed: 0.6
12 | })
13 |
14 | class Car extends React.Component {
15 | render() {
16 | return (
17 |
18 |
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
34 | }
35 |
36 | const animate = (one, two, three) => {
37 | t
38 | .sequence([
39 | t.animate({
40 | el: one,
41 | translateX: helpers.transition({
42 | from: 10,
43 | to: 420
44 | })
45 | }),
46 |
47 | t.animate({
48 | el: two,
49 | translateX: helpers.transition({
50 | from: 5,
51 | to: 540
52 | }),
53 | offset: helpers.startAfter(200)
54 | }),
55 |
56 | t.animate({
57 | el: three,
58 | translateX: helpers.transition({
59 | from: 2,
60 | to: 640
61 | }),
62 | offset: helpers.startBefore(600)
63 | })
64 | ])
65 | .start()
66 | }
67 |
68 | export class Timing extends React.Component {
69 | componentDidMount() {
70 | animate('#one', '#two', '#three')
71 | }
72 |
73 | render() {
74 | return (
75 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/core/bezier.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | type aA1 = number
4 |
5 | type aA2 = number
6 |
7 | type aT = number
8 |
9 | type aX = number
10 |
11 | type aA = number
12 |
13 | type aB = number
14 |
15 | type mX1 = number
16 |
17 | type mX2 = number
18 |
19 | type mY1 = number
20 |
21 | type mY2 = number
22 |
23 | const A = (aA1: aA1, aA2: aA2): number => {
24 | return 1.0 - 3.0 * aA2 + 3.0 * aA1
25 | }
26 |
27 | const B = (aA1: aA1, aA2: aA2): number => {
28 | return 3.0 * aA2 - 6.0 * aA1
29 | }
30 | const C = (aA1: aA1): number => {
31 | return 3.0 * aA1
32 | }
33 |
34 | const calcBezier = (aT: aT, aA1: aA1, aA2: aA2): number => {
35 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT
36 | }
37 |
38 | const getSlope = (aT: aT, aA1: aA1, aA2: aA2): number => {
39 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
40 | }
41 |
42 | const createBezierInst = () => {
43 | const kSplineTableSize = 11
44 | const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
45 |
46 | const binarySubdivide = (
47 | aX: aX,
48 | aA: aA,
49 | aB: aB,
50 | mX1: mX1,
51 | mX2: mX2
52 | ): number => {
53 | let currentX,
54 | currentT,
55 | i = 0
56 | do {
57 | currentT = aA + (aB - aA) / 2.0
58 | currentX = calcBezier(currentT, mX1, mX2) - aX
59 | if (currentX > 0.0) {
60 | aB = currentT
61 | } else {
62 | aA = currentT
63 | }
64 | } while (Math.abs(currentX) > 0.0000001 && ++i < 10)
65 | return currentT
66 | }
67 |
68 | const newtonRaphsonIterate = (
69 | aX: aX,
70 | aGuessT: number,
71 | mX1: mX1,
72 | mX2: mX2
73 | ): number => {
74 | for (let i = 0; i < 4; ++i) {
75 | const currentSlope = getSlope(aGuessT, mX1, mX2)
76 | if (currentSlope === 0.0) return aGuessT
77 | const currentX = calcBezier(aGuessT, mX1, mX2) - aX
78 | aGuessT -= currentX / currentSlope
79 | }
80 | return aGuessT
81 | }
82 |
83 | const bezier = (mX1: mX1, mY1: mY1, mX2: mX2, mY2: mY2): Function | void => {
84 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) return
85 | let sampleValues = new Float32Array(kSplineTableSize)
86 |
87 | if (mX1 !== mY1 || mX2 !== mY2) {
88 | for (let i = 0; i < kSplineTableSize; ++i) {
89 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
90 | }
91 | }
92 |
93 | const getTForX = aX => {
94 | let intervalStart = 0.0
95 | let currentSample = 1
96 | const lastSample = kSplineTableSize - 1
97 |
98 | for (
99 | ;
100 | currentSample !== lastSample && sampleValues[currentSample] <= aX;
101 | ++currentSample
102 | ) {
103 | intervalStart += kSampleStepSize
104 | }
105 |
106 | --currentSample
107 |
108 | const dist =
109 | (aX - sampleValues[currentSample]) /
110 | (sampleValues[currentSample + 1] - sampleValues[currentSample])
111 | const guessForT = intervalStart + dist * kSampleStepSize
112 | const initialSlope = getSlope(guessForT, mX1, mX2)
113 |
114 | if (initialSlope >= 0.001) {
115 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
116 | } else if (initialSlope === 0.0) {
117 | return guessForT
118 | } else {
119 | return binarySubdivide(
120 | aX,
121 | intervalStart,
122 | intervalStart + kSampleStepSize,
123 | mX1,
124 | mX2
125 | )
126 | }
127 | }
128 |
129 | return (x: number): number => {
130 | if (mX1 === mY1 && mX2 === mY2) return x
131 | if (x === 0) return 0
132 | if (x === 1) return 1
133 | return calcBezier(getTForX(x), mY1, mY2)
134 | }
135 | }
136 |
137 | return bezier
138 | }
139 |
140 | const bezier = createBezierInst()
141 |
142 | export { bezier }
143 |
--------------------------------------------------------------------------------
/flow-typed/npm/rebound_vx.x.x.js:
--------------------------------------------------------------------------------
1 | // flow-typed signature: 0cdd208c23ed98da0d92478e18c1c0ad
2 | // flow-typed version: <
>/rebound_v0.1.0/flow_v0.68.0
3 |
4 | /**
5 | * This is an autogenerated libdef stub for:
6 | *
7 | * 'rebound'
8 | *
9 | * Fill this stub out by replacing all the `any` types.
10 | *
11 | * Once filled out, we encourage you to share your work with the
12 | * community by sending a pull request to:
13 | * https://github.com/flowtype/flow-typed
14 | */
15 |
16 | declare module 'rebound' {
17 | declare module.exports: any;
18 | }
19 |
20 | /**
21 | * We include stubs for each file inside this npm package in case you need to
22 | * require those files directly. Feel free to delete any files that aren't
23 | * needed.
24 | */
25 | declare module 'rebound/__tests__/reboundSpec' {
26 | declare module.exports: any;
27 | }
28 |
29 | declare module 'rebound/dist/rebound' {
30 | declare module.exports: any;
31 | }
32 |
33 | declare module 'rebound/rollup.config' {
34 | declare module.exports: any;
35 | }
36 |
37 | declare module 'rebound/src/BouncyConversion' {
38 | declare module.exports: any;
39 | }
40 |
41 | declare module 'rebound/src/Loopers' {
42 | declare module.exports: any;
43 | }
44 |
45 | declare module 'rebound/src/MathUtil' {
46 | declare module.exports: any;
47 | }
48 |
49 | declare module 'rebound/src/OrigamiValueConverter' {
50 | declare module.exports: any;
51 | }
52 |
53 | declare module 'rebound/src/PhysicsState' {
54 | declare module.exports: any;
55 | }
56 |
57 | declare module 'rebound/src/Spring' {
58 | declare module.exports: any;
59 | }
60 |
61 | declare module 'rebound/src/SpringConfig' {
62 | declare module.exports: any;
63 | }
64 |
65 | declare module 'rebound/src/SpringSystem' {
66 | declare module.exports: any;
67 | }
68 |
69 | declare module 'rebound/src/index' {
70 | declare module.exports: any;
71 | }
72 |
73 | declare module 'rebound/src/onFrame' {
74 | declare module.exports: any;
75 | }
76 |
77 | declare module 'rebound/src/types' {
78 | declare module.exports: any;
79 | }
80 |
81 | declare module 'rebound/src/util' {
82 | declare module.exports: any;
83 | }
84 |
85 | // Filename aliases
86 | declare module 'rebound/__tests__/reboundSpec.js' {
87 | declare module.exports: $Exports<'rebound/__tests__/reboundSpec'>;
88 | }
89 | declare module 'rebound/dist/rebound.js' {
90 | declare module.exports: $Exports<'rebound/dist/rebound'>;
91 | }
92 | declare module 'rebound/rollup.config.js' {
93 | declare module.exports: $Exports<'rebound/rollup.config'>;
94 | }
95 | declare module 'rebound/src/BouncyConversion.js' {
96 | declare module.exports: $Exports<'rebound/src/BouncyConversion'>;
97 | }
98 | declare module 'rebound/src/Loopers.js' {
99 | declare module.exports: $Exports<'rebound/src/Loopers'>;
100 | }
101 | declare module 'rebound/src/MathUtil.js' {
102 | declare module.exports: $Exports<'rebound/src/MathUtil'>;
103 | }
104 | declare module 'rebound/src/OrigamiValueConverter.js' {
105 | declare module.exports: $Exports<'rebound/src/OrigamiValueConverter'>;
106 | }
107 | declare module 'rebound/src/PhysicsState.js' {
108 | declare module.exports: $Exports<'rebound/src/PhysicsState'>;
109 | }
110 | declare module 'rebound/src/Spring.js' {
111 | declare module.exports: $Exports<'rebound/src/Spring'>;
112 | }
113 | declare module 'rebound/src/SpringConfig.js' {
114 | declare module.exports: $Exports<'rebound/src/SpringConfig'>;
115 | }
116 | declare module 'rebound/src/SpringSystem.js' {
117 | declare module.exports: $Exports<'rebound/src/SpringSystem'>;
118 | }
119 | declare module 'rebound/src/index.js' {
120 | declare module.exports: $Exports<'rebound/src/index'>;
121 | }
122 | declare module 'rebound/src/onFrame.js' {
123 | declare module.exports: $Exports<'rebound/src/onFrame'>;
124 | }
125 | declare module 'rebound/src/types.js' {
126 | declare module.exports: $Exports<'rebound/src/types'>;
127 | }
128 | declare module 'rebound/src/util.js' {
129 | declare module.exports: $Exports<'rebound/src/util'>;
130 | }
131 |
--------------------------------------------------------------------------------
/docs/helpers.md:
--------------------------------------------------------------------------------
1 | # helpers
2 |
3 | A special object that contains various utilities for -
4 |
5 | * performing `from` - `to` based animations
6 |
7 | * performing timing based animations
8 |
9 | * creating custom easing curves
10 |
11 | * reading the animation info.
12 |
13 | ## `from` - `to` based animations
14 |
15 | To perform `from` - `to` based animation i.e transitioning from one state to another, use the method `transition`.
16 |
17 | ```js
18 | import { createTimeline, helpers } from 'animated-timeline'
19 |
20 | const t = createTimeline({
21 | duration: 2000,
22 | iterations: 2
23 | })
24 |
25 | t.animate({
26 | translateX: helpers.transition({
27 | from: 20, // 20px
28 | to: 50 // 50px
29 | }),
30 | scale: helpers.transition({
31 | from: 2,
32 | to: 1
33 | })
34 | }).start()
35 | ```
36 |
37 | ## Timing based animations
38 |
39 | When performing timing based animations, data binding won't work. You'll have to use `el` property for specifying the element using `refs` or selectors (id or class name)
40 |
41 | * **`startAfter`**
42 |
43 | Use this method to start an animation at a specified time after the previous animation ends.
44 |
45 | ```js
46 | import { createTimeline, helpers } from 'animated-timeline'
47 |
48 | const t = createTimeline({
49 | duration: 2000,
50 | iterations: 2
51 | })
52 |
53 | t.sequence([
54 | t.animate({
55 | el: '#custom-element-id',
56 | scale: 2
57 | }),
58 |
59 | t.animate({
60 | el: '#my-custom-id',
61 | translateX: '30px',
62 | scale: 2,
63 | offset: helpers.startAfter(2000) // Start the animation at 2 seconds after the previous animation ends.
64 | })
65 | ]).start()
66 | ```
67 |
68 | * **`startBefore`**
69 |
70 | Use this method to start an animation at a specified time before the previous animation ends
71 |
72 | ```js
73 | import { createTimeline, helpers } from 'animated-timeline'
74 |
75 | const t = createTimeline({
76 | duration: 2000,
77 | iterations: 2
78 | })
79 |
80 | t.sequence([
81 | t.animate({
82 | el: '#custom-element-id',
83 | scale: 2
84 | }),
85 |
86 | t.animate({
87 | el: '#my-custom-id',
88 | translateX: '30px',
89 | scale: 2,
90 | offset: helpers.startBefore(2000) // Start the animation at 2 seconds before the previous animation ends.
91 | })
92 | ]).start()
93 | ```
94 |
95 | * **`times`**
96 |
97 | Use this method to start animation at times after the previous animation ends
98 |
99 | ```js
100 | import { createTimeline, helpers } from 'animated-timeline'
101 |
102 | const t = createTimeline({
103 | duration: 2000,
104 | iterations: 2
105 | })
106 |
107 | t.sequence([
108 | t.animate({
109 | el: '#custom-element-id',
110 | scale: 2
111 | }),
112 |
113 | t.animate({
114 | el: '#my-custom-id',
115 | translateX: '30px',
116 | scale: 2,
117 | offset: helpers.times(2)
118 | })
119 | ]).start()
120 | ```
121 |
122 | ## Creating custom easing curves
123 |
124 | Create a custom easing curve with **4** control points.
125 |
126 | ```js
127 | import { createTimeline, helpers } from 'animated-timeline'
128 |
129 | // Registers the curve name `SampleCurve`
130 | const myCustomCurve = helpers.createEasingCurve('SampleCurve', [
131 | 0.21,
132 | 0.34,
133 | 0.45,
134 | -0.98
135 | ])
136 |
137 | const t = createTimeline({
138 | duration: 2000,
139 | iterations: 2,
140 | easing: myCustomCurve
141 | })
142 |
143 | t.animate({
144 | scale: 2
145 | }).start()
146 | ```
147 |
148 | ## Reading information
149 |
150 | **Get the available easing curve names**
151 |
152 | ```js
153 | helpers.getAvailableEasings()
154 | ```
155 |
156 | Returns an array of available easing curve names
157 |
158 | **Get the available transform properties**
159 |
160 | ```js
161 | helpers.getAvailableTransforms()
162 | ```
163 |
164 | Returns an array of available transform properties.
165 |
166 | ## API
167 |
168 | ### `helpers.transition`
169 |
170 | A function that accepts an object with two properties, `from` and `to`.
171 |
172 | ```js
173 | helpers.transition({ from: 1, to: 2})
174 | ```
175 |
176 | ### `helpers.startAfter`
177 |
178 | A function that accepts a timeout value.
179 |
180 | ```js
181 | helpers.startAfter(2000)
182 | ```
183 |
184 | ### `helpers.startBefore`
185 |
186 | A function that accepts a timeout value.
187 |
188 | ```js
189 | helpers.startBefore(2000)
190 | ```
191 |
192 | ### `helpers.times`
193 |
194 | A function that accepts a unit value.
195 |
196 | ```js
197 | helpers.times(3)
198 | ```
199 |
200 | ### `helpers.createEasingCurve`
201 |
202 | A function that accepts two arguments, a curve name and an array of four control points and returns the curve.
203 |
204 | ```js
205 | helpers.createEasingCurve('my_custom_curve', [1, 2, 3, 4])
206 | ```
207 |
208 | See next ▶️
209 |
210 | [Component API](./Component.md)
211 |
212 | [Timeline API](./Timeline.md)
213 |
214 | [Keyframes API](./Keyframes.md)
215 |
216 | [Spring API](./Spring.md)
217 |
218 | [Animation properties](./properties.md)
219 |
--------------------------------------------------------------------------------
/src/components/Animate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { animated } from '../core/engine'
5 | import { noop } from '../utils/noop'
6 | import { createMover } from '../core/createMover'
7 |
8 | /**
9 | * Animate component is used to perform playback based animations with a declarative API.
10 | *
11 | * Features -
12 | *
13 | * 1. Animate elements by defining props for timing and animation model.
14 | * 2. Control the animation execution at any progress on state updates
15 | * 3. Seek the animation to change the animation position along its timeline
16 | * 4. Lifecycle hooks, which gets executed during different phases of an animation
17 | *
18 | * Tradeoffs -
19 | *
20 | * 1. Cannot perform sequence based animations and timing based animations
21 | * 2. Promise based APIs for oncancel and onfinish events are not available
22 | * 3. Controls for time-based execution are directly not available on the instance, and they are accessible only via flags or * lifecycle hooks.
23 | *
24 | */
25 | export class Animate extends React.Component {
26 | // Animated instance
27 | ctrl = null
28 |
29 | // seek the animation
30 | seek = null
31 |
32 | // Stores all the elements which will be animated
33 | elements = []
34 |
35 | static propTypes = {
36 | autoplay: PropTypes.bool,
37 |
38 | timingProps: PropTypes.object,
39 | animationProps: PropTypes.object,
40 |
41 | lifecyle: PropTypes.shape({
42 | onUpdate: PropTypes.func,
43 | onStart: PropTypes.func,
44 | onComplete: PropTypes.func
45 | }),
46 |
47 | // Can accepts a number or a callback which receives the timing props and should return a number
48 | seekAnimation: PropTypes.oneOfType([
49 | PropTypes.number,
50 | PropTypes.func,
51 | PropTypes.string
52 | ]),
53 |
54 | stop: PropTypes.bool,
55 | start: PropTypes.bool,
56 | reset: PropTypes.bool,
57 | restart: PropTypes.bool,
58 | reverse: PropTypes.bool,
59 | finish: PropTypes.bool
60 | }
61 |
62 | static defaultProps = {
63 | // Autoplay the animation
64 | autoplay: true,
65 |
66 | // Animation lifecyle
67 | lifecycle: {
68 | // Called when the animation is updating
69 | onUpdate: noop,
70 | // Called when the animation has started
71 | onStart: noop,
72 | // Invoked when the animation is completed
73 | onComplete: noop
74 | },
75 |
76 | // Change the animation position along its timeline
77 | seekAnimation: 0,
78 |
79 | // Control the animation execution
80 | start: false,
81 | stop: false,
82 | reset: false,
83 | restart: false,
84 | reverse: false,
85 | finish: false
86 | }
87 |
88 | componentDidMount() {
89 | this.ctrl = animated({
90 | // Animate all the children
91 | el: this.elements,
92 |
93 | // Props for both the models (timing and animation) are fragmented in core (src/core/engine.js)
94 | // Timeline model props
95 | ...this.props.timingProps,
96 | // Animation model props
97 | ...this.props.animationProps,
98 |
99 | // autoplay the animation
100 | autoplay: this.props.autoplay || true
101 | })
102 |
103 | // Add lifecyle hooks to the animated instance
104 | this.addLifecycle(this.props.lifecycle, this.ctrl)
105 |
106 | // Animation controls (start, stop, reset, reverse, restart)
107 | this.enableControls(this.props, this.ctrl)
108 |
109 | this.seekAnimation()
110 | }
111 |
112 | componentDidUpdate() {
113 | // Update the animation state using the controls
114 | this.enableControls(this.props, this.ctrl)
115 |
116 | // Update the animation position in timeline on changing the input value
117 | this.seekAnimation()
118 | }
119 |
120 | componentWillUnmount() {
121 | // Cancel the animation
122 | this.ctrl && this.cancel(this.elements)
123 | }
124 |
125 | enableControls = (props, ctrl) => {
126 | if (props.stop) ctrl.stop()
127 | if (props.start) ctrl.start()
128 | if (props.reverse) ctrl.reverse()
129 | if (props.reset) ctrl.reset()
130 | if (props.restart) ctrl.restart()
131 | if (props.finish) ctrl.finish()
132 | }
133 |
134 | addLifecycle = (lifecycle, ctrl) => {
135 | if (lifecycle.onStart) ctrl.onStart = lifecycle.onStart
136 | if (lifecycle.onComplete) ctrl.onComplete = lifecycle.onComplete
137 | // NOTE: Do not call setState inside onUpdate hook because React prevents infinite loops
138 | if (lifecycle.onUpdate) ctrl.onUpdate = lifecycle.onUpdate
139 | }
140 |
141 | addElements = element => {
142 | this.elements = [...this.elements, element]
143 | }
144 |
145 | resolveChildren = () => {
146 | let { children } = this.props
147 |
148 | if (!Array.isArray(children)) children = [children]
149 |
150 | return children.map((child, i) =>
151 | React.cloneElement(child, { key: i, ref: this.addElements })
152 | )
153 | }
154 |
155 | seekAnimation = () => {
156 | if (this.props.seekAnimation) {
157 | this.seek = createMover(this.ctrl)
158 |
159 | this.seek(this.props.seekAnimation)
160 | }
161 | }
162 |
163 | cancel = elements => {
164 | this.ctrl.oncancel(elements).then(res => res)
165 | }
166 |
167 | render() {
168 | return this.resolveChildren()
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/utils/engineUtils.js:
--------------------------------------------------------------------------------
1 | import tags from 'html-tags'
2 | import svgTags from 'svg-tag-names'
3 |
4 | export const DOMELEMENTS = [...tags, ...svgTags]
5 |
6 | export const stringContains = (str, text) => {
7 | return str.indexOf(text) > -1
8 | }
9 |
10 | export const isArray = obj => Array.isArray(obj)
11 |
12 | export const minMaxValue = (val, min, max) => Math.min(Math.max(val, min), max)
13 |
14 | export const isObject = object =>
15 | stringContains(Object.prototype.toString.call(object), 'Object')
16 |
17 | export const isSVG = el => el instanceof SVGElement
18 |
19 | export const isDOM = el => el.nodeType || isSVG(el)
20 |
21 | export const isString = val => typeof val === 'string'
22 |
23 | export const isFunc = val => typeof val === 'function'
24 |
25 | export const isUnd = val => typeof val === 'undefined'
26 |
27 | export const isHex = val => /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(val)
28 |
29 | export const isRgb = val => /^rgb/.test(val)
30 |
31 | export const isHsl = val => /^hsl/.test(val)
32 |
33 | export const isCol = val => isHex(val) || isRgb(val) || isHsl(val)
34 |
35 | export const isPath = val => isObject(val) && val.hasOwnProperty('totalLength')
36 |
37 | export const stringToHyphens = str => {
38 | return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
39 | }
40 |
41 | export const selectString = str => {
42 | if (isCol(str)) return
43 | try {
44 | let elements = document.querySelectorAll(str)
45 | return elements
46 | } catch (e) {
47 | return
48 | }
49 | }
50 |
51 | export const filterArray = (arr, callback) => {
52 | const len = arr.length
53 | const thisArg = arguments.length >= 2 ? arguments[1] : void 0
54 | let result = []
55 | for (let i = 0; i < len; i++) {
56 | if (i in arr) {
57 | const val = arr[i]
58 | if (callback.call(thisArg, val, i, arr)) {
59 | result.push(val)
60 | }
61 | }
62 | }
63 | return result
64 | }
65 |
66 | export const flattenArray = arr => {
67 | return arr.reduce((a, b) => a.concat(isArray(b) ? flattenArray(b) : b), [])
68 | }
69 |
70 | export const toArray = o => {
71 | if (isArray(o)) return o
72 | if (isString(o)) o = selectString(o) || o
73 | if (o instanceof NodeList || o instanceof HTMLCollection)
74 | return [].slice.call(o)
75 | return [o]
76 | }
77 |
78 | export const arrayContains = (arr, val) => {
79 | return arr.some(a => a === val)
80 | }
81 |
82 | export const clone = o => {
83 | let clone = {}
84 | for (let p in o) clone[p] = o[p]
85 | return clone
86 | }
87 |
88 | export const replaceObjectProps = (o1, o2) => {
89 | let o = clone(o1)
90 | for (let p in o1) o[p] = o2.hasOwnProperty(p) ? o2[p] : o1[p]
91 | return o
92 | }
93 |
94 | export const mergeObjects = (o1, o2) => {
95 | let o = clone(o1)
96 | for (let p in o2) o[p] = isUnd(o1[p]) ? o2[p] : o1[p]
97 | return o
98 | }
99 |
100 | export const rgbToRgba = rgbValue => {
101 | const rgb = /rgb\((\d+,\s*[\d]+,\s*[\d]+)\)/g.exec(rgbValue)
102 | return rgb ? `rgba(${rgb[1]},1)` : rgbValue
103 | }
104 |
105 | export const hexToRgba = hexValue => {
106 | const rgx = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
107 | const hex = hexValue.replace(rgx, (m, r, g, b) => r + r + g + g + b + b)
108 | const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
109 | const r = parseInt(rgb[1], 16)
110 | const g = parseInt(rgb[2], 16)
111 | const b = parseInt(rgb[3], 16)
112 | return `rgba(${r},${g},${b},1)`
113 | }
114 |
115 | export const hslToRgba = hslValue => {
116 | const hsl =
117 | /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g.exec(hslValue) ||
118 | /hsla\((\d+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)/g.exec(hslValue)
119 | const h = parseInt(hsl[1]) / 360
120 | const s = parseInt(hsl[2]) / 100
121 | const l = parseInt(hsl[3]) / 100
122 | const a = hsl[4] || 1
123 |
124 | const hue2rgb = (p, q, t) => {
125 | if (t < 0) t += 1
126 | if (t > 1) t -= 1
127 | if (t < 1 / 6) return p + (q - p) * 6 * t
128 | if (t < 1 / 2) return q
129 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
130 | return p
131 | }
132 | let r, g, b
133 | if (s == 0) {
134 | r = g = b = l
135 | } else {
136 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s
137 | const p = 2 * l - q
138 | r = hue2rgb(p, q, h + 1 / 3)
139 | g = hue2rgb(p, q, h)
140 | b = hue2rgb(p, q, h - 1 / 3)
141 | }
142 | return `rgba(${r * 255},${g * 255},${b * 255},${a})`
143 | }
144 |
145 | export const colorToRgb = val => {
146 | if (isRgb(val)) return rgbToRgba(val)
147 | if (isHex(val)) return hexToRgba(val)
148 | if (isHsl(val)) return hslToRgba(val)
149 | }
150 |
151 | // Get the unit from value
152 | export const getUnit = val => {
153 | val = String(val)
154 | const split = /([\+\-]?[0-9#\.]+)(%|px|em|rem|in|cm|mm|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(
155 | val.replace(/\s/g, '')
156 | )
157 | if (split) return split[2]
158 | }
159 |
160 | // From > to based animations, time based animations
161 | export const getRelativeValue = (to, from) => {
162 | // Partition the `to` value (+=200) => ['+=', '+=', index:0, input: '+=200']
163 | const operator = /^(\*=|\+=|-=)/.exec(to)
164 | if (!operator) return to
165 | // Get the unit if there is any
166 | const u = getUnit(to) || 0
167 | const x = parseFloat(from)
168 | const y = parseFloat(to.replace(operator[0], ''))
169 | switch (operator[0][0]) {
170 | case '+':
171 | return x + y + u
172 | case '-':
173 | return x - y + u
174 | case '*':
175 | return x * y + u
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/timeline.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`createTimeline Should create a timeline instance 1`] = `
4 | Object {
5 | "a": [Function],
6 | "abbr": [Function],
7 | "address": [Function],
8 | "altGlyph": [Function],
9 | "altGlyphDef": [Function],
10 | "altGlyphItem": [Function],
11 | "animatables": Array [],
12 | "animate": [Function],
13 | "animateColor": [Function],
14 | "animateMotion": [Function],
15 | "animateTransform": [Function],
16 | "animation": [Function],
17 | "animations": Array [],
18 | "area": [Function],
19 | "article": [Function],
20 | "aside": [Function],
21 | "audio": [Function],
22 | "autoplay": false,
23 | "b": [Function],
24 | "base": [Function],
25 | "bdi": [Function],
26 | "bdo": [Function],
27 | "began": false,
28 | "blockquote": [Function],
29 | "body": [Function],
30 | "br": [Function],
31 | "button": [Function],
32 | "cancel": [Function],
33 | "canvas": [Function],
34 | "caption": [Function],
35 | "children": Array [],
36 | "circle": [Function],
37 | "cite": [Function],
38 | "clipPath": [Function],
39 | "code": [Function],
40 | "col": [Function],
41 | "colgroup": [Function],
42 | "color-profile": [Function],
43 | "completed": false,
44 | "currentTime": 0,
45 | "cursor": [Function],
46 | "data": [Function],
47 | "datalist": [Function],
48 | "dd": [Function],
49 | "defs": [Function],
50 | "del": [Function],
51 | "delay": 200,
52 | "desc": [Function],
53 | "details": [Function],
54 | "dfn": [Function],
55 | "dialog": [Function],
56 | "direction": "alternate",
57 | "discard": [Function],
58 | "div": [Function],
59 | "dl": [Function],
60 | "dt": [Function],
61 | "duration": 0,
62 | "ellipse": [Function],
63 | "em": [Function],
64 | "embed": [Function],
65 | "feBlend": [Function],
66 | "feColorMatrix": [Function],
67 | "feComponentTransfer": [Function],
68 | "feComposite": [Function],
69 | "feConvolveMatrix": [Function],
70 | "feDiffuseLighting": [Function],
71 | "feDisplacementMap": [Function],
72 | "feDistantLight": [Function],
73 | "feDropShadow": [Function],
74 | "feFlood": [Function],
75 | "feFuncA": [Function],
76 | "feFuncB": [Function],
77 | "feFuncG": [Function],
78 | "feFuncR": [Function],
79 | "feGaussianBlur": [Function],
80 | "feImage": [Function],
81 | "feMerge": [Function],
82 | "feMergeNode": [Function],
83 | "feMorphology": [Function],
84 | "feOffset": [Function],
85 | "fePointLight": [Function],
86 | "feSpecularLighting": [Function],
87 | "feSpotLight": [Function],
88 | "feTile": [Function],
89 | "feTurbulence": [Function],
90 | "fieldset": [Function],
91 | "figcaption": [Function],
92 | "figure": [Function],
93 | "filter": [Function],
94 | "finish": [Function],
95 | "font": [Function],
96 | "font-face": [Function],
97 | "font-face-format": [Function],
98 | "font-face-name": [Function],
99 | "font-face-src": [Function],
100 | "font-face-uri": [Function],
101 | "footer": [Function],
102 | "foreignObject": [Function],
103 | "form": [Function],
104 | "frameLoop": [Function],
105 | "g": [Function],
106 | "getAnimationProgress": [Function],
107 | "getAnimationProgressByElement": [Function],
108 | "getAnimationTime": [Function],
109 | "getAnimationTimeByElement": [Function],
110 | "getAnimations": [Function],
111 | "getComputedTiming": [Function],
112 | "getCurrentTime": [Function],
113 | "getCurrentTimeByElement": [Function],
114 | "glyph": [Function],
115 | "glyphRef": [Function],
116 | "h1": [Function],
117 | "h2": [Function],
118 | "h3": [Function],
119 | "h4": [Function],
120 | "h5": [Function],
121 | "h6": [Function],
122 | "handler": [Function],
123 | "hatch": [Function],
124 | "hatchpath": [Function],
125 | "head": [Function],
126 | "header": [Function],
127 | "hgroup": [Function],
128 | "hkern": [Function],
129 | "hr": [Function],
130 | "html": [Function],
131 | "i": [Function],
132 | "iframe": [Function],
133 | "image": [Function],
134 | "img": [Function],
135 | "input": [Function],
136 | "ins": [Function],
137 | "iterations": 1,
138 | "kbd": [Function],
139 | "keygen": [Function],
140 | "label": [Function],
141 | "legend": [Function],
142 | "li": [Function],
143 | "line": [Function],
144 | "linearGradient": [Function],
145 | "link": [Function],
146 | "listener": [Function],
147 | "main": [Function],
148 | "map": [Function],
149 | "mark": [Function],
150 | "marker": [Function],
151 | "mask": [Function],
152 | "math": [Function],
153 | "menu": [Function],
154 | "menuitem": [Function],
155 | "mesh": [Function],
156 | "meshgradient": [Function],
157 | "meshpatch": [Function],
158 | "meshrow": [Function],
159 | "meta": [Function],
160 | "metadata": [Function],
161 | "meter": [Function],
162 | "missing-glyph": [Function],
163 | "mpath": [Function],
164 | "nav": [Function],
165 | "noscript": [Function],
166 | "object": [Function],
167 | "offset": 0,
168 | "ol": [Function],
169 | "onComplete": [Function],
170 | "onStart": [Function],
171 | "onUpdate": [Function],
172 | "oncancel": [Function],
173 | "onfinish": Promise {},
174 | "optgroup": [Function],
175 | "option": [Function],
176 | "output": [Function],
177 | "p": [Function],
178 | "param": [Function],
179 | "path": [Function],
180 | "pattern": [Function],
181 | "paused": true,
182 | "picture": [Function],
183 | "polygon": [Function],
184 | "polyline": [Function],
185 | "pre": [Function],
186 | "prefetch": [Function],
187 | "progress": 0,
188 | "q": [Function],
189 | "radialGradient": [Function],
190 | "rb": [Function],
191 | "rect": [Function],
192 | "remaining": 2,
193 | "reset": [Function],
194 | "restart": [Function],
195 | "reverse": [Function],
196 | "reversed": false,
197 | "rp": [Function],
198 | "rt": [Function],
199 | "rtc": [Function],
200 | "ruby": [Function],
201 | "s": [Function],
202 | "samp": [Function],
203 | "script": [Function],
204 | "section": [Function],
205 | "seek": [Function],
206 | "select": [Function],
207 | "sequence": [Function],
208 | "set": [Function],
209 | "setSpeed": [Function],
210 | "slot": [Function],
211 | "small": [Function],
212 | "solidColor": [Function],
213 | "solidcolor": [Function],
214 | "source": [Function],
215 | "span": [Function],
216 | "speed": 1,
217 | "start": [Function],
218 | "stop": [Function],
219 | "strong": [Function],
220 | "style": [Function],
221 | "sub": [Function],
222 | "summary": [Function],
223 | "sup": [Function],
224 | "svg": [Function],
225 | "switch": [Function],
226 | "symbol": [Function],
227 | "table": [Function],
228 | "tbody": [Function],
229 | "tbreak": [Function],
230 | "td": [Function],
231 | "template": [Function],
232 | "text": [Function],
233 | "textArea": [Function],
234 | "textPath": [Function],
235 | "textarea": [Function],
236 | "tfoot": [Function],
237 | "th": [Function],
238 | "thead": [Function],
239 | "time": [Function],
240 | "title": [Function],
241 | "tr": [Function],
242 | "track": [Function],
243 | "tref": [Function],
244 | "tspan": [Function],
245 | "u": [Function],
246 | "ul": [Function],
247 | "unknown": [Function],
248 | "use": [Function],
249 | "var": [Function],
250 | "video": [Function],
251 | "view": [Function],
252 | "vkern": [Function],
253 | "wbr": [Function],
254 | }
255 | `;
256 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Animated Timeline
2 |
3 |   [](https://travis-ci.org/nitin42/Timeline)
4 |
5 | > Create playback based animations in React
6 |
7 | ## Table of contents
8 |
9 | * [Introduction](#introduction)
10 |
11 | * [Another animation library ?](#another-animation-library)
12 |
13 | * [Features](#features)
14 |
15 | * [Performance](#performance)
16 |
17 | * [Install](#install)
18 |
19 | * [Browser support](#browser-support)
20 |
21 | * [Usage](#usage)
22 |
23 | * [Animation types](#animation-types)
24 |
25 | * [Animation values](#animation-values)
26 |
27 | * [Documentation](#documentation)
28 |
29 | * [Todos](#todos)
30 |
31 | ## Introduction
32 |
33 | **animated-timeline** is an animation library (not really) for React which makes it painless to create playback based animations.
34 |
35 | ## Another animation library ?
36 |
37 | Nope! Though you can use it as a library. The main goal of this project is to provide -
38 |
39 | * utilities to create animation tools
40 |
41 | * low-level APIs to create a fitting abstraction on top of this project
42 |
43 | * APIs for composing animations that transition from one state to another, use loops, callbacks and timer APIs to create interactive animations
44 |
45 | ## Concepts
46 |
47 | `animated-timeline` works on two models, timing and animation model.
48 |
49 | ### Timing model
50 |
51 | Timing model manages the time and keeps track of current progress in a timeline.
52 |
53 | ### Animation model
54 |
55 | Animation model, on the other hand, describes how an animation could look like at any give time or it can be thought of as state of an animation at a particular point of time.
56 |
57 | Using both the models, we can synchronize the timing and visual changes to the document.
58 |
59 | ## Features
60 |
61 | * Controls for time-based execution of an animation
62 |
63 | * Create sequence based animations
64 |
65 | * Timing based animations
66 |
67 | * Change the animation position along the timeline by seeking the animation
68 |
69 | * Keyframes
70 |
71 | * Promise based APIs
72 |
73 | * Interactive animations based on changing inputs
74 |
75 | * Spring physics and bounciness
76 |
77 | ## Performance
78 |
79 | Style mutations and style reads are batched internally to speed up the performance and avoid document reflows.
80 |
81 | ## Install
82 |
83 | ```
84 | npm install animated-timeline
85 | ```
86 |
87 | or if you use yarn
88 |
89 | ```
90 | yarn add animated-timeline
91 | ```
92 |
93 | **This project also depends on `react` and `react-dom` so make sure you've them installed.**
94 |
95 | ## Browser support
96 |
97 | | Chrome | Safari | IE / EDGE | Firefox | Opera |
98 | | ------ | :----: | --------: | ------: | ----: |
99 | | 24+ | 6+ | 10+ | 32+ | 15+ |
100 |
101 | ## Usage
102 |
103 | `animated-timeline` provides three ways to do animations:
104 |
105 | * [Component API](./docs/Component.md)
106 |
107 | * [Timeline API](./docs/Timeline.md)
108 |
109 | * [Spring physics API](./docs/Spring.md)
110 |
111 | **Example usage with component API**
112 |
113 | ```js
114 | import React from 'react'
115 | import { Animate, helpers } from 'animated-timeline'
116 |
117 | const styles = {
118 | width: '20px',
119 | height: '20px',
120 | backgroundColor: 'pink'
121 | }
122 |
123 | // Properties for timing model
124 | const timingProps = {
125 | duration: 1000
126 | }
127 |
128 | // Properties for animation model
129 | const animationProps = {
130 | rotate: helpers.transition({ from: 360, to: 180 })
131 | }
132 |
133 | function App() {
134 | return (
135 |
136 |
137 |
138 | )
139 | }
140 | ```
141 |
142 |
143 |
144 |
145 |
146 | [Read the detailed API reference for Component API](./docs/Component.md)
147 |
148 | **Example usage with `Timeline` API**
149 |
150 | ```js
151 | import React from 'react'
152 | import { createTimeline, helpers } from 'animated-timeline'
153 |
154 | const styles = {
155 | width: '20px',
156 | height: '20px',
157 | backgroundColor: 'pink'
158 | }
159 |
160 | const t = createTimeline({
161 | direction: 'alternate',
162 | iterations: 1
163 | })
164 |
165 | class App extends React.Component {
166 | componentDidMount() {
167 | t
168 | .animate({
169 | opacity: helpers.transition({ from: 0.2, to: 0.8 }),
170 | rotate: helpers.transition({ from: 360, to: 180 })
171 | })
172 | .start()
173 | }
174 |
175 | render() {
176 | return
177 | }
178 | }
179 | ```
180 |
181 | [Read the detailed API reference for `Timeline` API](./docs/Timeline.md)
182 |
183 | **Example usage with spring physics**
184 |
185 | ```js
186 | import React from 'react'
187 |
188 | import { Spring } from 'animated-timeline'
189 |
190 | const styles = {
191 | width: '20px',
192 | height: '20px',
193 | backgroundColor: 'pink'
194 | }
195 |
196 | const s = Spring({ friction: 4, tension: 2 })
197 |
198 | // or
199 |
200 | // const s = Spring({ bounciness: 14, speed: 12 })
201 |
202 | class SpringSystem extends React.Component {
203 | componentDidMount() {
204 | s.animate({
205 | property: 'scale',
206 | map: {
207 | inputRange: [0, 1],
208 | outputRange: [1, 1.5]
209 | }
210 | })
211 | }
212 |
213 | render() {
214 | return (
215 | s.setValue(0)}
217 | onMouseDown={() => s.setValue(1)}
218 | style={styles}
219 | />
220 | )
221 | }
222 | }
223 | ```
224 |
225 |
226 |
227 |
228 |
229 | [Read the detailed API reference for spring physics](./docs/Spring.md)
230 |
231 | ## Animation types
232 |
233 | ### Sequence based animations
234 |
235 |
236 |
237 |
238 |
239 | [](https://codesandbox.io/s/6j08xylw7n)
240 |
241 | ### Timing based animations
242 |
243 |
244 |
245 |
246 |
247 | [](https://codesandbox.io/s/92lm0xrl44)
248 |
249 | ### Staggered animation
250 |
251 |
252 |
253 |
254 |
255 | [](https://codesandbox.io/s/743n1z9826)
256 |
257 | ### Keyframes
258 |
259 |
260 |
261 |
262 |
263 | [](https://codesandbox.io/s/92lm0xrl44)
264 |
265 | ### Changing the animation position
266 |
267 | You can also change the animation position along its timeline with an input value.
268 |
269 |
270 |
271 |
272 |
273 | [](https://codesandbox.io/s/kkjn9jq6k7)
274 |
275 | ### Spring based animations
276 |
277 |
278 |
279 |
280 |
281 | [](https://codesandbox.io/s/75l1z6jzq)
282 |
283 | ### More examples
284 |
285 | * [Using animation lifecycle hooks](./examples/Lifecycle/index.js)
286 |
287 | * [Using promise based APIs to manage `completion` and `cancellation` events for an animation](./examples/Promise/index.js)
288 |
289 | * [Using timer APIs to perform Animation](./examples/Extra/speed.js)
290 |
291 | ## Animation values
292 |
293 | * **For transforms**
294 |
295 | ```js
296 | t.animate({
297 | scale: 1,
298 | rotateX: '360deg' // with or without unit
299 | })
300 | ```
301 |
302 | * **For css properties**
303 |
304 | ```js
305 | t.animate({
306 | width: '20px'
307 | })
308 | ```
309 |
310 | * **Defining values using objects**
311 |
312 | ```js
313 | t.animate({
314 | rotate: {
315 | value: 360, // 360deg
316 | duration: 3000,
317 | delay: 200,
318 | direction: 'alternate'
319 | }
320 | })
321 | ```
322 |
323 | Check out [this](./docs/properties) list to see which properties you can use when defining the animation values using objects.
324 |
325 | * **`from` - `to` based animation values**
326 |
327 | ```js
328 | import { helpers } from 'animated-timeline'
329 |
330 | t.animate({
331 | scale: helpers.transition({ from: 2, to: 1 })
332 | })
333 | ```
334 |
335 | Read more about `helpers` object [here](./docs/helpers.md).
336 |
337 | * **Timing based animation values**
338 |
339 | Use property `offset` to perform timing animations
340 |
341 | ```js
342 | import { helpers } from 'animated-timeline'
343 |
344 | t
345 | .sequence([
346 | t.animate({
347 | el: '.one',
348 | scale: 2
349 | }),
350 |
351 | t.animate({ el: '.two', scale: 1, offset: helpers.startAfter(2000) })
352 | ])
353 | .start()
354 | ```
355 |
356 | You can set a value for a property with or without any unit such as `px`, `em`, `rem`, `in`, `cm`, `mm`, `vw`, `vh`, `vmin`, `vmax`, `deg`, `rad`, `turn` etc.
357 |
358 | ## Documentation
359 |
360 | [Check out the detailed documentation for `animated-timeline`.](./docs)
361 |
362 | ## Todos
363 |
364 | * [ ] ReasonML port of the core engine
365 |
366 | * [ ] timing model based on scroll position and gestures ?
367 |
368 | * [ ] Synchronize engine time with speed coefficient
369 |
370 | * [ ] Refactor tween data structure for more flexibility
371 |
372 | * [x] Use data binding
373 |
--------------------------------------------------------------------------------
/docs/Component.md:
--------------------------------------------------------------------------------
1 | # Component API
2 |
3 | `animated-timeline` provides an `Animate` component to animate the elements using a declarative API.
4 |
5 | ## Examples
6 |
7 | * [Basic example](../examples/Animate-Component/Basic.js)
8 |
9 | * [Advance example](../examples/Animate-Component/Advance.js)
10 |
11 | * [Playback controls](../examples/Animate-Component/Controls.js)
12 |
13 | ## Example usage
14 |
15 | ```js
16 | import React from 'react'
17 |
18 | import { Animate } from 'animated-timeline'
19 |
20 | // Properties for timing model
21 | const timingProps = {
22 | duration: 1000,
23 | direction: 'alternate',
24 | iterations: Infinity
25 | }
26 |
27 | // Properties for animation model
28 | const animationProps = {
29 | rotate: '360deg',
30 | scale: 2
31 | }
32 |
33 | function App() {
34 | return (
35 |
36 | Hello World
37 |
38 | )
39 | }
40 | ```
41 |
42 |
43 |
44 |
45 |
46 | [Learn more about animation and timing model](../README.md#concepts)
47 |
48 | ## Props
49 |
50 | ### `timingProps`
51 |
52 | Accepts an object of timing properties like `duration`, `delay`, `iterations` etc.
53 |
54 | Check out [this](./properties.md#timing-properties) list of available timing properties.
55 |
56 | ```js
57 |
60 | ```
61 |
62 | ### `animationProps`
63 |
64 | Accepts an object of animation properties like `rotation`, `scale`, `width` and all other css and transform properties.
65 |
66 | Check out [this](./properties.md#animation-properties) list of all the animation properties.
67 |
68 | ```js
69 |
70 | ```
71 |
72 | ### `autoplay`
73 |
74 | Autoplay the animation. Default value is `true`.
75 |
76 | ```js
77 |
78 | ```
79 |
80 | ### `seekAnimation`
81 |
82 | Use this prop to change the animation position along its timeline with an input value. Accepts a number or a callback function that returns a number.
83 |
84 | ```js
85 | state = { value: 10 }
86 |
87 | function App() {
88 | return (
89 |
90 | Hello World
91 |
92 | )
93 | }
94 | ```
95 |
96 | or with a callback function
97 |
98 | ```js
99 | state = { value: 10 }
100 |
101 | function callback(props) {
102 | return props.duration - state.value * 20
103 | }
104 |
105 | function App() {
106 | return (
107 |
108 | Hello World
109 |
110 | )
111 | }
112 | ```
113 |
114 | ### `lifecycle`
115 |
116 | An object with following methods -
117 |
118 | #### `onStart`
119 |
120 | `onStart` is invoked when the animation starts.
121 |
122 | ```js
123 | function App() {
124 | return (
125 | {
128 | if (props.began) {
129 | console.log('Animation started!')
130 | }
131 | }
132 | }}>
133 | Hello World
134 |
135 | )
136 | }
137 | ```
138 |
139 | #### `onUpdate`
140 |
141 | `onUpdate` is invoked when the animation updates (called each frame).
142 |
143 | ```js
144 | function App() {
145 | return (
146 | {
149 | props.progress - state.value * 10
150 | }
151 | }}>
152 | Hello World
153 |
154 | )
155 | }
156 | ```
157 |
158 | You can use `onUpdate` lifecycle hook to update an input value by syncing it with the animation progress while seeking the animation. Below is an example -
159 |
160 | ```js
161 | import React from 'react'
162 | import { Animate } from 'animated-timeline'
163 |
164 | class App extends React.Component {
165 | state = {
166 | value: 0
167 | }
168 |
169 | render() {
170 | return (
171 |
172 | {
177 | this.state.value = props.progress
178 | }
179 | }}
180 | seekAnimation={({ duration }) => duration - this.state.value * 20}>
181 |
182 |
183 | this.setState({ value: e.target.value })}
189 | />
190 |
191 | )
192 | }
193 | }
194 | ```
195 |
196 |
197 |
198 | #### `onComplete`
199 |
200 | `onComplete` is invoked when the animation completes.
201 |
202 | ```js
203 | function App() {
204 | return (
205 | {
208 | if (props.completed) {
209 | console.log('Animation completed!')
210 |
211 | console.log('Restarting the animation...')
212 |
213 | // Restart the animation
214 | props.controller.restart()
215 | }
216 | }
217 | }}>
218 | Hello World
219 |
220 | )
221 | }
222 | ```
223 |
224 | The above three lifecycle hooks receive the following props -
225 |
226 | ```js
227 | {
228 | completed: boolean, // Animation completed or not
229 | progress: number, // Current animation progress
230 | duration: number, // Current animation duration
231 | remaining: number, // Remaining iterations
232 | reversed: boolean, // Is the animation direction reversed ?
233 | currentTime: number, // Current time of animation
234 | began: boolean, // Animation started or not
235 | paused: boolean, // Is animation paused ?
236 | controller: {
237 | start: () => void, // Start the animation
238 | stop: () => void, // Stop the animation
239 | restart: () => void, // Restart the animation
240 | reverse: () => void, // Reverse the animation
241 | reset: () => void, // Reset the animation
242 | finish: () => void // Finish the animation immediately
243 | }
244 | }
245 | ```
246 |
247 | ### `start`
248 |
249 | Start the animation. Default is `false`
250 |
251 | ```js
252 | state = { start: false }
253 |
254 |
255 | ```
256 |
257 | ### `stop`
258 |
259 | Stop the animation. Default is `false`
260 |
261 | ```js
262 | state = { start: false }
263 |
264 |
265 | ```
266 |
267 | ### `reset`
268 |
269 | Reset the animation. Default is `false`
270 |
271 | ```js
272 | state = { reset: false }
273 |
274 |
275 | ```
276 |
277 | ### `reverse`
278 |
279 | Reverse the animation. Default is `false`
280 |
281 | ```js
282 | state = { reverse: false }
283 |
284 |
285 | ```
286 |
287 | ### `finish`
288 |
289 | Finish the animation immediately. Default is `false`
290 |
291 | ```js
292 | state = { finish: false }
293 |
294 |
295 | ```
296 |
297 | ### `restart`
298 |
299 | Restart the animation. Default is `false`
300 |
301 | ```js
302 | state = { restart: false }
303 |
304 |
305 | ```
306 |
307 | ## Extra
308 |
309 | ### Using Keyframes with `Animate` component
310 |
311 | ```js
312 | import { Animate, Keyframes } from 'animated-timeline'
313 |
314 | const x = new Keyframes()
315 | .add({
316 | value: 10,
317 | duration: 1000
318 | })
319 | .add({
320 | value: 50,
321 | duration: 2000,
322 | offset: 0.8
323 | })
324 | .add({
325 | value: 0,
326 | duration: 3000
327 | })
328 |
329 | function App() {
330 | return (
331 |
340 |
342 | )
343 | }
344 | ```
345 |
346 | [Read more about the `Keyframes` API](Keyframes.md)
347 |
348 | ### Using `helpers` object with `Animate` component
349 |
350 | ```js
351 | import { Animate, helpers } from 'animated-timeline'
352 |
353 | function App() {
354 | return (
355 |
366 |
368 | )
369 | }
370 | ```
371 |
372 | [Read more about the `helpers` object](./helpers.md)
373 |
374 | ## FAQs
375 |
376 | * **I need to do something when an animation ends**
377 |
378 | Use `onComplete` lifecycle hook
379 |
380 | * **How can I sync an animation progress value with an input value ?**
381 |
382 | Use `onUpdate` lifecycle hook
383 |
384 | ```js
385 |
{
389 | // do something here with the `progress` value
390 | // onUpdate is called each frame
391 | }
392 | }}
393 | ```
394 |
395 | * **I need to control the animation with changing input**
396 |
397 | Use `seekAnimation` prop.
398 |
399 | ```js
400 |
401 | ```
402 |
403 | ## Trade-offs
404 |
405 | * You cannot perform sequence based animations. Use [`Timeline API`](./Timeline.md) instead.
406 |
407 | * Promise based APIs for oncancel and onfinish events are not available. Use [`Timeline API`](./Timeline.md) instead.
408 |
409 | * Controls for time-based execution are directly not available on the instance, and they are accessible only via flags
410 |
411 | See next ▶️
412 |
413 | [Timeline API](./Timeline.md)
414 |
415 | [Spring API](./Spring.md)
416 |
417 | [Keyframes API](./Keyframes.md)
418 |
419 | [helpers object](./helpers.md)
420 |
421 | [Animation properties](./properties.md)
422 |
--------------------------------------------------------------------------------
/docs/Spring.md:
--------------------------------------------------------------------------------
1 | # Spring API
2 |
3 | ## Examples
4 |
5 | * [Basic](../examples/spring/Spring.js)
6 |
7 | * [Changing velocity](../examples/spring/Velocity.js)
8 |
9 | * [Bounciness](../examples/spring/Bounciness.js)
10 |
11 | * [Blending colors](../examples/spring/Blend.js)
12 |
13 | * [Spring callback functions](../examples/spring/Callback.js)
14 |
15 | * [Spring playback controls](../examples/spring/Controls.js)
16 |
17 | * [Handling interpolations](../examples/spring/Interpolations.js)
18 |
19 | * [Multiple instances of `Spring`](../examples/spring/Multiple.js)
20 |
21 | * [Promise based API](../examples/spring/SpringPromise.js)
22 |
23 | * [Start the animation on mount](../examples/spring/Start.js)
24 |
25 | **`Spring`**
26 |
27 | A function that creates a spring system with an optional options object.
28 |
29 | ```js
30 | const s = Spring()
31 | ```
32 |
33 | **Passing options to `Spring`**
34 |
35 | You can pass an option object with two properties to `Spring` function. You can either pass properties `friction` and `tension` or `bounciness` and `speed`.
36 |
37 | ```js
38 | const s = Spring({ friction: 10, tension: 5 })
39 | ```
40 |
41 | or
42 |
43 | ```js
44 | const s = Spring({ bounciness: 23, speed: 12 })
45 | ```
46 |
47 | **`animate({ options })`**
48 |
49 | To animate an element using spring physics, use the `animate` method. The `animate` method accepts a property to animate, spring options, a callback to handle interpolations and an option to toggle oscillation.
50 |
51 | ```js
52 | Spring().animate({
53 | property: string, // transform or css property
54 | map: {
55 | inputRange: [A, B],
56 | outputRange: [C, D]
57 | },
58 | blend: {
59 | colors: [hex1, hex2],
60 | range: [A, B]
61 | },
62 | interpolation: (style, value, options) => void,
63 | shouldOscillate: boolean
64 | })
65 | ```
66 |
67 | Example -
68 |
69 | ```js
70 | const s = Spring({ friction: 15, tension: 4 })
71 |
72 | class App extends React.Component {
73 | componentDidMount() {
74 | s.animate({
75 | property: 'scale',
76 | map: {
77 | inputRange: [0, 1],
78 | outputRange: [1, 2]
79 | }
80 | })
81 | }
82 |
83 | render() {
84 | return (
85 | s.setValue(0)}
87 | onMouseDown={() => s.setValue(1)}
88 | />
89 | )
90 | }
91 | }
92 | ```
93 |
94 | **`property`**
95 |
96 | Specify the property of an element you wish to animate. It can be a transform or css property.
97 |
98 | ```js
99 | Spring().animate({ property: 'scale' })
100 | ```
101 |
102 | **`map`**
103 |
104 | An object with two properties, `inputRange` and `outputRange`.
105 |
106 | `inputRange` accepts input ranges which will be handled via `setValue(input_range_value)` method and `outputRange` accepts output ranges which determine the output value of the property.
107 |
108 | For example -
109 |
110 | ```js
111 | const s = Spring()
112 |
113 | s.animate({ property: 'scale', map: { inputRange: [0, 1], outputRange: [1, 2] } })
114 |
115 | s.setValue(1) // Set the input value 1. This will map to output value 2
116 | ```
117 |
118 | So the value of property `scale` in this case will be `2`.
119 |
120 | **`blend`**
121 |
122 | Similar to `map` but use this property only when animating a color property like `backgroundColor`. It accepts an object with two properties, `colors` an array of hex color codes, and `range` an array of input range to mix the hex color codes.
123 |
124 | For example -
125 |
126 | ```js
127 | const s = Spring()
128 |
129 | s.animate({
130 | property: 'backgroundColor',
131 | blend: { colors: ['#FF0000', '#800000'], range: [0, 200] }
132 | })
133 |
134 | s.setValue(input_range)
135 |
136 | // Pass any input value between the range 0-200.
137 |
138 | // Eg - s.setValue(40)
139 | ```
140 |
141 | Check out [this](../examples/spring/Blend.js) example.
142 |
143 | **`interpolation`**
144 |
145 | To handle interpolations, use the callback `interpolation`. The callback function receives the `style` object of the element being animated, the current spring `value` and [helper options](#helper-options).
146 |
147 | ```js
148 | const s = Spring({ friction: 15, tension: 3 })
149 |
150 | class App extends React.Component {
151 | state = {
152 | translateX: '',
153 | backgroundColor: '#a8123a'
154 | }
155 |
156 | componentDidMount() {
157 | s.animate({
158 | property: 'border-radius',
159 | map: {
160 | inputRange: [0, 1],
161 | outputRange: ['1px', '40px']
162 | },
163 | interpolation: (style, value, options) =>
164 | this.handleInterpolations(value, options)
165 | })
166 | }
167 |
168 | componentWillUnmount() {
169 | s.remove()
170 | }
171 |
172 | handleInterpolations = (value, options) => {
173 | this.setState({
174 | translateX: options.em(options.mapValues(value, 3, 40, 0, 1)),
175 | backgroundColor: options.interpolateColor(
176 | value,
177 | '#4a79c4',
178 | '#a8123a',
179 | 0,
180 | 60
181 | )
182 | })
183 | }
184 |
185 | render() {
186 | return (
187 | s.setValue(0)}
189 | onMouseDown={() => s.setValue(1)}
190 | style={{
191 | ...styles,
192 | transform: `translateX(${this.state.translateX})`,
193 | backgroundColor: this.state.backgroundColor
194 | }}
195 | />
196 | )
197 | }
198 | }
199 | ```
200 |
201 | **`shouldOscillate`**
202 |
203 | Toggle the oscillation using `shouldOscillate` flag.
204 |
205 | ```js
206 | Spring().animate({ shouldOscillate: false })
207 | ```
208 |
209 | **`setValue`**
210 |
211 | A method that accepts an input value and starts the animation.
212 |
213 | ```js
214 | s.setValue(input_value)
215 | ```
216 |
217 | ### Playback controls
218 |
219 | **`start()`**
220 |
221 | Starts the animation.
222 |
223 | > Note - This method will only work if the spring is in paused state. Use `setValue` to start the animation initially and then use this playback method to control the animation execution.
224 |
225 | **`stop()`**
226 |
227 | Stop the animaton
228 |
229 | **`startAt(input_value)`**
230 |
231 | Start the animation with an input value.
232 |
233 | **`moveTo(value)`**
234 |
235 | Changes the position of an element without starting the animation. This is useful for moving elements to a different position with the value (without calling the animation). After moving to a different position, use `setValue(value)` to start the animation from that position. A good example is dragging of the elements
236 |
237 | **`seek(value)`**
238 |
239 | Seek the animation with an input value.
240 |
241 | **`reset()`**
242 |
243 | Reset the animation
244 |
245 | **`reverse()`**
246 |
247 | Reverse the animation
248 |
249 | Check out [this](../examples/spring/Controls
250 | .js) example for using the playback controls
251 |
252 | ### Utilities
253 |
254 | **`Spring().infinite(startValue, endValue, duration)`**
255 |
256 | Run infinite iterations of spring animation. Accepts a start value to start the animation from, an end value to terminate the animation at and a duration value. Check out [this](../examples/spring/Callback.js) example.
257 |
258 | **`Spring().setValueVelocity({ value: some_value, velocity: some_velocity_value })`**
259 |
260 | Sets both, the value and velocity, and starts the animation.
261 |
262 | **`Spring().remove()`**
263 |
264 | Clears all the subscriptions and deregister the spring.
265 |
266 | **`Spring().exceeded()`**
267 |
268 | Returns true or false. It determines whether the spring exceeded the input value passed to `setValue` or not.
269 |
270 | **`state()`**
271 |
272 | ```js
273 | const s = Spring()
274 |
275 | s.state()
276 | ```
277 |
278 | Returns an object (given below) which describes the current state of a spring.
279 |
280 | ```js
281 | {
282 | currentValue: number,
283 | // Value at which spring will be at rest
284 | endValue: number,
285 | // Current velocity
286 | velocity: number,
287 | // Is at rest ?
288 | springAtRest: boolean,
289 | // Is oscillating ?
290 | isOscillating: boolean,
291 | // Exceeded the end value ?
292 | exceeded: boolean
293 | }
294 | ```
295 |
296 | **`Spring().oncancel`**
297 |
298 | Returns a promise which gets resolved when the animation is cancelled. Check out [this](../examples/spring/SpringPromise.js) example
299 |
300 | ### Callback functions
301 |
302 | Spring callback functions are invoked during different phases of animation.
303 |
304 | **`Spring().onStart`**
305 |
306 | invoked when the animation starts
307 |
308 | ```js
309 | Spring().onStart = props => console.log('Animation started...')
310 | ```
311 |
312 | **`Spring().onRest`**
313 |
314 | invoked when the spring is at rest.
315 |
316 | ```js
317 | const s = Spring()
318 |
319 | s.onRest = props => s.infinite(0, 1, 2000)
320 | ```
321 |
322 | ### Helper options
323 |
324 | `interpolation` callback also receives some helper options which are given below -
325 |
326 | * **`mapValues(value: number, fromLow: number, fromHigh: number, toLow: number, toHigh: number)`**
327 |
328 | Accepts spring value, `from range` and `to range`. `from range` is usually the input range defined in `map`.
329 |
330 | Example -
331 |
332 | ```js
333 | const s = Spring()
334 |
335 | function applyInterpolation(value, options) {
336 | setState({
337 | translateX: options.mapValues(value, 0, 1, 10, 20)
338 | })
339 | }
340 |
341 | s.animate({
342 | property: 'scale',
343 | map: {
344 | inputRange: [0, 1],
345 | outputRange: [1, 2]
346 | },
347 | interpolation: (style, value, options) => applyInterpolation(value, options)
348 | })
349 | ```
350 |
351 | * **`interpolateColor(value: number, startColorStr: string, endColorStr: string, fromLow: number = 0, fromHigh: number = 1)`**
352 |
353 | Accepts spring value, `startColorStr`, `endColorStr` and an optional input range `fromLow` and `fromHigh`.
354 |
355 | Example -
356 |
357 | ```js
358 | const s = Spring()
359 |
360 | function applyInterpolation(value, options) {
361 | setState({
362 | backgroundColor: options.interpolateColor(value, '#4286f4', '#3a774f', 0, 200)
363 | })
364 | }
365 |
366 | s.animate({
367 | property: 'scale',
368 | map: {
369 | inputRange: [0, 1],
370 | outputRange: [1, 2]
371 | },
372 | interpolation: (style, value, options) => applyInterpolation(value, options)
373 | })
374 | ```
375 |
376 | * **`radiansToDegrees(radians: number)`** - Convert radians to degrees.
377 |
378 | * **`degreesToRadians(degrees: number)`** - Convert degrees to radians.
379 |
380 | * **`em`** - Convert value to em
381 |
382 | * **`px`** - Convert value to px
383 |
384 | * **`deg`** - Convert value to deg
385 |
386 | * **`rem`** - Convert value to rem
387 |
388 | * **`rad`** - Convert value to rad
389 |
390 | * **`grad`** - Convert value to grad
391 |
392 | * **`turn`** - Convert value to turn
393 |
394 | Check out [this](../examples/spring/Interpolations.js) example for more details about helper options
395 |
396 | See next ▶️
397 |
398 | [Component API](./Component.md)
399 |
400 | [Timeline API](./Timeline.md)
401 |
402 | [Keyframes API](./Keyframes.md)
403 |
404 | [helpers object](./helpers.md)
405 |
406 | [Animation properties](./properties.md)
407 |
--------------------------------------------------------------------------------
/src/spring/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import invariant from 'invariant'
4 | import * as React from 'react'
5 | import _R from 'rebound'
6 |
7 | import { getAnimationType } from '../core/engine'
8 | import { batchMutation } from '../core/batchMutations'
9 | import { DOMELEMENTS } from '../utils/engineUtils'
10 |
11 | import {
12 | deg,
13 | px,
14 | rem,
15 | em,
16 | rad,
17 | grad,
18 | turn,
19 | parseValue
20 | } from '../utils/springUtils'
21 |
22 | // Property name
23 | type property = string
24 |
25 | // Main spring instance
26 | type SPRING = Object
27 |
28 | // Element to animate
29 | type element = HTMLElement
30 |
31 | // Spring callback function props
32 | type callbackProps = {
33 | currentValue: number,
34 | // Value at which spring will be at rest
35 | endValue: number,
36 | // Current velocity
37 | velocity: number,
38 | // Is at rest ?
39 | springAtRest: boolean,
40 | // Is overshoot clamping enabled ?
41 | isOscillating: boolean,
42 | // Exceeded the end value
43 | exceeded: boolean
44 | }
45 |
46 | // Options for main 'Spring' function
47 | type springOptions = {
48 | friction?: number,
49 | tension?: number,
50 | bounciness?: number,
51 | speed?: number
52 | }
53 |
54 | // Options passed to interpolation callback function
55 | type interpolateOptions = {
56 | // Map values from one range to another range
57 | mapValues: (
58 | value: number, // Spring value
59 | f1: number, // From range one
60 | f2: number, // From range two
61 | t1: number, // Target range one
62 | t2: number // Target range two
63 | ) => void,
64 | // Interpolate hex colors with or without an input range
65 | interpolateColor: (
66 | value: number, // Spring value
67 | c1: string, // Hex one
68 | c2: string, // Hex two
69 | f1?: number, // Input range one
70 | f2?: number // Input range two
71 | ) => void,
72 | // Convert to degree
73 | radiansToDegrees: (radians: number) => {},
74 | // Convert to radian
75 | degreesToRadians: (degrees: number) => {},
76 | // Convert to pixel
77 | px: (value: number) => string,
78 | // Convert to degrees
79 | deg: (value: number) => string,
80 | // Convert to radians
81 | rad: (value: number) => string,
82 | // Convert to gradians
83 | grad: (value: number) => string,
84 | // Convert to turn
85 | turn: (value: number) => string,
86 | // Convert to em
87 | em: (value: number) => string,
88 | // Convert to rem
89 | rem: (value: number) => string
90 | }
91 |
92 | type interpolation = (
93 | style: Object,
94 | value: string | number,
95 | options: interpolateOptions
96 | ) => void
97 |
98 | const isColorProperty = (property: property): boolean =>
99 | property.includes('Color') || property.includes('color')
100 |
101 | // Set the initial styles of the element which will be animated
102 | const setInitialStyles = (
103 | element: element,
104 | { property, value, type }: { property: any, value: string, type: string }
105 | ): void => {
106 | if (type === 'transform') {
107 | // Batching style mutation reduces recalcs/sec
108 | batchMutation(() => (element.style.transform = `${property}(${value})`))
109 | } else if (type === 'css') {
110 | batchMutation(() => (element.style[property] = value))
111 | }
112 | }
113 |
114 | // Spring callback props
115 | const getCallbackProps = (instance: SPRING): callbackProps => ({
116 | ...instance.state()
117 | })
118 |
119 | // Default spring config
120 | const defaultSpring = () => new _R.SpringSystem().createSpring(13, 3)
121 |
122 | // Spring config with tension and friction
123 | const createSpring = (friction: number = 13, tension: number = 3): SPRING =>
124 | new _R.SpringSystem().createSpring(friction, tension)
125 |
126 | // Spring config with bounciness and speed
127 | const createSpringWithBounciness = (
128 | bounciness: number = 20,
129 | speed: number = 10
130 | ): SPRING =>
131 | new _R.SpringSystem().createSpringWithBouncinessAndSpeed(bounciness, speed)
132 |
133 | // For data binding
134 | const createElement = (
135 | element: string,
136 | instance: SPRING
137 | ): React.ComponentType => {
138 | // $FlowFixMe
139 | class SpringElement extends React.Component {
140 | target: HTMLElement | null
141 |
142 | componentDidMount() {
143 | // Assignment to forwardref.current means this will throw an error for legacy ref API
144 | // TODO: Refactor
145 | // $FlowFixMe
146 | instance.element =
147 | this.props.forwardref === null
148 | ? this.target
149 | : this.props.forwardref.current
150 | }
151 |
152 | addRef = target => {
153 | this.target = target
154 | }
155 |
156 | render(): React.Node {
157 | // $FlowFixMe
158 | const { forwardref, ...rest } = this.props
159 |
160 | // $FlowFixMe
161 | return React.createElement(element, {
162 | ...rest,
163 | ref: forwardref === null ? this.addRef : forwardref
164 | })
165 | }
166 | }
167 |
168 | // $FlowFixMe
169 | return React.forwardRef((props, ref) => (
170 |
171 | ))
172 | }
173 |
174 | export function Spring(options: springOptions): SPRING {
175 | let spring
176 |
177 | if (options && typeof options === 'object') {
178 | const { friction, tension, bounciness, speed } = options
179 |
180 | if (bounciness || speed) {
181 | if (!(friction || tension)) {
182 | spring = createSpringWithBounciness(bounciness, speed)
183 | } else {
184 | throw new Error(
185 | `Cannot configure ${
186 | friction !== undefined ? 'friction' : 'tension'
187 | } property with ${bounciness !== undefined ? 'bounciness' : 'speed'}.`
188 | )
189 | }
190 | } else if (friction || tension) {
191 | if (!(bounciness || speed)) {
192 | spring = createSpring(friction, tension)
193 | } else {
194 | throw new Error(
195 | `Cannot configure ${
196 | bounciness !== undefined ? 'bounciness' : 'speed'
197 | } property with ${friction !== undefined ? 'friction' : 'tension'}.`
198 | )
199 | }
200 | } else {
201 | if (process.env.NODE_ENV !== 'production') {
202 | console.info(
203 | 'Using default spring constants: { friction: 13, tension: 3 }'
204 | )
205 | }
206 | spring = defaultSpring()
207 | }
208 | } else {
209 | if (process.env.NODE_ENV !== 'production') {
210 | console.info(
211 | 'Using default spring constants: { friction: 13, tension: 3 }'
212 | )
213 | }
214 | spring = defaultSpring()
215 | }
216 |
217 | Object.assign(
218 | spring,
219 | DOMELEMENTS.reduce((getters: Object, alias: string): Object => {
220 | getters[alias.toLowerCase()] = createElement(alias.toLowerCase(), spring)
221 | return getters
222 | }, {})
223 | )
224 |
225 | // Map values from one range to another range
226 | const springMap = _R.MathUtil.mapValueInRange
227 |
228 | // Interpolate color (hex value) using the value received from instance.getCurrentValue()
229 | const springInterpolateColor = _R.util.interpolateColor
230 |
231 | // rAF
232 | let id = null
233 |
234 | // Timeout id for running infinite iterations of an animation
235 | let timeoutId: TimeoutID
236 |
237 | spring.animate = ({
238 | el, // Can be ref or selector (id or classname). Use this property only when chaining .animate({}) calls (in other cases, you'll be relying on data binding)
239 | property, // Property to be animated
240 | map = { inputRange: [0, 1], outputRange: [1, 1.5] },
241 | blend = { colors: ['#183a72', '#85c497'], range: [] },
242 | interpolation = (style, value, options) => {},
243 | shouldOscillate = true // Flag to toggle oscillations in-between
244 | }: {
245 | el?: element,
246 | // $FlowFixMe
247 | property: any,
248 | map?: {
249 | inputRange: Array, // Input ranges
250 | outputRange: Array // Output ranges
251 | },
252 | blend?: {
253 | colors: Array, // Input color hex codes
254 | range?: Array // Input range to mix the colors
255 | },
256 | interpolation?: interpolation,
257 | shouldOscillate?: boolean
258 | }) => {
259 | invariant(
260 | !Array.isArray(el) || typeof el === 'string' || typeof el === 'object',
261 | typeof spring.element !== undefined,
262 | 'Can only pass a selector (id or class) or a reference to the property "el" or use data binding instead.'
263 | )
264 |
265 | invariant(
266 | typeof property === 'string',
267 | `Expected property to be a string but instead got a ${typeof property}.`
268 | )
269 |
270 | invariant(
271 | typeof interpolation === 'function',
272 | `Expected interpolate to be a function but instead got a ${typeof interpolation}.`
273 | )
274 |
275 | if (!shouldOscillate) spring.setOvershootClampingEnabled(true)
276 |
277 | // Reference to the element which will be animated
278 | let element
279 |
280 | // Property type (css or transform)
281 | let type = ''
282 |
283 | // type: HTMLElement
284 | if (typeof el === 'object') {
285 | // must be a 'ref'
286 | element = el
287 | } else if (typeof el === 'string') {
288 | // id or classname
289 | element = document.querySelector(el)
290 | } else if (spring.element !== undefined) {
291 | // Data binding
292 | element = spring.element
293 | } else {
294 | throw new Error(`Received an invalid element type ${typeof element}.`)
295 | }
296 |
297 | if (getAnimationType(element, property) === 'transform') {
298 | type = 'transform'
299 | } else if (getAnimationType(element, property) === 'css') {
300 | type = 'css'
301 | }
302 |
303 | // Set the initial styles of the animation property of the element we want to animate
304 | // The values are derived from the options (map or blend)
305 | setInitialStyles(element, {
306 | property,
307 | value: isColorProperty(property) ? blend.colors[0] : map.outputRange[0],
308 | type
309 | })
310 |
311 | spring.addListener({
312 | // Called when the spring moves
313 | onSpringActivate: (spr: SPRING): void => {
314 | if (spring.onStart && typeof spring.onStart === 'function') {
315 | spring.onStart(getCallbackProps(spr))
316 | }
317 | },
318 | // Called when the spring is at rest
319 | onSpringAtRest: (spr: SPRING): void => {
320 | if (spring.onRest && typeof spring.onRest === 'function') {
321 | spring.onRest(getCallbackProps(spr))
322 | }
323 | },
324 | onSpringUpdate: (spr: SPRING): void => {
325 | let val = spr.getCurrentValue()
326 |
327 | if (!isColorProperty(property)) {
328 | // For transforms, layout and other props
329 | const { inputRange, outputRange } = map
330 |
331 | // Get the unit from the value
332 | const unit =
333 | parseValue(outputRange[0])[2] || parseValue(outputRange[1])[2] || ''
334 |
335 | // Output ranges
336 | const t1 = Number(parseValue(outputRange[0])[1]) || 1
337 |
338 | const t2 = Number(parseValue(outputRange[1])[1]) || 1.5
339 |
340 | // Map the values from input range to output range
341 | val = String(springMap(val, inputRange[0], inputRange[1], t1, t2)).concat(unit)
342 | } else if (isColorProperty(property)) {
343 | // For color props only
344 | const { colors, range } = blend
345 |
346 | // Interpolate hex values with an input range
347 | if (range && (Array.isArray(range) && range.length === 2)) {
348 | // Value is converted to RGB scale
349 |
350 | val = springInterpolateColor(
351 | val,
352 | colors[0],
353 | colors[1],
354 | range[0],
355 | range[1]
356 | )
357 | } else {
358 | // Ignore the input range
359 | val = springInterpolateColor(val, colors[0], colors[1])
360 | }
361 | }
362 |
363 | id = window.requestAnimationFrame(() => {
364 | // Interpolations are batched first because they may re-initialise the 'transform' property.
365 |
366 | // Callback should receive unitless values (units can be appended afterwards using the options)
367 | interpolation(
368 | // Pass style object of the element
369 | // We can either use setState to update the element style or directly mutate the DOM element
370 | element.style,
371 | !isColorProperty(property)
372 | ? Number(parseValue(String(val))[1]) || val
373 | : val,
374 | {
375 | // Map values from one range to another range
376 | mapValues: _R.MathUtil.mapValueInRange,
377 |
378 | // Interpolate hex colors with or without an input range
379 | interpolateColor: _R.util.interpolateColor,
380 |
381 | // Convert degrees and radians
382 | radiansToDegrees: _R.util.radiansToDegrees,
383 | degreesToRadians: _R.util.degreesToRadians,
384 |
385 | px, // Convert to pixel
386 | deg, // Convert to degrees
387 | rad, // Convert to radians
388 | grad, // Convert to gradians
389 | turn, // Convert to turn
390 | em, // Convert to em
391 | rem // Convert to rem
392 | }
393 | )
394 |
395 | if (type === 'transform') {
396 | if (!element.style.transform.includes(property)) {
397 | // If interpolation callback initializes 'transform' property, then simply append the required property.
398 | element.style.transform = element.style.transform.concat(
399 | `${property}(${val})`
400 | )
401 | } else {
402 | element.style.transform = `${property}(${val})`
403 | }
404 | } else if (type === 'css') {
405 | element.style[property] = `${val}`
406 | }
407 | })
408 | }
409 | })
410 |
411 | // Support chaining
412 | return spring
413 | }
414 |
415 | // Set a new value and start the animation
416 | spring.setValue = spring.setEndValue
417 |
418 | // Set a new value and velocity, and start the animation
419 | spring.setValueVelocity = ({
420 | value,
421 | velocity
422 | }: {
423 | value: number,
424 | velocity: number
425 | }): void => spring.setVelocity(velocity).setValue(value)
426 |
427 | // Stop the animation
428 | spring.stop = () => {
429 | spring.setAtRest()
430 | // or
431 | // spring.setCurrentValue(spring.getCurrentValue())
432 | }
433 |
434 | // Change the position of an element without starting the animation
435 | // This is useful for moving elements to a different position with the value (without calling the animation).
436 | // After moving to a different position, use spring.setValue(value) to start the animation from that position.
437 | // A good example is dragging of the elements
438 | spring.moveTo = (val: number): void => spring.setCurrentValue(val).stop()
439 |
440 | // Immediately start the animation with a value
441 | spring.startAt = (val: number): void => spring.setValue(val)
442 |
443 | // Reset the animation
444 | spring.reset = () => spring.setCurrentValue(-1)
445 |
446 | // Reverse direction of the animation
447 | spring.reverse = () => spring.setValue(-spring.getCurrentValue())
448 |
449 | // Change the position of an element
450 | spring.seek = (val: number): void => spring.setValue(val)
451 |
452 | // Start the animation
453 | spring.start = () =>
454 | spring.setValue(spring.getEndValue() - spring.getCurrentValue())
455 |
456 | // Infinite iterations
457 | spring.infinite = (
458 | startValue: number,
459 | endValue: number,
460 | duration: number
461 | ): void => {
462 | spring.setValue(startValue)
463 |
464 | timeoutId = setTimeout(() => {
465 | spring.setValue(endValue)
466 | }, duration || 1000)
467 | }
468 |
469 | // This ensures that we don't cause a memory leak
470 | spring.remove = () => {
471 | // Deregister the spring
472 | spring.removeAllListeners()
473 | // Clear the timeout for infinite iterations
474 | timeoutId && clearTimeout(timeoutId)
475 | // Cancel the animation
476 | id && window.cancelAnimationFrame(id)
477 | }
478 |
479 | // This determines whether the spring exceeded the end value
480 | spring.exceeded = () => spring.isOvershooting()
481 |
482 | // Spring state
483 | spring.state = () => ({
484 | // Current oscillation value
485 | currentValue: spring.getCurrentValue(),
486 | // Value at which spring will be at rest
487 | endValue: spring.getEndValue(),
488 | // Current velocity
489 | velocity: spring.getVelocity(),
490 | // Is at rest ?
491 | springAtRest: spring.isAtRest(),
492 | // Is oscillating ?
493 | isOscillating: spring.isOvershootClampingEnabled(),
494 | // Exceeded the end value
495 | exceeded: spring.exceeded()
496 | })
497 |
498 | // Promise based API for cancelling/deregistering a spring
499 | spring.oncancel = () => {
500 | let res = args => {}
501 |
502 | function createPromise() {
503 | return window.Promise && new Promise(resolve => (res = resolve))
504 | }
505 |
506 | const promise = createPromise()
507 |
508 | // Deregister the spring (also removes all the listeners)
509 | spring.destroy()
510 |
511 | timeoutId && clearTimeout(timeoutId)
512 |
513 | id && window.cancelAnimationFrame(id)
514 |
515 | res({ msg: 'Animation cancelled.' })
516 |
517 | return promise
518 | }
519 |
520 | return spring
521 | }
522 |
--------------------------------------------------------------------------------
/docs/Timeline.md:
--------------------------------------------------------------------------------
1 | # Timeline API
2 |
3 | Using the `Timeline` API, you can create interactive animations with loops, callbacks, promises, variables, timer APIs and animation lifecycle hooks.
4 |
5 | To animate an element using the `Timeline` API, you will need to specify properties for **timing model** like `duration`, `delay`, `iterations` and **animation model** like `transform`, `color`, `opacity` etc. You can read more about the timing and animation properties [here.](./properties.md)
6 |
7 | ## Examples
8 |
9 | * [Basic](../examples/Timeline/basic.js)
10 |
11 | * [Sequencing](../examples/Timeline/sequence.js)
12 |
13 | * [Offset based animations](../examples/Timeline/timing.js)
14 |
15 | * [Staggered animation](../examples/Timeline/SStaggered.js)
16 |
17 | * [Multiple elements with one Timeline instance](../examples/Timeline/Multiple.js)
18 |
19 | * [Seeking the animation position](../examples/Seeking/basic.js)
20 |
21 | * [Promise based API](../examples/Promise/index.js)
22 |
23 | * [Using animation lifecycle hooks](../examples/Lifecycle/index.js)
24 |
25 | * [Keyframes](../examples/Keyframes/index.js)
26 |
27 | * [Finish the animation immediately](../examples/Extra/Finish.js)
28 |
29 | * [Changing speed in-between the running animation](../examples/Extra/speed.js)
30 |
31 | ## `createTimeline`
32 |
33 | `createTimeline` function accepts an object of timing properties (optional) and creates a timeline object which is use to animate the elements by synchronising visual changes to the document with time.
34 |
35 | ```js
36 | const t = createTimeline()
37 |
38 | // or
39 |
40 | const t = createTimeline({ ...timingProps })
41 | ```
42 |
43 | **Specifying properties for timing model**
44 |
45 | ```js
46 | import { createTimeline } from 'animated-timeline'
47 |
48 | const t = createTimeline({
49 | delay: 2000,
50 | duration: 4000,
51 | speed: 0.8,
52 | direction: 'alternate'
53 | })
54 | ```
55 |
56 | Check out [this](./properties.md#timing-properties) list of all the timing properties.
57 |
58 | **`createTimeline().animate({ ...animationProps })`**
59 |
60 | `animate` accepts an object of animation properties and one or more elements when performing sequence based animations or timing based animations.
61 |
62 | ```js
63 | const t = createTimeline()
64 |
65 | t.animate({
66 | scale: 2,
67 | rotate: '360deg'
68 | })
69 | ```
70 |
71 | **Specifying properties for animation model**
72 |
73 | ```js
74 | import { createTimeline, helpers } from 'animated-timeline'
75 |
76 | const t = createTimeline({
77 | delay: 2000,
78 | duration: 4000,
79 | speed: 0.8,
80 | direction: 'alternate'
81 | })
82 |
83 | class App extends React.Component {
84 | componentDidMount() {
85 | t
86 | .animate({
87 | scale: helpers.transition({
88 | from: 2,
89 | to: 1
90 | })
91 | })
92 | .start()
93 | }
94 |
95 | render() {
96 | return
97 | }
98 | }
99 | ```
100 |
101 | Check out [this](./properties.md#animation-properties) list of all the animation properties.
102 |
103 | ### Sequence based animations
104 |
105 | **`createTimeline().sequence([t1, t2, ...])`**
106 |
107 | `.sequence([t1, t2, ...])` takes an array of animation properties for different elements and creates a sequence based animation.
108 |
109 | When performing sequence based animations, data binding won't work. You will have to specify the element explicitly using `el` property when animating only one element or `multipleEl` when animating multiple elements.
110 |
111 | ```js
112 | import React from 'react'
113 |
114 | import { createTimeline, helpers } from 'animated-timeline'
115 |
116 | const t = createTimeline({
117 | delay: 2000,
118 | duration: 4000,
119 | speed: 0.8,
120 | direction: 'alternate'
121 | })
122 |
123 | const one = React.createRef()
124 |
125 | const two = React.createRef()
126 |
127 | const animate = (one, two) => {
128 | t
129 | .sequence([
130 | t.animate({
131 | el: one.current,
132 | scale: helpers.transition({
133 | from: 2,
134 | to: 1
135 | })
136 | }),
137 |
138 | t.animate({
139 | el: two.current,
140 | rotate: '360deg'
141 | })
142 | ])
143 | .start()
144 | }
145 |
146 | class App extends React.Component {
147 | componentDidMount() {
148 | animate()
149 | }
150 |
151 | render() {
152 | return (
153 |
154 |
155 |
156 |
157 | )
158 | }
159 | }
160 | ```
161 |
162 | > Along with the refs, you can also use selectors (id or class name) for specifying the element.
163 |
164 | ### Timing based animations
165 |
166 | Use property `offset` to perform timing based animations.
167 |
168 | When performing timing based animations, data binding won't work. You will have to specify the element explicitly using `el` property when animating only one element or `multipleEl` when animating multiple elements.
169 |
170 | ```js
171 | import React from 'react'
172 |
173 | import { createTimeline, helpers } from 'animated-timeline'
174 |
175 | const t = createTimeline({
176 | delay: 2000,
177 | duration: 4000,
178 | speed: 0.8,
179 | direction: 'alternate'
180 | })
181 |
182 | const one = React.createRef()
183 |
184 | const two = React.createRef()
185 |
186 | const animate = (one, two) => {
187 | t
188 | .sequence([
189 | t.animate({
190 | el: one.current,
191 | scale: helpers.transition({
192 | from: 2,
193 | to: 1
194 | })
195 | }),
196 |
197 | t.animate({
198 | el: two.current,
199 | rotate: '360deg',
200 | // Start this animation at 2 seconds after the previous animation ends.
201 | offset: helpers.startAfter(2000)
202 | })
203 | ])
204 | .start()
205 | }
206 |
207 | class App extends React.Component {
208 | componentDidMount() {
209 | animate()
210 | }
211 |
212 | render() {
213 | return (
214 |
215 |
216 |
217 |
218 | )
219 | }
220 | }
221 | ```
222 |
223 | Read more about the `offset` property and timing based functions [here](./helpers#timing-based-animations)
224 |
225 | ### Animating multiple elements
226 |
227 | **With data binding**
228 |
229 | ```js
230 | import React from 'react'
231 |
232 | import { createTimeline, helpers } from 'animated-timeline'
233 |
234 | const t = createTimeline({
235 | iterations: Infinity,
236 | direction: 'alternate',
237 | duration: 2000,
238 | easing: 'easeInOutSine'
239 | })
240 |
241 | class MultipleElem extends React.Component {
242 | componentDidMount() {
243 | t
244 | .animate({
245 | scale: helpers.transition({
246 | from: 2,
247 | to: 1
248 | }),
249 | delay: (element, i) => i * 750
250 | })
251 | .start()
252 | }
253 |
254 | renderNodes = n => {
255 | let children = []
256 |
257 | for (let i = 0; i < n; i++) {
258 | children.push(
259 | React.createElement(t.div, {
260 | style: { width: 50, height: 50, backgroundColor: 'mistyrose' },
261 | key: i
262 | })
263 | )
264 | }
265 |
266 | return children
267 | }
268 |
269 | render() {
270 | return {this.renderNodes(3)}
271 | }
272 | }
273 | ```
274 |
275 | **With `multipleEl` property**
276 |
277 | ```js
278 | import React from 'react'
279 |
280 | import { boxStyles } from '../styles'
281 |
282 | import { createTimeline, helpers } from '../../build/animated-timeline.min.js'
283 |
284 | const t = createTimeline({
285 | iterations: Infinity,
286 | direction: 'alternate',
287 | duration: 2000,
288 | easing: 'easeInOutSine'
289 | })
290 |
291 | export class Staggered extends React.Component {
292 | componentDidMount() {
293 | t
294 | .animate({
295 | multipleEl: ['.two', 'one'],
296 | rotate: helpers.transition({
297 | from: 180,
298 | to: 360
299 | })
300 | })
301 | .start()
302 | }
303 |
304 | render() {
305 | return (
306 |
307 |
311 | Hello World
312 |
313 | )
314 | }
315 | }
316 | ```
317 |
318 | ### Seeking the animation
319 |
320 | **`createMover(timeline_instance)`**
321 |
322 | You can change an animation position along its timeline using `createMover` function. `createMover` function accepts a timeline instance and creates a function that moves or changes an animation position.
323 |
324 | ```js
325 | import React from 'react'
326 |
327 | import { createTimeline, helpers, createMover } from 'animated-timeline'
328 |
329 | const t = createTimeline({
330 | speed: 1,
331 | iterations: 1,
332 | direction: 'alternate',
333 | easing: 'easeInOutSine'
334 | })
335 |
336 | // Pass the timeline instance
337 | const seekAnimation = createMover(t)
338 |
339 | class App extends React.Component {
340 | state = { value: 0 }
341 |
342 | componentDidMount() {
343 | t.animate({
344 | scale: helpers.transition({
345 | from: 4,
346 | to: 2
347 | })
348 | })
349 | }
350 |
351 | handleChange = e => {
352 | this.setState({
353 | value: e.target.value
354 | })
355 |
356 | seekAnimation(this.state.value)
357 |
358 | // or with a callback function
359 |
360 | // This will seek the animation from the reverse direction
361 | // seekAnimation(({ duration }) => duration - this.state.value * 10)
362 | }
363 |
364 | render() {
365 | return (
366 |
367 |
368 |
375 |
376 | )
377 | }
378 | }
379 | ```
380 |
381 |
382 |
383 |
384 |
385 | The callback function passed to `seekAnimation` receives the following properties -
386 |
387 | ```js
388 | {
389 | duration: number, // Animation duration
390 | iterations: number, // Total iterations
391 | progress: number, // Animation progress
392 | offset: number, // Offset value (for timing based animations)
393 | delay: number, // Animation delay
394 | currentTime: number // Current time of an animation
395 | }
396 | ```
397 |
398 | ### Animation lifecycle
399 |
400 | Animation lifecycle hooks gets executed during different phases of an animation. They are accessible directly via the timeline instance.
401 |
402 | **`onStart`**
403 |
404 | `onStart` is invoked when the animation starts.
405 |
406 | ```js
407 | const t = createTimeline({ ...props })
408 |
409 | t.animate({ ...props }).start()
410 |
411 | t.onStart = props => {
412 | console.log(`Animation started: ${props.began}`)
413 | }
414 | ```
415 |
416 | **`onUpdate`**
417 |
418 | `onUpdate` is invoked when the animation updates (called each frame).
419 |
420 | ```js
421 | const t = createTimeline({ ...props })
422 |
423 | t.animate({ ...props }).start()
424 |
425 | t.onUpdate = props => {
426 | console.log('Updating...')
427 | }
428 | ```
429 |
430 | You can use `onUpdate` lifecycle hook to update an input value by syncing it with the animation progress while seeking the animation. Below is an example -
431 |
432 | ```js
433 | import React from 'react'
434 |
435 | import { createTimeline, createMover } from 'animated-timeline'
436 |
437 | const t = createTimeline({ duration: 2000 })
438 |
439 | const seekAnimation = createMover(t)
440 |
441 | class App extends React.Component {
442 | state = {
443 | value: 0
444 | }
445 |
446 | componentDidMount() {
447 | t
448 | .animate({
449 | scale: {
450 | value: 2,
451 | duration: 4000
452 | }
453 | })
454 | .start()
455 |
456 | t.onUpdate = ({ progress }) => {
457 | this.state.value = progress
458 | }
459 | }
460 |
461 | render() {
462 | return (
463 |
464 |
465 | {
471 | this.setState({ value: e.target.value })
472 | seekAnimation(this.state.value)
473 | }}
474 | />
475 |
476 | )
477 | }
478 | }
479 | ```
480 |
481 | **`onComplete`**
482 |
483 | `onComplete` is invoked when the animation completes.
484 |
485 | ```js
486 | const t = createTimeline({ ...props })
487 |
488 | t.animate({ ...props }).start()
489 |
490 | t.onComplete = props => {
491 | console.log(`Animation completed: ${props.completed}`)
492 | }
493 | ```
494 |
495 | The above three lifecycle hooks receive the following props -
496 |
497 | ```js
498 | {
499 | completed: boolean, // Animation completed or not
500 | progress: number, // Current animation progress
501 | duration: number, // Current animation duration
502 | remaining: number, // Remaining iterations
503 | reversed: boolean, // Is the animation direction reversed ?
504 | currentTime: number, // Current time of animation
505 | began: boolean, // Animation started or not
506 | paused: boolean, // Is animation paused ?
507 | controller: {
508 | start: () => void, // Start the animation
509 | stop: () => void, // Stop the animation
510 | restart: () => void, // Restart the animation
511 | reverse: () => void, // Reverse the animation
512 | reset: () => void, // Reset the animation
513 | finish: () => void // Finish the animation immediately
514 | }
515 | }
516 | ```
517 |
518 | ## Promise API
519 |
520 | **`onfinish`**
521 |
522 | `onfinish` is resolved when the animation is finished.
523 |
524 | ```js
525 | const t = createTimeline({ ...props })
526 |
527 | t.animate({ ...props }).start()
528 |
529 | t.onfinish.then(res => console.log(res))
530 | ```
531 |
532 | **`oncancel`**
533 |
534 | `oncancel` function accepts an element (via selector or ref) and is resolved when an animation is interrupted / cancelled. It removes the element which is being animated from the timeline.
535 |
536 | ```js
537 | const t = createTimeline({ ...props })
538 |
539 | t.animate({ ...props }).start()
540 |
541 | t.oncancel('.one').then(res => console.log(res))
542 | ```
543 |
544 | [Check out the detailed examples of using promise API](../examples/Promise/index.js)
545 |
546 | ## Altering timing model
547 |
548 | **`getAnimations()`**
549 |
550 | You can also alter the timing model i.e the timing properties. For example - changing the speed of an animation after 3 seconds or changing the duration. In those cases, you will be using `getAnimations()` method.
551 |
552 | `getAnimations()` is accessible via the timeline instance. It returns an array of running animations.
553 |
554 | ```js
555 | import React from 'react'
556 |
557 | import { createTimeline, helpers } from 'animated-timeline'
558 |
559 | const t = createTimeline({
560 | direction: 'alternate',
561 | easing: 'easeInOutSine',
562 | iterations: Infinity,
563 | speed: 0.5
564 | })
565 |
566 | const animate = (one, two) => {
567 | t
568 | .sequence([
569 | t.animate({
570 | el: one,
571 | scale: helpers.transition({
572 | from: 2,
573 | to: 1
574 | })
575 | }),
576 |
577 | t.animate({
578 | el: two,
579 | rotate: '360deg',
580 | offset: helpers.startBefore(1200)
581 | })
582 | ])
583 | .start()
584 | }
585 |
586 | class App extends React.Component {
587 | componentDidMount() {
588 | animate('#speed-one', '#speed-two')
589 |
590 | // Change the speed after 3s
591 | setTimeout(() => {
592 | t.getAnimations().forEach(animation => {
593 | animation.setSpeed(0.2)
594 | })
595 | }, 3000)
596 | }
597 |
598 | render() {
599 | return (
600 |
601 |
602 |
603 |
604 | )
605 | }
606 | }
607 | ```
608 |
609 | ## Changing animation speed
610 |
611 | **`setSpeed(speed)`**
612 |
613 | To change the animation speed, use the method `setSpeed` which is accessible via the timeline instance.
614 |
615 | ```js
616 | const t = createTimeline({
617 | duration: 200,
618 | speed: 0.6
619 | })
620 |
621 | t.setSpeed(0.9)
622 | ```
623 |
624 | ## Animation playback controls
625 |
626 | **`start()`**
627 |
628 | Starts an animation
629 |
630 | ```js
631 | const t = createTimeline({ ...props })
632 |
633 | t.start()
634 | ```
635 |
636 | **`stop()`**
637 |
638 | Stops an animation
639 |
640 | ```js
641 | createTimeline({ ...props }).stop()
642 | ```
643 |
644 | **`finish()`**
645 |
646 | Immediately finish an animation
647 |
648 | ```js
649 | createTimeline({ ...props }).finish()
650 | ```
651 |
652 | Checkout [this](../examples/Extra/Finish.js) example for `finish()` control.
653 |
654 | **`reset()`**
655 |
656 | Resets an animation
657 |
658 | ```js
659 | createTimeline({ ...props }).reset()
660 | ```
661 |
662 | **`reverse()`**
663 |
664 | Reverse an animation
665 |
666 | ```js
667 | createTimeline({ ...props }).reverse()
668 | ```
669 |
670 | **`restart()`**
671 |
672 | Restart an animation
673 |
674 | ```js
675 | createTimeline({ ...props }).restart()
676 | ```
677 |
678 | **`cancel()`**
679 |
680 | cancel the animation
681 |
682 | ```js
683 | createTimeline({ ...props }).cancel()
684 | ```
685 |
686 | Use `cancel()` to cancel the animation when updating the state inside `onUpdate` lifecycle hook.
687 |
688 | ## Utilities
689 |
690 | **`getAnimationTime()`**
691 |
692 | Returns the total running time of an animation.
693 |
694 | ```js
695 | createTimeline({ ...props }).getAnimationTime()
696 | ```
697 |
698 | **`getAnimationTimeByElement(element)`**
699 |
700 | Returns the total running time of an animation by element.
701 |
702 | ```js
703 | createTimeline({ ...props }).getAnimationTimeByElement('.one')
704 | ```
705 |
706 | **`getCurrentTime()`**
707 |
708 | Returns the current time of an animation
709 |
710 | ```js
711 | createTimeline({ ...props }).getCurrentTime()
712 | ```
713 |
714 | **`getCurrentTimeByElement(element)`**
715 |
716 | Returns the current time of an animation by element.
717 |
718 | ```js
719 | createTimeline({ ...props }).getCurrentTimeByElement('.one')
720 | ```
721 |
722 | **`getAnimationProgress()`**
723 |
724 | Returns the current animation progress.
725 |
726 | ```js
727 | createTimeline({ ...props }).getAnimationProgress()
728 | ```
729 |
730 | **`getAnimationProgressByElement(element)`**
731 |
732 | Returns the current animation progress by element.
733 |
734 | ```js
735 | createTimeline({ ...props }).getCurrentProgressByElement('.one')
736 | ```
737 |
738 | **`getComputedTiming()`**
739 |
740 | Returns an object of timing properties -
741 |
742 | ```js
743 | {
744 | activeTime, // Time in which animation will be active
745 | currentTime, // Current time of animation
746 | progress, // Current animation progress
747 | currentIteration // Current iterations of an animation
748 | }
749 | ```
750 |
751 | **`getAnimations()`**
752 |
753 | Returns an array of running animations.
754 |
755 | ```js
756 | createTimeline({ ...props })
757 | .getAnimations()
758 | .forEach(animation => {
759 | animation.setSpeed(0.5)
760 | })
761 | ```
762 |
763 | See next ▶️
764 |
765 | [Component API](./Component.md)
766 |
767 | [Spring API](./Spring.md)
768 |
769 | [Keyframes API](./Keyframes.md)
770 |
771 | [helpers object](./helpers.md)
772 |
773 | [Animation properties](./properties.md)
774 |
--------------------------------------------------------------------------------
/src/core/engine.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Most part of the code is taken from https://github.com/juliangarnier/anime
3 | *
4 | * Modifications -
5 | *
6 | * 1. Lifecycle hooks
7 | * 2. Animation speed is now configured via different instances and params passed to the main instance.
8 | * 3. Serialised values for performing **from** - **to** animations
9 | * 4. New defaults for instance and tweens parameters.
10 | *
11 | * Additions -
12 | *
13 | * 1. Style reads and writes are now batched to avoid layout calc. in each frame ✅
14 | * 2. Timing APIs
15 | * 3. APIs for getting information about active instances in an animation using getAnimations()
16 | * 4. Declarative API for Timeline component (React)
17 | * 5. Promise based API for oncancel event
18 | * 6. Finish the animation immediately using finish()
19 | */
20 |
21 | import React from 'react'
22 | import invariant from 'invariant'
23 | import tags from 'html-tags'
24 | import svgTags from 'svg-tag-names'
25 |
26 | import {
27 | isArray,
28 | isObject,
29 | isSVG,
30 | isDOM,
31 | isString,
32 | isFunc,
33 | isUnd,
34 | isHex,
35 | isPath,
36 | isRgb,
37 | isHsl,
38 | isCol,
39 | stringContains,
40 | stringToHyphens,
41 | selectString,
42 | filterArray,
43 | toArray,
44 | arrayContains,
45 | flattenArray,
46 | clone,
47 | mergeObjects,
48 | replaceObjectProps,
49 | rgbToRgba,
50 | hexToRgba,
51 | hslToRgba,
52 | colorToRgb,
53 | getRelativeValue,
54 | getUnit
55 | } from '../utils/engineUtils'
56 | import {
57 | getDefaultTweensParams,
58 | getDefaultInstanceParams
59 | } from '../utils/defaults'
60 |
61 | import { easings } from './easing'
62 |
63 | import { bezier } from './bezier'
64 |
65 | import {
66 | getTransformUnit,
67 | getTransformValue,
68 | validTransforms
69 | } from './transforms'
70 |
71 | import {
72 | batchMutation,
73 | batchRead,
74 | exceptions,
75 | emptyScheduledJobs
76 | } from './batchMutations'
77 |
78 | let transformString
79 |
80 | const DOMELEMENTS = [...tags, ...svgTags]
81 |
82 | // oncancel promise flag
83 | let cancelled = false
84 |
85 | const minMaxValue = (val, min, max) => Math.min(Math.max(val, min), max)
86 |
87 | const log = (...args) => console.log(args)
88 |
89 | const evaluateValue = (val, animatable) => {
90 | if (typeof val !== 'function') return val
91 | // Useful for staggered animations
92 | return val(animatable.element, animatable.id)
93 | }
94 |
95 | // Get the css value for the property from the style object
96 | export const getCSSValue = (el, prop) => {
97 | if (prop in el.style) {
98 | return getComputedStyle(el).getPropertyValue(stringToHyphens(prop)) || '0'
99 | }
100 | }
101 |
102 | // Get the animation type property i.e 'transform', 'css'
103 | export const getAnimationType = (el, prop) => {
104 | if (isDOM(el) && arrayContains(validTransforms, prop)) return 'transform'
105 | if (isDOM(el) && (el.getAttribute(prop) || (isSVG(el) && el[prop])))
106 | return 'attribute'
107 | if (isDOM(el) && (prop !== 'transform' && getCSSValue(el, prop))) return 'css'
108 | if (el[prop] != null) return 'object'
109 | }
110 |
111 | // Get the value of animation property of an element
112 | export const getOriginalelementValue = (element, propName) => {
113 | switch (getAnimationType(element, propName)) {
114 | case 'transform':
115 | return getTransformValue(element, propName)
116 | case 'css':
117 | return getCSSValue(element, propName)
118 | case 'attribute':
119 | return element.getAttribute(propName)
120 | }
121 |
122 | return element[propName] || 0
123 | }
124 |
125 | // Validates and returns the value like 20px, 360deg, 0.4
126 | export const validateValue = (val, unit) => {
127 | if (isCol(val)) {
128 | return colorToRgb(val)
129 | }
130 |
131 | const originalUnit = getUnit(val)
132 | const unitLess = originalUnit
133 | ? val.substr(0, val.length - originalUnit.length)
134 | : val
135 | return unit && !/\s/g.test(val) ? unitLess + unit : unitLess
136 | }
137 |
138 | // Creates an object of value 500px => { original: "500", numbers: [500], strings: ["", "px"] }
139 | const decomposeValue = (val, unit) => {
140 | const rgx = /-?\d*\.?\d+/g
141 | const value = validateValue(isPath(val) ? val.totalLength : val, unit) + ''
142 |
143 | return {
144 | original: value,
145 | numbers: value.match(rgx) ? value.match(rgx).map(Number) : [0],
146 | strings: isString(val) || unit ? value.split(rgx) : []
147 | }
148 | }
149 |
150 | // Parse the elements and returns an array of elements
151 | export const parseElements = elements => {
152 | const elementsArray = elements
153 | ? flattenArray(
154 | isArray(elements) ? elements.map(toArray) : toArray(elements)
155 | )
156 | : []
157 |
158 | return filterArray(
159 | elementsArray,
160 | (item, pos, self) => self.indexOf(item) === pos
161 | )
162 | }
163 |
164 | // Returns an array of elements which will be animated
165 | export const getAnimatables = elements => {
166 | const parsed = parseElements(elements)
167 | return parsed.map((t, i) => {
168 | return { element: t, id: i, total: parsed.length }
169 | })
170 | }
171 |
172 | // Normalize tweens and animation properties
173 | const normalizePropertyTweens = (prop, tweenSettings) => {
174 | let settings = clone(tweenSettings)
175 | // from-to based prop values
176 | if (isArray(prop)) {
177 | const l = prop.length
178 | const isFromTo = l === 2 && !isObject(prop[0])
179 | if (!isFromTo) {
180 | // Duration divided by the number of tweens
181 | if (!isFunc(tweenSettings.duration))
182 | settings.duration = tweenSettings.duration / l
183 | } else {
184 | // Transform [from, to] values shorthand to a valid tween value
185 | prop = { value: prop }
186 | }
187 | }
188 |
189 | return toArray(prop)
190 | .map((v, i) => {
191 | // Default delay value should be applied only on the first tween
192 | const delay = !i ? tweenSettings.delay : 0
193 | let obj = isObject(v) ? v : { value: v }
194 | // Set default delay value
195 | if (isUnd(obj.delay)) obj.delay = delay
196 | return obj
197 | })
198 | .map(k => mergeObjects(k, settings))
199 | }
200 |
201 | // Get the animation properties
202 | const getProperties = (instanceSettings, tweenSettings, params) => {
203 | // store animation properties
204 | let properties = []
205 | // Merge instance params and tween params
206 | const settings = mergeObjects(instanceSettings, tweenSettings)
207 | for (let p in params) {
208 | if (!settings.hasOwnProperty(p) && (p !== 'el' || p !== 'multipleEl')) {
209 | properties.push({
210 | name: p,
211 | offset: settings['offset'],
212 | tweens: normalizePropertyTweens(params[p], tweenSettings)
213 | })
214 | }
215 | }
216 | return properties
217 | }
218 |
219 | // Normalize tween values
220 | const normalizeTweenValues = (tween, animatable) => {
221 | let t = {}
222 | for (let p in tween) {
223 | let value = evaluateValue(tween[p], animatable)
224 | // from-to based ?
225 | if (isArray(value)) {
226 | value = value.map(v => evaluateValue(v, animatable))
227 | if (value.length === 1) value = value[0]
228 | }
229 | t[p] = value
230 | }
231 | t.duration = parseFloat(t.duration)
232 | t.delay = parseFloat(t.delay)
233 | return t
234 | }
235 |
236 | // If we have an array of control points, then create a custom bezier curve or return the easing name using the val
237 | const normalizeEasing = val => {
238 | return isArray(val) ? bezier.apply(this, val) : easings[val]
239 | }
240 |
241 | // Create a normalise data structure of tween properties
242 | const normalizeTweens = (prop, animatable) => {
243 | let previousTween
244 |
245 | return prop.tweens.map(t => {
246 | let tween = normalizeTweenValues(t, animatable)
247 | // This may be transform value like 360deg or from to based animation values like [1, 2]
248 | const tweenValue = tween.value
249 | const originalValue = getOriginalelementValue(animatable.element, prop.name)
250 | const previousValue = previousTween
251 | ? previousTween.to.original
252 | : originalValue
253 | const from = isArray(tweenValue) ? tweenValue[0] : previousValue
254 | const to = getRelativeValue(
255 | isArray(tweenValue) ? tweenValue[1] : tweenValue,
256 | from
257 | )
258 | const unit = getUnit(to) || getUnit(from) || getUnit(originalValue)
259 | tween.from = decomposeValue(from, unit)
260 | tween.to = decomposeValue(to, unit)
261 | tween.start = previousTween ? previousTween.end : prop.offset
262 | tween.end = tween.start + tween.delay + tween.duration
263 | tween.easing = normalizeEasing(tween.easing)
264 | tween.elasticity = (1000 - minMaxValue(tween.elasticity, 1, 999)) / 1000
265 | previousTween = tween
266 | return tween
267 | })
268 | }
269 |
270 | const setTweenProgress = {
271 | css: (el, p, v) => batchMutation(() => (el.style[p] = v)),
272 | attribute: (el, p, v) => batchMutation(() => el.setAttribute(p, v)),
273 | object: (el, p, v) => (el[p] = v),
274 | transform: (el, p, v, transforms, id) => {
275 | if (!transforms[id]) transforms[id] = []
276 | transforms[id].push(`${p}(${v})`)
277 | }
278 | }
279 |
280 | // Create an object of animation properties for an element with animation type
281 | function createAnimation(animatable, prop) {
282 | const animType = getAnimationType(animatable.element, prop.name)
283 | if (animType) {
284 | const tweens = normalizeTweens(prop, animatable)
285 | return {
286 | type: animType,
287 | property: prop.name,
288 | animatable: animatable,
289 | tweens: tweens,
290 | duration: tweens[tweens.length - 1].end,
291 | delay: tweens[0].delay
292 | }
293 | }
294 | }
295 |
296 | // Create animation object using array of properties
297 | function getAnimations(animatables, properties) {
298 | return filterArray(
299 | flattenArray(
300 | animatables.map(animatable => {
301 | return properties.map(prop => {
302 | return createAnimation(animatable, prop)
303 | })
304 | })
305 | ),
306 | a => !isUnd(a)
307 | )
308 | }
309 |
310 | // Get the animation offset from the animation instance
311 | function getInstanceoffsets(type, animations, instanceSettings, tweenSettings) {
312 | const isDelay = type === 'delay'
313 | if (animations.length) {
314 | return (isDelay ? Math.min : Math.max).apply(
315 | Math,
316 | animations.map(anim => anim[type])
317 | )
318 | } else {
319 | return isDelay
320 | ? tweenSettings.delay
321 | : instanceSettings.offset + tweenSettings.delay + tweenSettings.duration
322 | }
323 | }
324 |
325 | const hasLifecycleHook = params => {
326 | const hooks = ['onStart', 'onUpdate', 'tick', 'onComplete']
327 |
328 | const errorMsg =
329 | 'Lifecycle hook cannot be passed as a parameter to Timeline function. They are accessible only via the timeline instance.'
330 |
331 | hooks.forEach(hook => {
332 | if (params.hasOwnProperty(hook)) {
333 | delete params[hook]
334 |
335 | console.error(errorMsg)
336 | }
337 | })
338 | }
339 |
340 | // For data binding
341 | function createElement(element, instance) {
342 | class _Timeline extends React.PureComponent {
343 | targets = []
344 |
345 | constructor(props) {
346 | super(props)
347 |
348 | if (!instance.elements || !Array.isArray(instance.elements)) {
349 | instance.elements = []
350 | }
351 | }
352 |
353 | componentDidMount() {
354 | instance.elements = [...instance.elements, this.targets]
355 | }
356 |
357 | componentWillUnmount() {
358 | // Clear all the references so as to avoid any memory leaks.
359 | instance.elements = []
360 | }
361 |
362 | addTargets = target => {
363 | this.targets = [...this.targets, target]
364 | }
365 |
366 | render() {
367 | return React.createElement(element, {
368 | ...this.props,
369 | ref: this.addTargets
370 | })
371 | }
372 | }
373 |
374 | return _Timeline
375 | }
376 |
377 | // Create a new animation object which contains data about the element which will be animated and its animation properties, also the instance properties and tween properties.
378 | function createNewInstance(params) {
379 | // Lifecycle hook should not be an animation property
380 | hasLifecycleHook(params)
381 |
382 | const instanceSettings = replaceObjectProps(
383 | getDefaultInstanceParams(),
384 | params
385 | )
386 |
387 | const tweenSettings = replaceObjectProps(getDefaultTweensParams(), params)
388 |
389 | const animatables = getAnimatables(params.el || params.multipleEl)
390 |
391 | const properties = getProperties(instanceSettings, tweenSettings, params)
392 | const animations = getAnimations(animatables, properties)
393 |
394 | return mergeObjects(instanceSettings, {
395 | children: [],
396 | animatables: animatables,
397 | animations: animations,
398 | duration: getInstanceoffsets(
399 | 'duration',
400 | animations,
401 | instanceSettings,
402 | tweenSettings
403 | ),
404 | delay: getInstanceoffsets(
405 | 'delay',
406 | animations,
407 | instanceSettings,
408 | tweenSettings
409 | )
410 | })
411 | }
412 |
413 | // Active animation instances
414 | let activeInstances = []
415 | let raf = 0
416 |
417 | const engine = (() => {
418 | function start() {
419 | raf = requestAnimationFrame(step)
420 | }
421 | function step(t) {
422 | const activeLength = activeInstances.length
423 | if (activeLength) {
424 | let i = 0
425 | while (i < activeLength) {
426 | // Call frame loop for all active instances
427 | if (activeInstances[i]) {
428 | activeInstances[i].frameLoop(t)
429 | }
430 | i++
431 | }
432 | start()
433 | } else {
434 | cancelAnimationFrame(raf)
435 | raf = 0
436 | }
437 | }
438 | return start
439 | })()
440 |
441 | function animated(params = {}) {
442 | let now,
443 | startTime,
444 | lastTime = 0
445 |
446 | let instance = createNewInstance(params)
447 |
448 | let res = null
449 |
450 | function createPromise() {
451 | return window.Promise && new Promise(resolve => (res = resolve))
452 | }
453 |
454 | let promise = createPromise()
455 |
456 | function toggleInstanceDirection() {
457 | instance.reversed = !instance.reversed
458 | }
459 |
460 | // If the direction is reverse, then make the playback rate negative (don't expose this as an imperative workaround to the user)
461 | function adjustTime(time) {
462 | return instance.reversed ? instance.duration - time : time
463 | }
464 |
465 | function syncInstanceChildren(time) {
466 | const children = instance.children
467 | const childrenLength = children.length
468 | if (time >= instance.currentTime) {
469 | for (let i = 0; i < childrenLength; i++) children[i].seek(time)
470 | } else {
471 | for (let i = childrenLength; i--; ) children[i].seek(time)
472 | }
473 | }
474 |
475 | function batchStyleUpdates(instance, id, transforms, transformString) {
476 | let el = instance.animatables[id].element
477 |
478 | if (transforms[id]) {
479 | return batchMutation(
480 | () => (el.style[transformString] = transforms[id].join(' '))
481 | )
482 | }
483 | }
484 |
485 | function setAnimationsProgress(insTime) {
486 | let i = 0
487 | let transforms = {}
488 | const animations = instance.animations
489 | const animationsLength = animations.length
490 | while (i < animationsLength) {
491 | const anim = animations[i]
492 | const animatable = anim.animatable
493 | const tweens = anim.tweens
494 | const tweenLength = tweens.length - 1
495 | let tween = tweens[tweenLength]
496 | // Only check for keyframes if there is more than one tween
497 | if (tweenLength)
498 | tween = filterArray(tweens, t => insTime < t.end)[0] || tween
499 | const elapsed =
500 | minMaxValue(insTime - tween.start - tween.delay, 0, tween.duration) /
501 | tween.duration
502 | const eased = isNaN(elapsed) ? 1 : tween.easing(elapsed, tween.elasticity)
503 | const strings = tween.to.strings
504 | let numbers = []
505 | let progress
506 | const toNumbersLength = tween.to.numbers.length
507 | for (let n = 0; n < toNumbersLength; n++) {
508 | let value
509 | const toNumber = tween.to.numbers[n]
510 | const fromNumber = tween.from.numbers[n]
511 |
512 | value = fromNumber + eased * (toNumber - fromNumber)
513 |
514 | numbers.push(value)
515 | }
516 | // Manual Array.reduce for better performances
517 | const stringsLength = strings.length
518 | if (!stringsLength) {
519 | progress = numbers[0]
520 | } else {
521 | progress = strings[0]
522 | for (let s = 0; s < stringsLength; s++) {
523 | const a = strings[s]
524 | const b = strings[s + 1]
525 | const n = numbers[s]
526 | if (!isNaN(n)) {
527 | if (!b) {
528 | progress += n + ' '
529 | } else {
530 | progress += n + b
531 | }
532 | }
533 | }
534 | }
535 |
536 | setTweenProgress[anim.type](
537 | animatable.element,
538 | anim.property,
539 | progress,
540 | transforms,
541 | animatable.id
542 | )
543 | anim.currentValue = progress
544 | i++
545 | }
546 |
547 | const transformsLength = Object.keys(transforms).length
548 | if (transformsLength) {
549 | for (let id = 0; id < transformsLength; id++) {
550 | if (!transformString) {
551 | const t = 'transform'
552 | transformString = batchRead(() => getCSSValue(document.body, t))
553 | ? t
554 | : `-webkit-${t}`
555 | }
556 |
557 | batchStyleUpdates(instance, id, transforms, transformString)
558 | }
559 | }
560 | instance.currentTime = insTime
561 | instance.progress = insTime / instance.duration * 100
562 | }
563 |
564 | function registerLifecycleHook(cb) {
565 | // Props for a lifecyle hook
566 | const {
567 | completed,
568 | progress,
569 | duration,
570 | remaining,
571 | reversed,
572 | currentTime,
573 | began,
574 | paused,
575 | start,
576 | stop,
577 | restart,
578 | reverse,
579 | reset,
580 | finish
581 | } = instance
582 |
583 | // Methods to control execution of an animation
584 | const controller = {
585 | start,
586 | stop,
587 | restart,
588 | reverse,
589 | reset,
590 | finish
591 | }
592 |
593 | if (instance[cb])
594 | instance[cb]({
595 | completed,
596 | progress,
597 | duration,
598 | remaining,
599 | reversed,
600 | currentTime,
601 | began,
602 | paused,
603 | controller
604 | })
605 | }
606 |
607 | function countIteration() {
608 | if (instance.remaining && instance.remaining !== true) {
609 | instance.remaining--
610 | }
611 | }
612 |
613 | // Set the animation instance progress using the engine time
614 | // BUG: synchronise the engine time with speed coefficient
615 | function setInstanceProgress(engineTime) {
616 | const insDuration = instance.duration
617 | const insoffset = instance.offset
618 | const insStart = insoffset + instance.delay
619 | const insCurrentTime = instance.currentTime
620 | const insReversed = instance.reversed
621 | const insTime = adjustTime(engineTime)
622 | if (instance.children.length) syncInstanceChildren(insTime)
623 | if (insTime >= insStart || !insDuration) {
624 | if (!instance.began) {
625 | instance.began = true
626 | registerLifecycleHook('onStart')
627 | }
628 | }
629 | if (insTime > insoffset && insTime < insDuration) {
630 | // Update the style and apply the transforms here
631 | setAnimationsProgress(insTime)
632 | } else {
633 | if (insTime <= insoffset && insCurrentTime !== 0) {
634 | // Animation completed!
635 | setAnimationsProgress(0)
636 | if (insReversed) countIteration()
637 | }
638 | if (
639 | (insTime >= insDuration && insCurrentTime !== insDuration) ||
640 | !insDuration
641 | ) {
642 | // Run the animation for a value defind for duration property
643 | setAnimationsProgress(insDuration)
644 | if (!insReversed) countIteration()
645 | }
646 | }
647 |
648 | registerLifecycleHook('onUpdate')
649 |
650 | if (process.env.NODE_ENV !== 'production') {
651 | // Catch errors occurred due to a scheduled job.
652 | exceptions()
653 | }
654 |
655 | if (engineTime >= insDuration) {
656 | // remaining not equals to 1 ?
657 | if (instance.remaining) {
658 | startTime = now
659 | // Change the direction
660 | if (instance.direction === 'alternate') toggleInstanceDirection()
661 | } else {
662 | // Loops done! So animation can be stopped.
663 | instance.stop()
664 | // Mark the flag completed
665 | if (!instance.completed) {
666 | instance.completed = true
667 |
668 | // Animations are done so remove the hint ('will-change')
669 | removeHints(instance.animatables)
670 |
671 | // Clear any scheduled job
672 | emptyScheduledJobs()
673 |
674 | registerLifecycleHook('onComplete')
675 |
676 | if ('Promise' in window) {
677 | // Resolve the promise only if haven't cancelled the animation
678 | if (!cancelled) {
679 | res({ msg: 'Animation completed!' })
680 | }
681 | promise = createPromise()
682 | }
683 | }
684 | }
685 | lastTime = 0
686 | }
687 | }
688 |
689 | // For data binding
690 | Object.assign(
691 | instance,
692 | DOMELEMENTS.reduce((getters, alias) => {
693 | getters[alias] = createElement(alias.toLowerCase(), instance)
694 | return getters
695 | }, {})
696 | )
697 |
698 | instance.reset = function() {
699 | const direction = instance.direction
700 | const loops = instance.iterations
701 | instance.currentTime = 0
702 | instance.progress = 0
703 | instance.paused = true
704 | instance.began = false
705 | instance.completed = false
706 | instance.reversed = direction === 'reverse'
707 | instance.remaining = direction === 'alternate' && loops === 1 ? 2 : loops
708 | setAnimationsProgress(0)
709 | // Also reset the child nodes
710 | for (let i = instance.children.length; i--; ) {
711 | instance.children[i].reset()
712 | }
713 | }
714 |
715 | instance.frameLoop = function(t) {
716 | let speedInParams = false
717 |
718 | now = t
719 | if (!startTime) startTime = now
720 |
721 | if (params.speed) {
722 | speedInParams = true
723 | }
724 |
725 | const speedCoefficient = () =>
726 | speedInParams ? params.speed : instance.speed ? instance.speed : 1
727 |
728 | const engineTime = (lastTime + now - startTime) * speedCoefficient()
729 | setInstanceProgress(engineTime)
730 | }
731 |
732 | // Default speed
733 | instance.speed = 1
734 |
735 | instance.setSpeed = speed => {
736 | invariant(
737 | typeof speed === 'number' || typeof speed === 'string',
738 | `setSpeed() expected a number or string value for speed but instead got ${typeof speed}.`
739 | )
740 |
741 | // Update both the coefficients (because a user can define params.speed, so we will overwrite it.)
742 | params.speed = speed
743 | instance.speed = speed
744 | }
745 |
746 | // Use createMover instead with more options to change the animation position.
747 | instance.seek = function(time) {
748 | setInstanceProgress(adjustTime(time))
749 | }
750 |
751 | instance.stop = function() {
752 | const i = activeInstances.indexOf(instance)
753 | if (i > -1) activeInstances.splice(i, 1)
754 |
755 | instance.paused = true
756 | }
757 |
758 | instance.start = function() {
759 | if (!instance.paused) return
760 | instance.paused = false
761 | startTime = 0
762 | lastTime = adjustTime(instance.currentTime)
763 | // Push the instances which will be animated
764 | activeInstances.push(instance)
765 | if (!raf) engine()
766 | }
767 |
768 | instance.reverse = function() {
769 | toggleInstanceDirection()
770 | startTime = 0
771 | lastTime = adjustTime(instance.currentTime)
772 | }
773 |
774 | instance.restart = function() {
775 | instance.stop()
776 | instance.reset()
777 | instance.start()
778 | }
779 |
780 | // Identity function
781 | instance.sequence = (...args) => instance
782 |
783 | // Use this method only when a 'setState' call is batched inside the lifecyle hook 'onUpdate' to avoid any memory leaks.
784 | instance.cancel = () => raf && cancelAnimationFrame(raf)
785 |
786 | // Timing APIs
787 |
788 | // Traverse the children and set the property value
789 | function traverseAndSet(element, property) {
790 | if (instance.children.length !== 0) {
791 | let value
792 |
793 | const elementsArray = parseElements(element)
794 |
795 | for (let j = instance.children.length; j--; ) {
796 | const animations = instance.children[j].animations
797 |
798 | for (let a = animations.length; a--; ) {
799 | if (arrayContains(elementsArray, animations[a].animatable.element)) {
800 | value = instance.children[j][property]
801 | }
802 | }
803 | }
804 |
805 | return value
806 | }
807 | }
808 |
809 | instance.getAnimationTime = function() {
810 | const iterations =
811 | instance.iterations === Infinity ? 1 : Number(instance.iterations)
812 |
813 | return instance.duration * iterations
814 | }
815 |
816 | instance.getAnimationTimeByElement = function(element) {
817 | invariant(
818 | typeof element === 'string' || typeof element === 'object',
819 | `Received an invalid element type ${typeof element}.`
820 | )
821 |
822 | return traverseAndSet(element, 'duration')
823 | }
824 |
825 | instance.getCurrentTime = function() {
826 | return Number(instance.currentTime).toFixed(2)
827 | }
828 |
829 | instance.getCurrentTimeByElement = function(element) {
830 | invariant(
831 | typeof element === 'string' || typeof element === 'object',
832 | `Received an invalid element type ${typeof element}.`
833 | )
834 |
835 | let currentTime = traverseAndSet(element, 'currentTime')
836 |
837 | return Number(currentTime).toFixed(2)
838 | }
839 |
840 | instance.getAnimationProgress = function() {
841 | return Number(instance.progress).toFixed(2)
842 | }
843 |
844 | instance.getAnimationProgressByElement = function(element) {
845 | invariant(
846 | typeof element === 'string' || typeof element === 'object',
847 | `Received an invalid element type ${typeof element}.`
848 | )
849 |
850 | let progress = traverseAndSet(element, 'progress')
851 |
852 | return Number(progress).toFixed(2)
853 | }
854 |
855 | instance.getComputedTiming = function() {
856 | return {
857 | activeTime: instance.getAnimationTime() || null,
858 | currentTime: Number(instance.getCurrentTime()) || null,
859 | progress: Number(instance.getAnimationProgress()) || null,
860 | currentIteration:
861 | instance.iterations === Infinity
862 | ? Infinity
863 | : instance.iterations - instance.remaining === 0
864 | ? 1
865 | : instance.iterations - instance.remaining
866 | }
867 | }
868 |
869 | // Mutate the active instances through this method
870 | instance.getAnimations = () => {
871 | if (activeInstances.length !== 0) {
872 | return activeInstances
873 | }
874 |
875 | return []
876 | }
877 |
878 | instance.finish = () => {
879 | instance.completed = true
880 | instance.paused = true
881 | instance.currentTime = 0
882 | instance.duration = 1000
883 | instance.progress = instance.direction === 'normal' ? 100 : 0
884 | instance.remaining = 0
885 | instance.reversed = instance.direction === 'normal' ? false : true
886 | }
887 |
888 | // Promise based APIs
889 |
890 | instance.onfinish = promise
891 |
892 | instance.oncancel = elements => {
893 | let res = null
894 |
895 | function createPromise() {
896 | return window.Promise && new Promise(resolved => (res = resolved))
897 | }
898 |
899 | let prm = createPromise()
900 |
901 | const elementsArray = parseElements(elements)
902 |
903 | function removeNodes(a, elements, animations, res) {
904 | if (arrayContains(elements, animations[a].animatable.element)) {
905 | const node = animations[a].animatable.element
906 | animations.splice(a, 1)
907 | if (!animations.length) {
908 | instance.paused = true
909 | // This ensures that the onfinish promise is not resolved if the elements are removed
910 | if (!cancelled) cancelled = true
911 | res({ element: node, msg: 'Removed the element from the timeline' })
912 | }
913 | }
914 | }
915 |
916 | for (let i = activeInstances.length; i--; ) {
917 | const instance = activeInstances[i]
918 | if (instance.animations.length === 0 && instance.children.length !== 0) {
919 | for (let j = instance.children.length; j--; ) {
920 | const animations = instance.children[j].animations
921 |
922 | for (let a = animations.length; a--; ) {
923 | removeNodes(a, elementsArray, animations, res)
924 | }
925 | }
926 | } else {
927 | const animations = instance.animations
928 | for (let a = animations.length; a--; ) {
929 | removeNodes(a, elementsArray, animations, res)
930 | }
931 | }
932 | }
933 |
934 | return prm
935 | }
936 |
937 | instance.reset()
938 |
939 | if (instance.autoplay) instance.start()
940 |
941 | return instance
942 | }
943 |
944 | const removeHints = instances => {
945 | instances.forEach(instance => {
946 | instance.element.style['will-change'] = ''
947 | })
948 | }
949 |
950 | function createTimeline(params) {
951 | let tl = animated(params)
952 | tl.stop()
953 | tl.duration = 0
954 | tl.animate = function(instancesParams) {
955 | tl.children.forEach(i => {
956 | i.began = true
957 | i.completed = true
958 | })
959 | toArray(instancesParams).forEach(instanceParams => {
960 | let insParams = mergeObjects(
961 | instanceParams,
962 | replaceObjectProps(getDefaultTweensParams(), params || {})
963 | )
964 | // Use data binding when no elements are specified explicitly
965 | insParams.el = insParams.el || insParams.multipleEl || (tl.elements || [])
966 | const tlDuration = tl.duration
967 | const insoffset = insParams.offset
968 | insParams.autoplay = false
969 | insParams.direction = tl.direction
970 | insParams.offset = isUnd(insoffset)
971 | ? tlDuration
972 | : getRelativeValue(insoffset, tlDuration)
973 | tl.began = true
974 | tl.completed = true
975 | tl.seek(insParams.offset)
976 | // Start animating the next children node
977 | const ins = animated(insParams)
978 | ins.began = true
979 | ins.completed = true
980 | if (ins.duration > tlDuration) tl.duration = ins.duration
981 | tl.children.push(ins)
982 | })
983 | tl.seek(0)
984 | tl.reset()
985 | if (tl.autoplay) tl.restart()
986 |
987 | return tl
988 | }
989 | return tl
990 | }
991 |
992 | const getAvailableTransforms = () => validTransforms
993 |
994 | export { animated, createTimeline, getAvailableTransforms }
995 |
--------------------------------------------------------------------------------