├── .gitignore ├── src ├── Player.js ├── main.js └── Timeline.js ├── dist ├── Player.js ├── main.js └── Timeline.js ├── package.json ├── examples └── simple.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | package 4 | build 5 | -------------------------------------------------------------------------------- /src/Player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function createPlayer (timeline, scrollTarget) { 4 | const duration = timeline.duration || 1; 5 | const frames = []; 6 | let raf; 7 | 8 | function onScroll () { 9 | const time = scrollTarget.pageYOffset; 10 | 11 | let frame = frames[time] 12 | 13 | if (!frame) { 14 | const t = (time >= duration) ? duration : time; 15 | frame = frames[time] = timeline(t / duration); 16 | } 17 | 18 | window.requestAnimationFrame(frame); 19 | } 20 | 21 | onScroll(); 22 | scrollTarget.addEventListener('scroll', onScroll); 23 | 24 | 25 | return { 26 | stop: function () { 27 | scrollTarget.removeEventListener('scroll', onScroll); 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /dist/Player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createPlayer = createPlayer; 7 | function createPlayer(timeline, scrollTarget) { 8 | var duration = timeline.duration || 1; 9 | var frames = []; 10 | var raf = undefined; 11 | 12 | function onScroll() { 13 | var time = scrollTarget.pageYOffset; 14 | 15 | var frame = frames[time]; 16 | 17 | if (!frame) { 18 | var t = time >= duration ? duration : time; 19 | frame = frames[time] = timeline(t / duration); 20 | } 21 | 22 | window.requestAnimationFrame(frame); 23 | } 24 | 25 | onScroll(); 26 | scrollTarget.addEventListener('scroll', onScroll); 27 | 28 | return { 29 | stop: function stop() { 30 | scrollTarget.removeEventListener('scroll', onScroll); 31 | } 32 | 33 | }; 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rotoscope", 3 | "version": "0.2.0", 4 | "description": "Timeline animation framework", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "start": "python -m SimpleHTTPServer 8000 & open http://localhost:8000/examples/simple.html", 8 | "build": "mkdirp ./build && browserify -r ./src/main.js:rotoscope -o build/main.js -t [ babelify --presets [ es2015 ] ]", 9 | "prepublish": "babel src --presets babel-preset-es2015 --out-dir dist", 10 | "test": "test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/turissinitechnologies/rotoscope.git" 15 | }, 16 | "keywords": [ 17 | "animation", 18 | "timeline", 19 | "parallax" 20 | ], 21 | "author": "David Turissini ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/turissinitechnologies/rotoscope/issues" 25 | }, 26 | "homepage": "https://github.com/turissinitechnologies/rotoscope#readme", 27 | "devDependencies": { 28 | "babel": "^6.5.2", 29 | "babel-cli": "^6.5.1", 30 | "babel-preset-es2015": "^6.5.0", 31 | "babelify": "^7.2.0", 32 | "browserify": "^13.0.0", 33 | "jspm-server": "^1.0.1", 34 | "mkdirp": "^0.5.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { createPlayer } from './Player'; 4 | import { createTimeline } from './Timeline'; 5 | 6 | 7 | function createParallax (scrollTarget, bounds = {}, timeline) { 8 | return { 9 | startsAt (offsetY) { 10 | const cloneBounds = Object.assign(bounds, { 11 | start: offsetY 12 | }); 13 | 14 | return createParallax(scrollTarget, cloneBounds, timeline); 15 | }, 16 | 17 | bounds (b) { 18 | return createParallax(scrollTarget, b, timeline); 19 | }, 20 | 21 | duration (offsetY) { 22 | const cloneBounds = Object.assign(bounds, { 23 | duration: offsetY 24 | }); 25 | 26 | return createParallax(scrollTarget, cloneBounds, timeline); 27 | }, 28 | 29 | animate (factory) { 30 | const animateTimeline = factory(createTimeline()); 31 | 32 | return createParallax(scrollTarget, bounds, animateTimeline); 33 | 34 | }, 35 | 36 | start () { 37 | const playerTimelineBounds = Object.assign(bounds, { 38 | fill: 'both' 39 | }); 40 | 41 | const playerTimeline = createTimeline().appendChild(scrollTarget, playerTimelineBounds); 42 | 43 | return createPlayer(timeline, scrollTarget); 44 | } 45 | 46 | } 47 | }; 48 | 49 | 50 | 51 | 52 | export function createRotoscope (scrollElement) { 53 | return createParallax(scrollElement); 54 | }; 55 | 56 | export { 57 | createTimeline 58 | }; 59 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createTimeline = undefined; 7 | exports.createRotoscope = createRotoscope; 8 | 9 | var _Player = require('./Player'); 10 | 11 | var _Timeline = require('./Timeline'); 12 | 13 | function createParallax(scrollTarget) { 14 | var bounds = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 15 | var timeline = arguments[2]; 16 | 17 | return { 18 | startsAt: function startsAt(offsetY) { 19 | var cloneBounds = Object.assign(bounds, { 20 | start: offsetY 21 | }); 22 | 23 | return createParallax(scrollTarget, cloneBounds, timeline); 24 | }, 25 | bounds: function bounds(b) { 26 | return createParallax(scrollTarget, b, timeline); 27 | }, 28 | duration: function duration(offsetY) { 29 | var cloneBounds = Object.assign(bounds, { 30 | duration: offsetY 31 | }); 32 | 33 | return createParallax(scrollTarget, cloneBounds, timeline); 34 | }, 35 | animate: function animate(factory) { 36 | var animateTimeline = factory((0, _Timeline.createTimeline)()); 37 | 38 | return createParallax(scrollTarget, bounds, animateTimeline); 39 | }, 40 | start: function start() { 41 | var playerTimelineBounds = Object.assign(bounds, { 42 | fill: 'both' 43 | }); 44 | 45 | var playerTimeline = (0, _Timeline.createTimeline)().appendChild(scrollTarget, playerTimelineBounds); 46 | 47 | return (0, _Player.createPlayer)(timeline, scrollTarget); 48 | } 49 | }; 50 | }; 51 | 52 | function createRotoscope(scrollElement) { 53 | return createParallax(scrollElement); 54 | }; 55 | 56 | exports.createTimeline = _Timeline.createTimeline; -------------------------------------------------------------------------------- /src/Timeline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function createTimeline (clips = []) { 4 | 5 | function getClipsAtTime (timelineTime) { 6 | return clips.filter((clip) => { 7 | const fill = clip.fill; 8 | let start = clip.offset; 9 | let end = start + clip.duration; 10 | 11 | if (fill === 'none' && (timelineTime >= start && timelineTime <= end)) { 12 | return clip; 13 | } else if (fill === 'backwards' && (timelineTime <= end)) { 14 | return clip; 15 | } else if (fill === 'forwards' && (timelineTime >= start)) { 16 | return clip; 17 | } else if (fill === 'both'){ 18 | return clip; 19 | } 20 | }); 21 | }; 22 | 23 | const drawFunctions = []; 24 | 25 | const timeline = function (time) { 26 | const duration = timeline.duration; 27 | const timelineTime = duration * time; 28 | const clips = getClipsAtTime(timelineTime); 29 | 30 | const drawFunctions = clips.map(function (clip) { 31 | const offset = clip.offset; 32 | const fill = clip.fill; 33 | const duration = clip.duration; 34 | const end = duration + offset; 35 | let clipTime = timelineTime - offset; 36 | 37 | if ((fill === 'backwards' || fill === 'both') && (timelineTime < offset)) { 38 | clipTime = 0; 39 | } else if ((fill === 'forwards' || fill === 'both') && (timelineTime > end)) { 40 | clipTime = duration; 41 | } 42 | 43 | const clipPercent = clipTime / duration; 44 | return clip.clip(clipPercent); 45 | }); 46 | 47 | return function () { 48 | drawFunctions.forEach(function (h) { 49 | if (typeof h === 'function') { 50 | h(); 51 | } 52 | }); 53 | } 54 | } 55 | 56 | timeline.chainChild = function (child, clip, relativeBounds = {}) { 57 | const childBounds = clips.filter((clip) => { 58 | return clip.clip === child; 59 | })[0]; 60 | 61 | const relativeOffset = (typeof relativeBounds.offset === 'number') ? relativeBounds.offset : 0; 62 | 63 | const offset = childBounds.offset + childBounds.duration + relativeOffset; 64 | 65 | const bounds = Object.assign(relativeBounds, { 66 | offset: offset 67 | }); 68 | 69 | return this.appendChild(clip, bounds); 70 | 71 | } 72 | 73 | timeline.appendChild = function (clip, { offset = 0, fill = 'both', duration }) { 74 | let clipDuration = 1; 75 | 76 | if (typeof duration === 'number') { 77 | clipDuration = duration; 78 | } else if (typeof clip.duration === 'number') { 79 | clipDuration = clip.duration; 80 | } 81 | 82 | const timeBounds = { 83 | offset, 84 | fill, 85 | duration: clipDuration 86 | }; 87 | 88 | const clipsClone = clips.slice(0); 89 | clipsClone.push({ 90 | clip, 91 | offset, 92 | fill, 93 | duration: clipDuration 94 | }); 95 | 96 | return createTimeline(clipsClone); 97 | 98 | }; 99 | 100 | Object.defineProperty(timeline, 'duration', { 101 | get: function () { 102 | return clips.reduce((seed, clip) => { 103 | if (clip.offset + clip.duration > seed) { 104 | return clip.offset + clip.duration; 105 | } 106 | 107 | return seed; 108 | }, 0); 109 | } 110 | }); 111 | 112 | return timeline; 113 | 114 | }; 115 | -------------------------------------------------------------------------------- /dist/Timeline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createTimeline = createTimeline; 7 | function createTimeline() { 8 | var clips = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; 9 | 10 | 11 | function getClipsAtTime(timelineTime) { 12 | return clips.filter(function (clip) { 13 | var fill = clip.fill; 14 | var start = clip.offset; 15 | var end = start + clip.duration; 16 | 17 | if (fill === 'none' && timelineTime >= start && timelineTime <= end) { 18 | return clip; 19 | } else if (fill === 'backwards' && timelineTime <= end) { 20 | return clip; 21 | } else if (fill === 'forwards' && timelineTime >= start) { 22 | return clip; 23 | } else if (fill === 'both') { 24 | return clip; 25 | } 26 | }); 27 | }; 28 | 29 | var drawFunctions = []; 30 | 31 | var timeline = function timeline(time) { 32 | var duration = timeline.duration; 33 | var timelineTime = duration * time; 34 | var clips = getClipsAtTime(timelineTime); 35 | 36 | var drawFunctions = clips.map(function (clip) { 37 | var offset = clip.offset; 38 | var fill = clip.fill; 39 | var duration = clip.duration; 40 | var end = duration + offset; 41 | var clipTime = timelineTime - offset; 42 | 43 | if ((fill === 'backwards' || fill === 'both') && timelineTime < offset) { 44 | clipTime = 0; 45 | } else if ((fill === 'forwards' || fill === 'both') && timelineTime > end) { 46 | clipTime = duration; 47 | } 48 | 49 | var clipPercent = clipTime / duration; 50 | return clip.clip(clipPercent); 51 | }); 52 | 53 | return function () { 54 | drawFunctions.forEach(function (h) { 55 | if (typeof h === 'function') { 56 | h(); 57 | } 58 | }); 59 | }; 60 | }; 61 | 62 | timeline.chainChild = function (child, clip) { 63 | var relativeBounds = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; 64 | 65 | var childBounds = clips.filter(function (clip) { 66 | return clip.clip === child; 67 | })[0]; 68 | 69 | var relativeOffset = typeof relativeBounds.offset === 'number' ? relativeBounds.offset : 0; 70 | 71 | var offset = childBounds.offset + childBounds.duration + relativeOffset; 72 | 73 | var bounds = Object.assign(relativeBounds, { 74 | offset: offset 75 | }); 76 | 77 | return this.appendChild(clip, bounds); 78 | }; 79 | 80 | timeline.appendChild = function (clip, _ref) { 81 | var _ref$offset = _ref.offset; 82 | var offset = _ref$offset === undefined ? 0 : _ref$offset; 83 | var _ref$fill = _ref.fill; 84 | var fill = _ref$fill === undefined ? 'both' : _ref$fill; 85 | var duration = _ref.duration; 86 | 87 | var clipDuration = 1; 88 | 89 | if (typeof duration === 'number') { 90 | clipDuration = duration; 91 | } else if (typeof clip.duration === 'number') { 92 | clipDuration = clip.duration; 93 | } 94 | 95 | var timeBounds = { 96 | offset: offset, 97 | fill: fill, 98 | duration: clipDuration 99 | }; 100 | 101 | var clipsClone = clips.slice(0); 102 | clipsClone.push({ 103 | clip: clip, 104 | offset: offset, 105 | fill: fill, 106 | duration: clipDuration 107 | }); 108 | 109 | return createTimeline(clipsClone); 110 | }; 111 | 112 | Object.defineProperty(timeline, 'duration', { 113 | get: function get() { 114 | return clips.reduce(function (seed, clip) { 115 | if (clip.offset + clip.duration > seed) { 116 | return clip.offset + clip.duration; 117 | } 118 | 119 | return seed; 120 | }, 0); 121 | } 122 | }); 123 | 124 | return timeline; 125 | }; -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple parallax example 5 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rotoscope 2 | 3 | Timeline based parallax library. Make parallax effortless and enjoyable! 4 | 5 | ## Installation 6 | ``` 7 | npm install rotoscope 8 | ``` 9 | 10 | ## Build and run 11 | 12 | ``` 13 | $ npm run build 14 | $ npm start 15 | ``` 16 | 17 | ## Examples 18 | http://turissinitechnologies.github.io/rotoscope/ 19 | 20 | ## Why Rotoscope? 21 | Parallax is an awesome effect that can give your website and web app that surprise and delight that users will love you for. While parallax is awesome, it is not trivial to implement beyond simple translates. For complex animations, a more robust tool is needed and that tool is Rotoscope. 22 | 23 | Rotoscope itself is a super lightweight parallax library that works on the idea of timeline-based animations. This library specializes only in parallax and is 100% dependency free. It is also compatible with existing javascript animation frameworks like Greensock. 24 | 25 | To make parallax drawing as performant as possible, rotoscope implements a two part render cycle: Updating and then Drawing. 26 | Separating the render cycle into updating and drawing allows your animations to becomes very complex without taking a performance hit. Your users will love the results! 27 | 28 | Rotoscope is also based on timelines, which allow you to treat complex animations as a single unit. This will change the way you think about animation and also promote animations that are DRY, testable and really, really fun to show off. 29 | 30 | If you are building complex animations or parallax effects, rotoscope will becomes an indispensible tool. 31 | 32 | ## Features 33 | - Timeline based animations in your parallax 34 | - High performance rendering 35 | - Very flexible Clip interface that separates update and drawing 36 | - Immutable API 37 | - Promotes DRY parallax animations 38 | - Supports animation chaining in parallax 39 | - Compatible with most existing animation libraries. 40 | - Video parallax 41 | 42 | 43 | ### Hello World example 44 | 45 | To begin our parallax, we start by creating a `rotoscope` instance. The `rotoscope` object is what enables us to have super performant parallax. All methods on this object are chainable and immutable. This means that when any of the methods below are called, you are returned a different `rotoscope` instance. This prevents unintended side effects from creeping into other, unrelated parts of your code. 46 | 47 | We should start by importing our `createRotoscope` function to create a rotoscope instance with a scroll target. A `scroll target` is any element that will emit `scroll events`. In most cases, this will probably be the window: 48 | 49 | ``` 50 | import { createRotoscope } from 'rotoscope'; 51 | 52 | const rotoscope = createRotoscope(window); 53 | ``` 54 | 55 | After we create a `rotoscope` object, we should define its `bounds`. `bounds` simply describe when `scroll events` should effect parallax. In the example below, we should see animation updates between scroll positions 50 and 150. 56 | 57 | ``` 58 | const rotoscopeWithBounds = rotoscope.bounds({ 59 | start: 50, 60 | duration: 100 61 | }); 62 | 63 | ``` 64 | 65 | We should now add some animations. By default, `rotoscope` instances are created with a single root `timeline` that encompasses the entire animation. This `timeline` is the argument in the animation factory example below. We first create a `greenBallClip`, which is a custom clip function that takes time as an argument: 66 | 67 | ``` 68 | const rotoscopeWithAnimations = rotoscopeWithBounds.animate(function (timeline) { 69 | const greenBallClip = function (time) { 70 | var dist = 400; 71 | var y = -time * dist; 72 | var translate = 'scale3d(' + time + ', ' + time + ', 1)'; 73 | 74 | return function () { 75 | greenBall.style.transform = translate; 76 | }; 77 | }; 78 | 79 | return timeline.appendChild(greenBallClip, { 80 | start: 0, 81 | duration: 100 82 | }); 83 | 84 | }) 85 | 86 | ``` 87 | 88 | 89 | We can then start listening to scroll events and begin parallaxing 90 | 91 | ``` 92 | rotoscopeWithAnimations.start(); 93 | ``` 94 | 95 | 96 | ### Clip 97 | 98 | In the `animation factory` above, we create a `Clip` function and append it to the timeline. `Clip` functions are what drives the animation and are broken down into two parts: Update and Draw. The first part, Update, is the calculation that happens before Draw. This part of the process takes a single argument, `time`, that is always a value between 0 and 1. You an think of `time` as percent. 99 | 100 | The Update function should take the `time` and calculate what the next frame will look like. It should return a function that will actually draw that frame. This is a core concept in `rotoscope` and enables powerful and performant animations. 101 | 102 | In the following example, we create a clip that moves an element, greenBall, anywhere between 0 and 400 pixels: 103 | 104 | ``` 105 | const greenBallClip = function (time) { 106 | var dist = 400; 107 | var y = time * dist; 108 | var translate = 'translate3d(' + y + 'px, 0, 0)'; 109 | 110 | return function () { 111 | greenBall.style.transform = translate; 112 | }; 113 | }; 114 | 115 | ``` 116 | 117 | ### Timeline 118 | 119 | Timelines allow us to compose animations together to truly make something special. They behave like trees (Like the DOM) and have children, which are `clip` functions and other timelines. They have an immutable API so appending a child will return a completely new `timeline` instance. 120 | 121 | Adding our greenBallClip to a timeline: 122 | 123 | ``` 124 | const myTimeline = createTimeline(); 125 | 126 | myTimeline.appendChild(greenBallClip, { 127 | offset: 0, 128 | duration: 10, 129 | fill: 'both' 130 | }); 131 | 132 | ``` 133 | 134 | You can also 'glue' two `clip` functions together on a timeline. Say, for instance, that you wanted a red ball to move after the green ball was done. You could manually define an offset for the redball relative to the green ball, but that would be fragile. You can instead use the `chainChild` method on timeline to chain `clip` functions. This method takes three arguments: a `clip` function that already exists on the timeline. Another `clip` argument that will be chained to the end of the first argument. An `offset` object that defines time offsets -relative- to the first `clip`: 135 | 136 | ``` 137 | myTimeline.chainChild(greenBallClip, redBallClip, { 138 | offset: 0, 139 | duration: 10 140 | }); 141 | ``` 142 | 143 | In addition to `offset`, `duration`, you should also specify `fill` mode when appending a `clip` to a timeline. `fill` describes what happens when the timeline time is out of bounds beyond the `offset` and `duration`. It has 4 values: 144 | - `none` - Nothing happens, clip is skipped entirely. 145 | - `backwards` - When time is before clip, clip should be updated with time 0 146 | - `forwards` - When time is after clip, clip should be updated with time 1 147 | - `both` - Default. Clip will always be updated regardless of the time 148 | 149 | --------------------------------------------------------------------------------