├── src ├── index.js ├── native.js ├── timeline │ ├── index.js │ ├── native.js │ └── timeline.js ├── Interval.js ├── animationFrame.js ├── tween-value-factories.js └── tween.js ├── .gitignore ├── examples ├── demo2 │ ├── bg.jpg │ ├── GitHub-Mark-64px.png │ ├── demo.html │ ├── index.html │ ├── app.css │ ├── Scrubber.js │ └── app.js ├── demo1 │ ├── sounds │ │ ├── crr.mp3 │ │ ├── pf1.mp3 │ │ ├── pf2.mp3 │ │ ├── pf3.mp3 │ │ └── gameover.mp3 │ ├── app.css │ ├── demo.html │ ├── index.html │ ├── app.js │ ├── GameOver.js │ └── Game.js ├── demo1b │ ├── sounds │ │ ├── crr.mp3 │ │ ├── pf1.mp3 │ │ ├── pf2.mp3 │ │ ├── pf3.mp3 │ │ └── gameover.mp3 │ ├── flakes │ │ ├── flake1.jpg │ │ └── flake1.png │ ├── demo.html │ ├── index.html │ ├── app.js │ ├── GameOver.js │ ├── app.css │ └── Game.js ├── webpack.build.config.js ├── global.css ├── demo5 │ ├── app.css │ ├── demo.html │ ├── index.html │ ├── app.js │ └── Demo5.js ├── demo4 │ ├── app.css │ ├── demo.html │ ├── index.html │ ├── app.js │ └── Demo4.js ├── demo3 │ ├── demo.html │ ├── index.html │ ├── app.css │ ├── app.js │ └── RollButton.js ├── index.html └── webpack.config.js ├── package-npm.js ├── webpack.config.js ├── package.json └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './tween'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | node_modules/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /examples/demo2/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo2/bg.jpg -------------------------------------------------------------------------------- /examples/demo1/sounds/crr.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1/sounds/crr.mp3 -------------------------------------------------------------------------------- /examples/demo1/sounds/pf1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1/sounds/pf1.mp3 -------------------------------------------------------------------------------- /examples/demo1/sounds/pf2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1/sounds/pf2.mp3 -------------------------------------------------------------------------------- /examples/demo1/sounds/pf3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1/sounds/pf3.mp3 -------------------------------------------------------------------------------- /examples/demo1b/sounds/crr.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/sounds/crr.mp3 -------------------------------------------------------------------------------- /examples/demo1b/sounds/pf1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/sounds/pf1.mp3 -------------------------------------------------------------------------------- /examples/demo1b/sounds/pf2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/sounds/pf2.mp3 -------------------------------------------------------------------------------- /examples/demo1b/sounds/pf3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/sounds/pf3.mp3 -------------------------------------------------------------------------------- /examples/demo1b/flakes/flake1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/flakes/flake1.jpg -------------------------------------------------------------------------------- /examples/demo1b/flakes/flake1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/flakes/flake1.png -------------------------------------------------------------------------------- /examples/demo1/sounds/gameover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1/sounds/gameover.mp3 -------------------------------------------------------------------------------- /examples/demo1b/sounds/gameover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo1b/sounds/gameover.mp3 -------------------------------------------------------------------------------- /examples/demo2/GitHub-Mark-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbox/react-imation/HEAD/examples/demo2/GitHub-Mark-64px.png -------------------------------------------------------------------------------- /src/native.js: -------------------------------------------------------------------------------- 1 | // not using export * because react-native is weird 2 | 3 | import * as tween from './tween'; 4 | 5 | export default tween; 6 | -------------------------------------------------------------------------------- /package-npm.js: -------------------------------------------------------------------------------- 1 | var p = require('./package'); 2 | 3 | p.main='index'; 4 | p.scripts=p.devDependencies=undefined; 5 | 6 | module.exports = p; 7 | -------------------------------------------------------------------------------- /examples/webpack.build.config.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | var config = require('./webpack.config'); 4 | 5 | config.output.path = 'examples/js'; 6 | 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /src/timeline/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import raf from 'raf'; 3 | import timelineFactory from './timeline'; 4 | 5 | const timeline = timelineFactory(React, raf); 6 | 7 | export default { ...timeline }; 8 | -------------------------------------------------------------------------------- /src/timeline/native.js: -------------------------------------------------------------------------------- 1 | import React from 'react-native'; 2 | import timelineFactory from './timeline'; 3 | 4 | const timeline = timelineFactory(React, requestAnimationFrame); 5 | 6 | export default { ...timeline }; 7 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | h1, h2, h3 { 7 | font-weight: 100; 8 | } 9 | 10 | a { 11 | color: hsl(200, 50%, 50%); 12 | } 13 | 14 | a.active { 15 | color: hsl(20, 50%, 50%); 16 | } 17 | 18 | .breadcrumbs a { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /examples/demo5/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | color scheme... 3 | D8CAA8 4 | 5C832F 5 | 284907 6 | 382513 7 | 363942 8 | */ 9 | 10 | html, body { 11 | margin: 0; 12 | padding: 0; 13 | background: #5c832f; 14 | font-family: 'Expletus Sans', cursive; 15 | color: #D8CAA8; 16 | font-weight: 700; 17 | } 18 | 19 | body { 20 | width: 100%; 21 | overflow-x: hidden; 22 | } -------------------------------------------------------------------------------- /examples/demo4/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | color scheme... 3 | D8CAA8 4 | 5C832F 5 | 284907 6 | 382513 7 | 363942 8 | */ 9 | 10 | html, body { 11 | margin: 0; 12 | padding: 0; 13 | background: #5c832f; 14 | font-family: 'Expletus Sans', cursive; 15 | color: #D8CAA8; 16 | font-weight: 700; 17 | } 18 | 19 | body { 20 | width: 100%; 21 | overflow-x: hidden; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/demo1/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | color scheme... 3 | D8CAA8 4 | 5C832F 5 | 284907 6 | 382513 7 | 363942 8 | */ 9 | 10 | html, body { 11 | margin: 0; 12 | padding: 0; 13 | background: #5c832f; 14 | font-family: 'Expletus Sans', cursive; 15 | color: #D8CAA8; 16 | font-weight: 700; 17 | } 18 | 19 | body { 20 | width: 100%; 21 | overflow-x: hidden; 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/demo2/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo3/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo4/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo5/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo2/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo3/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo4/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo5/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/demo3/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | color scheme... 3 | D8CAA8 4 | 5C832F 5 | 284907 6 | 382513 7 | 363942 8 | */ 9 | 10 | html, body { 11 | margin: 0; 12 | padding: 0; 13 | background: #5c832f; 14 | font-family: 'Expletus Sans', cursive; 15 | color: #D8CAA8; 16 | font-weight: 700; 17 | } 18 | 19 | body { 20 | width: 100%; 21 | overflow-x: hidden; 22 | } 23 | 24 | a, a:visited, a:active { 25 | color: #D8CAA8; 26 | } 27 | -------------------------------------------------------------------------------- /examples/demo1/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/demo1b/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/demo1/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/demo1b/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo Example 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | React Imation Examples 3 | 4 | 5 |

React Imation Examples

