├── .editorconfig ├── .gitignore ├── README.md ├── index.js ├── package.json └── test └── spec └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{*.json,*.yml}] 16 | indent_size = 2 17 | indent_style = space 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime* 2 | .DS_Store 3 | .grunt 4 | .sass-cache 5 | _SpecRunner.html 6 | build 7 | dist 8 | node_modules 9 | npm-debug.log 10 | tag.prop 11 | tmp/ 12 | coverage/ 13 | docs/ 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![100% test coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg) 2 | 3 | # timeout-raf 4 | > Bare-bones animation-friendly cancelable timeouts via requestAnimationFrame 5 | 6 | Provides the familiarity of `setTimeout`, but via `requestAnimationFrame`. 7 | Deferred callbacks can be fired immediately or killed. 8 | See "usage" below. 9 | 10 | 11 | #### Important to consider 12 | The `requestAnimationFrame` approach means your callbacks will likely not fire exactly when you originally instructed. 13 | If things are running at a smooth 60fps, callbacks should generally fire within 16ms of being told to. 14 | Run the tests to see typical results. 15 | 16 | 17 | ## Installation 18 | Install via npm. 19 | 20 | ```sh 21 | $ npm i timeout-raf 22 | ``` 23 | 24 | 25 | ## Usage 26 | Require the module - it exposes a single factory function (so there's no need to instantiate instances). 27 | Call as you would `window.setTimeout`, passing a callback and duration. 28 | 29 | ```js 30 | var timeout = require('timeout-raf'); 31 | 32 | // typical usage; console.log is called after 1s 33 | timeout(function () { 34 | console.log('1 second later...'); 35 | }, 1000); 36 | ``` 37 | 38 | 39 | ### Passing context as a parameter 40 | You can pass context via an optional third parameter, allowing you to define the context of the callback. 41 | 42 | ```js 43 | var awesomeObject = {awesome: 'clearly'}; 44 | 45 | // logs `clearly` after (about) 1 second 46 | timeout(function () { 47 | console.log(this.awesome); 48 | }, 1000, awesomeObject); 49 | ``` 50 | 51 | 52 | ### Passing context normally 53 | You can also pass context per usual by binding the callback to any object. 54 | 55 | ```js 56 | var awesomeObject = {awesome: 'clearly'}; 57 | 58 | // also logs `clearly` after (about) 1 second 59 | timeout(function () { 60 | console.log(this.awesome); 61 | }.bind(awesomeObject), 1000); 62 | ``` 63 | 64 | 65 | ### Killing a timeout 66 | Keep a reference to the timeout and then call its `kill()` method to prevent it from ever firing its callback. 67 | 68 | ```js 69 | // never writes to the console... 70 | var to = timeout(function () { 71 | console.log('I am never heard from again'); 72 | }, 1000); 73 | 74 | to.kill(); 75 | ``` 76 | 77 | 78 | ### Impatient-friendly 79 | You can tell a timeout to fire its callback at any time earlier than originally requested. 80 | Keep a reference to the timeout and call its `fire` method if you get antsy. 81 | The callback will fire immediately, and never again. 82 | 83 | ```js 84 | // fired after 1 second and never again 85 | var to = timeout(function () { 86 | console.log('I could only wait a second!'); 87 | }, 2000); 88 | 89 | timeout(function () { 90 | to.fire(); 91 | }, 1000); 92 | ``` 93 | 94 | 95 | ## Tests 96 | 97 | Tests can be run in-browser via `npm test`. 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Bare-bones animation-friendly cancelable timeouts via requestAnimationFrame 5 | * @module timeoutRaf 6 | * @return {timeoutRaf} - Instance factory 7 | */ 8 | module.exports = (function () { 9 | /** 10 | * @class Ticker 11 | * @classdesc Simple object to keep a clean self-reference to itself 12 | * 13 | * @param {Function} fn - Function to be called upon timeout 14 | * @param {Number} del - Timeout duration (delay) 15 | * @param {Object} ctx - Context to run fn within 16 | */ 17 | function Ticker(fn, del, ctx) { 18 | this.start = Date.now(); 19 | this.context = ctx; 20 | this.delay = del; 21 | this.func = fn; 22 | 23 | this._tick = this._tick.bind(this); 24 | this.kill = this.kill.bind(this); 25 | 26 | this.id = requestAnimationFrame(this._tick); 27 | } 28 | 29 | /** 30 | * Kills the timeout without firing this.func 31 | * @return {Null} 32 | */ 33 | Ticker.prototype.kill = function () { 34 | cancelAnimationFrame(this.id); 35 | 36 | this.context = null; 37 | this.func = null; 38 | this.id = null; 39 | }; 40 | 41 | /** 42 | * Calls this.func immediately, 43 | * @return {Null} 44 | */ 45 | Ticker.prototype.fire = function () { 46 | this.func.call(this.context); 47 | this.kill(); 48 | }; 49 | 50 | /** 51 | * @private _tick 52 | * Internal tick method to ascertain updated request ID 53 | * @return {Null} 54 | */ 55 | Ticker.prototype._tick = function () { 56 | if ((Date.now() - this.start) >= this.delay) { 57 | this.func.call(this.context); 58 | this.kill(); 59 | } else { 60 | this.id = requestAnimationFrame(this._tick); 61 | } 62 | }; 63 | 64 | /** 65 | * @function 66 | * requestAnimationFrame-powered timeout functionality 67 | * 68 | * @param {Function} fn - Function to be called upon timeout 69 | * @param {Number} del - Timeout duration (delay) 70 | * @param {Object} ctx - Context to run fn within 71 | * @return {Object} Ticker 72 | */ 73 | return function (fn, del, ctx) { 74 | return new Ticker(fn, del, ctx); 75 | }; 76 | })(); 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timeout-raf", 3 | "description": "Bare-bones animation-friendly cancelable timeouts via requestAnimationFrame", 4 | "author": "Tyler Peterson ", 5 | "version": "1.1.4", 6 | "main": "./index.js", 7 | "scripts": { 8 | "coverage": "mkdir -p coverage && browserify test/spec/test.js -p tape-istanbul/plugin | tape-run | tape-istanbul --output coverage/coverage.json && npm run report", 9 | "report": "istanbul report --root coverage lcov && open coverage/lcov-report/index.html", 10 | "demo": "mkdir -p tmp/ && browserify test/examples/js/demo.js -o tmp/demo.js", 11 | "test": "browserify test/spec/test.js | browser-run | tap-spec", 12 | "docs": "jsdoc index.js -p -d docs/", 13 | "lint": "xo ./index.js || true && xo ./test/spec/test.js || true" 14 | }, 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "browser-run": "^4.0.2", 18 | "browserify": "^14.3.0", 19 | "browserify-istanbul": "^2.0.0", 20 | "istanbul": "^0.4.5", 21 | "jsdoc": "^3.4.3", 22 | "tap-spec": "^4.1.1", 23 | "tape": "^4.8.0", 24 | "tape-istanbul": "^1.1.1", 25 | "tape-run": "^3.0.0", 26 | "xo": "latest" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/tylerjpeterson/timeout-raf/issues" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/tylerjpeterson/timeout-raf" 34 | }, 35 | "homepage": "https://github.com/tylerjpeterson/timeout-raf", 36 | "contributors": [ 37 | "" 38 | ], 39 | "xo": { 40 | "globals": [ 41 | "requestAnimationFrame", 42 | "cancelAnimationFrame", 43 | "document", 44 | "module", 45 | "require", 46 | "window" 47 | ] 48 | }, 49 | "keywords": [ 50 | "requestAnimationFrame", 51 | "timeout", 52 | "raf", 53 | "setTimeout", 54 | "clearTimeout" 55 | ], 56 | "engines": { 57 | "node": ">=0.10.3" 58 | }, 59 | "license": "MIT" 60 | } 61 | -------------------------------------------------------------------------------- /test/spec/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const timeoutRaf = require('./../..'); 5 | 6 | /** 7 | * TimeoutRaf tape tests 8 | * @see https://github.com/substack/tape 9 | */ 10 | test('should be a function', assert => { 11 | assert.equal(typeof timeoutRaf, 'function'); 12 | assert.end(); 13 | }); 14 | 15 | test('should fire when expected', assert => { 16 | const t = Date.now(); 17 | const e = 0; 18 | 19 | timeoutRaf(() => { 20 | const diff = e - t < 100; 21 | assert.equal(diff, true); 22 | assert.end(); 23 | }, 1000); 24 | }); 25 | 26 | test('should keep context', assert => { 27 | const ctx = { 28 | testprop: 'exists' 29 | }; 30 | 31 | timeoutRaf(function () { 32 | assert.equal(this.testprop, 'exists'); 33 | assert.end(); 34 | }, 1000, ctx); 35 | }); 36 | 37 | test('should keep traditional context', assert => { 38 | const ctx = {testprop: 'exists'}; 39 | 40 | timeoutRaf(function () { 41 | assert.equal(this.testprop, 'exists'); 42 | assert.end(); 43 | }.bind(ctx), 1000); 44 | }); 45 | 46 | test('should be killable', assert => { 47 | let v = 1; 48 | 49 | const timeout1 = timeoutRaf(() => { 50 | v = 2; 51 | }, 1000); 52 | 53 | timeoutRaf(() => { 54 | timeout1.kill(); 55 | }, 500); 56 | 57 | timeoutRaf(() => { 58 | assert.equal(v, 1); 59 | assert.end(); 60 | }, 1500); 61 | }); 62 | 63 | test('should fire immediately when told to', assert => { 64 | let v = 1; 65 | 66 | const timeout1 = timeoutRaf(() => { 67 | v = 2; 68 | }, 1500); 69 | 70 | timeoutRaf(() => { 71 | timeout1.fire(); 72 | assert.equal(v, 2); 73 | assert.end(); 74 | 75 | setTimeout(() => { 76 | window.close(); 77 | }, 1000); 78 | }, 1000); 79 | }); 80 | --------------------------------------------------------------------------------