Permalink
Browse files

animitters build working in node, amd, browser global

  • Loading branch information...
0 parents commit 89fddeb1a6a7bec2e0cc2d0605f095e78ba17472 @hapticdata committed Nov 18, 2012
Showing with 1,237 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +13 −0 Makefile
  3. +42 −0 README.md
  4. +472 −0 animitter.js
  5. +11 −0 animitter.min.js
  6. +194 −0 lib/animitter.js
  7. +215 −0 lib/events.js
  8. +26 −0 lib/utils.js
  9. +21 −0 package.json
  10. +91 −0 test/animitter-node.js
  11. +106 −0 test/animitter-requirejs.js
  12. +14 −0 tools/_pre.js
  13. BIN tools/compiler.jar
  14. +29 −0 tools/concat.js
3 .gitignore
@@ -0,0 +1,3 @@
+node_modules
+.DS_STORE
+.jshintrc
13 Makefile
@@ -0,0 +1,13 @@
+REPORTER ?= list
+
+all: test build
+
+build-lib:
+ node tools/concat.js
+build-min:
+ java -jar tools/compiler.jar --js=animitter.js --js_output_file=animitter.min.js --compilation_level=ADVANCED_OPTIMIZATIONS --output_wrapper="(function(){%output%}());"
+build: build-lib build-min
+test:
+ mocha --reporter $(REPORTER) test/*
+
+.PHONY: test
42 README.md
@@ -0,0 +1,42 @@
+#Animitter
+##Event-based animation in browser and in Node
+_by [Kyle Phillips](http://haptic-data.com)_
+
+Animitter is a combination of an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) and a feature-filled animation loop.
+It uses `requestAnimationFrame` with an automatic fallback to `setTimeout` and offers several
+additional features, such as asynchronous execution of the next frame.
+
+#Installation:
+##Node.js:
+
+ npm install animitter
+
+##Browser
+copy `./animitter.js` or `./animitter.min.js` into your project
+
+ <script src="js/animitter.js"></script>
+or with **require.js/amd**:
+
+ require(['animitter'], function( animitter ){});
+
+#Basic use:
+##start a new animation loop immediately
+ var loop = animitter(function(){
+ //do something
+ }).start();
+##start a new animation loop, listen to its built-in events
+ var loop = animitter(function(){
+ //do something
+ });
+
+ loop.on('update', function(){
+ if( this.frameCount > 99 ){
+ this.complete();
+ }
+ });
+
+ loop.on('complete', function(){
+ //done
+ });
+
+ loop.start();
472 animitter.js
@@ -0,0 +1,472 @@
+//###Env: Browser + Node
+//*Author:* [Kyle Phillips](http://hapticdata.com)
+//*published under the MIT license*
+
+//animator factory for creating [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/DOM/window.requestAnimationFrame) callbacks
+//and simplifying their cancellation.
+//Animations can use `start()` and `stop()` repeatedly to pause, and `complete()` once to finish.
+
+//**_Basic use:_**
+//
+// animitter(function(){
+// //do this every time
+// if(Math.random() > 0.9) this.complete();
+// });
+
+var animitter = (function(){
+ //utils.js
+ var utils = (function(){
+ var module = {}, exports = {};
+ module.exports = exports;
+ var env, isArray, inherits;
+ //find which environment we are in
+ if( typeof require === 'function' ){
+ env = (typeof define === 'function' && define.amd) ? 'requirejs' : 'node';
+ } else {
+ env = "browser";
+ }
+
+ isArray = Array.isArray || function(a){
+ return a.toString() == '[object Array]';
+ };
+ //same as Node.js' inherits
+ inherits = function(ctor, superCtor) {
+ ctor.super_ = superCtor;
+ ctor.prototype = Object.create(superCtor.prototype, {
+ constructor: {
+ value: ctor,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+ };
+ exports.env = env;
+ exports.isArray = isArray;
+ exports.inherits = inherits;
+ return module.exports;
+ }());
+ //events.js
+ var events = (function(){
+ var module = {}, exports = {};
+ module.exports = exports;
+
+ //This is the Node.js [EventEmitter](https://github.com/joyent/node/blob/master/lib/events.js)
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ var isArray = Array.isArray || utils.isArray;
+
+ function EventEmitter() {}
+ exports.EventEmitter = EventEmitter;
+
+ // By default EventEmitters will print a warning if more than
+ // 10 listeners are added to it. This is a useful default which
+ // helps finding memory leaks.
+ //
+ // Obviously not all Emitters should be limited to 10. This function allows
+ // that to be increased. Set to zero for unlimited.
+ var defaultMaxListeners = 10;
+ EventEmitter.prototype.setMaxListeners = function(n) {
+ if (!this._events) this._events = {};
+ this._maxListeners = n;
+ };
+
+ /** @expose */
+ EventEmitter.prototype.emit = function() {
+ var type = arguments[0], l, args, i;
+ // If there is no 'error' event listener then throw.
+ if (type === 'error') {
+ if (!this._events || !this._events.error ||
+ (isArray(this._events.error) && !this._events.error.length))
+ {
+ if (arguments[1] instanceof Error) {
+ throw arguments[1]; // Unhandled 'error' event
+ } else {
+ throw new Error("Uncaught, unspecified 'error' event.");
+ }
+ }
+ }
+
+ if (!this._events) return false;
+ var handler = this._events[type];
+ if (!handler) return false;
+
+ if (typeof handler == 'function') {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ l = arguments.length;
+ args = new Array(l - 1);
+ for (i = 1; i < l; i++) args[i - 1] = arguments[i];
+ handler.apply(this, args);
+ }
+ return true;
+
+ } else if (isArray(handler)) {
+ l = arguments.length;
+ args = new Array(l - 1);
+ for (i = 1; i < l; i++) args[i - 1] = arguments[i];
+
+ var listeners = handler.slice();
+ for (i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].apply(this, args);
+ }
+ return true;
+
+ } else {
+ return false;
+ }
+ };
+ /** @expose */
+ EventEmitter.prototype.addListener = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('addListener only takes instances of Function');
+ }
+
+ if (!this._events) this._events = {};
+
+ // To avoid recursion in the case that type == "newListeners"! Before
+ // adding it to the listeners, first emit "newListeners".
+ this.emit('newListener', type, typeof listener.listener === 'function' ?
+ listener.listener : listener);
+
+ if (!this._events[type]) {
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ } else if (isArray(this._events[type])) {
+
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+
+ } else {
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+
+ }
+
+ // Check for listener leak
+ if (isArray(this._events[type]) && !this._events[type].warned) {
+ var m;
+ if (this._maxListeners !== undefined) {
+ m = this._maxListeners;
+ } else {
+ m = defaultMaxListeners;
+ }
+
+ if (m && m > 0 && this._events[type].length > m) {
+ this._events[type].warned = true;
+ console.error('(node) warning: possible EventEmitter memory ' +
+ 'leak detected. %d listeners added. ' +
+ 'Use emitter.setMaxListeners() to increase limit.',
+ this._events[type].length);
+ console.trace();
+ }
+ }
+
+ return this;
+ };
+ /** @expose **/
+ EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+ /** @expose **/
+ EventEmitter.prototype.once = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('.once only takes instances of Function');
+ }
+
+ var self = this;
+ function g() {
+ self.removeListener(type, g);
+ listener.apply(this, arguments);
+ }
+
+ g.listener = listener;
+ self.on(type, g);
+
+ return this;
+ };
+ /** @expose */
+ EventEmitter.prototype.removeListener = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('removeListener only takes instances of Function');
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (!this._events || !this._events[type]) return this;
+
+ var list = this._events[type];
+
+ if (isArray(list)) {
+ var position = -1;
+ for (var i = 0, length = list.length; i < length; i++) {
+ if (list[i] === listener ||
+ (list[i].listener && list[i].listener === listener))
+ {
+ position = i;
+ break;
+ }
+ }
+
+ if (position < 0) return this;
+ list.splice(position, 1);
+ if (list.length === 0)
+ delete this._events[type];
+ } else if (list === listener || (list.listener && list.listener === listener)) {
+ delete this._events[type];
+ }
+
+ return this;
+ };
+ /** @expose */
+ EventEmitter.prototype.removeAllListeners = function(type) {
+ if (arguments.length === 0) {
+ this._events = {};
+ return this;
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (type && this._events && this._events[type]) this._events[type] = null;
+ return this;
+ };
+ /** @expose */
+ EventEmitter.prototype.listeners = function(type) {
+ if (!this._events) this._events = {};
+ if (!this._events[type]) this._events[type] = [];
+ if (!isArray(this._events[type])) {
+ this._events[type] = [this._events[type]];
+ }
+ return this._events[type];
+ };
+ return module.exports;
+ }());
+ //animitter.js
+ var animitter = (function(){
+ var module = {}, exports = {};
+ module.exports = exports;
+ var root = typeof window === 'object' ? this : {};
+ var inherits = typeof utils !== 'undefined' && utils.inherits ? utils.inherits : require('util').inherits;
+ var EventEmitter = typeof events !== 'undefined' && events.EventEmitter ? events.EventEmitter : require('events').EventEmitter;
+
+ // [Polyfill]([Erik Moller polyfill](http://paulirish.com/2011/requestanimationframe-for-smart-animating/) for requestAnimationFrame and cancelAnimationFrame
+ (function(){
+ var lastTime = 0;
+ var vendors = ['ms', 'moz', 'webkit', 'o'];
+ for(var x = 0; x < vendors.length && !root.requestAnimationFrame; ++x) {
+ root.requestAnimationFrame = root[vendors[x]+'RequestAnimationFrame'];
+ root.cancelAnimationFrame = root[vendors[x]+'CancelAnimationFrame'] || root[vendors[x]+'CancelRequestAnimationFrame'];
+ }
+
+ if (!root.requestAnimationFrame)
+ root.requestAnimationFrame = function(callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+
+ if (!root.cancelAnimationFrame)
+ root.cancelAnimationFrame = function(id) {
+ clearTimeout(id);
+ };
+ }());
+
+ //##Animator object
+ //###Extends: [events.EventEmitter](./events.html)
+ // *animation.Animator*
+ // has the following properties:
+ //
+ // * **frameCount** *{Number}* current frame # in animation, increases every *'update'*
+ // * **animating** *{Boolean}* true if currently running
+ // * **completed** *{Boolean}* true if **complete()** has been called
+ /** @expose */
+ var Animator = function( opts ){
+ opts = opts || {};
+ /** @expose */
+ this.frameCount = 0;
+ /** @expose */
+ this.animating = false;
+ /** @expose */
+ this.completed = false;
+ /** @expose */
+ this.async = (opts.async === true);
+ };
+
+ var methods = {
+ //####myAnimation.complete()
+ //stops the animation and emits *'complete'*
+ complete: function(){
+ this.animating = false;
+ this.completed = true;
+ this.stop();
+ this.emit('complete', this);
+ return this;
+ },
+ //same as .on('complete',fn)
+ onComplete: function(fn){
+ if(!fn)return;
+ this.on('complete',fn);
+ return this;
+ },
+ //same as .on('update',fn)
+ onUpdate: function(fn){
+ if(!fn)return;
+ this.on('update',fn);
+ return this;
+ },
+ //same as .on('stop',fn)
+ onStop: function(fn){
+ if(!fn)return;
+ this.on('stop',fn);
+ return this;
+ },
+ //####myAnimation.start(function(){})
+ //start the animation
+ //optional **callback** will be attached to .on('update',fn)
+ start: function( callback ){
+ //dont let a second animation start on the same object
+ //use *.on('update',fn)* instead
+ if(this.animating){
+ return this;
+ }
+ //emit **start** once at the beginning
+ this.emit('start', this);
+ exports.running += 1;
+ this.animating = true;
+
+ var self = this;
+ var step = function(){
+ self.frameCount++;
+ if( self.async ){
+ self.emit('update', function(){
+ self.animating = true;
+ drawFrame();
+ }, self);
+ return false;
+ } else {
+ self.emit('update', self);
+ return true;
+ }
+ };
+
+ if(callback !== undefined){
+ this.on('update', callback);
+ }
+
+ var drawFrame = function(){
+ if( step() ){
+ if(self.animating){
+ self.request = root.requestAnimationFrame(drawFrame);
+ } else {
+ root.cancelAnimationFrame(self.request);
+ }
+ }
+ };
+ drawFrame();
+
+ return this;
+ },
+ //####myAnimation.stop()
+ //stops the animation but does not mark as completed
+ stop: function(){
+ this.animating = false;
+ exports.running -= 1;
+ this.emit('stop', this);
+ return this;
+ }
+ };
+
+ inherits(Animator, EventEmitter);
+ for(var method in methods){
+ Animator.prototype[method] = methods[method];
+ }
+
+ //####anim(function(){}).start();
+ //creates an animator but does not start it
+ //**alternately:** anim({ async: true }, function( next ){ next(); })
+ /** @expose */
+ module.exports = exports = function( opts, fn ){
+ if( arguments.length === 1){
+ fn = opts;
+ opts = {};
+ }
+ var _instance = new Animator( opts );
+ if( fn ) _instance.on('update', fn);
+ return _instance;
+ };
+ //log how many animations are running
+ /** @expose */
+ exports.running = 0;
+ //####anim.async(function( next ){ next(); });
+ //creates an asynchronous animation where the next frame is queued
+ //once next(); is invoked
+ /** @expose */
+ exports.async = function( opts, fn ){
+ if( arguments.length === 1){
+ fn = opts;
+ opts = {};
+ }
+ opts.async = true;
+ return exports.create( opts, fn );
+ };
+ //####anim.create( function(){} )
+ //creates a new animator and auto-starts it
+ //this is the most direct way to use it but will result in missing
+ //at least one *.on('update',fn)* applied after
+ /** @expose */
+ exports.create = function( opts, fn ){
+ if( arguments.length === 1 ){
+ fn = opts;
+ opts = {};
+ }
+ var _instance = new Animator(opts);
+ return _instance.start(fn);
+ };
+ //####anim.defer( function(){} ).start()
+ //creates a new animator without calling start
+ //this allows you to attach events without missing any frames
+ /** @expose */
+ exports.defer = exports;
+ /** @expose */
+ exports.Animator = Animator;
+ /** @expose */
+ exports.EventEmitter = EventEmitter;
+
+ if( typeof define === 'function' && define['amd']){
+ define(function(){ return exports; });
+ } else if( typeof window === 'object' ){
+ window['animitter'] = exports;
+ }
+ return module.exports;
+ }());
+ return animitter;
+}());
+animitter.version = "0.1.0";
11 animitter.min.js
@@ -0,0 +1,11 @@
+(function(){var e=!0,g=!1,h,k={},l={};k.exports=l;var n,p;n="function"===typeof require?"function"===typeof define&&define.g?"requirejs":"node":"browser";p=Array.isArray||function(a){return"[object Array]"==a.toString()};l.h=n;l.isArray=p;l.c=function(a,c){a.l=c;a.prototype=Object.create(c.prototype,{constructor:{value:a,enumerable:g,writable:e,configurable:e}})};h=k.exports;var r;function s(){}var t={},u={};t.exports=u;var v=Array.isArray||h.isArray;u.EventEmitter=s;
+s.prototype.emit=function(){var a=arguments[0],c,b;if("error"===a&&(!this.a||!this.a.error||v(this.a.error)&&!this.a.error.length)){if(arguments[1]instanceof Error)throw arguments[1];throw Error("Uncaught, unspecified 'error' event.");}if(!this.a)return g;var d=this.a[a];if(!d)return g;if("function"==typeof d){switch(arguments.length){case 1:d.call(this);break;case 2:d.call(this,arguments[1]);break;case 3:d.call(this,arguments[1],arguments[2]);break;default:a=arguments.length;c=Array(a-1);for(b=1;b<
+a;b++)c[b-1]=arguments[b];d.apply(this,c)}return e}if(v(d)){a=arguments.length;c=Array(a-1);for(b=1;b<a;b++)c[b-1]=arguments[b];d=d.slice();b=0;for(a=d.length;b<a;b++)d[b].apply(this,c);return e}return g};
+s.prototype.addListener=function(a,c){if("function"!==typeof c)throw Error("addListener only takes instances of Function");this.a||(this.a={});this.emit("newListener",a,"function"===typeof c.b?c.b:c);this.a[a]?v(this.a[a])?this.a[a].push(c):this.a[a]=[this.a[a],c]:this.a[a]=c;if(v(this.a[a])&&!this.a[a].f){var b;if((b=void 0!==this.d?this.d:10)&&0<b&&this.a[a].length>b)this.a[a].f=e,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",
+this.a[a].length),console.trace()}return this};s.prototype.on=s.prototype.addListener;s.prototype.once=function(a,c){function b(){d.removeListener(a,b);c.apply(this,arguments)}if("function"!==typeof c)throw Error(".once only takes instances of Function");var d=this;b.b=c;d.on(a,b);return this};
+s.prototype.removeListener=function(a,c){if("function"!==typeof c)throw Error("removeListener only takes instances of Function");if(!this.a||!this.a[a])return this;var b=this.a[a];if(v(b)){for(var d=-1,i=0,q=b.length;i<q;i++)if(b[i]===c||b[i].b&&b[i].b===c){d=i;break}if(0>d)return this;b.splice(d,1);0===b.length&&delete this.a[a]}else(b===c||b.b&&b.b===c)&&delete this.a[a];return this};
+s.prototype.removeAllListeners=function(a){if(0===arguments.length)return this.a={},this;a&&(this.a&&this.a[a])&&(this.a[a]=null);return this};s.prototype.listeners=function(a){this.a||(this.a={});this.a[a]||(this.a[a]=[]);v(this.a[a])||(this.a[a]=[this.a[a]]);return this.a[a]};r=t.exports;
+(function(){function a(a){a=a||{};this.frameCount=0;this.completed=this.animating=g;this.async=a.async===e}var c={},b={};c.exports=b;for(var d="object"===typeof window?this:{},i="undefined"!==typeof h&&h.c?h.c:require("util").c,q="undefined"!==typeof r&&r.EventEmitter?r.EventEmitter:require("events").EventEmitter,w=0,j=["ms","moz","webkit","o"],m=0;m<j.length&&!d.requestAnimationFrame;++m)d.requestAnimationFrame=d[j[m]+"RequestAnimationFrame"],d.cancelAnimationFrame=d[j[m]+"CancelAnimationFrame"]||
+d[j[m]+"CancelRequestAnimationFrame"];d.requestAnimationFrame||(d.requestAnimationFrame=function(a){var b=(new Date).getTime(),c=Math.max(0,16-(b-w)),d=setTimeout(function(){a(b+c)},c);w=b+c;return d});d.cancelAnimationFrame||(d.cancelAnimationFrame=function(a){clearTimeout(a)});j={complete:function(){this.animating=g;this.completed=e;this.stop();this.emit("complete",this);return this},i:function(a){if(a)return this.on("complete",a),this},k:function(a){if(a)return this.on("update",a),this},j:function(a){if(a)return this.on("stop",
+a),this},start:function(a){function c(){var a;f.frameCount++;f.async?(f.emit("update",function(){f.animating=e;c()},f),a=g):(f.emit("update",f),a=e);a&&(f.animating?f.e=d.requestAnimationFrame(c):d.cancelAnimationFrame(f.e))}if(this.animating)return this;this.emit("start",this);b.running+=1;this.animating=e;var f=this;if(void 0!==a)this.on("update",a);c();return this},stop:function(){this.animating=g;b.running-=1;this.emit("stop",this);return this}};i(a,q);for(var x in j)a.prototype[x]=j[x];c.exports=
+b=function(b,c){1===arguments.length&&(c=b,b={});var d=new a(b);if(c)d.on("update",c);return d};b.running=0;b.async=function(a,c){1===arguments.length&&(c=a,a={});a.async=e;return b.create(a,c)};b.create=function(b,c){1===arguments.length&&(c=b,b={});return(new a(b)).start(c)};b.defer=b;b.Animator=a;b.EventEmitter=q;"function"===typeof define&&define.amd?define(function(){return b}):"object"===typeof window&&(window.animitter=b);return c.exports})().version="0.1.0";}());
194 lib/animitter.js
@@ -0,0 +1,194 @@
+var root = typeof window === 'object' ? this : {};
+var inherits = typeof utils !== 'undefined' && utils.inherits ? utils.inherits : require('util').inherits;
+var EventEmitter = typeof events !== 'undefined' && events.EventEmitter ? events.EventEmitter : require('events').EventEmitter;
+
+// [Polyfill]([Erik Moller polyfill](http://paulirish.com/2011/requestanimationframe-for-smart-animating/) for requestAnimationFrame and cancelAnimationFrame
+(function(){
+ var lastTime = 0;
+ var vendors = ['ms', 'moz', 'webkit', 'o'];
+ for(var x = 0; x < vendors.length && !root.requestAnimationFrame; ++x) {
+ root.requestAnimationFrame = root[vendors[x]+'RequestAnimationFrame'];
+ root.cancelAnimationFrame = root[vendors[x]+'CancelAnimationFrame'] || root[vendors[x]+'CancelRequestAnimationFrame'];
+ }
+
+ if (!root.requestAnimationFrame)
+ root.requestAnimationFrame = function(callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+
+ if (!root.cancelAnimationFrame)
+ root.cancelAnimationFrame = function(id) {
+ clearTimeout(id);
+ };
+}());
+
+//##Animator object
+//###Extends: [events.EventEmitter](./events.html)
+// *animation.Animator*
+// has the following properties:
+//
+// * **frameCount** *{Number}* current frame # in animation, increases every *'update'*
+// * **animating** *{Boolean}* true if currently running
+// * **completed** *{Boolean}* true if **complete()** has been called
+/** @expose */
+var Animator = function( opts ){
+ opts = opts || {};
+ /** @expose */
+ this.frameCount = 0;
+ /** @expose */
+ this.animating = false;
+ /** @expose */
+ this.completed = false;
+ /** @expose */
+ this.async = (opts.async === true);
+};
+
+var methods = {
+ //####myAnimation.complete()
+ //stops the animation and emits *'complete'*
+ complete: function(){
+ this.animating = false;
+ this.completed = true;
+ this.stop();
+ this.emit('complete', this);
+ return this;
+ },
+ //same as .on('complete',fn)
+ onComplete: function(fn){
+ if(!fn)return;
+ this.on('complete',fn);
+ return this;
+ },
+ //same as .on('update',fn)
+ onUpdate: function(fn){
+ if(!fn)return;
+ this.on('update',fn);
+ return this;
+ },
+ //same as .on('stop',fn)
+ onStop: function(fn){
+ if(!fn)return;
+ this.on('stop',fn);
+ return this;
+ },
+ //####myAnimation.start(function(){})
+ //start the animation
+ //optional **callback** will be attached to .on('update',fn)
+ start: function( callback ){
+ //dont let a second animation start on the same object
+ //use *.on('update',fn)* instead
+ if(this.animating){
+ return this;
+ }
+ //emit **start** once at the beginning
+ this.emit('start', this);
+ exports.running += 1;
+ this.animating = true;
+
+ var self = this;
+ var step = function(){
+ self.frameCount++;
+ if( self.async ){
+ self.emit('update', function(){
+ self.animating = true;
+ drawFrame();
+ }, self);
+ return false;
+ } else {
+ self.emit('update', self);
+ return true;
+ }
+ };
+
+ if(callback !== undefined){
+ this.on('update', callback);
+ }
+
+ var drawFrame = function(){
+ if( step() ){
+ if(self.animating){
+ self.request = root.requestAnimationFrame(drawFrame);
+ } else {
+ root.cancelAnimationFrame(self.request);
+ }
+ }
+ };
+ drawFrame();
+
+ return this;
+ },
+ //####myAnimation.stop()
+ //stops the animation but does not mark as completed
+ stop: function(){
+ this.animating = false;
+ exports.running -= 1;
+ this.emit('stop', this);
+ return this;
+ }
+};
+
+inherits(Animator, EventEmitter);
+for(var method in methods){
+ Animator.prototype[method] = methods[method];
+}
+
+//####anim(function(){}).start();
+//creates an animator but does not start it
+//**alternately:** anim({ async: true }, function( next ){ next(); })
+/** @expose */
+module.exports = exports = function( opts, fn ){
+ if( arguments.length === 1){
+ fn = opts;
+ opts = {};
+ }
+ var _instance = new Animator( opts );
+ if( fn ) _instance.on('update', fn);
+ return _instance;
+};
+//log how many animations are running
+/** @expose */
+exports.running = 0;
+//####anim.async(function( next ){ next(); });
+//creates an asynchronous animation where the next frame is queued
+//once next(); is invoked
+/** @expose */
+exports.async = function( opts, fn ){
+ if( arguments.length === 1){
+ fn = opts;
+ opts = {};
+ }
+ opts.async = true;
+ return exports.create( opts, fn );
+};
+//####anim.create( function(){} )
+//creates a new animator and auto-starts it
+//this is the most direct way to use it but will result in missing
+//at least one *.on('update',fn)* applied after
+/** @expose */
+exports.create = function( opts, fn ){
+ if( arguments.length === 1 ){
+ fn = opts;
+ opts = {};
+ }
+ var _instance = new Animator(opts);
+ return _instance.start(fn);
+};
+//####anim.defer( function(){} ).start()
+//creates a new animator without calling start
+//this allows you to attach events without missing any frames
+/** @expose */
+exports.defer = exports;
+/** @expose */
+exports.Animator = Animator;
+/** @expose */
+exports.EventEmitter = EventEmitter;
+
+if( typeof define === 'function' && define['amd']){
+ define(function(){ return exports; });
+} else if( typeof window === 'object' ){
+ window['animitter'] = exports;
+}
215 lib/events.js
@@ -0,0 +1,215 @@
+
+//This is the Node.js [EventEmitter](https://github.com/joyent/node/blob/master/lib/events.js)
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var isArray = Array.isArray || utils.isArray;
+
+function EventEmitter() {}
+exports.EventEmitter = EventEmitter;
+
+// By default EventEmitters will print a warning if more than
+// 10 listeners are added to it. This is a useful default which
+// helps finding memory leaks.
+//
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+var defaultMaxListeners = 10;
+EventEmitter.prototype.setMaxListeners = function(n) {
+ if (!this._events) this._events = {};
+ this._maxListeners = n;
+};
+
+/** @expose */
+EventEmitter.prototype.emit = function() {
+ var type = arguments[0], l, args, i;
+ // If there is no 'error' event listener then throw.
+ if (type === 'error') {
+ if (!this._events || !this._events.error ||
+ (isArray(this._events.error) && !this._events.error.length))
+ {
+ if (arguments[1] instanceof Error) {
+ throw arguments[1]; // Unhandled 'error' event
+ } else {
+ throw new Error("Uncaught, unspecified 'error' event.");
+ }
+ }
+ }
+
+ if (!this._events) return false;
+ var handler = this._events[type];
+ if (!handler) return false;
+
+ if (typeof handler == 'function') {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ l = arguments.length;
+ args = new Array(l - 1);
+ for (i = 1; i < l; i++) args[i - 1] = arguments[i];
+ handler.apply(this, args);
+ }
+ return true;
+
+ } else if (isArray(handler)) {
+ l = arguments.length;
+ args = new Array(l - 1);
+ for (i = 1; i < l; i++) args[i - 1] = arguments[i];
+
+ var listeners = handler.slice();
+ for (i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].apply(this, args);
+ }
+ return true;
+
+ } else {
+ return false;
+ }
+};
+/** @expose */
+EventEmitter.prototype.addListener = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('addListener only takes instances of Function');
+ }
+
+ if (!this._events) this._events = {};
+
+ // To avoid recursion in the case that type == "newListeners"! Before
+ // adding it to the listeners, first emit "newListeners".
+ this.emit('newListener', type, typeof listener.listener === 'function' ?
+ listener.listener : listener);
+
+ if (!this._events[type]) {
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ } else if (isArray(this._events[type])) {
+
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+
+ } else {
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+
+ }
+
+ // Check for listener leak
+ if (isArray(this._events[type]) && !this._events[type].warned) {
+ var m;
+ if (this._maxListeners !== undefined) {
+ m = this._maxListeners;
+ } else {
+ m = defaultMaxListeners;
+ }
+
+ if (m && m > 0 && this._events[type].length > m) {
+ this._events[type].warned = true;
+ console.error('(node) warning: possible EventEmitter memory ' +
+ 'leak detected. %d listeners added. ' +
+ 'Use emitter.setMaxListeners() to increase limit.',
+ this._events[type].length);
+ console.trace();
+ }
+ }
+
+ return this;
+};
+/** @expose **/
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+/** @expose **/
+EventEmitter.prototype.once = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('.once only takes instances of Function');
+ }
+
+ var self = this;
+ function g() {
+ self.removeListener(type, g);
+ listener.apply(this, arguments);
+ }
+
+ g.listener = listener;
+ self.on(type, g);
+
+ return this;
+};
+/** @expose */
+EventEmitter.prototype.removeListener = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('removeListener only takes instances of Function');
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (!this._events || !this._events[type]) return this;
+
+ var list = this._events[type];
+
+ if (isArray(list)) {
+ var position = -1;
+ for (var i = 0, length = list.length; i < length; i++) {
+ if (list[i] === listener ||
+ (list[i].listener && list[i].listener === listener))
+ {
+ position = i;
+ break;
+ }
+ }
+
+ if (position < 0) return this;
+ list.splice(position, 1);
+ if (list.length === 0)
+ delete this._events[type];
+ } else if (list === listener || (list.listener && list.listener === listener)) {
+ delete this._events[type];
+ }
+
+ return this;
+};
+/** @expose */
+EventEmitter.prototype.removeAllListeners = function(type) {
+ if (arguments.length === 0) {
+ this._events = {};
+ return this;
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (type && this._events && this._events[type]) this._events[type] = null;
+ return this;
+};
+/** @expose */
+EventEmitter.prototype.listeners = function(type) {
+ if (!this._events) this._events = {};
+ if (!this._events[type]) this._events[type] = [];
+ if (!isArray(this._events[type])) {
+ this._events[type] = [this._events[type]];
+ }
+ return this._events[type];
+};
26 lib/utils.js
@@ -0,0 +1,26 @@
+var env, isArray, inherits;
+//find which environment we are in
+if( typeof require === 'function' ){
+ env = (typeof define === 'function' && define.amd) ? 'requirejs' : 'node';
+} else {
+ env = "browser";
+}
+
+isArray = Array.isArray || function(a){
+ return a.toString() == '[object Array]';
+};
+//same as Node.js' inherits
+inherits = function(ctor, superCtor) {
+ ctor.super_ = superCtor;
+ ctor.prototype = Object.create(superCtor.prototype, {
+ constructor: {
+ value: ctor,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+};
+exports.env = env;
+exports.isArray = isArray;
+exports.inherits = inherits;
21 package.json
@@ -0,0 +1,21 @@
+{
+ "name": "animitter",
+ "version": "0.1.0",
+ "description": "Animitter is an animation loop + EventEmitter for browser, node or amd.",
+ "main": "lib/animitter.js",
+ "directories": {
+ "test": "test"
+ },
+ "scripts": {
+ "test": "make test"
+ },
+ "repository": "http://github.com/hapticdata/animitter",
+ "author": "Kyle Phillips",
+ "license": "MIT",
+ "repositories" : [
+ { "type" : "git", "url" : "http://github.com/hapticdata/toxiclibsjs" }
+ ],
+ "maintainers" : [
+ { "name" : "Kyle Phillips", "email" : "kyle@haptic-data.com" }
+ ]
+}
91 test/animitter-node.js
@@ -0,0 +1,91 @@
+var anim = require('../lib/animitter'),
+ assert = require('assert');
+var _ = require('underscore');
+
+function makeHandlers ( expectedUpdates, done ){
+ var frame = 0, completed = false, updated = 0, stopped = false;
+ return {
+ loop: function loop (){
+ if(this.frameCount === 5 ) this.complete();
+ },
+
+ onUpdate: function ( ){
+ updated++;
+ frame = this.frameCount;
+ if( completed && updated === expectedUpdates ) done();
+ },
+
+ onStop: function (){
+ stopped = true;
+ },
+
+ onComplete: function (){
+ if( this.frameCount === 5 && frame === 4 && stopped && this.completed && !this.animating ) completed = true;
+ }
+ };
+}
+
+
+describe('Animation module', function (){
+ it('should give me animation module', function (done){
+ if(anim.Animator.prototype.emit !== undefined){
+ done();
+ } else {
+ done(Error("Animation did not have emit"));
+ }
+ });
+
+ it('create is called 5 times, update called 4 times (starting at frame 2), stop and complete once,', function (done){
+ var lis = makeHandlers( 4, done );
+ anim
+ .create(lis.loop)
+ .on('update',lis.onUpdate )
+ .on('stop', lis.onStop)
+ .once('complete', lis.onComplete);
+ });
+
+
+ it('creates a deferred animation, update called 5 times (starting at frame 1), stop and complete once', function (done){
+ var lis = makeHandlers( 5, done );
+ anim
+ .defer( lis.loop )
+ .on('update', lis.onUpdate)
+ .on('stop', lis.onStop)
+ .once('complete', lis.onComplete)
+ .start();
+ });
+
+
+ describe('running counter', function(){
+ var threads = [];
+ _(100).times(function(){
+ threads.push(anim.defer(function(){
+ //empty loop
+ }));
+ });
+ it('should have 0 animations running', function(){
+ assert.equal(anim.running, 0);
+ });
+
+ it('should have 100 animations running', function(){
+ _(threads).each(function( thread ){
+ thread.start();
+ });
+ assert.equal(anim.running, 100);
+ });
+
+ it('should have 0 animations running', function(){
+ _(threads).each(function( thread, i ){
+ //complete() and stop() should both drop the running counter
+ if( i % 2 == 0){
+ thread.stop();
+ } else {
+ thread.complete();
+ }
+ });
+ assert.equal(anim.running, 0);
+ });
+
+ });
+
+});
106 test/animitter-requirejs.js
@@ -0,0 +1,106 @@
+/*global describe,it*/
+var requirejs = require('requirejs'),
+ _ = require('underscore'),
+ assert = require('assert');
+
+
+requirejs.config({
+ baseUrl: process.cwd() +'/'
+});
+
+describe('load module', function(){
+ it('should load animitter', function( done ){
+ requirejs(['animitter'], function (anim){
+ done();
+ function makeHandlers ( expectedUpdates, done ){
+ var frame = 0, completed = false, updated = 0, stopped = false;
+ return {
+ loop: function loop (){
+ if(this.frameCount === 5 ) this.complete();
+ },
+
+ onUpdate: function ( ){
+ updated++;
+ frame = this.frameCount;
+ if( completed && updated === expectedUpdates ) done();
+ },
+
+ onStop: function (){
+ stopped = true;
+ },
+
+ onComplete: function (){
+ if( this.frameCount === 5 && frame === 4 && stopped && this.completed && !this.animating ) completed = true;
+ }
+ };
+ }
+
+
+ describe('Animation module', function (){
+ it('should give me animation module', function (done){
+ if(anim.Animator.prototype.emit !== undefined){
+ done();
+ } else {
+ done(Error("Animation did not have emit"));
+ }
+ });
+
+ it('create is called 5 times, update called 4 times (starting at frame 2), stop and complete once,', function (done){
+ this.timeout(10000);
+ var lis = makeHandlers( 4, done );
+ anim
+ .create(lis.loop)
+ .on('update',lis.onUpdate )
+ .on('stop', lis.onStop)
+ .once('complete', lis.onComplete);
+ });
+
+
+ it('creates a deferred animation, update called 5 times (starting at frame 1), stop and complete once', function (done){
+ this.timeout(10000);
+ var lis = makeHandlers( 5, done );
+ anim
+ .defer( lis.loop )
+ .on('update', lis.onUpdate)
+ .on('stop', lis.onStop)
+ .once('complete', lis.onComplete)
+ .start();
+ });
+
+
+ describe('running counter', function(){
+ var threads = [];
+ _(100).times(function(){
+ threads.push(anim.defer(function(){
+ //empty loop
+ }));
+ });
+ it('should have 0 animations running', function(){
+ assert.equal(anim.running, 0);
+ });
+
+ it('should have 100 animations running', function(){
+ _(threads).each(function( thread ){
+ thread.start();
+ });
+ assert.equal(anim.running, 100);
+ });
+
+ it('should have 0 animations running', function(){
+ _(threads).each(function( thread, i ){
+ //complete() and stop() should both drop the running counter
+ if( i % 2 === 0){
+ thread.stop();
+ } else {
+ thread.complete();
+ }
+ });
+ assert.equal(anim.running, 0);
+ });
+
+ });
+
+ });
+ });
+ });
+});
14 tools/_pre.js
@@ -0,0 +1,14 @@
+//###Env: Browser + Node
+//*Author:* [Kyle Phillips](http://hapticdata.com)
+//*published under the MIT license*
+
+//animator factory for creating [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/DOM/window.requestAnimationFrame) callbacks
+//and simplifying their cancellation.
+//Animations can use `start()` and `stop()` repeatedly to pause, and `complete()` once to finish.
+
+//**_Basic use:_**
+//
+// animitter(function(){
+// //do this every time
+// if(Math.random() > 0.9) this.complete();
+// });
BIN tools/compiler.jar
Binary file not shown.
29 tools/concat.js
@@ -0,0 +1,29 @@
+var fs = require('fs'),
+ version = require('../package').version;
+
+var src = './lib/';
+
+var output = String(fs.readFileSync(__dirname+'/_pre.js'));
+output += '\nvar animitter = (function(){';
+
+[ 'utils',
+ 'events',
+ 'animitter'
+].forEach(function( module ){
+ output += "\n\t//"+module+".js";
+ output += "\n\tvar "+module+" = (function(){";
+ output += "\n\t\tvar module = {}, exports = {};";
+ output += "\n\t\tmodule.exports = exports;";
+ output += '\n\t\t'+ String(fs.readFileSync(src+module+'.js')).replace(/\n/g,'\n\t\t');
+ output += [
+ '\n\t\treturn module.exports;',
+ '\t}());'
+ ].join('\n');
+});
+output += [
+ '\n\treturn animitter;',
+ '}());',
+ 'animitter.version = "'+version+'";'
+].join('\n');
+
+fs.writeFileSync('./animitter.js', output );

0 comments on commit 89fddeb

Please sign in to comment.