(function (window, undefined) { var document = window.document, navigator = window.navigator, hasOwn_ = Object.prototype.hasOwnProperty, slice_ = Array.prototype.slice; var tracking = { type: {}, all: function(selector, opt_element) { return Array.prototype.slice.call( (opt_element || document).querySelectorAll(selector)); }, augment: function(C, P) { var instance = this; // Chaining C and P constructors function BuiltClass() { P.apply(this, arguments); C.apply(this, arguments); } // TempClass to avoid invoke P constructor twice during BuiltClass.prototype = new TempClass() function TempClass() {} TempClass.prototype = P.prototype; BuiltClass.superclass = P.prototype; BuiltClass.prototype = new TempClass(); BuiltClass.prototype.constructor = BuiltClass; tracking.merge(BuiltClass.prototype, C.prototype); return BuiltClass; }, bind: function(fn, opt_context, var_args1) { var instance = this; if (var_args1 !== undefined) { var_args1 = slice_.call(arguments, 2); } return function(var_args2) { if (var_args2 !== undefined) { var_args2 = slice_.call(arguments); if (var_args1 !== undefined) { var_args2 = var_args2.concat(var_args1); } } return fn.apply(opt_context || instance, var_args2 || var_args1); }; }, forEach: function(o, fn, opt_context) { var instance = this, key; if (Array.isArray(o)) { o.forEach(function() { fn.apply(opt_context || this, arguments); }); } else { for (key in o) { if (hasOwn_.call(o, key)) { fn.call(opt_context || o, o[key], key, o); } } } return o; }, isNode: function(o) { return o.nodeType || this.isWindow(o); }, isString: function(o) { return typeof o === 'string'; }, isWindow: function(o) { return !!(o && o.alert && o.document); }, merge: function(target, o) { var instance = this, key; for (key in o) { if (hasOwn_.call(o, key)) { target[key] = o[key]; } } return target; }, one: function(selector, opt_element) { var instance = this; if (instance.isNode(selector)) { return selector; } return (opt_element || document).querySelector(selector); } }; tracking.math = { /* * Euclidean distance between two points P(x0, y0) and P(x1, y1). */ distance: function(x0, y0, x1, y1) { var dx = x1-x0, dy = y1-y0; return Math.sqrt(dx*dx + dy*dy); } }; // tracking.Attribute var Attribute = function() { var instance = this; instance.attrs_ = {}; }; Attribute.prototype = { attrs_: null, setAttrs: function(attrMap, silent) { var instance = this; tracking.forEach(attrMap, function(attrValue, attrName) { if (silent) { instance.attrs_[attrName] = attrValue; } else { instance.set(attrName, attrValue); } }); }, set: function(attrName, attrValue) { var instance = this, changeFn = instance[attrName + 'Change_']; instance.attrs_[attrName] = attrValue; if (changeFn) { changeFn.call(instance, attrValue, attrName); } return attrValue; }, get: function(attrName) { var instance = this; return instance.attrs_[attrName]; }, getAttrs: function() { var instance = this; return instance.attrs_; }, linkAttr: function(attrName, attrTarget, opt_set) { var instance = this, changeFnName = attrName + 'Change_', changeFn = instance[changeFnName]; instance[changeFnName] = function(val) { if (changeFn) { changeFn.apply(this, arguments); } attrTarget.set(attrName, val); }; if (opt_set) { attrTarget.set(attrName, instance.get(attrName)); } } }; tracking.Attribute = Attribute; // tracking.DomElement var DomElement = function(opt_config) { var instance = this; instance.setAttrs(tracking.merge({ height: 240, visible: true, width: 320 }, opt_config), true); }; DomElement.prototype = { domElement: null, heightChange_: function(val) { var instance = this; instance.domElement.height = val; }, hide: function() { var instance = this; instance.set('visible', false); return instance; }, render: function(opt_selector) { var instance = this; instance.heightChange_(instance.get('height')); instance.visibleChange_(instance.get('visible')); instance.widthChange_(instance.get('width')); tracking.one(opt_selector || document.body).appendChild( instance.domElement); return instance; }, show: function() { var instance = this; instance.set('visible', true); return instance; }, visibleChange_: function(val) { var instance = this; instance.domElement.style.display = val ? 'block' : 'none'; }, widthChange_: function(val) { var instance = this; instance.domElement.width = val; } }; tracking.DomElement = tracking.augment(DomElement, tracking.Attribute); // tracking.Canvas var Canvas = function(opt_config) { var instance = this; instance.createCanvas_(); }; Canvas.prototype = { context: null, createCanvas_: function() { var instance = this, domElement = document.createElement('canvas'); instance.domElement = domElement; instance.context = domElement.getContext('2d'); }, getImageData: function(opt_x, opt_y, opt_width, opt_height) { var instance = this, x = opt_x || 0, y = opt_y || 0, width = opt_width || instance.get('width'), height = opt_height || instance.get('height'); return instance.context.getImageData(x, y, width, height); }, setImageData: function(data, opt_x, opt_y) { var instance = this, x = opt_x || 0, y = opt_y || 0; instance.context.putImageData(data, x, y); return instance; }, forEach: function(imageData, fn, opt_jump) { var instance = this, width = imageData.width, height = imageData.height, data = imageData.data, jump = opt_jump || 1, i = 0, j = 0, w; for (i = 0; i < height; i+=jump) { for (j = 0; j < width; j+=jump) { w = i*width*4 + j*4; fn.call(instance, data[w], data[w+1], data[w+2], data[w+3], w, i, j, imageData); } } }, loadImage: function(src, opt_fn, opt_x, opt_y, opt_width, opt_height) { var instance = this, x = opt_x || 0, y = opt_y || 0, width = opt_width || instance.get('width'), height = opt_height || instance.get('height'), context = instance.context; if (context) { var img = new Image(); img.onload = function() { context.drawImage(img, x, y, width, height); if (opt_fn) { opt_fn.call(instance); } img = null; }; img.src = src; } }, toDataURL: function(opt_format) { var instance = this; return instance.domElement.toDataURL(opt_format || 'image/png'); }, transform: function(fn) { var instance = this, imageData = instance.getImageData(), data = imageData.data, value; instance.forEach(imageData, function(r, g, b, a, w, i, j) { var value = fn.apply(instance, arguments); data[w] = value[0]; data[w+1] = value[1]; data[w+2] = value[2]; data[w+3] = value[3]; }); instance.setImageData(imageData); return instance; } }; tracking.Canvas = tracking.augment(Canvas, tracking.DomElement); // tracking.Video var Video = function(opt_config) { var instance = this; instance.setAttrs(tracking.merge({ autoplay: true, controls: true }, opt_config), true); instance.trackers_ = {}; instance.createVideo_(); instance.createCanvas_(); }; Video.prototype = { canvas: null, trackers_: null, createCanvas_: function() { var instance = this; instance.canvas = new tracking.Canvas(); instance.linkAttr('height', instance.canvas, true); instance.linkAttr('width', instance.canvas, true); }, createVideo_: function() { var instance = this, autoplay = instance.get('autoplay'), domElement = document.createElement('video'); domElement.autoplay = autoplay; domElement.controls = instance.get('controls'); instance.domElement = domElement; if (autoplay) { // Firefox 18.0a1 (2012-08-31) doesn't respect autoplay property set, force play. instance.play(); } }, getVideoCanvasImageData: function() { var instance = this; instance.syncVideoCanvas(); return instance.canvas.getImageData(); }, load: function() { var instance = this, domElement = instance.domElement; domElement.load.apply(domElement, arguments); return instance; }, loop_: function() { var instance = this, i = 0, trackers = instance.trackers_, type; tracking.forEach(trackers, function(trackerGroup, trackerName) { type = tracking.type[trackerName]; if (type.track) { type.track(trackerGroup, instance); } }); if (Object.keys(trackers).length) { requestAnimationFrame(function loop() { instance.loop_(); }); } }, pause: function() { var instance = this, domElement = instance.domElement; domElement.pause.apply(domElement, arguments); return instance; }, play: function() { var instance = this, domElement = instance.domElement; domElement.play.apply(domElement, arguments); return instance; }, renderVideoCanvas: function(opt_selector) { var instance = this; instance.syncVideoCanvas(); tracking.one(opt_selector || document.body).appendChild( instance.canvas.domElement); return instance; }, srcChange_: function(stream) { var instance = this; instance.domElement.src = stream; }, stopTracking: function() { var instance = this; instance.trackers_ = {}; }, syncVideoCanvas: function() { var instance = this, domElement = instance.domElement, width = instance.get('width'), height = instance.get('height'); if (domElement.readyState === domElement.HAVE_ENOUGH_DATA) { instance.canvas.context.drawImage( instance.domElement, 0, 0, width, height); } return instance; }, toDataURL: function(opt_format) { var instance = this; instance.syncVideoCanvas(); return instance.canvas.toDataURL(opt_format); }, track: function(config) { var instance = this, type = tracking.type[config.type.toUpperCase()], trackers = instance.trackers_; if (!type) { throw Error('A tracker type should be specified.'); } if (!trackers[type.NAME]) { trackers[type.NAME] = []; } trackers[type.NAME].push(config); if (Object.keys(trackers).length === 1) { instance.loop_(); } } }; tracking.Video = tracking.augment(Video, tracking.DomElement); // tracking.VideoCamera var VideoCamera = function(opt_config) { var instance = this; instance.setAttrs(tracking.merge({ audio: true }, opt_config), true); instance.initUserMedia_(); }; VideoCamera.prototype = { initUserMedia_: function() { var instance = this; navigator.getUserMedia( // Firefox 18.0a1 (2012-08-31) doesn't support audio yet, since we don't do nothing with audio, remove it for now. navigator.mozGetUserMedia ? { video: true } : { audio: instance.get('audio'), video: true }, tracking.bind(instance.defSuccessHandler_, instance), tracking.bind(instance.defErrorHandler_, instance)); }, defSuccessHandler_: function(stream) { var instance = this; try { instance.set('src', URL.createObjectURL(stream)); } catch (err) { // Firefox 18.0a1 (2012-08-31) doesn't require create object URL instance.set('src', stream); } }, defErrorHandler_: function(err) { throw Error(err); } }; tracking.VideoCamera = tracking.augment(VideoCamera, tracking.Video); // self.Int32Array polyfill if (!self.Int32Array) { self.Int32Array = Array; self.Float32Array = Array; } // self.Uint8ClampedArray polyfill if (!self.Uint8ClampedArray) { self.Uint8ClampedArray = Array; } // window.URL polyfill if (!window.URL) { window.URL = window.URL || window.webkitURL || window.msURL || window.oURL; } // getUserMedia polyfill if (!navigator.getUserMedia) { navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); } // requestAnimationFrame polyfill by Erik Möller (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o'], x; for(x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }()); window.tracking = tracking; }( window ));