├── media ├── Logo.png ├── basic.gif ├── mover.gif ├── spring.gif ├── timing.gif ├── Animate.gif ├── basic-2.gif ├── onUpdate.gif ├── oncancel.gif ├── sequence.gif ├── keyframes.gif └── staggered.gif ├── .gitignore ├── .travis.yml ├── src ├── utils │ ├── noop.js │ ├── getEasings.js │ ├── properties.js │ ├── createEasingCurve.js │ ├── springUtils.js │ ├── defaults.js │ └── engineUtils.js ├── core │ ├── keyframes.js │ ├── timeline.js │ ├── batchMutations.js │ ├── transforms.js │ ├── createMover.js │ ├── easing.js │ ├── bezier.js │ └── engine.js ├── index.js ├── components │ └── Animate.js └── spring │ └── index.js ├── .babelrc ├── examples ├── styles.js ├── Extra │ ├── Finish.js │ └── speed.js ├── Timeline │ ├── basic.js │ ├── Multiple.js │ ├── Staggered.js │ ├── sequence.js │ └── timing.js ├── spring │ ├── Start.js │ ├── Spring.js │ ├── Blend.js │ ├── Velocity.js │ ├── SpringPromise.js │ ├── Callback.js │ ├── Multiple.js │ ├── Controls.js │ ├── Bounciness.js │ └── Interpolations.js ├── Animate-Component │ ├── Basic.js │ ├── Advance.js │ └── Controls.js ├── Promise │ └── index.js ├── Keyframes │ └── index.js ├── Lifecycle │ └── index.js └── Seeking │ └── basic.js ├── demo ├── index.html └── index.js ├── docs ├── README.md ├── properties.md ├── Keyframes.md ├── helpers.md ├── Component.md ├── Spring.md └── Timeline.md ├── .flowconfig ├── __tests__ ├── easings.test.js ├── createCurve.test.js ├── __snapshots__ │ ├── keyframes.test.js.snap │ └── timeline.test.js.snap ├── transforms.test.js ├── timeline.test.js ├── keyframes.test.js ├── animationProperties.test.js └── batching.test.js ├── flow-typed └── npm │ ├── svg-tag-names_vx.x.x.js │ ├── html-tags_vx.x.x.js │ ├── invariant_vx.x.x.js │ ├── engine-fork_vx.x.x.js │ ├── fastdom_vx.x.x.js │ ├── prop-types_vx.x.x.js │ └── rebound_vx.x.x.js ├── webpack.config.js ├── package.json └── README.md /media/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/Logo.png -------------------------------------------------------------------------------- /media/basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/basic.gif -------------------------------------------------------------------------------- /media/mover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/mover.gif -------------------------------------------------------------------------------- /media/spring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/spring.gif -------------------------------------------------------------------------------- /media/timing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/timing.gif -------------------------------------------------------------------------------- /media/Animate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/Animate.gif -------------------------------------------------------------------------------- /media/basic-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/basic-2.gif -------------------------------------------------------------------------------- /media/onUpdate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/onUpdate.gif -------------------------------------------------------------------------------- /media/oncancel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/oncancel.gif -------------------------------------------------------------------------------- /media/sequence.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/sequence.gif -------------------------------------------------------------------------------- /media/keyframes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/keyframes.gif -------------------------------------------------------------------------------- /media/staggered.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/Animated-Timeline/HEAD/media/staggered.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | cache 4 | dist 5 | .cache 6 | yarn-error.log 7 | demo 8 | .DS_Store 9 | build 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | notifications: 5 | email: false 6 | script: 7 | - npm run test 8 | -------------------------------------------------------------------------------- /src/utils/noop.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // 'inst' is the animation engine instance 4 | export const noop = (inst: Object): Object => inst 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react", "flow"], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "transform-class-properties" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/styles.js: -------------------------------------------------------------------------------- 1 | export const boxStyles = { 2 | width: '20px', 3 | height: '20px', 4 | backgroundColor: 'pink', 5 | marginTop: 40, 6 | marginLeft: 40 7 | } 8 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Timeline 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This is the documentation for `animated-timeline`. 4 | 5 | ## Table of contents 6 | 7 | * [Timeline API](./Timeline.md) 8 | 9 | * [Component API](./Component.md) 10 | 11 | * [Spring physics API](./Spring.md) 12 | 13 | * [Keyframes](./Keyframes.md) 14 | 15 | * [helpers module](./helpers.md) -------------------------------------------------------------------------------- /src/utils/getEasings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { easings } from '../core/easing' 4 | 5 | type easing = string 6 | 7 | type easingNames = Array 8 | 9 | export const getAvailableEasings = (): easingNames => { 10 | const names = [] 11 | 12 | Object.keys(easings).forEach((easing: easing) => names.push(easing)) 13 | 14 | return names 15 | } 16 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules 3 | /media 4 | /dist 5 | /.cache 6 | /demo 7 | /examples 8 | /flow-typed 9 | /__tests__ 10 | 11 | 12 | [include] 13 | /src 14 | 15 | [libs] 16 | 17 | [lints] 18 | 19 | [options] 20 | 21 | [strict] 22 | -------------------------------------------------------------------------------- /__tests__/easings.test.js: -------------------------------------------------------------------------------- 1 | import { helpers } from '../src' 2 | 3 | const { getAvailableEasings, createCurve } = helpers 4 | 5 | describe('Easings', () => { 6 | it('Should return array of easings name available', () => { 7 | const easings = getAvailableEasings() 8 | 9 | expect(Array.isArray(easings)).toBe(true) 10 | // Considering we haven't created a new easing curve 11 | expect(easings.length).toBe(28) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /__tests__/createCurve.test.js: -------------------------------------------------------------------------------- 1 | import { animated } from '../src/core/engine' 2 | import { easings } from '../src/core/easing' 3 | 4 | import { createEasingCurve } from '../src/utils/createEasingCurve' 5 | 6 | describe('Create bezier curve', () => { 7 | it('Should create a custom bezier curve with a name', () => { 8 | createEasingCurve('SampleCurve', [0.21, 0.34, 0.45, -0.98]) 9 | 10 | expect(typeof easings['SampleCurve']).toBe('function') 11 | expect(easings['SampleCurve']).toBeTruthy() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/keyframes.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Keyframes Should return array of frames 1`] = ` 4 | Array [ 5 | Object { 6 | "duration": 3000, 7 | "elasticity": 900, 8 | "value": 500, 9 | }, 10 | Object { 11 | "duration": 1000, 12 | "value": 1000, 13 | }, 14 | Object { 15 | "duration": 3000, 16 | "elasticity": 900, 17 | "value": 500, 18 | }, 19 | Object { 20 | "duration": 1000, 21 | "value": 1000, 22 | }, 23 | ] 24 | `; 25 | -------------------------------------------------------------------------------- /__tests__/transforms.test.js: -------------------------------------------------------------------------------- 1 | import { helpers } from '../src' 2 | 3 | const available_transforms = ["translateX", "translateY", "translateZ", "rotate", "rotateX", "rotateY", "rotateZ", "scale", "scaleX", "scaleY", "scaleZ", "skewX", "skewY", "perspective"] 4 | 5 | describe('Available transforms', () => { 6 | it('should return an array of available transforms', () => { 7 | const transforms = helpers.getAvailableTransforms() 8 | 9 | expect(Array.isArray(transforms)).toBe(true) 10 | expect(transforms).toEqual(available_transforms) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /__tests__/timeline.test.js: -------------------------------------------------------------------------------- 1 | import { createTimeline } from '../src' 2 | 3 | describe('createTimeline', () => { 4 | it('Should create a timeline instance', () => { 5 | const props = { 6 | delay: 200, 7 | direction: 'alternate', 8 | speed: 0.2, 9 | duration: 1000 10 | } 11 | 12 | const timeline = createTimeline(props) 13 | 14 | expect(typeof timeline).toEqual('object') 15 | expect(timeline).toMatchSnapshot() 16 | expect(timeline.delay).toBe(200) 17 | expect(timeline.direction).toBe('alternate') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /flow-typed/npm/svg-tag-names_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 90b0985e8e929df0f0d9618cdd35e520 2 | // flow-typed version: <>/svg-tag-names_v1.1.1/flow_v0.69.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'svg-tag-names' 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 'svg-tag-names' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /__tests__/keyframes.test.js: -------------------------------------------------------------------------------- 1 | import { Keyframes } from '../src' 2 | 3 | describe('Keyframes', () => { 4 | it('Should return array of frames', () => { 5 | const { frames } = new Keyframes() 6 | .add({ 7 | value: 500, 8 | duration: 3000, 9 | elasticity: 900, 10 | }) 11 | .add({ value: 1000, duration: 1000 }) 12 | .add({ 13 | value: 500, 14 | duration: 3000, 15 | elasticity: 900, 16 | }) 17 | .add({ value: 1000, duration: 1000 }) 18 | 19 | expect(Array.isArray(frames)).toBe(true) 20 | expect(frames).toMatchSnapshot() 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/core/keyframes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import invariant from 'invariant' 4 | 5 | // Animation attributes 6 | type attributes = Object 7 | 8 | type keyframes = Array 9 | 10 | // API Inspired from https://github.com/mattdesl/keyframes 11 | export function Keyframes() { 12 | this.frames = [] 13 | } 14 | 15 | Keyframes.prototype.add = function(values: attributes): keyframes { 16 | invariant( 17 | typeof values === 'object', 18 | `Expected values to be an object instead got a ${typeof values}.` 19 | ) 20 | 21 | this.frames.push(values) 22 | 23 | // Allow chaining of multiple values 24 | return this 25 | } 26 | -------------------------------------------------------------------------------- /examples/Extra/Finish.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 | }) 12 | 13 | export class Finish extends React.Component { 14 | componentDidMount() { 15 | t 16 | .animate({ 17 | scale: helpers.transition({ 18 | from: 2, 19 | to: 1 20 | }) 21 | }) 22 | .start() 23 | } 24 | 25 | render() { 26 | return t.finish()} /> 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/Timeline/basic.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 | iterations: Infinity, 9 | direction: 'alternate', 10 | duration: 2000, 11 | easing: 'easeInOutSine' 12 | }) 13 | 14 | export class BasicTimeline extends React.Component { 15 | componentDidMount() { 16 | t 17 | .animate({ 18 | scale: helpers.transition({ 19 | from: 2, 20 | to: 1 21 | }) 22 | }) 23 | .start() 24 | } 25 | 26 | render() { 27 | return 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/spring/Start.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 SpringStart extends React.Component { 10 | componentDidMount() { 11 | s 12 | .animate({ 13 | property: 'scale', 14 | map: { 15 | inputRange: [0, 1], 16 | outputRange: [1, 1.5] 17 | } 18 | }) 19 | .startAt(1) 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 | 26 |
27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/properties.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type value = number | string 4 | 5 | type fromTo = { 6 | from: value, 7 | to: value 8 | } 9 | 10 | type values = Array 11 | 12 | // Serialize the values 13 | // Used for transition from one state to another state 14 | export const transition = ({ 15 | from, 16 | to 17 | }: { 18 | from: value, 19 | to: value 20 | }): values => [from, to] 21 | 22 | // Multiplies the original value 23 | export const times = (val: value): string => `*=${val}` 24 | 25 | // Start at a part. time after the previous animation 26 | export const startAfter = (val: value): string => `+=${val}` 27 | 28 | // Start at a part. time before the previous animation 29 | export const startBefore = (val: value): string => `-=${val}` 30 | -------------------------------------------------------------------------------- /examples/spring/Spring.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 SpringSystem extends React.Component { 10 | componentDidMount() { 11 | s.animate({ 12 | property: 'scale', 13 | map: { 14 | inputRange: [0, 1], 15 | outputRange: [1, 1.5] 16 | } 17 | }) 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | s.setValue(0)} 25 | onMouseDown={() => s.setValue(1)} 26 | style={boxStyles} 27 | /> 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/core/timeline.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import invariant from 'invariant' 4 | 5 | import { createTimeline } from './engine' 6 | 7 | /** 8 | * Creates a new 'Animated' instance with timeline properties (delay, duration, iterations) and bridges both the models, 9 | * Animation and Timeline. 'Animated' is then used to collect the animatable properties or can be chained for performing sequence 10 | * based animation or offset based animations similar to web animation API 11 | * 12 | * Eg- 13 | * 14 | * const Animated = createTimeline({ 15 | * ...timelineprops 16 | * }) 17 | * 18 | * Animated.value({ ...animationprops }) 19 | */ 20 | 21 | export const Timeline = (timingProps: Object): Object => { 22 | timingProps = timingProps || {} 23 | 24 | return createTimeline(timingProps) 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/animationProperties.test.js: -------------------------------------------------------------------------------- 1 | import { helpers } from '../src' 2 | 3 | describe('From-to animation properties', () => { 4 | it('Should serialise the animation properties', () => { 5 | const fromTo = helpers.transition({ from: 400, to: 500 }) 6 | 7 | expect(Array.isArray(fromTo)).toBe(true) 8 | expect(fromTo.length).toBe(2) 9 | 10 | const times = helpers.times(3) 11 | 12 | expect(typeof times).toBe('string') 13 | expect(times).toBe('*=3') 14 | 15 | const startAfter = helpers.startAfter(2000) 16 | 17 | expect(typeof startAfter).toBe('string') 18 | expect(startAfter).toBe('+=2000') 19 | 20 | const startBefore = helpers.startBefore(2000) 21 | 22 | expect(typeof startBefore).toBe('string') 23 | expect(startBefore).toBe('-=2000') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /examples/spring/Blend.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 SpringBlend extends React.Component { 10 | componentDidMount() { 11 | s.animate({ 12 | property: 'backgroundColor', 13 | blend: { 14 | colors: ['#4a79c4', '#a8123a'], 15 | range: [0, 200] 16 | } 17 | }) 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | s.setValue(20)} 26 | onMouseDown={() => s.setValue(140)} 27 | /> 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/Timeline/Multiple.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 | iterations: Infinity, 9 | direction: 'alternate', 10 | duration: 1200, 11 | easing: 'easeInOutSine' 12 | }) 13 | 14 | export class MultipleElements extends React.Component { 15 | componentDidMount() { 16 | t 17 | .animate({ 18 | translateX: helpers.transition({ 19 | from: 0, 20 | to: 90 21 | }) 22 | }) 23 | .start() 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 30 | Multiple Elements 31 | 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/Animate-Component/Basic.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 | // Properties for animation model 19 | const animationProps = { 20 | rotate: '360deg', 21 | scale: 2 22 | } 23 | 24 | export class AnimateBasic extends Component { 25 | render() { 26 | return ( 27 |
28 | 29 |
30 | 31 |
32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /__tests__/batching.test.js: -------------------------------------------------------------------------------- 1 | import { batchMutation, batchRead } from '../src/core/batchMutations' 2 | 3 | describe('Batch style mutations', () => { 4 | it('should schedule a job at turn of next frame using rAF', () => { 5 | const mutation = (el) => el.style.transform = 'rotate(23.deg)' 6 | 7 | const el = { 8 | style: { 9 | transform: '' 10 | } 11 | } 12 | 13 | const writeId = batchMutation(() => mutation(el)) 14 | 15 | expect(typeof writeId).toBe('function') 16 | }) 17 | 18 | it('should schedule a job at turn of next frame using rAF', () => { 19 | const reads = (el) => el.style.transform 20 | 21 | const el = { 22 | style: { 23 | transform: 'scale(2)' 24 | } 25 | } 26 | 27 | const readId = batchRead(() => reads(el)) 28 | 29 | expect(typeof readId).toBe('function') 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/Promise/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 PromiseAPI extends React.Component { 16 | componentDidMount() { 17 | t 18 | .animate({ 19 | scale: helpers.transition({ 20 | from: 2, 21 | to: 1 22 | }) 23 | }) 24 | .start() 25 | 26 | t.onfinish.then(res => console.log(res)) 27 | } 28 | 29 | cancelAnimation = () => t.oncancel('#one').then(res => console.log(res)) 30 | 31 | render() { 32 | return 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/createEasingCurve.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import invariant from 'invariant' 4 | import { bezier } from '../core/bezier' 5 | import { easings } from '../core/easing' 6 | 7 | type curveName = 'string' 8 | 9 | type controlPoints = Array 10 | 11 | // Creates a custome easing function using the bezier curve control points 12 | // https://github.com/gre/bezier-easing 13 | export function createEasingCurve( 14 | name: curveName, 15 | points: controlPoints 16 | ): curveName { 17 | invariant( 18 | typeof name === 'string', 19 | `Expected easing curve name to be a string instead got a ${typeof name}.` 20 | ) 21 | 22 | invariant( 23 | Array.isArray(points), 24 | `Expected points to be an array instead got a ${typeof points}.` 25 | ) 26 | 27 | easings[name] = bezier(points[0], points[1], points[2], points[3]) 28 | 29 | return name 30 | } 31 | -------------------------------------------------------------------------------- /examples/spring/Velocity.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 SpringVelocity extends React.Component { 10 | componentDidMount() { 11 | s.animate({ 12 | property: 'scale', 13 | map: { 14 | inputRange: [0, 1], 15 | outputRange: [1, 1.5] 16 | } 17 | }) 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | s.setValueVelocity({ value: 0, velocity: 20 })} 25 | onMouseDown={() => 26 | s.setValueVelocity({ value: 1, velocity: 30 }) 27 | } 28 | style={boxStyles} 29 | /> 30 |
31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/spring/SpringPromise.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 SpringPromise extends React.Component { 10 | componentDidMount() { 11 | s.animate({ 12 | property: 'scale', 13 | map: { 14 | inputRange: [0, 1], 15 | outputRange: [1, 1.5] 16 | } 17 | }) 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | s.setValue(0)} 26 | onMouseDown={() => s.setValue(1)} 27 | /> 28 | 31 |
32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createEasingCurve } from './utils/createEasingCurve' 2 | import { Keyframes } from './core/keyframes' 3 | import { transition, times, startAfter, startBefore } from './utils/properties' 4 | import { getAvailableEasings } from './utils/getEasings' 5 | import { getAvailableTransforms } from './core/engine' 6 | import { Timeline as createTimeline } from './core/timeline' 7 | import { createMover } from './core/createMover' 8 | import { Animate } from './components/Animate' 9 | import { Spring } from './spring' 10 | 11 | // Helpers can be shared across instances of Timeline and Playback components (from - to, changing color values, creating bezier curves, sequencing by a offset value) 12 | export const helpers = { 13 | createEasingCurve, 14 | transition, 15 | times, 16 | startAfter, 17 | startBefore, 18 | getAvailableEasings, 19 | getAvailableTransforms 20 | } 21 | 22 | export { createTimeline, Keyframes, Animate, createMover, Spring } 23 | -------------------------------------------------------------------------------- /examples/spring/Callback.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 SpringCallback extends React.Component { 10 | componentDidMount() { 11 | s.animate({ 12 | property: 'scale', 13 | map: { 14 | inputRange: [0, 1], 15 | outputRange: [1, 1.5] 16 | } 17 | }) 18 | 19 | s.onRest = (inst) => s.infinite(0, 1, 2000) 20 | 21 | s.onStart = (inst) => console.log('Motion started...') 22 | } 23 | 24 | componentWillUnmount() { 25 | s.remove() 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 | s.setValue(0)} 33 | onMouseDown={() => s.setValue(1)} 34 | style={boxStyles} 35 | /> 36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/Timeline/Staggered.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 | iterations: Infinity, 9 | direction: 'alternate', 10 | duration: 2000, 11 | easing: 'easeInOutSine' 12 | }) 13 | 14 | export class Staggered extends React.Component { 15 | componentDidMount() { 16 | t 17 | .animate({ 18 | scale: helpers.transition({ 19 | from: 2, 20 | to: 1 21 | }), 22 | delay: (element, i) => i * 750 23 | }) 24 | .start() 25 | } 26 | 27 | renderNodes = n => { 28 | let children = [] 29 | 30 | for (let i = 0; i < n; i++) { 31 | children.push(React.createElement(t.div, { style: boxStyles, key: i })) 32 | } 33 | 34 | return children 35 | } 36 | 37 | render() { 38 | return {this.renderNodes(3)} 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/Keyframes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | createTimeline, 5 | Keyframes, 6 | helpers 7 | } from '../../build/animated-timeline.min.js' 8 | 9 | import { boxStyles } from '../styles' 10 | 11 | const t = createTimeline({ 12 | duration: 3000, 13 | elasticity: 1900, 14 | easing: 'easeInOutSine', 15 | direction: 'alternate', 16 | iterations: Infinity 17 | }) 18 | 19 | const x = new Keyframes() 20 | .add({ 21 | value: 1 22 | }) 23 | .add({ 24 | value: 10, 25 | offset: 0.25 26 | }) 27 | .add({ 28 | value: 20, 29 | offset: 0.5 30 | }) 31 | .add({ 32 | value: 30, 33 | offset: 1 34 | }) 35 | 36 | export class KeyframesExample extends React.Component { 37 | componentDidMount() { 38 | t 39 | .animate({ 40 | borderRadius: x.frames, 41 | backgroundColor: helpers.transition({ 42 | from: '#f989a7', 43 | to: '#f9b570' 44 | }) 45 | }) 46 | .start() 47 | } 48 | 49 | render() { 50 | return 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/springUtils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type value = string | number 4 | 5 | const withUnit = (value: value, unit: string): string => { 6 | value = String(value) 7 | if (value.includes(unit)) return value 8 | return value.concat(unit) 9 | } 10 | 11 | // Helpers for assigning units 12 | export const deg = (value: value): string => withUnit(value, 'deg') 13 | export const px = (value: value): string => withUnit(value, 'px') 14 | export const em = (value: value): string => withUnit(value, 'em') 15 | export const rem = (value: value): string => withUnit(value, 'rem') 16 | export const rad = (value: value): string => withUnit(value, 'rad') 17 | export const grad = (value: value): string => withUnit(value, 'grad') 18 | export const turn = (value: value): string => withUnit(value, 'turn') 19 | 20 | const UNITS = /([\+\-]?[0-9#\.]+)(%|px|em|rem|in|cm|mm|vw|vh|vmin|vmax|deg|rad|turn)?$/ 21 | 22 | // Get the unit from value 23 | export const parseValue = (value: value): string | Array => { 24 | value = String(value) 25 | const split = UNITS.exec(value.replace(/\s/g, '')) 26 | if (split) return split 27 | 28 | return '' 29 | } 30 | -------------------------------------------------------------------------------- /src/core/batchMutations.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fastdom from 'fastdom' 4 | 5 | type id = Function | null 6 | 7 | // Style updates 8 | let writeId = null 9 | 10 | // Style reads 11 | let readId = null 12 | 13 | // Scheduled jobs are stored in the queues which are emptied at the turn of next frame using rAF. This helps in reducing recalcs/sec and speed up the animation performance. 14 | 15 | // Batch style mutations 16 | export const batchMutation = (mutation: Function): id => { 17 | writeId = fastdom.mutate(() => { 18 | return mutation() 19 | }) 20 | 21 | return writeId 22 | } 23 | 24 | // Batch style reads 25 | export const batchRead = (reads: Function): id => { 26 | readId = fastdom.measure(() => { 27 | return reads() 28 | }) 29 | 30 | return readId 31 | } 32 | 33 | // In case we don't have the current node on which the mutations were applied, catch the exceptions. 34 | export const exceptions = () => { 35 | fastdom.catch = error => { 36 | console.error(error) 37 | } 38 | } 39 | 40 | // Clear the scheduled jobs 41 | export const emptyScheduledJobs = () => { 42 | fastdom.clear(readId) 43 | fastdom.clear(writeId) 44 | } 45 | -------------------------------------------------------------------------------- /flow-typed/npm/html-tags_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 255d53c3646e80b02a54cb9991842ef7 2 | // flow-typed version: <>/html-tags_v2.0.0/flow_v0.69.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'html-tags' 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 'html-tags' { 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 'html-tags/void' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'html-tags/index' { 31 | declare module.exports: $Exports<'html-tags'>; 32 | } 33 | declare module 'html-tags/index.js' { 34 | declare module.exports: $Exports<'html-tags'>; 35 | } 36 | declare module 'html-tags/void.js' { 37 | declare module.exports: $Exports<'html-tags/void'>; 38 | } 39 | -------------------------------------------------------------------------------- /flow-typed/npm/invariant_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: cca1950aaa43efb3253ea427ba9e7b67 2 | // flow-typed version: <>/invariant_v2.2.4/flow_v0.68.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'invariant' 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 'invariant' { 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 'invariant/browser' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'invariant/invariant' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'invariant/browser.js' { 35 | declare module.exports: $Exports<'invariant/browser'>; 36 | } 37 | declare module 'invariant/invariant.js' { 38 | declare module.exports: $Exports<'invariant/invariant'>; 39 | } 40 | -------------------------------------------------------------------------------- /flow-typed/npm/engine-fork_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 959f93ccc97dbe27c4c82a73bee6a67a 2 | // flow-typed version: <>/engine-fork_v3.0.6/flow_v0.68.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'engine-fork' 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 'engine-fork' { 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 'engine-fork/anime' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'engine-fork/anime.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'engine-fork/anime.js' { 35 | declare module.exports: $Exports<'engine-fork/anime'>; 36 | } 37 | declare module 'engine-fork/anime.min.js' { 38 | declare module.exports: $Exports<'engine-fork/anime.min'>; 39 | } 40 | -------------------------------------------------------------------------------- /examples/spring/Multiple.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 SpringMultiple extends React.Component { 10 | constructor(props) { 11 | super(props) 12 | 13 | this.one = React.createRef() 14 | this.two = React.createRef() 15 | } 16 | 17 | componentDidMount() { 18 | s 19 | .animate({ 20 | el: this.one.current, 21 | property: 'scale', 22 | map: { 23 | inputRange: [0, 1], 24 | outputRange: [1, 1.5] 25 | } 26 | }) 27 | .animate({ 28 | el: this.two.current, 29 | property: 'rotate', 30 | map: { 31 | inputRange: [0, 1], 32 | outputRange: ['180deg', '360deg'] 33 | } 34 | }) 35 | } 36 | 37 | render() { 38 | return ( 39 |
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 | 46 | 47 | 48 | 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 |
32 | 40 |
41 | 42 | 49 |
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 | 56 | 59 | 60 | 63 | 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 | ![author](https://img.shields.io/badge/author-Nitin%20Tulswani-blue.svg) ![size](https://img.shields.io/badge/size-35.5%20KB-brightgreen.svg) [![Build Status](https://travis-ci.org/nitin42/Timeline.svg?branch=beta0)](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 | [![Edit 6j08xylw7n](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/6j08xylw7n) 240 | 241 | ### Timing based animations 242 | 243 |

244 | 245 |

246 | 247 | [![Edit 92lm0xrl44](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/92lm0xrl44) 248 | 249 | ### Staggered animation 250 | 251 |

252 | 253 |

254 | 255 | [![Edit 743n1z9826](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/743n1z9826) 256 | 257 | ### Keyframes 258 | 259 |

260 | 261 |

262 | 263 | [![Edit 92lm0xrl44](https://codesandbox.io/static/img/play-codesandbox.svg)](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 | [![Edit kkjn9jq6k7](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/kkjn9jq6k7) 274 | 275 | ### Spring based animations 276 | 277 |

278 | 279 |

280 | 281 | [![Edit 75l1z6jzq](https://codesandbox.io/static/img/play-codesandbox.svg)](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 | --------------------------------------------------------------------------------