├── LICENSE ├── README.md ├── package.json ├── src ├── scheduler.js ├── ticker.js └── timaline.js └── tonicExample.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kevin Boudot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timaline 2 | 3 | Timaline is a requestAnimationFrame based tasks scheduler. 4 | 5 | * [To Do](#to-do) 6 | * [Installation](#installation) 7 | * [Features](#features) 8 | * [Browser compatibility](#browser-compatibility) 9 | * [Documentation](#documentation) 10 | * [Use](#use) 11 | * [Options](#options) 12 | * [Methods](#methods) 13 | * [Informations](#informations) 14 | * [Examples](#examples) 15 | * [Simple task](#simple-task) 16 | * [Chained tasks](#chained-tasks) 17 | * [Shortcuts use](#shortcuts-use) 18 | * [Update manually](#update-manually) 19 | * [Destroy on the fly](#destroy-on-the-fly) 20 | 21 | ### To Do 22 | 23 | - [x] Reduce RAF Delta impact by "prev or next" frame 24 | - [x] Speed 25 | - [x] Repeat 26 | - [x] User custom loop (manually update) 27 | - [x] Disable RAF when web page is not active 28 | - [ ] Playback control 29 | - [ ] RAF Delta impact reducing by average calculation 30 | - [ ] Reduce RAF Delta aftereffect 31 | 32 | ### Installation 33 | 34 | [![NPM](https://nodei.co/npm/Timaline.png?mini=true)](https://www.npmjs.com/package/timaline>) 35 | 36 | ### Demo 37 | 38 | [Test timaline in your browser](https://tonicdev.com/npm/timaline) 39 | 40 | ### Features 41 | 42 | - Set callback functions 43 | - Wait time in millisecond 44 | - AddClass to a node 45 | - RemoveClass to a node 46 | - Destroy everything 47 | - Control speed 48 | - Control repeat 49 | - Update manually 50 | - Pause when tab is not visible 51 | 52 | ### Browser compatibility 53 | 54 | IE 10+ 55 | 56 | ## Documentation 57 | 58 | ### Use 59 | 60 | ```js 61 | var Timaline = require('Timaline'); 62 | ```` 63 | 64 | ### Options 65 | 66 | You can pass 3 options to constructor, and you can combine them : 67 | 68 | ```js 69 | 70 | var timeline = new Timaline({ 71 | loop: false, // default : true 72 | speed: 0.5, // default : 1 73 | repeat: 3 // default : 0 74 | }); 75 | ``` 76 | 77 | ### Methods 78 | 79 | #### .wait(time) 80 | 81 | Wait time. 82 | 83 | ##### properties 84 | 85 | ###### `time` (`Integer`) 86 | 87 | Time to wait in millisecond 88 | 89 | #### .set(callback) 90 | 91 | Call your function. 92 | 93 | ##### properties 94 | 95 | ###### `callback` (`Function`) 96 | 97 | The fonction you need to call 98 | 99 | #### .addClass(el, classname) 100 | 101 | AddClass shortcut. 102 | 103 | ##### properties 104 | 105 | ###### `el` (`Node`) 106 | 107 | Your dom element 108 | 109 | ###### `classname` (`String`) 110 | 111 | Your class name 112 | 113 | #### .removeClass(el, classname) 114 | 115 | RemoveClass shortcut. 116 | 117 | ##### properties 118 | 119 | ###### `el` (`Node`) 120 | 121 | Your dom element 122 | 123 | ###### `classname` (`String`) 124 | 125 | Your class name 126 | 127 | #### .destroy() 128 | 129 | Roughly destroy your timeline. 130 | 131 | ###### `speed` (`Float`) 132 | 133 | Speed will control entire timeline (default: 1) 134 | 135 | ###### `repeat` (`Float`) 136 | 137 | Repeat your timeline as many times as you like (default: 0) 138 | 139 | ### Informations 140 | 141 | When you set a callback, infos are available : 142 | 143 | ```js 144 | var delay = new Timaline(); 145 | 146 | delay 147 | .wait(200) 148 | .set(function(infos){ 149 | console.log(infos); 150 | }); 151 | ``` 152 | 153 | ```js 154 | var infos = { 155 | index: 2, // The index of your task 156 | keyframe: { 157 | forecast: 3000, // Time forecast by set wait time 158 | real: 2996, // Real time (with RAF Delta) 159 | shift: -4 // Shift between both 160 | } 161 | }; 162 | ``` 163 | 164 | 165 | ### Examples 166 | 167 | #### Simple task 168 | 169 | This example is a simple delayed task, similar as a simple window.setTimeout : 170 | 171 | ```js 172 | var delay = new Timaline(); 173 | 174 | delay 175 | .wait(200) 176 | .set(function(infos){ 177 | console.log(infos); 178 | }); 179 | ``` 180 | 181 | #### Chained tasks 182 | 183 | This example is a chained tasks : 184 | 185 | ```js 186 | var timeline = new Timaline(); 187 | 188 | timeline 189 | .wait(1000) 190 | .set(function(infos){ 191 | console.log('task index :' + infos.index ); 192 | }) 193 | .wait(2000) 194 | .set(function(infos){ 195 | console.log('task index :' + infos.index ); 196 | }); 197 | ``` 198 | 199 | #### Shortcuts use 200 | 201 | If you need, you can use shortcuts methods during your timeline : 202 | (consider that the used classes exist in your stylesheet) 203 | 204 | ```js 205 | var $header = document.getElementById('header'); 206 | var $container = document.getElementById('container'); 207 | var $footer = document.getElementById('footer'); 208 | 209 | var hidePage = new Timaline(); 210 | 211 | hidePage 212 | .set(function(infos){ 213 | console.log('This page will disappear in a while.'); 214 | }) 215 | .wait(1000) 216 | .addClass($header, 'fadeOut') 217 | .wait(800) 218 | .removeClass($container, 'shown') 219 | .wait(1000) 220 | .addClass($footer, 'scaleOut') 221 | .set(function(infos){ 222 | console.log('That\'s all folks!'); 223 | }); 224 | ``` 225 | 226 | #### Update manually 227 | 228 | This example show how to update Timaline with your own loop : 229 | 230 | ```js 231 | var delay = new Timaline({ 232 | raf: false 233 | }); 234 | 235 | function loop( timestamp ) { 236 | delay.update( timestamp ); 237 | requestAnimationFrame( loop ); 238 | } 239 | 240 | requestAnimationFrame( loop ); 241 | 242 | delay 243 | .wait(200) 244 | .set(function(infos){ 245 | console.log(infos); 246 | }); 247 | ``` 248 | 249 | #### Destroy on the fly 250 | 251 | A timeline is destructs when it is finished, but sometimes you need to roughly destroy it before the end : 252 | 253 | ```js 254 | // Create a new timeline 255 | 256 | var timeline = new Timaline(); 257 | 258 | timeline 259 | .wait(3000) 260 | .set(function(infos){ 261 | console.log('I\'ll never be call.'); 262 | }) 263 | .wait(1000) 264 | .set(function(infos){ 265 | console.log('me too.'); 266 | }); 267 | 268 | // ..and create a new one that will remove 269 | // the first one before its end 270 | 271 | var destroy = new Timaline(); 272 | 273 | destroy 274 | .wait(1000) 275 | .set(function(infos){ 276 | timeline.destroy(); 277 | }); 278 | ``` 279 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timaline", 3 | "description": "timaline is a requestAnimationFrame based tasks scheduler.", 4 | "version": "0.0.22", 5 | "author": "Kevin Boudot (http://www.kevinboudot.me/)", 6 | "license": "MIT", 7 | "main": "src/timaline.js", 8 | "tonicExampleFilename": "tonicExample.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/kevinboudot/timaline" 12 | }, 13 | "keywords": [ 14 | "timing", 15 | "timer", 16 | "setTimeout", 17 | "timeout", 18 | "tasks", 19 | "timeline", 20 | "RAF", 21 | "RequestAnimationFrame", 22 | "schedule", 23 | "speed", 24 | "repeat" 25 | ], 26 | "dependencies": { 27 | "raf": "^3.4.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/scheduler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | 4 | // Require Ticker 5 | 6 | const Ticker = require( './ticker.js' ) 7 | 8 | 9 | // Create Scheduler 10 | 11 | const Scheduler = function( options ) { 12 | this.options = options 13 | this.infos = {} 14 | this.isProcessing = false 15 | this.tasks = { 16 | queue: [], 17 | check: [] 18 | } 19 | this.taskCount = 0 20 | this.queueCount = 0 21 | this.bound = null 22 | Ticker.setOptions( this.options ) 23 | } 24 | 25 | 26 | // Public 27 | 28 | 29 | /** 30 | * add() add a function to the tasks queue 31 | * 32 | * @param {Function} callback 33 | * @return {Integer} delay (time in millisecond) 34 | */ 35 | 36 | Scheduler.prototype.add = function( callback, delay ) { 37 | 38 | this.tasks.queue.push( { 39 | callback: callback, 40 | delay: delay, 41 | index: this.taskCount++ 42 | } ) 43 | 44 | if ( !this.isProcessing ) { 45 | this.bound = this._tick.bind( this ) 46 | Ticker.start( this.bound ) 47 | this.isProcessing = true 48 | } 49 | 50 | } 51 | 52 | /** 53 | * update() update with custom loop 54 | */ 55 | 56 | Scheduler.prototype.update = function( time ) { 57 | Ticker.update( time ) 58 | } 59 | 60 | 61 | /** 62 | * destroy() remove all scheduled tasks 63 | */ 64 | 65 | Scheduler.prototype.destroy = function() { 66 | 67 | this.tasks.queue = [] 68 | 69 | if ( this.isProcessing ) { 70 | Ticker.end( this.bound ) 71 | this.isProcessing = false 72 | } 73 | 74 | } 75 | 76 | 77 | // Private 78 | 79 | /** 80 | * _onComplete() is called when timeline is finished 81 | * 82 | */ 83 | 84 | Scheduler.prototype._onComplete = function() { 85 | 86 | if ( this.options.repeat !== this.queueCount ) { 87 | 88 | let i = this.tasks.check.length 89 | 90 | while ( i-- ) { 91 | 92 | const task = this.tasks.check[ i ] 93 | task.startTime = null 94 | this.tasks.queue.push( task ) 95 | } 96 | 97 | this.queueCount++ 98 | 99 | } else { 100 | this.destroy() 101 | } 102 | 103 | } 104 | 105 | 106 | 107 | /** 108 | * _tick() is the RAF Handler 109 | */ 110 | 111 | Scheduler.prototype._tick = function( currentTime ) { 112 | 113 | // Save Timing infos 114 | 115 | this._saveInfos( currentTime ) 116 | 117 | // Loop queue 118 | 119 | if ( this.infos.delta ) { 120 | this._loopQueue( currentTime ) 121 | } 122 | 123 | } 124 | 125 | 126 | /** 127 | * _saveInfos() save RAF Timing infos 128 | * 129 | * @return {Integer} currentTime (time in millisecond) 130 | */ 131 | 132 | Scheduler.prototype._saveInfos = function( currentTime ) { 133 | 134 | if ( !this.infos.last ) { 135 | this.infos.last = currentTime 136 | } else { 137 | this.infos.delta = currentTime - this.infos.last 138 | this.infos.last = currentTime 139 | this.infos.fps = 1 / ( this.infos.delta / 1000 ) 140 | } 141 | 142 | } 143 | 144 | 145 | /** 146 | * _loopQueue() loop through queue to check if task 147 | * need to be processed 148 | * 149 | * @return {Integer} currentTime (time in millisecond) 150 | */ 151 | 152 | Scheduler.prototype._loopQueue = function( currentTime ) { 153 | 154 | let i = this.tasks.queue.length 155 | 156 | while ( i-- ) { 157 | 158 | // Get current task 159 | 160 | const task = this.tasks.queue[ i ] 161 | 162 | if ( task ) { 163 | 164 | // Save task start time if not exist 165 | 166 | if ( !task.startTime ) { 167 | task.startTime = currentTime 168 | } 169 | 170 | // Predict task end time by delay 171 | 172 | task.endTime = task.startTime + task.delay 173 | 174 | // get adjusted endTime because of delta difference 175 | 176 | task.adjustedTime = ( task.endTime - ( this.infos.delta / 2 ) ) 177 | 178 | // If Delay Time is elapsed 179 | 180 | if ( currentTime >= task.adjustedTime ) { 181 | 182 | // Save Timeshift after adjustment 183 | 184 | task.timeShift = currentTime - task.endTime 185 | 186 | this._processTask( task ) 187 | 188 | } 189 | 190 | } 191 | 192 | } 193 | 194 | } 195 | 196 | 197 | /** 198 | * _prepareResponse() prepare callback response 199 | * 200 | * @return {Object} task 201 | */ 202 | 203 | Scheduler.prototype._prepareResponse = function( task ) { 204 | 205 | const response = { 206 | keyframe: { 207 | forecast: task.delay, 208 | real: task.delay + task.timeShift, 209 | shift: task.timeShift 210 | }, 211 | index: task.index 212 | } 213 | 214 | return response 215 | 216 | } 217 | 218 | 219 | /** 220 | * _processTask() process task and remove it to queue 221 | * 222 | * @return {Object} task 223 | */ 224 | 225 | Scheduler.prototype._processTask = function( task ) { 226 | 227 | const response = this._prepareResponse( task ) 228 | 229 | // Launch Callback 230 | 231 | task.callback( response ) 232 | 233 | // Add task to checked task 234 | 235 | this.tasks.check.push( task ) 236 | 237 | // Remove this processed task from tasks queue 238 | 239 | this.tasks.queue.splice( this.tasks.queue.indexOf( task ), 1 ) 240 | 241 | // No more tasks in queue ? 242 | 243 | if ( !this.tasks.queue.length ) { 244 | this._onComplete() 245 | } 246 | 247 | } 248 | 249 | 250 | // Wrap as a module 251 | 252 | module.exports = Scheduler 253 | -------------------------------------------------------------------------------- /src/ticker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | 4 | // Require RAF 5 | 6 | const Raf = require( 'raf' ) 7 | 8 | 9 | // Create Ticker 10 | 11 | const Ticker = function() { 12 | this.instance = null 13 | this.lostTime = 0 14 | this.blurTime = 0 15 | this.tickers = [] 16 | this.raf = false 17 | this._observe() 18 | } 19 | 20 | 21 | // Public 22 | 23 | /** 24 | * setOptions() 25 | * 26 | * @return {Function} ticker 27 | */ 28 | 29 | Ticker.prototype.setOptions = function( options ) { 30 | this.raf = options.raf 31 | } 32 | 33 | 34 | /** 35 | * start() add given ticker to the RAF instance 36 | * 37 | * @return {Function} ticker 38 | */ 39 | 40 | Ticker.prototype.start = function( ticker ) { 41 | 42 | if ( !this.tickers.length ) { 43 | this._process() 44 | } 45 | 46 | this.tickers.push( ticker ) 47 | 48 | } 49 | 50 | 51 | /** 52 | * end() remove given ticker from the RAF instance 53 | * 54 | * @return {Function} ticker 55 | */ 56 | 57 | Ticker.prototype.end = function( ticker ) { 58 | 59 | this.tickers.splice( this.tickers.indexOf( ticker ), 1 ) 60 | 61 | if ( this.raf ) { 62 | 63 | if ( !this.tickers.length ) { 64 | Raf.cancel( this.instance ) 65 | } 66 | 67 | } 68 | 69 | } 70 | 71 | 72 | /** 73 | * update() update with custom loop 74 | * 75 | */ 76 | 77 | Ticker.prototype.update = function( time ) { 78 | 79 | this._process( time ) 80 | 81 | } 82 | 83 | 84 | // Private 85 | 86 | /** 87 | * _observe() to check document visibility state 88 | */ 89 | 90 | Ticker.prototype._observe = function( currentTime ) { 91 | document.addEventListener( 'visibilitychange', function() { 92 | if ( document.visibilityState === 'hidden' ) { 93 | this.blurTime = Date.now() 94 | } 95 | if ( document.visibilityState === 'visible' ) { 96 | const now = Date.now() 97 | this.lostTime += ( now - this.blurTime ) 98 | this.blurTime = 0 99 | } 100 | } ) 101 | } 102 | 103 | 104 | /** 105 | * _process() is the RAF Handler 106 | */ 107 | 108 | Ticker.prototype._process = function( currentTime ) { 109 | 110 | if ( this.raf ) { 111 | this.instance = Raf( this._process.bind( this ) ) 112 | } 113 | 114 | let i = this.tickers.length 115 | 116 | while ( i-- ) { 117 | this.tickers[ i ]( currentTime - this.lostTime ) 118 | } 119 | 120 | } 121 | 122 | 123 | // Wrap as a module 124 | 125 | module.exports = new Ticker() 126 | -------------------------------------------------------------------------------- /src/timaline.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | 4 | // Require Scheduler 5 | 6 | const Scheduler = require( './scheduler.js' ) 7 | 8 | 9 | // Create Timaline 10 | 11 | const Timaline = function( options ) { 12 | 13 | options = options ? options : {} 14 | 15 | this.options = { 16 | raf: options.raf !== false, 17 | speed: options.speed ? options.speed : 1, 18 | repeat: options.repeat ? options.repeat : 0 19 | } 20 | 21 | this.delay = 0 22 | this.scheduler = new Scheduler( this.options ) 23 | } 24 | 25 | /** 26 | * set() call function 27 | * 28 | * @param {function} callback 29 | */ 30 | 31 | Timaline.prototype.set = function( callback ) { 32 | this.scheduler.add( callback, this.delay ) 33 | return this 34 | } 35 | 36 | 37 | /** 38 | * wait() add delay 39 | * 40 | * @param {integer} delay 41 | */ 42 | 43 | Timaline.prototype.wait = function( delay ) { 44 | this.delay += ( delay * this.options.speed ) 45 | return this 46 | } 47 | 48 | 49 | /** 50 | * addClass() add class to a given node 51 | * 52 | * @param {node} el 53 | * @param {string} className 54 | */ 55 | 56 | Timaline.prototype.addClass = function( el, className ) { 57 | this.set( function() { 58 | const classList = el.classList || el[ 0 ].classList 59 | classList.add( className ) 60 | } ) 61 | return this 62 | } 63 | 64 | 65 | /** 66 | * removeClass() remove class to a given node 67 | * 68 | * @param {node} el 69 | * @param {string} className 70 | */ 71 | 72 | Timaline.prototype.removeClass = function( el, className ) { 73 | this.set( function() { 74 | const classList = el.classList || el[ 0 ].classList 75 | classList.remove( className ) 76 | } ) 77 | return this 78 | } 79 | 80 | 81 | /** 82 | * update() update with custom loop 83 | */ 84 | 85 | Timaline.prototype.update = function( time ) { 86 | this.scheduler.update( time ) 87 | } 88 | 89 | 90 | /** 91 | * destroy() cancel remove all scheduled tasks 92 | */ 93 | 94 | Timaline.prototype.destroy = function() { 95 | this.scheduler.destroy() 96 | } 97 | 98 | 99 | // Wrap as a module 100 | 101 | module.exports = Timaline 102 | -------------------------------------------------------------------------------- /tonicExample.js: -------------------------------------------------------------------------------- 1 | const Timaline = require( 'timaline' ) 2 | const timeline = new Timaline() 3 | 4 | timeline 5 | .wait( 500 ) 6 | .set( function( infos ) { 7 | console.log( infos.keyframe.real + ' ≈ ' + infos.keyframe.forecast ) 8 | } ) 9 | .wait( 1000 ) 10 | .set( function( infos ) { 11 | console.log( infos.keyframe.real + ' ≈ ' + infos.keyframe.forecast ) 12 | } ) 13 | --------------------------------------------------------------------------------