6 | 14 | -------------------------------------------------------------------------------- /examples/demo2/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | color scheme... 3 | D8CAA8 4 | 5C832F 5 | 284907 6 | 382513 7 | 363942 8 | */ 9 | 10 | html, body { 11 | margin: 0; 12 | padding: 0; 13 | background: #5c832f; 14 | font-family: 'Expletus Sans', cursive; 15 | color: #D8CAA8; 16 | font-weight: 700; 17 | } 18 | 19 | body { 20 | width: 100%; 21 | overflow: hidden; 22 | } 23 | 24 | a, a:visited, a:active { 25 | color: #D8CAA8; 26 | } 27 | 28 | p { 29 | font-size: 24px; 30 | } 31 | 32 | h1,h2 { 33 | font-size: 76px; 34 | display: block; 35 | text-align: center; 36 | margin-bottom: 150px; 37 | } 38 | 39 | h1 { 40 | margin:0; 41 | padding: 50px 0 0 0; 42 | pointer-events: none; 43 | } 44 | 45 | h3 { 46 | font-size: 88px; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /examples/demo4/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo4 from './Demo4'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | Fork me on GitHub 15 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | ReactDOM.render(, document.getElementById('example')); 22 | -------------------------------------------------------------------------------- /examples/demo1/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Flakes from './Game'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | Fork me on GitHub 15 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | ReactDOM.render(, document.getElementById('example')); 22 | -------------------------------------------------------------------------------- /examples/demo1b/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Flakes from './Game'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | Fork me on GitHub 15 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | ReactDOM.render(, document.getElementById('example')); 22 | -------------------------------------------------------------------------------- /examples/demo5/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Demo4 from './Demo5'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | Fork me on GitHub 15 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | ReactDOM.render(, document.getElementById('example')); 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var plugins = [ 4 | new webpack.DefinePlugin({ 5 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 6 | }) 7 | ]; 8 | 9 | if (process.env.COMPRESS) { 10 | plugins.push( 11 | new webpack.optimize.UglifyJsPlugin({ 12 | compressor: { 13 | warnings: false 14 | } 15 | }) 16 | ); 17 | } 18 | 19 | module.exports = { 20 | 21 | output: { 22 | library: 'ReactSparkScroll', 23 | libraryTarget: 'umd' 24 | }, 25 | 26 | resolve: { 27 | alias: { 28 | underscore: 'lodash' 29 | } 30 | }, 31 | 32 | externals: [ 33 | { 34 | "react": { 35 | root: "React", 36 | commonjs2: "react", 37 | commonjs: "react", 38 | amd: "react" 39 | } 40 | } 41 | ], 42 | 43 | module: { 44 | loaders: [ 45 | { test: /\.js$/, loader: 'babel-loader' } 46 | ] 47 | }, 48 | 49 | node: { 50 | Buffer: false 51 | }, 52 | 53 | plugins: plugins 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /examples/demo2/Scrubber.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | const inc = x => x + 1; 4 | 5 | const styles = { 6 | controlRange: { display: 'inline-block', 7 | lineHeight: '30px', 8 | verticalAlign: 'middle', 9 | width: 'calc(100% - 60px)' }, 10 | controlValue: { display: 'inline-block', 11 | lineHeight: '30px', 12 | verticalAlign: 'middle', 13 | color: 'white', 14 | fontSize: '11px', 15 | width: '80px', 16 | paddingLeft: '5px' } 17 | } 18 | 19 | export default class Scrubber extends Component { 20 | static defaultProps = { value: 0, min: 0, max: 100, name: null } 21 | 22 | render() { 23 | const {min,max,value,onChangeValue,name} = this.props; 24 | 25 | return
26 | onChangeValue(Number(event.target.value)) }/> 29 |
{value} of {max}
30 |
31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var webpack = require('webpack'); 4 | 5 | function isDirectory(dir) { 6 | return fs.lstatSync(dir).isDirectory(); 7 | } 8 | 9 | console.log('process.env.NODE_ENV:', process.env.NODE_ENV); 10 | console.log('here:',path.resolve('.')); 11 | 12 | module.exports = { 13 | 14 | devtool: 'inline-source-map', 15 | 16 | entry: fs.readdirSync(__dirname).reduce(function (entries, dir) { 17 | var isDraft = dir.charAt(0) === '_'; 18 | 19 | if (!isDraft && isDirectory(path.join(__dirname, dir))) 20 | entries[dir] = path.join(__dirname, dir, 'app.js'); 21 | 22 | return entries; 23 | }, {}), 24 | 25 | output: { 26 | path: 'examples/__build__', 27 | filename: '[name].js', 28 | chunkFilename: '[id].chunk.js', 29 | publicPath: '/__build__/' 30 | }, 31 | 32 | module: { 33 | loaders: [ 34 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?stage=0' } 35 | ] 36 | }, 37 | 38 | resolve: { 39 | alias: { 40 | 'react-imation': path.resolve(__dirname + '../../src/') 41 | } 42 | }, 43 | 44 | plugins: [ 45 | new webpack.optimize.CommonsChunkPlugin('shared.js'), 46 | new webpack.DefinePlugin({ 47 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 48 | }) 49 | ] 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /src/Interval.js: -------------------------------------------------------------------------------- 1 | // An easy way to repeatedly set an interval with a 2 | // component. 3 | // It extracts away the react lifecycle challenges 4 | // so that all you have to think about is what to do 5 | // every tick and how to schedule the next interval. 6 | 7 | import React, { Component, PropTypes } from 'react'; 8 | 9 | export default class Interval extends Component { 10 | static propTypes = { 11 | onTick: PropTypes.func.isRequired, 12 | } 13 | 14 | static defaultProps = { 15 | children: null, 16 | } 17 | 18 | componentWillMount() { 19 | this._updateTick(this.props.onTick); 20 | 21 | this.scheduleNextTick = delay => { 22 | // NOTE: Cancel previous scheduled timeout: 23 | // this means that calling scheduleTick multiple 24 | // times will only schedule the last tick, and previous 25 | // calls will be cancelled. 26 | clearTimeout(this.timeoutId); 27 | this.timeoutId = setTimeout(this.tick, delay); 28 | }; 29 | 30 | this.rafId = requestAnimationFrame(this.tick); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | if (this.props.onTick !== nextProps.onTick) { 35 | this._updateTick(nextProps.onTick); 36 | } 37 | } 38 | 39 | componentWillUnmount() { 40 | cancelAnimationFrame(this.rafId); 41 | cancelTimeout(this.timeoutId); 42 | } 43 | 44 | _updateTick(onTick) { 45 | this.tick = () => onTick(this.scheduleNextTick); 46 | } 47 | 48 | render() { 49 | return this.props.children; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/animationFrame.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import raf, { cancel as cancelRaf } from 'raf'; 3 | 4 | // Simple ticking decorator that manages destroying 5 | // requestAnimationFrame when component unmounts. 6 | // All you have to supply is the `callback` function 7 | // which gets called every tick. 8 | 9 | export function animationFrame(callback) { 10 | 11 | return DecoratedComponent => class AnimationFrame extends Component { 12 | componentWillMount() { 13 | this.tick = () => { 14 | callback(this.props); 15 | raf(this.tick); 16 | } 17 | } 18 | 19 | componentDidMount() { 20 | this.rafId = raf(this.tick); 21 | } 22 | 23 | componentWillUnmount() { 24 | cancelRaf(this.rafId); 25 | } 26 | 27 | render() { 28 | return 29 | } 30 | } 31 | } 32 | 33 | // Simple ticking component, just supply `onTick` prop 34 | export class AnimationFrame extends Component { 35 | static propTypes = { 36 | onTick: PropTypes.func.isRequired, 37 | } 38 | 39 | static defaultProps = { 40 | children: null, 41 | } 42 | 43 | componentWillMount() { 44 | this.tick = () => { 45 | this.props.onTick(); 46 | raf(this.tick); 47 | } 48 | } 49 | 50 | componentDidMount() { 51 | this.rafId = raf(this.tick); 52 | } 53 | 54 | componentWillUnmount() { 55 | cancelRaf(this.rafId); 56 | } 57 | 58 | render() { 59 | return this.props.children; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/demo5/Demo5.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {tween, ease} from 'react-imation'; 3 | import {Timeline} from 'react-imation/timeline'; 4 | import {Easer} from 'functional-easing'; 5 | 6 | const easeIn = ease(new Easer().using('in-cubic')); 7 | const easeOut = ease(new Easer().using('out-cubic')); 8 | 9 | const ballDiameter = 50; 10 | 11 | const containerStyle = { 12 | position: 'relative', 13 | width: '100vw', 14 | height: '100vh', 15 | }; 16 | 17 | const ballContainerStyle = { 18 | position: 'absolute', 19 | left: '50%', 20 | top: 'calc(100vh - 450px)', 21 | width: ballDiameter, 22 | transform: 'translate(-50%, -50%)' 23 | } 24 | 25 | export default class Demo5 extends Component { 26 | render() { 27 | return ( 28 |
29 |
30 | 31 | {({tween:twn}) => { 32 | 33 | const y = twn([ 34 | [0, easeIn(0)], 35 | [50, easeOut(400)], 36 | [100, 0], 37 | ]); 38 | 39 | return
54 | }} 55 |
56 |
57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/tween-value-factories.js: -------------------------------------------------------------------------------- 1 | import {createTweenValueFactory} from './tween'; 2 | 3 | export const rgb = createTweenValueFactory(value => `rgb(${value.join(',')})`); 4 | export const rgba = createTweenValueFactory(value => `rgba(${value.join(',')})`); 5 | export const scale = createTweenValueFactory(value => `scale(${value.join(',')})`); 6 | export const deg = createTweenValueFactory(value => `${value}deg`); 7 | export const grad = createTweenValueFactory(value => `${value}grad`); 8 | export const rad = createTweenValueFactory(value => `${value}rad`); 9 | export const turn = createTweenValueFactory(value => `${value}turn`); 10 | export const rotate = createTweenValueFactory(value => `rotate(${value})`, deg); 11 | export const rotateX = createTweenValueFactory(value => `rotateX(${value})`, deg); 12 | export const rotateY = createTweenValueFactory(value => `rotateY(${value})`, deg); 13 | export const rotateZ = createTweenValueFactory(value => `rotateZ(${value})`, deg); 14 | export const skewX = createTweenValueFactory(value => `skewX(${value})`, deg); 15 | export const skewY = createTweenValueFactory(value => `skewY(${value})`, deg); 16 | export const px = createTweenValueFactory(value => `${value}px`); 17 | export const em = createTweenValueFactory(value => `${value}em`); 18 | export const vw = createTweenValueFactory(value => `${value}vw`); 19 | export const vh = createTweenValueFactory(value => `${value}vh`); 20 | export const percent = createTweenValueFactory(value => `${value}%`); 21 | export const translateX = createTweenValueFactory(value => `translateX(${value})`, px); 22 | export const translateY = createTweenValueFactory(value => `translateY(${value})`, px); 23 | export const translate = createTweenValueFactory(value => `translate(${value.join(',')})`, px); 24 | export const translate3d = createTweenValueFactory(value => `translate3d(${value.join(',')})`, px); 25 | -------------------------------------------------------------------------------- /examples/demo3/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import RollButton from './RollButton'; 4 | 5 | class App extends Component { 6 | 7 | render() { 8 | return ( 9 |
10 | 11 | Fork me on GitHub 16 |
22 |
23 |
24 | button inspired by precursor app 25 |
26 |
27 | 41 |
42 |
43 |
44 |
45 | ) 46 | } 47 | } 48 | 49 | ReactDOM.render(, document.getElementById('example')); 50 | -------------------------------------------------------------------------------- /examples/demo1/GameOver.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {combine} from 'react-imation'; 3 | import {Timeline, Timeliner} from 'react-imation/timeline'; 4 | import {scale, rotateY} from 'react-imation/tween-value-factories'; 5 | import {Easer} from 'functional-easing'; 6 | import RollButton from '../demo3/RollButton'; 7 | 8 | export default class GameOver extends Component { 9 | componentDidMount() { 10 | this.props.gameOverSound.play(); 11 | } 12 | render() { 13 | const {playAgain, score} = this.props; 14 | 15 | return ( 16 | 17 | {({tween}) => 18 |
26 |
33 | Game Over 34 |
35 |
36 | {score.toLocaleString()} 37 |
38 |
39 | 53 |
54 |
55 | }
) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/demo1b/GameOver.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {combine} from 'react-imation'; 3 | import {Timeline, Timeliner} from 'react-imation/timeline'; 4 | import {scale, rotateY} from 'react-imation/tween-value-factories'; 5 | import {Easer} from 'functional-easing'; 6 | import RollButton from '../demo3/RollButton'; 7 | import ReactDOM from 'react-dom'; 8 | window.rrr2 = ReactDOM; 9 | 10 | export default class GameOver extends Component { 11 | componentDidMount() { 12 | this.props.gameOverSound.play(); 13 | } 14 | render() { 15 | const {playAgain, score} = this.props; 16 | 17 | return ( 18 | 19 | {({tween}) => 20 |
28 |
35 | Game Over 36 |
37 |
38 | {score.toLocaleString()} 39 |
40 |
41 | 55 |
56 |
57 | }
) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/demo2/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Easer} from 'functional-easing'; 4 | import {Timeline} from 'react-imation/timeline'; 5 | import {rotate, percent} from 'react-imation/tween-value-factories'; 6 | import Scrubber from './Scrubber'; 7 | import {Motion} from 'react-motion'; 8 | 9 | const easeOutBounce = new Easer().using('out-bounce'); 10 | const easeOutSine = new Easer().using('out-sine'); 11 | 12 | const MIN_TIME = 0; 13 | const MAX_TIME = 100; 14 | 15 | const styles = { 16 | scrubber: { 17 | width: '100%', 18 | boxSizing: 'border-box', 19 | }, 20 | ball: { 21 | borderRadius: '50%', 22 | width: '20px', 23 | height: '20px', 24 | position: 'absolute', 25 | backgroundColor: 'white', 26 | opacity: 0.2 27 | } 28 | }; 29 | 30 | class App extends Component { 31 | 32 | render() { 33 | return ( 34 | 39 | {({tween,time, playing, togglePlay, setTime}) => { 40 | 41 | const top = (100 + 40 * Math.sin(time/5)); 42 | const left = tween([ 43 | [MIN_TIME, 0], 44 | [MAX_TIME, 100] 45 | ], easeOutSine); 46 | 47 | return
48 | 49 | 50 | {interpolated => 51 |
57 | } 58 | 59 |
65 | 66 |

71 | spin 72 |

73 | 74 | 77 | 78 | 84 | 85 |
} 86 | 87 | } 88 | ) 89 | } 90 | } 91 | 92 | ReactDOM.render(, document.getElementById('example')); 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-imation", 3 | "version": "0.5.3", 4 | "description": "functional tweening and animation for react", 5 | "main": "./src/index", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/gilbox/react-imation.git" 9 | }, 10 | "homepage": "https://github.com/gilbox/react-imation/blob/latest/README.md", 11 | "bugs": "https://github.com/gilbox/react-imation/issues", 12 | "scripts": { 13 | "build-global": "rm -rf build/global && NODE_ENV=production webpack src/index.js build/global/react-imation.js && NODE_ENV=production COMPRESS=1 webpack src/index.js build/global/react-imation.min.js && echo \"gzipped, the global build is `gzip -c build/global/react-imation.min.js | wc -c` bytes\"", 14 | "build-npm": "rm -rf build/npm && babel -d build/npm ./src --stage 0 && cp README.md build/npm && find -X build/npm -type d -name __tests__ | xargs rm -rf && node -p 'p=require(\"./package-npm\");JSON.stringify(p,null,2)' > build/npm/package.json", 15 | "examples": "rm -rf examples/js && webpack-dev-server --config examples/webpack.config.js --content-base examples", 16 | "examples-fast": "rm -rf examples/js && NODE_ENV=production webpack-dev-server --config examples/webpack.config.js --content-base examples", 17 | "examples-build": "rm -rf examples/js && webpack --config examples/webpack.build.config.js", 18 | "test": "jsxhint . && karma start", 19 | "publish": "npm publish ./build/npm", 20 | "prepush": "npm run examples-build" 21 | }, 22 | "authors": [ 23 | "Gil Birman" 24 | ], 25 | "license": "MIT", 26 | "devDependencies": { 27 | "babel": "^5.8.21", 28 | "babel-core": "^5.8.22", 29 | "babel-loader": "^5.3.2", 30 | "bundle-loader": "^0.5.4", 31 | "classnames": "^2.1.3", 32 | "elegant-react": "^0.2.2", 33 | "expect": "^1.1.0", 34 | "functional-easing": "^1.0.8", 35 | "gsap": "^1.16.1", 36 | "jsxhint": "^0.12.1", 37 | "karma": "^0.12.28", 38 | "karma-chrome-launcher": "^0.1.7", 39 | "karma-cli": "0.0.4", 40 | "karma-firefox-launcher": "^0.1.3", 41 | "karma-mocha": "^0.1.10", 42 | "karma-sourcemap-loader": "^0.3.2", 43 | "karma-webpack": "^1.3.1", 44 | "lodash": "^3.10.1", 45 | "mocha": "^2.0.1", 46 | "react": "^0.14.0", 47 | "react-derive": "^0.1.1", 48 | "react-dom": "^0.14.0", 49 | "react-motion": "^0.3.0", 50 | "react-stateful-stream": "^0.3.0", 51 | "react-three": "0.7.1", 52 | "react-track": "^0.2.1", 53 | "rekapi": "^1.4.4", 54 | "rf-changelog": "^0.4.0", 55 | "rx": "2.3.18", 56 | "three": "^0.71.0", 57 | "updeep": "^0.10.1", 58 | "webpack": "^1.4.13", 59 | "webpack-dev-server": "^1.6.6" 60 | }, 61 | "peerDependencies": { 62 | }, 63 | "dependencies": { 64 | "raf": "^3.1.0" 65 | }, 66 | "tags": [ 67 | "react", 68 | "react-native", 69 | "animation" 70 | ], 71 | "keywords": [ 72 | "react", 73 | "react-native", 74 | "react-component", 75 | "animation", 76 | "tweening", 77 | "easing" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /src/timeline/timeline.js: -------------------------------------------------------------------------------- 1 | import {tween} from '../tween'; 2 | 3 | export default function timelineFactory(React, raf) { 4 | const {Component} = React; 5 | 6 | class Timeline extends Component { 7 | // todo: prop types & default props 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | 12 | const {timeliner=new Timeliner(props)} = props; 13 | 14 | this.state = { 15 | time: timeliner.time 16 | }; 17 | 18 | this.timeliner = timeliner; 19 | } 20 | 21 | componentDidMount() { 22 | if (this.props.playOnMount) this.timeliner.play(); 23 | this._addListener(); 24 | } 25 | 26 | componentWillUnmount() { 27 | this._removeListener(); 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.timeliner && nextProps.timeliner !== this.timeliner) { 32 | this._removeListener(); 33 | this.timeliner = nextProps.timeliner; 34 | this._addListener(); 35 | } 36 | if (this.timeliner) { 37 | Object.keys(nextProps).forEach(key => 38 | this.timeliner[key] = nextProps[key]); 39 | } 40 | } 41 | 42 | _addListener() { 43 | this._listenerId = this.timeliner.addListener(time => this.setState({time})); 44 | } 45 | 46 | _removeListener() { 47 | this.timeliner.removeListener(this._listenerId); 48 | } 49 | 50 | render() { 51 | return this.props.children(this.timeliner) 52 | } 53 | } 54 | 55 | class Timeliner { 56 | 57 | constructor(options={}) { 58 | this.listeners = {}; 59 | this.lastListenerId = 0; 60 | 61 | this._tick = ::this._tick; 62 | this.play = ::this.play; 63 | this.pause = ::this.pause; 64 | this.setPlay = ::this.setPlay; 65 | this.setTime = ::this.setTime; 66 | this.playFrom = ::this.playFrom; 67 | this.togglePlay = ::this.togglePlay; 68 | 69 | Object.keys(options).forEach(option => 70 | this[option] || (this[option] = options[option]) 71 | ); 72 | 73 | this.setTime(options.initialTime || this.time || 0); 74 | this.increment = this.increment || 1; 75 | 76 | if (this.playing) raf(this._tick); 77 | } 78 | 79 | _emitChange() { 80 | Object.keys(this.listeners).forEach(id => 81 | this.listeners[id](this.time) 82 | ); 83 | } 84 | 85 | _tick() { 86 | const {playing, time} = this; 87 | 88 | if (time >= this.max) { 89 | if (this.onComplete) this.onComplete(time); 90 | if (this.loop) { 91 | this.setTime(this.min); 92 | } else { 93 | this.playing = false; 94 | } 95 | } else { 96 | this.setTime(time + this.increment); 97 | this._emitChange(); 98 | } 99 | 100 | if (playing) raf(this._tick); 101 | } 102 | 103 | setTime(time) { 104 | this.time = (typeof time === 'function') ? time(this.time) : time; 105 | this.tween = (keyframes, easer) => tween(time, keyframes, easer); 106 | if (!this.playing) this._emitChange(); 107 | } 108 | 109 | play() { this.setPlay(true) } 110 | pause() { this.setPlay(false) } 111 | 112 | playFrom(time) { 113 | this.setTime(time); 114 | this.setPlay(true); 115 | } 116 | 117 | setPlay(playing) { 118 | if (!this.playing && playing) raf(this._tick); 119 | this.playing = playing; 120 | } 121 | 122 | togglePlay(playing) { 123 | if (!this.playing) raf(this._tick); 124 | this.playing = !this.playing; 125 | } 126 | 127 | addListener(callback) { 128 | this.listeners[++this.lastListenerId] = callback; 129 | return this.lastListenerId; 130 | } 131 | 132 | removeListener(id) { 133 | delete this.listeners[id]; 134 | } 135 | } 136 | 137 | return {Timeline, Timeliner}; 138 | } 139 | -------------------------------------------------------------------------------- /examples/demo4/Demo4.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Timeline} from 'react-imation/timeline'; 3 | import stateful from 'react-stateful-stream'; 4 | import {rotateX, rotateY, rotateZ, 5 | translateY, turn, vh, rgba} from 'react-imation/tween-value-factories'; 6 | import {Easer} from 'functional-easing'; 7 | import u from 'updeep'; 8 | const immutable = u({}); 9 | 10 | const easeInOut = new Easer().using('in-out'); 11 | const wiggleTypes = [ rotateX, rotateY, rotateZ ]; 12 | 13 | const charStyle = { 14 | display: 'inline-block', 15 | fontWeight: 'bold', 16 | transformOrigin: 'center bottom', 17 | lineHeight: '50px', 18 | cursor: 'pointer', 19 | fontSize: '80px' }; 20 | 21 | const dropStyle = { 22 | ...charStyle, 23 | transformOrigin: 'center middle' }; 24 | 25 | @stateful( 26 | immutable({ 27 | chars: "reactimation".split(''), 28 | dropped: [], 29 | wiggleType: 0, 30 | wiggleIndex: -1 }), 31 | edit => ({ 32 | setState: updates => edit(u(updates)), 33 | drop: index => edit(u({dropped: { [index]: true }})) 34 | })) 35 | export default class Demo4 extends Component { 36 | componentDidMount() { 37 | const wiggle = () => setTimeout(() =>{ 38 | const {chars, setState, dropped} = this.props; 39 | const charCount = chars.length; 40 | 41 | if (dropped.filter(x=>x).length === charCount) { 42 | setState({dropped: []}); // start over 43 | wiggle(); 44 | return; 45 | } 46 | 47 | let rand; 48 | do { rand = ~~(charCount * Math.random()) } while (dropped[rand]); 49 | setState({ 50 | wiggleIndex: rand, 51 | wiggleType: ~~(wiggleTypes.length * Math.random()) 52 | }); 53 | wiggle(); 54 | }, 800 + (Math.random()*1500)); 55 | 56 | wiggle(); 57 | } 58 | 59 | render() { 60 | const {chars, wiggleIndex, wiggleType, dropped, drop} = this.props; 61 | const charCount = chars.length; 62 | 63 | const wiggleTransform = wiggleTypes[wiggleType]; 64 | const wiggleKeyframes = [ 65 | [0, {transform: wiggleTransform(0)}], 66 | [10, {transform: wiggleTransform(25)}], 67 | [40, {transform: wiggleTransform(-30)}], 68 | [60, {transform: wiggleTransform(30)}], 69 | [100, {transform: wiggleTransform(0)}], 70 | ]; 71 | 72 | const dropKeyframesRotate = [ 73 | [0, {color: rgba(255,255,255,1), transform: rotateX(turn(0)) }], 74 | [100, {color: rgba(255,255,255,0), transform: rotateX(turn(5)) }], 75 | ]; 76 | 77 | const dropKeyframesMove = [ 78 | [0, { transform: translateY(vh(0)) }], 79 | [100, { transform: translateY(vh(50)) }], 80 | ]; 81 | 82 | return
83 |
84 | {chars.map((char,i) => { 85 | if (dropped[i]) { // drop the letter 86 | return ( 87 | 88 | {({tween}) => 89 |
90 |
96 |
97 | }) 98 | 99 | } else if (i === wiggleIndex) { // wiggle the letter 100 | return ( 101 | 102 | {({tween}) => 103 |
drop(i)} // when the user clicks on a wiggling letter, drop it 105 | key={i} 106 | style={{ 107 | ...charStyle, 108 | ...tween(wiggleKeyframes, easeInOut)}} 109 | children={char} /> 110 | }) 111 | 112 | } else { // just render the letter 113 | return
114 | } 115 | })} 116 |
117 |
118 | } 119 | } 120 | -------------------------------------------------------------------------------- /examples/demo1b/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | color scheme... 3 | D8CAA8 4 | 5C832F 5 | 284907 6 | 382513 7 | 363942 8 | */ 9 | 10 | html, body { 11 | margin: 0; 12 | padding: 0; 13 | background: #5c832f; 14 | font-family: 'Expletus Sans', cursive; 15 | color: #D8CAA8; 16 | font-weight: 700; 17 | } 18 | 19 | body { 20 | width: 100%; 21 | overflow-x: hidden; 22 | } 23 | 24 | a, a:visited, a:active { 25 | color: #D8CAA8; 26 | } 27 | 28 | p { 29 | font-size: 24px; 30 | } 31 | 32 | h1,h2 { 33 | font-size: 76px; 34 | display: block; 35 | text-align: center; 36 | margin-bottom: 150px; 37 | } 38 | 39 | h1 { 40 | margin:0; 41 | padding: 50px 0 0 0; 42 | pointer-events: none; 43 | } 44 | 45 | h3 { 46 | font-size: 88px; 47 | } 48 | 49 | .hero { 50 | position: relative; 51 | height: 100vh; 52 | } 53 | 54 | .down-arrow { 55 | position: absolute; 56 | bottom: 100px; 57 | left: 50%; 58 | margin-left: -22px; 59 | font-size: 100px; 60 | color: #7ea54e; 61 | } 62 | 63 | .intro { 64 | margin: 0; 65 | } 66 | 67 | .halfFrame { 68 | top: 0; 69 | position: fixed; 70 | height: 50vh; 71 | border: 1px solid black; 72 | opacity: 0.5; 73 | width: 100%; 74 | } 75 | 76 | /* pin */ 77 | .pin { 78 | position: relative; 79 | height: 100vh; 80 | width: 100%; 81 | top: 0; 82 | } 83 | 84 | .pin-pin { 85 | position: fixed; 86 | } 87 | 88 | .pin-unpin { 89 | position: absolute; 90 | bottom: 0; 91 | top: auto; 92 | } 93 | 94 | .pin-txt { 95 | position: absolute; 96 | top: 0; 97 | margin: 0; 98 | text-align: center; 99 | width: 100%; 100 | } 101 | 102 | .pin-cont { 103 | position: relative; 104 | height: 1400px; 105 | } 106 | 107 | .pin-cont-proxy { 108 | position: absolute; 109 | height: 100%; 110 | } 111 | 112 | /* slide, reveal */ 113 | .slide-proxy { 114 | height:200px; 115 | } 116 | 117 | .slide, 118 | .reveal { 119 | position: absolute; 120 | display: block; 121 | height: 100%; 122 | width: 100%; 123 | background: blue; 124 | z-index: 5; 125 | } 126 | 127 | .reveal { 128 | overflow: hidden; 129 | } 130 | 131 | .slide-txt, 132 | .reveal-txt { 133 | position: absolute; 134 | top: 50%; 135 | margin: -70px 0 0 0; 136 | width: 100%; 137 | text-align: center; 138 | } 139 | 140 | .slide-spacer { 141 | height: 600px; 142 | } 143 | 144 | /* unpin */ 145 | .unpin-txt { 146 | position: absolute; 147 | top: 100%; 148 | margin: 10px; 149 | width: 100%; 150 | text-align: center; 151 | z-index: 5; 152 | } 153 | 154 | /* parallax */ 155 | 156 | .parallax-cont { 157 | position: relative; 158 | overflow: hidden; 159 | height: 300px; 160 | } 161 | 162 | .parallax-cont-proxy { 163 | position: absolute; 164 | height: 100%; 165 | } 166 | 167 | .parallax-shadow { 168 | position: absolute; 169 | top: 0; 170 | left: 0; 171 | bottom: 0; 172 | right: 0; 173 | height: 300px; 174 | z-index:3; 175 | -webkit-box-shadow: inset 0px 0px 35px 0px rgba(0,0,0,0.75); 176 | -moz-box-shadow: inset 0px 0px 35px 0px rgba(0,0,0,0.75); 177 | box-shadow: inset 0px 0px 35px 0px rgba(0,0,0,0.75); 178 | 179 | } 180 | 181 | .parallax-img { 182 | background: url('bg.jpg'); 183 | height: 800px; 184 | position: absolute; 185 | width: 100%; 186 | z-index: 1; 187 | } 188 | 189 | .parallax-txt { 190 | position: absolute; 191 | z-index: 2; 192 | left: 50%; 193 | margin-left: -180px; 194 | } 195 | 196 | /* misc */ 197 | .fade1 { 198 | opacity: .5; 199 | } 200 | .fade2 { 201 | opacity: .3; 202 | } 203 | 204 | .hide { 205 | display: none; 206 | } 207 | 208 | .spacer50 { 209 | height: 50vh; 210 | } 211 | 212 | .spacer10 { 213 | height: 10vh; 214 | } 215 | 216 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 217 | display: none !important; 218 | } 219 | 220 | .center { 221 | display: block; 222 | margin: auto; 223 | text-align: center; 224 | } 225 | 226 | .text-center { 227 | text-align: center; 228 | } 229 | 230 | 231 | /* - - - - */ 232 | 233 | .Image { 234 | position: absolute; 235 | height: 400px; 236 | width: 400px; 237 | } 238 | 239 | .example-enter { 240 | opacity: 0.01; 241 | transition: opacity .5s ease-in; 242 | } 243 | 244 | .example-enter.example-enter-active { 245 | opacity: 1; 246 | } 247 | 248 | .example-leave { 249 | opacity: 1; 250 | transition: opacity .5s ease-in; 251 | } 252 | 253 | .example-leave.example-leave-active { 254 | opacity: 0.01; 255 | } 256 | -------------------------------------------------------------------------------- /examples/demo3/RollButton.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Easer} from 'functional-easing'; 3 | import {ease} from 'react-imation'; 4 | import {Timeline, Timeliner} from 'react-imation/timeline'; 5 | import {translateY} from 'react-imation/tween-value-factories'; 6 | import stateful from 'react-stateful-stream'; 7 | import {track, derive} from 'react-derive'; 8 | import u from 'updeep'; 9 | const immutable = u({}); 10 | 11 | const easeOutElastic = new Easer().using('out-elastic').withParameters(2,.6); 12 | const easeOutSine = new Easer().using('out-sine'); 13 | 14 | const borderColor = 'rgba(255,255,255,1.0)'; 15 | const listStyle = { color: '#ccc' }; 16 | 17 | @stateful( 18 | ({initialIndex}) => immutable({ 19 | isOver: false, 20 | currentIndex: initialIndex || 0 21 | }), 22 | edit => ({ 23 | update: updates => edit(u(updates)) 24 | })) 25 | @derive( 26 | { @track('currentIndex') 27 | newList({currentIndex, list}) { // re-arrange list 28 | const currentItem = list[currentIndex]; 29 | return list.filter(item => item !== currentItem) 30 | .sort(() => Math.random() - .5); 31 | }, 32 | 33 | @track('currentIndex') 34 | partitionedList() { 35 | return [ this.newList().slice(0,-3), 36 | this.newList().slice(-3) ]; 37 | } 38 | }) 39 | @stateful( 40 | { timeliner: new Timeliner({max:53}) }) 41 | export default class RollButton extends Component { 42 | render() { 43 | const { currentIndex, 44 | staticText, 45 | update, 46 | isOver, 47 | list, 48 | partitionedList, 49 | width, 50 | height, 51 | timeliner, 52 | onClick } = this.props; 53 | const [topList, bottomList] = partitionedList; 54 | const currentText = list[currentIndex]; 55 | 56 | return ( 57 |
{ 74 | timeliner.playFrom(0); 75 | update({ 76 | currentIndex: ~~(Math.random() * list.length), 77 | isOver: true 78 | }); 79 | }} 80 | onMouseLeave={event => 81 | update({isOver: false})}> 82 |
88 | 89 |
90 | {staticText}  91 |
92 | 93 |
94 | {/* Notice that since we're using Timeline statelessly (by providing 95 | the timeliner prop), it can be removed from the DOM on mouse out */} 96 | {isOver && 97 | 98 | {({tween}) => 99 |
109 |
110 | {topList.map(item => 111 |
{item}
)} 112 |
113 |
114 |
{currentText}
115 | {bottomList.map(item => 116 |
123 | {item} 124 |
125 | )} 126 |
127 |
128 | }
} 129 | {currentText} 130 |
131 | 132 |
133 |
) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/tween.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The following concepts are essential to this code: 3 | * 4 | * A VALUE FACTORY is a function which 5 | * - accepts any number of arguments representing the desired value and 6 | * - returns a WRAPPED VALUE object 7 | * 8 | * A WRAPPED VALUE is an object with a `tween` method and a `resolveValue` method. 9 | * - `resolveValue` returns the formatted string representation of the value. 10 | * - `tween` returns the formatted string representation of the result of tweening 11 | * two different WRAPPED VALUEs created with the same VALUE FACTORY. 12 | * 13 | */ 14 | 15 | export const isNumber = x => typeof x === 'number'; 16 | export const isWrapped = x => !!x.tween; 17 | export const isNotWrapped = x => !x.tween; 18 | export const identity = x => x; 19 | 20 | function mapObject(fn) { 21 | const result = {}; 22 | Object.keys(this).forEach(key => result[key] = fn(this[key], key)); 23 | return result; 24 | } 25 | 26 | export function tweenValues(progress, a, b, easer) { 27 | // for added flexibility with easing, we don't enforce 28 | // that b is wrapped 29 | if (a.tween) 30 | return a.tween(progress, a, b, easer); 31 | 32 | // now we enforce that a and b are the same type 33 | if (process.env.NODE_ENV !== 'production') { 34 | if (typeof(b) !== typeof(a)) 35 | throw(Error(`Tried to tween mismatched types ${typeof(a)} !== ${typeof(b)}`)); 36 | } 37 | 38 | if (Array.isArray(a)) 39 | return a.map((value,index) => tweenValues(progress, value, b[index], easer)); 40 | 41 | if (typeof a === 'number') 42 | return a + easer(progress) * (b-a); 43 | 44 | // object 45 | return a::mapObject((v,k) => k !== 'ease' && tweenValues(progress, v, b[k], easer)) 46 | } 47 | 48 | export const resolveValue = x => 49 | x.resolveValue ? x.resolveValue() : // is wrapped value 50 | typeof x === 'number' ? x : // is number 51 | x::mapObject(resolveValue); // is object 52 | 53 | /** 54 | * ## tween 55 | * 56 | * `position` is a number representing the current timeline position 57 | * 58 | * `keyframes` is an array 59 | * - Each item in the array should be a touple (a 2-item array) where the first 60 | * value of the touples represent positions on the timeline. Note that your 61 | * keyframes must *already* be sorted, `tween` will **not** sort them for you. 62 | * - The second value of the touples represent values at the given time. 63 | * the values are either numbers, objects, or wrapped values (wrapped values may also be nested) 64 | * * when the values are numbers `tween` returns a (tweened) Number 65 | * * when the values are objects `tween` returns an object. 66 | * * when the values are wrapped values `tween` returns the resolved result of the wrapped 67 | * value (usually a string) 68 | * - may optionally provide an `ease` property specifying an easing function 69 | * Note that all Keyframe values should be exactly the same type or shape. 70 | * (a value factory may make exceptions to this rule. 71 | * when doing `ease(easer, a)`, `b` does not have to be wrapped in `ease()`) 72 | * 73 | * `ease` is an (optional) easing function which should accept a number 0..1 74 | * and return a number usually 0..1 but for certain types of easing 75 | * you might want to go outside of the 0..1 range. 76 | * 77 | * - Adding an `ease` property to a keyframe will override the `ease` 78 | * argument of the `tween()` function. 79 | * 80 | * - Wrapping a value with the `ease()` value factory will override 81 | * any keyframe or `tween()`-level easing. 82 | */ 83 | export function tween(position, keyframes, easer=identity) { 84 | 85 | // TODO: remove for v1.0 86 | if (process.env.NODE_ENV !== 'production') { 87 | if (!Array.isArray(keyframes)) { 88 | throw Error('tween: as of react-imation@0.5.0, keyframes must be an array') 89 | } 90 | } 91 | 92 | const positions = keyframes.map(k => k[0]); 93 | 94 | const n = positions.length-1; 95 | const position0 = positions[0]; 96 | const positionN = positions[n]; 97 | 98 | if (position <= position0) return resolveValue(keyframes[0][1]); 99 | if (position >= positionN) return resolveValue(keyframes[n][1]); 100 | 101 | let indexB = 0; 102 | while (position > positions[++indexB]); 103 | const indexA = indexB - 1; 104 | 105 | const positionA = positions[indexA]; 106 | const positionB = positions[indexB]; 107 | 108 | if (process.env.NODE_ENV !== 'production') { 109 | if (typeof positionA === 'function' || typeof positionB === 'function') { 110 | throw Error('Keyframes are not allowed to contain functions as keys', keyframes); 111 | } 112 | } 113 | 114 | const range = positionB - positionA; 115 | const delta = position - positionA; 116 | const progress = delta / range; 117 | 118 | return tweenValues( 119 | progress, 120 | keyframes[indexA][1], 121 | keyframes[indexB][1], 122 | keyframes[indexA][1].ease || easer); 123 | } 124 | 125 | /** 126 | * ## createTweenValueFactory 127 | * 128 | * The first argument, `formatter` should be a 1-arity function 129 | * which accepts an array (`value`) and returns the formatted result. 130 | * For example, `formatter` might transform the array `[100,0,255]` to "rgb(100,0,255)" 131 | * 132 | * The second (optional) argument, `defaultWrapper` will 133 | * be used to map the elements of the `value` array which is useful 134 | * for wrapping the values in a default unit (like px, %, deg, etc) 135 | * 136 | * return a value factory. 137 | */ 138 | export function createTweenValueFactory(formatter, defaultWrapper) { 139 | const tween = (progress, a, b, easer) => 140 | formatter(tweenValues(progress, a.value, b.value, easer)); 141 | 142 | const wrap = v => isWrapped(v) ? v : defaultWrapper(v); 143 | 144 | return defaultWrapper ? 145 | (...value) => 146 | new TweenValue(value.map(wrap), formatter, tween) 147 | : 148 | (...value) => 149 | new TweenValue(value, formatter, tween); 150 | } 151 | 152 | class TweenValue { 153 | constructor(value, formatter, tween) { 154 | this.value = value; 155 | this.formatter = formatter; 156 | this.tween = tween; 157 | } 158 | 159 | resolveValue() { 160 | return this.formatter(this.value.map(resolveValue)) 161 | } 162 | } 163 | 164 | /** 165 | * combine is a value factory that combines wrapped values (or numbers) 166 | * by seperating them with a space 167 | * 168 | * for example: 169 | * 170 | * combine(scale(0.9), translate3d(0,-160,0)) 171 | * 172 | * note that `scale(0.9)` and `translate3d(0,-160,0)` 173 | * both return wrapped values. So in the non-tweened case, 174 | * combine produces: 175 | * 176 | * "scale(0.9) translate3d(0,-160,0)" 177 | */ 178 | export function combine(...wrappedValues) { 179 | return new Combine(wrappedValues); 180 | } 181 | 182 | class Combine { 183 | constructor(wrappedValues) { 184 | this.wrappedValues = wrappedValues; 185 | } 186 | 187 | tween(progress, 188 | {wrappedValues: wrappedValuesA}, 189 | {wrappedValues: wrappedValuesB}, 190 | easer 191 | ) { 192 | return wrappedValuesA 193 | .map((wrappedValueA, index) => 194 | tweenValues(progress, wrappedValueA, wrappedValuesB[index], easer)) 195 | .join(' '); 196 | } 197 | 198 | resolveValue() { 199 | return this.wrappedValues.map(resolveValue).join(' '); 200 | } 201 | } 202 | 203 | /** 204 | * ease is a value factory that will apply 205 | * an easing function to any wrapped value or number. 206 | * Easing is applied between values a and b, but the 207 | * ease factory must wrap value a. 208 | * 209 | * Note: 210 | * Wrapping a value with the `ease()` value factory will override 211 | * tween and keyframe-level easing 212 | **/ 213 | export function ease(easer, wrappedValue) { 214 | if (typeof wrappedValue === 'undefined') { // curry 215 | return wrappedValue => ease(easer, wrappedValue); 216 | } 217 | 218 | return new Ease(easer, wrappedValue); 219 | } 220 | 221 | class Ease { 222 | constructor(easer, wrappedValue) { 223 | this.easer = easer; 224 | this.easedValue = wrappedValue; 225 | } 226 | 227 | tween(progress, wrappedValueA, wrappedValueB) { 228 | return tweenValues( 229 | progress, 230 | wrappedValueA.easedValue, 231 | // give flexibility not to wrap b value in the ease factory 232 | wrappedValueB.easedValue ? wrappedValueB.easedValue : wrappedValueB, 233 | this.easer || identity) 234 | } 235 | 236 | resolveValue() { 237 | return resolveValue(this.easedValue); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /examples/demo1/Game.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {tween, combine} from 'react-imation'; 3 | import {Timeline, Timeliner} from 'react-imation/timeline'; 4 | import {percent, translate3d, scale, translateX, rotateY} from 'react-imation/tween-value-factories'; 5 | import stateful from 'react-stateful-stream'; 6 | import {derive, track} from 'react-derive'; 7 | import {elegant as optimize} from 'elegant-react'; 8 | import {Easer} from 'functional-easing'; 9 | import GameOver from './GameOver'; 10 | import u from 'updeep'; 11 | const immutable = u({}); 12 | 13 | const easeOutSine = new Easer().using('out-sine'); 14 | const easeInSine = new Easer().using('in-sine'); 15 | const easeInBack = new Easer().using('in-back').withParameters(2.8); 16 | const MAX_DROPPED = 100; 17 | 18 | const fullViewportStyle = { 19 | position: 'absolute', height: '100vh', width: '100vw', overflow: 'hidden', minWidth: '800px' 20 | }; 21 | 22 | const flakeImages = ["http://i.imgur.com/jbSVFgy.png", 23 | "http://i.imgur.com/TT2lmN4.png", 24 | "http://i.imgur.com/do8589m.png", 25 | "http://i.imgur.com/3BxEO8i.png"]; 26 | 27 | const createFlake = id => 28 | ({ 29 | id, 30 | size: 22 + ~~(Math.random() * 30), 31 | rotationSpeed: Math.random() * 40 - 20, 32 | rotateX: ~~(Math.random()*50), 33 | rotateY: ~~(Math.random()*32), 34 | left: ~~(Math.random() * 100) + '%', 35 | drift: ~~(Math.random() * 40) - 15, 36 | image: flakeImages[~~(flakeImages.length * Math.random())], 37 | increment: 0.15 + Math.random()*0.2, 38 | }); 39 | 40 | const pffSounds = [1,2,3].map(i => new Audio(`sounds/pf${i}.mp3`)); 41 | const pffSoundsCount = pffSounds.length; 42 | const crrSound = new Audio('sounds/crr.mp3'); 43 | const gameOverSound = new Audio('sounds/gameover.mp3'); 44 | 45 | const randi = limit => ~~(Math.random() * limit); 46 | const playRandomPfSound = () => pffSounds[randi(pffSoundsCount)].play(); 47 | 48 | const flakeHasId = id => flake => flake.id === id; 49 | const concat = newItem => items => items.concat(newItem); 50 | const lengthIsLessThan = length => items => items.length < length; 51 | const increment = x => x + 1; 52 | const decrement = x => x - 1; 53 | 54 | @stateful( 55 | immutable( 56 | { flakes: [], 57 | droppedCount: 0, 58 | gameIsOver: false, 59 | score: 0 60 | }), 61 | edit => ({ 62 | addFlake: newFlake => edit(u({flakes: u.if(lengthIsLessThan(21),concat(newFlake)), 63 | droppedCount: increment })), 64 | removeFlake: flakeId => edit(u({flakes: u.reject(flakeHasId(flakeId)) })), 65 | explodeFlake: index => edit(u({flakes: { [index]: { explode: true } } })), 66 | playAgain: () => edit(u({gameIsOver: false, score: 0, flakes: [], droppedCount: 0})), 67 | gameOver: () => edit(u({gameIsOver: true})), 68 | addToScore: amount => edit(u({score: x => x + ~~amount })), 69 | })) 70 | export default class Game extends Component { 71 | render() { 72 | const {gameIsOver, flakes, score} = this.props; 73 | 74 | return ( 75 |
76 |
77 | {score.toLocaleString()} 78 |
79 | 80 | {(gameIsOver && !flakes.length) ? 81 | 82 | : 83 | } 84 |
85 | ) 86 | } 87 | } 88 | 89 | const shakeKeyframes = [ [0, 10], [5, -8], [10, 5], [15, 0] ]; 90 | 91 | @optimize({statics: ['score']}) // changing the 'score' prop won't cause re-render 92 | class Flakes extends Component { 93 | componentDidMount() { 94 | const {addFlake, gameOver} = this.props; 95 | 96 | // these ids are arbitrary, what's import is that they are unique 97 | let lastFlakeId = 0; 98 | 99 | // here we randomly create new flakes every so often 100 | const tick = () => { 101 | if (lastFlakeId > MAX_DROPPED) return gameOver(); 102 | 103 | addFlake(createFlake(++lastFlakeId)); 104 | setTimeout(tick, 400 + ~~(Math.random() * 600)); 105 | } 106 | tick(); 107 | } 108 | 109 | render() { 110 | const {flakes, explodeFlake, removeFlake, addToScore, droppedCount, lastPoints} = this.props; 111 | 112 | const createHandleSlash = (explode,playFrom,rotationSpeed,index,increment,size) => event => { 113 | event.stopPropagation(); 114 | if (explode) return; 115 | playFrom(t => Math.max(0, t-10)); // shake 116 | rotationSpeed>0 ? playRandomPfSound() : crrSound.play(); 117 | explodeFlake(index); 118 | addToScore( 119 | 10000 * rotationSpeed * increment / size * 120 | tween(droppedCount, [ 121 | [ 0, 1 ], 122 | [ MAX_DROPPED, 6 ] 123 | ], easeInSine)); 124 | }; 125 | 126 | return ( 127 | 128 | {({time, playFrom}) => 129 |
132 | 133 | 134 | 135 | {flakes.map(({id, increment, size, rotateX, rotateY, rotationSpeed, left, drift, image, explode}, index) => 136 | removeFlake(id, explode)}> 142 | {({time}) => { 143 | const handleSlash = createHandleSlash(explode,playFrom,rotationSpeed,index,increment,size); 144 | return
155 | 156 | removeFlake(id, explode)} 159 | {...{size, image, explode}} /> 160 | 161 |
162 | }}
163 | )} 164 | 165 |
166 | }
167 | ) 168 | } 169 | } 170 | 171 | @optimize // automatically optimize shouldComponentUpdate 172 | @stateful( 173 | immutable({ 174 | trails: {}, 175 | trailCount: 0 176 | }), 177 | edit => ({ 178 | addTrail: (x,y) => edit(state => u({ trails: { [state.trailCount]: {x,y} }, trailCount: increment }, state)), 179 | removeTrail: id => edit(u({ trails: u.omit(`${id}`) })) 180 | }) 181 | ) 182 | class Trails extends Component { // mouse trails 183 | render() { 184 | const {trails, addTrail, removeTrail} = this.props; 185 | 186 | return
addTrail(pageX, pageY)} 187 | style={{...fullViewportStyle}}> 188 | 189 | {Object.keys(trails).map(key => { 190 | const {x,y} = trails[key]; 191 | return ( 192 | removeTrail(key)}> 193 | {({time}) => 194 |
203 | }) 204 | })} 205 | 206 |
207 | } 208 | } 209 | 210 | // this array will be used to construct styles 211 | // for exploding snowflake fragments. Each exploding 212 | // snowflake has 4 fragments. 213 | const fragments = [1,2,3,4].map(index => ({ 214 | index, 215 | posV: index < 3 ? 'top':'bottom', 216 | posH: index % 2 ? 'left':'right', 217 | h: index < 3 ? -1:1, // direction of explosion along X-axis 218 | v: index % 2 ? 1:-1 // direction of explosion along Y-axis 219 | })); 220 | 221 | @optimize 222 | class Flake extends Component { 223 | render() { 224 | const {rotate, size, image, explode, onExploded} = this.props; 225 | 226 | if (!explode) return ( // regular snowflake: 227 |
) 234 | 235 | const halfSize = size/2; 236 | 237 | return ( // exploding snowflake: 238 | 239 | {({time}) => { 240 | const scaleAmount = tween(time, [ 241 | [0, 1], 242 | [5, 1.3], 243 | [60, 0] 244 | ]); 245 | return
251 |
252 | {fragments.map(({index, posV, posH, h, v}) => 253 |
)} 274 |
275 |
} 276 | }) 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /examples/demo1b/Game.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Object3D, PerspectiveCamera, Scene, Mesh} from 'react-three'; 3 | import THREE from 'three'; 4 | import {tween, combine} from 'react-imation'; 5 | import {Timeline, Timeliner} from 'react-imation/timeline'; 6 | import {percent, translate3d, scale, translateX, rotateY} from 'react-imation/tween-value-factories'; 7 | import stateful from 'react-stateful-stream'; 8 | import {derive, track} from 'react-derive'; 9 | import {elegant as optimize} from 'elegant-react'; 10 | import {Easer} from 'functional-easing'; 11 | import GameOver from './GameOver'; 12 | import u from 'updeep'; 13 | const immutable = u({}); 14 | 15 | // React.initializeTouchEvents(true); 16 | 17 | const easeOutSine = new Easer().using('out-sine'); 18 | const easeInSine = new Easer().using('in-sine'); 19 | const easeInBack = new Easer().using('in-back').withParameters(2.8); 20 | const MAX_DROPPED = 300; 21 | 22 | const fullViewportStyle = { 23 | position: 'absolute', height: '100vh', width: '100vw', overflow: 'hidden', minWidth: '800px' 24 | }; 25 | 26 | const flakeImages = ["flakes/flake1.png", 27 | "http://i.imgur.com/TT2lmN4.png", 28 | "http://i.imgur.com/do8589m.png", 29 | "http://i.imgur.com/3BxEO8i.png"]; 30 | 31 | // const flakeTextures = flakeImages.map(THREE.ImageUtils.loadTexture); 32 | 33 | const flakeMaterial = new THREE.MeshBasicMaterial({ 34 | map: THREE.ImageUtils.loadTexture( flakeImages[0] ), 35 | // alphaMap: THREE.ImageUtils.loadTexture( flakeImages[0] ), 36 | // premultiplyAlpha: true, 37 | transparent: true, 38 | }); 39 | 40 | const createFlake = (id, x) => 41 | ({ 42 | id, 43 | x, 44 | scale: 1 + (Math.random() * 3), 45 | rotationSpeed: Math.random() * 90 - 20, 46 | rotateX: ~~(Math.random()*1), 47 | rotateY: ~~(Math.random()*.7), 48 | // left: ~~(Math.random() * 100) + '%', 49 | drift: ~~(Math.random() * 40) - 15, 50 | image: flakeImages[~~(flakeImages.length * Math.random())], 51 | increment: 0.15 + Math.random()*0.2, 52 | }); 53 | 54 | const pffSounds = [1,2,3].map(i => new Audio(`sounds/pf${i}.mp3`)); 55 | const pffSoundsCount = pffSounds.length; 56 | const crrSound = new Audio('sounds/crr.mp3'); 57 | const gameOverSound = new Audio('sounds/gameover.mp3'); 58 | 59 | const randi = limit => ~~(Math.random() * limit); 60 | const playRandomPfSound = () => pffSounds[randi(pffSoundsCount)].play(); 61 | 62 | const flakeHasId = id => flake => flake.id === id; 63 | const concat = newItem => items => items.concat(newItem); 64 | const lengthIsLessThan = length => items => items.length < length; 65 | const increment = x => x + 1; 66 | const decrement = x => x - 1; 67 | 68 | @stateful( 69 | immutable( 70 | { flakes: [], 71 | droppedCount: 0, 72 | gameIsOver: false, 73 | score: 0 74 | }), 75 | edit => ({ 76 | addFlake: newFlake => edit(u({flakes: u.if(lengthIsLessThan(21),concat(newFlake)), 77 | droppedCount: increment })), 78 | removeFlake: flakeId => edit(u({flakes: u.reject(flakeHasId(flakeId)) })), 79 | explodeFlake: index => edit(u({flakes: { [index]: { explode: true } } })), 80 | playAgain: () => edit(u({gameIsOver: false, score: 0, flakes: [], droppedCount: 0})), 81 | gameOver: () => edit(u({gameIsOver: true})), 82 | addToScore: amount => edit(u({score: x => x + ~~amount })), 83 | })) 84 | export default class Game extends Component { 85 | render() { 86 | const {gameIsOver, flakes, score} = this.props; 87 | 88 | return ( 89 |
90 |
91 | {score.toLocaleString()} 92 |
93 | 94 | {(gameIsOver && !flakes.length) ? 95 | 96 | : 97 | } 98 |
99 | ) 100 | } 101 | } 102 | 103 | const shakeKeyframes = { 0: 10, 5: -8, 10: 5, 15: 0}; 104 | 105 | 106 | const zaxis = new THREE.Vector3( 0, 0, 1 ); 107 | const angleZ = angle => { 108 | const quaternion = new THREE.Quaternion(); 109 | quaternion.setFromAxisAngle(zaxis, angle ); 110 | return quaternion; 111 | } 112 | 113 | @optimize({statics: ['score']}) // changing the 'score' prop won't cause re-render 114 | class Flakes extends Component { 115 | constructor(props) { 116 | super(props); 117 | 118 | const { width, height, explodeFlake } = props; 119 | 120 | const halfHeight = height/2; 121 | const offset = -height/2; 122 | const aspectRatio = width / height; 123 | const cameraProps = { 124 | fov:75, aspect:aspectRatio, near:1, far:5000, 125 | position:new THREE.Vector3(0,0,600), 126 | lookat:new THREE.Vector3(0,0,0)}; 127 | 128 | const geometry = new THREE.PlaneBufferGeometry( 20, 20, 1 ); 129 | 130 | const createHandleSlash = (explode,playFrom,rotationSpeed,index,increment,scale) => event => { 131 | event.stopPropagation(); 132 | if (explode) return; 133 | playFrom(t => Math.max(0, t-10)); // shake 134 | rotationSpeed>0 ? playRandomPfSound() : crrSound.play(); 135 | explodeFlake(index); 136 | addToScore(10000 * rotationSpeed * increment / scale 137 | * tween(this.props.droppedCount, {0:1, [MAX_DROPPED]:6}, easeInSine)); 138 | }; 139 | 140 | this.state = { 141 | halfHeight, 142 | cameraProps, 143 | geometry, 144 | createHandleSlash 145 | } 146 | } 147 | 148 | componentDidMount() { 149 | const {addFlake, gameOver} = this.props; 150 | 151 | // these ids are arbitrary, what's import is that they are unique 152 | let lastFlakeId = 0; 153 | 154 | // here we randomly create new flakes every so often 155 | const tick = () => { 156 | if (lastFlakeId > MAX_DROPPED) return gameOver(); 157 | 158 | addFlake(createFlake(++lastFlakeId, ~~(Math.random() * this.props.width - this.props.width/2))); 159 | setTimeout(tick, 100 + ~~(Math.random() * 400)); 160 | } 161 | tick(); 162 | } 163 | 164 | render() { 165 | const { flakes, explodeFlake, removeFlake, addToScore, 166 | droppedCount, lastPoints, width, height } = this.props; 167 | 168 | const { halfHeight, cameraProps, geometry, createHandleSlash } = this.state; 169 | 170 | // const material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} ); 171 | const material = flakeMaterial; 172 | 173 | // var plane = new THREE.Mesh( geometry, material ); 174 | 175 | // const camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000); 176 | 177 | 178 | return ( 179 | 180 | {({time, playFrom}) => 181 | 182 | 183 | 184 | {/**/} 185 | 186 | {flakes.map(({id, x, scale, increment}) => 187 | removeFlake(id)}> 193 | {({tween}) => 194 | 195 | 203 | 204 | {removeFlake(id); crrSound.play();} } 207 | quaternion={angleZ(tween({ 0:0, 105: 120 }))}/> 208 | 209 | 210 | 211 | } 212 | )} 213 | 214 | 215 | {/*flakes.map(({id, increment, size, rotateX, rotateY, rotationSpeed, left, drift, image, explode}, index) => 216 | removeFlake(id, explode)}> 222 | {({time}) => { 223 | const handleSlash = createHandleSlash(explode,playFrom,rotationSpeed,index,increment,size); 224 | return
235 | 236 | { removeFlake(id, explode)} 239 | {...{size, image, explode}} />} 240 | 241 |
242 | }}
243 | )*/} 244 | 245 |
246 | }
247 | ) 248 | } 249 | } 250 | 251 | @optimize // automatically optimize shouldComponentUpdate 252 | @stateful( 253 | immutable({ 254 | trails: {}, 255 | trailCount: 0 256 | }), 257 | edit => ({ 258 | addTrail: (x,y) => edit(state => u({ trails: { [state.trailCount]: {x,y} }, trailCount: increment }, state)), 259 | removeTrail: id => edit(u({ trails: u.omit(`${id}`) })) 260 | }) 261 | ) 262 | class Trails extends Component { // mouse trails 263 | render() { 264 | const {trails, addTrail, removeTrail} = this.props; 265 | 266 | return
addTrail(pageX, pageY)} 267 | style={{...fullViewportStyle}}> 268 | 269 | {Object.keys(trails).map(key => { 270 | const {x,y} = trails[key]; 271 | return ( 272 | removeTrail(key)}> 273 | {({time}) => 274 |
283 | }) 284 | })} 285 | 286 |
287 | } 288 | } 289 | 290 | // this array will be used to construct styles 291 | // for exploding snowflake fragments. Each exploding 292 | // snowflake has 4 fragments. 293 | const fragments = [1,2,3,4].map(index => ({ 294 | index, 295 | posV: index < 3 ? 'top':'bottom', 296 | posH: index % 2 ? 'left':'right', 297 | h: index < 3 ? -1:1, // direction of explosion along X-axis 298 | v: index % 2 ? 1:-1 // direction of explosion along Y-axis 299 | })); 300 | 301 | @optimize 302 | class Flake extends Component { 303 | render() { 304 | const {rotate, size, image, explode, onExploded} = this.props; 305 | 306 | if (!explode) return ( // regular snowflake: 307 |
) 314 | 315 | const halfSize = size/2; 316 | 317 | return ( // exploding snowflake: 318 | 319 | {({time}) => { 320 | const scaleAmount = tween(time, {0:1, 5:1.3, 60:0}); 321 | return
327 |
328 | {fragments.map(({index, posV, posH, h, v}) => 329 |
)} 347 |
348 |
} 349 | }) 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-imation 2 | 3 | This library provides 4 | various composable utilities for creating complex timeline-based animation in a react-y component-driven fashion. 5 | 6 | npm install react-imation --save 7 | 8 | Since this is a library of composable utility functions and components that mostly 9 | don't rely on each other, there is no fully bundled import. This keeps `react-imation` 10 | light-weight. The following imports are available: 11 | 12 | - `react-imation` 13 | - `react-imation/animationFrame` 14 | - `react-imation/Interval` 15 | - `react-imation/timeline` 16 | - `react-imation/tween-value-factories` 17 | 18 | For react-native the following imports are available 19 | (support is limited to a subset of the above, atm): 20 | 21 | - `react-imation/native` 22 | - `react-imation/timeline/native` 23 | - `react-imation/tween-value-factories` 24 | 25 | ## Demos 26 | 27 | - [Demo1](http://gilbox.github.io/react-imation/examples/demo1/demo.html): [[source](https://github.com/gilbox/react-imation/blob/master/examples/demo1/Game.js)] `tween`, `` ***Exploding Snowflakes*** 28 | - [Demo2](http://gilbox.github.io/react-imation/examples/demo2/demo.html): [[source](https://github.com/gilbox/react-imation/blob/master/examples/demo2/app.js)] `tween`, ``, `react-motion` 29 | - [Demo3](http://gilbox.github.io/react-imation/examples/demo3/demo.html): [[source](https://github.com/gilbox/react-imation/blob/master/examples/demo3/RollButton.js)] `tween`, ``, `Timeliner` 30 | - [Demo4](http://gilbox.github.io/react-imation/examples/demo4/demo.html): [[source](https://github.com/gilbox/react-imation/blob/master/examples/demo4/Demo4.js)] `tween`, `` 31 | - [Demo5](http://gilbox.github.io/react-imation/examples/demo5/demo.html): [[source](https://github.com/gilbox/react-imation/blob/master/examples/demo5/Demo5.js)] `tween`, ``, `ease` 32 | 33 | Also check out [`react-track`](https://github.com/gilbox/react-track)'s' 34 | demo which combines `react-imation` tweening with 35 | DOM tracking. 36 | 37 | If you clone this repo you can run the demos locally via: 38 | 39 | npm install 40 | npm run examples 41 | 42 | #### In the Wild 43 | 44 | - [airbnb javascript](http://airbnb.io/projects/javascript/) 45 | - Your demo? ([edit this file](https://github.com/gilbox/react-imation/edit/master/README.md)) 46 | 47 | ## [`tween(currentFrame, keyframes, [ease])`](https://github.com/gilbox/react-imation/blob/master/src/tween.js) 48 | 49 | The first argument, `currentFrame` is a number representing the current 50 | position in the animation **timeline**. 51 | 52 | The aforementioned **timeline** is represented by the `keyframes` 53 | argument which is an array of `[key, value]` touples. 54 | The 2 components of each touple represents a timeline 55 | position and it's state, respectively. 56 | Note that `tween` assumes that the keyframes are sorted. 57 | 58 | ```jsx 59 | import {tween} from 'react-imation'; 60 | import {rotate} from 'react-imation/tween-value-factories'; 61 | 62 | // ...render: 63 |

69 | spin 70 |

71 | ``` 72 | 73 | *Note: Support for object typed `keyframes` param 74 | has been removed as of `react-imation@0.5.0`* 75 | 76 | Tweening values that require special formatting is 77 | super-easy. All you have to do is create a new 78 | tween value factory. Check out 79 | [`tween-value-factories.js`](https://github.com/gilbox/react-imation/blob/master/src/tween-value-factories.js) 80 | and you'll see what I mean. 81 | 82 | #### `tween`: tweening numbers 83 | 84 | While `tween` works with more sophisticated *wrapped* values as demonstrated 85 | above, it also works with regular numbers. Here are some examples: 86 | 87 | tween(0.5, [[0, 10], [1, 20]]); //=> 15 88 | 89 | tween(5, [[0, 10], [10, 20]]); //=> 15 90 | 91 | tween(10, [[0, 0 ], 92 | [20, 10], 93 | [30, 20]]); //=> 5 94 | 95 | tween(5, [[0,10], [5,0]]); //=> 5 96 | 97 | You can use this approach to tween styles: 98 | 99 | ```jsx 100 |

101 | spin 102 |

103 | ``` 104 | 105 | You can tween all of your styles this way and it will work fine. 106 | However, when you have a lot of styles this can get tedious and difficult 107 | to read. For this reason, `tween` supports using *wrapped values*. 108 | Read the next section about creating *wrapped values* using 109 | tween value factories (TvFs). 110 | 111 | #### `tween`: creating wrapped values with tween value factories (TvFs) 112 | 113 | Wrapped values represent complex values which we ultimately need 114 | to convert to strings in order to generate CSS values. We can 115 | create wrapped values easily with tween value factories. 116 | 117 | Here are the two most complex value factories: 118 | 119 | import {combine, ease} from 'react-imation'; 120 | 121 | Here are some simple value factories: 122 | 123 | import {rotate, turn, px, translateX} from 'react-imation/tween-value-factories'; 124 | 125 | I call these tween value factories *simple* because they are extremely easy to create. 126 | To create a simple tween value factory first import the `createTweenValueFactory` 127 | function 128 | 129 | import {createTweenValueFactory} from 'react-imation'; 130 | 131 | and then use it like this: 132 | 133 | const px = createTweenValueFactory(value => `${value}px`); 134 | const translate3d = createTweenValueFactory(value => `translate3d(${value.join(',')})`, px); 135 | 136 | now the value of `translate3d` is a function which can create *wrapped values*. 137 | For example, 138 | 139 | const t = translate3d(100, 50, 80); // instantiate a wrapped value `t` 140 | t.resolveValue(); //=> "translate3d(100px,50px,80px)" 141 | 142 | note that calling `resolveValue` on the wrapped value `t` returns it's 143 | string representation. You will never have to do this explicitly because 144 | the `tween` function does it for you. 145 | 146 | Consider `translate3d` again 147 | 148 | const px = createTweenValueFactory(value => `${value}px`); 149 | const translate3d = createTweenValueFactory(value => `translate3d(${value.join(',')})`, px); 150 | 151 | Notice that we are passing the tween value factory `px` as the second 152 | argument of `createTweenValueFactory`. This tells `createTweenValueFactory` 153 | to create a value factory that automatically wraps each of its arguments 154 | which are *plain numbers* utilizing another 155 | value factory (`px`) before passing it into it's own value factory. 156 | 157 | Consider the TVF `percent` 158 | 159 | const percent = createTweenValueFactory(value => `${value}%`); 160 | 161 | We can use this with the `translate3d` TvF 162 | 163 | const t = translate3d(percent(50), percent(20), 200); 164 | t.resolveValue(); //=> "translate3d(50%,20%,200px)" 165 | 166 | Note that since we did not wrap the third argument in a TvF, 167 | it was wrapped automatically by the `px` TvF and that is why 168 | calling `t.resolveValue()` produced `200px` for the third argument. 169 | 170 | #### `tween`: tweening wrapped values 171 | 172 | The real power and elegance of the `tween` function becomes apparent 173 | when you use it with TvFs (that produce wrapped values). 174 | One of the primary goals of react-imation is to create a highly 175 | readable and intuitive API for animation. 176 | 177 | 178 | const t = tween(30, [ [ 0, rotate(0) ], 179 | [60, rotate(360)] ]) 180 | 181 | t.resolveValue(); //=> "rotate(180deg)" 182 | 183 | In react we can use this in a style tag: 184 | 185 | ```jsx 186 |
192 | ``` 193 | 194 | #### `tween`: tweening object literals 195 | 196 | Tweening object literals means that we are 197 | actually tweening the values within those objects and returning 198 | a new object with a similar shape. This works 199 | with both numbers and wrapped values. 200 | 201 | ```jsx 202 |

208 | spin 209 |

210 | ``` 211 | 212 | The result is something like this: 213 | 214 | ```jsx 215 |

216 | spin 217 |

218 | ``` 219 | 220 | The real advantage of using object literals is 221 | that it allows you to tween multiple style properties 222 | in one `tween()`: 223 | 224 | ```jsx 225 |

231 | spin 232 |

233 | ``` 234 | 235 | the result is something like: 236 | 237 | ```jsx 238 |

240 | spin 241 |

242 | ``` 243 | 244 | **warning**: All keyframes in a single tween must have exactly the same 245 | properties. The only exception to this is when using easing. 246 | 247 | #### `tween`: easing 248 | 249 | An easing function is a function that accepts a single argument, 250 | `time` and returns `time`. There are many libraries out there already 251 | that provide easing functions, or you can write your own. The one 252 | I've been using is `functional-easing`. 253 | 254 | There are three ways to ease with `tween`: 255 | 256 | 1. Pass the easing function as the third argument to `tween`. 257 | 2. When tweening a plain object, add an `ease` property. 258 | The easing will apply to all properties in the keyframe. 259 | For example: 260 | 261 | import {Easer} from 'functional-easing'; 262 | const easeOutSine = new Easer().using('out-sine'); 263 | 264 |

270 | spin 271 |

272 | 273 | 3. Wrap a TvF in the `ease` TvF. The `ease` TvF will override 274 | any other type of easing. 275 | 276 |

282 | spin 283 |

284 | 285 | **Heads-up: Doing `rotate(ease(easeOutSine, 0))` 286 | instead of `ease(easeOutSine, rotate(0))` unfortunately 287 | does *not* work.** 288 | 289 | Note that we did not wrap `rotate(360)` with `ease()`. Wrapping the 290 | destination value is optional because the source's easing function 291 | is always the one that `tween` applies. 292 | 293 | The `ease()` TvF is automatically curried, so we can also use 294 | it like this: 295 | 296 | const easeOutSine = ease(new Easer().using('out-sine')); 297 | 298 |

304 | spin 305 |

306 | 307 | **Heads-up: Doing `rotate(easeOutSine(0))` instead of 308 | `easeOutSine(rotate(0))` unfortunately 309 | does *not* work.** 310 | 311 | #### `tween`: combine TvF 312 | 313 | `combine` works as you might expect. 314 | 315 | combine(rotate(90), translateX(100)) 316 | .resolveValue(); //=> "rotate(90deg) translateX(100px)" 317 | 318 | 319 | ## `` 320 | 321 | import Interval from 'react-imation/Interval'; 322 | 323 | Stateless component providing an 324 | easy way to repeatedly set an interval. 325 | It extracts away the react lifecycle challenges 326 | so that all you have to think about is what to do 327 | every tick and how to schedule the next interval. 328 | 329 | { 330 | console.log('tick!'); 331 | scheduleTick(1000); // schedule next tick for 1 second from now 332 | }} />; 333 | 334 | ## `animationFrame` 335 | 336 | import { animationFrame } from 'react-imation/animationFrame'; 337 | 338 | Stateless ticking decorator that manages destroying 339 | requestAnimationFrame when component unmounts. 340 | All you have to supply is the only argument, 341 | a `callback` function 342 | which gets called every tick. 343 | 344 | **ES7 Decorator:** (with class-based component) 345 | 346 | @animationFrame(({onTick}) => onTick()) 347 | class Foo extends Component { 348 | render() { 349 | return
{this.props.foo}
350 | } 351 | } 352 | 353 | **Functionally:** (with stateless component) 354 | 355 | animationFrame( 356 | ({onTick}) => onTick() 357 | )( 358 | props =>
{props.foo}
359 | ) 360 | 361 | In both examples above we assume an `onTick` prop 362 | is being passed down and it will handle each 363 | tick event. 364 | 365 | ## `` 366 | 367 | import { AnimationFrame } from 'react-imation/animationFrame'; 368 | 369 | Stateless ticking component. Just supply a callback 370 | function to `onTick` prop. 371 | 372 | console.log('tick'))}> 373 | 374 | 375 | ## [``](https://github.com/gilbox/react-imation/blob/master/src/timeline/timeline.js) 376 | 377 | import { Timeline, Timeliner } from 'react-imation/timeline' 378 | 379 | Timeline as a component is super-handy. It manages the state of `time`. 380 | 381 | ```jsx 382 | 387 | {({time, playing, togglePlay, setTime}) => 388 |
389 | 390 | The timeline is {playing ? '' : 'not '}playing!
391 | Current time is {time}.
392 | 393 | We can easily create a pause button like this:
394 | 397 | 398 |
399 | 400 | ... or jump around the timeline:
401 | 404 | 405 | ... and tween to spin some text: 406 |

412 | spin 413 |

414 | 415 |
416 | }
417 | ``` 418 | 419 | #### ``: overview 420 | 421 | It accepts a single child which should be a function. 422 | When rendered, Timeline calls the function by passing in as the first 423 | argument an instance of the `Timeliner` class. 424 | 425 | **Note:** Because this is a stateful component, it will work well for simple 426 | use-cases. If you have more complex needs, using the `timeliner` prop 427 | (described below) might get you a *bit* further, but consider using 428 | the following lighter-weight stateless abstractions instead: 429 | 430 | - [``](#interval-) 431 | - [`animationFrame`](#animationframe) 432 | - [``](#animationframe-) 433 | 434 | they compose well in a system 435 | with reactive state management. Check out 436 | [react-three-ejecta-boilerplate](https://github.com/gilbox/react-three-ejecta-boilerplate) 437 | which is an example that utilizes 438 | [react-stateful-stream](https://github.com/gilbox/react-stateful-stream) 439 | for state management. 440 | 441 | 442 | #### ``: the [**`Timeliner`**](https://github.com/gilbox/react-imation/blob/master/src/timeline/timeline.js) class and `timeliner` prop 443 | 444 | The [`Timeliner`](https://github.com/gilbox/react-imation/blob/master/src/timeline/timeline.js) 445 | class does the heavy lifting of scheduling animation 446 | frames and storing the value of `time`. When using the `` 447 | component you can provide or omit a `timeliner` prop. By omitting the 448 | `timeliner` prop you are instructing `` to instantiate and 449 | manage an instance of the `Timeliner` class all by itself. 450 | 451 | In many cases, 452 | omitting the `timeliner` prop works very well. However, sometimes you 453 | need the added flexibility of *lifting* the state management functionality 454 | outside of the `` component. Here's what it looks like when 455 | we provide a `timeliner` prop: 456 | 457 | ```jsx 458 | const timeliner = new Timeliner(); 459 | timeliner.play(); 460 | 461 |
462 | 468 | 469 | {this.state.showTimeline && 470 | 471 | {({time}) => 472 | `The current time is {time}` 473 | } 474 |
475 | ``` 476 | 477 | Notice how we can mount/unmount the `` component 478 | without losing it's state, and since the `timeliner` instance has 479 | been lifted outside of the `` component, when the component 480 | is re-mounted it works the same as if it had been mounted all along. 481 | 482 | The single most important property of the `Timeliner` class is `time`. 483 | Let's take a look at the function we passed in as the child of the 484 | `` component from the previous example: 485 | 486 | ({time}) => `The current time is {time}` 487 | 488 | Remember, when `` calls this function it will pass in 489 | an instance of the `Timeliner` class. Our function uses *object destructuring* 490 | to get the value of the `time` property. 491 | 492 | You can access *methods* on the `Timeliner` instance via destructuring 493 | as well. All of the methods exposed by `Timeliner` are automatically 494 | bound to the `Timeliner` instance so that they work in this way. 495 | 496 | #### ``: the partially applied `tween` function 497 | 498 | The Timeliner class exposes a `tween` method which is the same `tween` function 499 | we've discussed, with the first argument already applied. The following two expressions 500 | are equivalent: 501 | 502 | tween(timeliner.time, [[0,0], [60,100]]); 503 | 504 | timeliner.tween([[0,0], [60,100]]); 505 | 506 | The happy consequence is that with `` you can use destructuring 507 | to easily access `Timeliner#tween`: 508 | 509 | ```jsx 510 | 511 | {({tween}) => 512 |

516 | I change color! 517 |

518 | }
519 | ``` 520 | 521 | 522 | ## react-native support 523 | 524 | Supports react-native as of `v0.2.6`, however performance is not so good 525 | because react-native works best when native props are manipulated directly. 526 | --------------------------------------------------------------------------------