diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a59651..17b3386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ - -## v0.1.0 (2013-11-10) + +## v0.2.0 (2013-11-12) + + +#### Features + +* **GruntFile:** add concated and uglified version ([92fecaed](https://github.com/angular-adaptive/adaptive-motion/commit/92fecaed4f0365b8987a0a848921447081bdc349)) +* **conventional-changelog:** adds grunt-conventional-changelog ([432cf774](https://github.com/angular-adaptive/adaptive-motion/commit/432cf7742466b7152250869b930b9627e1e53661)) + + +#### Breaking Changes + +* src/adaptive-motion.js is now renamed into src/angular-adaptive-motion.js + ([92fecaed](https://github.com/angular-adaptive/adaptive-motion/commit/92fecaed4f0365b8987a0a848921447081bdc349)) diff --git a/Gruntfile.js b/Gruntfile.js index 6872031..124bb6e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,42 +1,70 @@ module.exports = function (grunt) { - require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); + require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); - // Default task. - grunt.registerTask('default', ['karma', 'jshint']); + // Default task. + grunt.registerTask('default', ['karma', 'jshint']); + grunt.registerTask('build', ['karma', 'jshint', 'concat', 'uglify']); - var karmaConfig = function(configFile, customOptions) { - var options = { configFile: configFile, keepalive: true }; - var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' }; - return grunt.util._.extend(options, customOptions, travisOptions); - }; + var karmaConfig = function(configFile, customOptions) { + var options = { configFile: configFile, keepalive: true }; + var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' }; + return grunt.util._.extend(options, customOptions, travisOptions); + }; - // Project configuration. - grunt.initConfig({ - karma: { - unit: { - options: karmaConfig('test/test.conf.js') - } - }, - jshint:{ - files:['src/**/*.js', 'test/**/*.js'], - options: { - curly:true, - eqeqeq:true, - immed:true, - latedef:true, - newcap:true, - noarg:true, - sub:true, - boss:true, - eqnull:true, - devel:true, - globals:{} - } - }, - changelog: { - options: { - dest: 'CHANGELOG.md' - } - } - }); -} + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('bower.json'), + meta: { + banner: '/*!\n' + + ' * <%= pkg.name %> v<%= pkg.version %>\n' + + ' * The MIT License\n' + + ' * Copyright (c) 2013 Jan Antala\n' + + ' */' + }, + uglify: { + options: { + preserveComments: 'some' + }, + dist: { + src: '<%= pkg.name %>.js', + dest: '<%= pkg.name %>.min.js' + } + }, + concat: { + options: { + process: true, + banner: '<%= meta.banner %>\n\n' + }, + dist: { + src: 'src/<%= pkg.name %>.js', + dest: '<%= pkg.name %>.js' + } + }, + karma: { + unit: { + options: karmaConfig('test/test.conf.js') + } + }, + jshint:{ + files:['src/**/*.js', 'test/**/*.js'], + options: { + curly:true, + eqeqeq:true, + immed:true, + latedef:true, + newcap:true, + noarg:true, + sub:true, + boss:true, + eqnull:true, + devel:true, + globals:{} + } + }, + changelog: { + options: { + dest: 'CHANGELOG.md' + } + }, + }); +}; diff --git a/README.md b/README.md index 1190b17..60bc0ab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# adaptive-motion v0.1.0 [![Build Status](https://travis-ci.org/angular-adaptive/adaptive-motion.png?branch=master)](https://travis-ci.org/angular-adaptive/adaptive-motion) +# adaptive-motion v0.2.0 [![Build Status](https://travis-ci.org/angular-adaptive/adaptive-motion.png?branch=master)](https://travis-ci.org/angular-adaptive/adaptive-motion) This module allows you to control an AngularJS app using web camera. @@ -25,8 +25,8 @@ To your `bower.json` file. Then run This will copy the angular-adaptive-motion files into your `bower_components` folder, along with its dependencies. Load the script files in your application: - - + + Add the **adaptive.motion** module as a dependency to your application module: diff --git a/src/adaptive-motion.js b/angular-adaptive-motion.js similarity index 99% rename from src/adaptive-motion.js rename to angular-adaptive-motion.js index 5da1ba0..db6bd2c 100644 --- a/src/adaptive-motion.js +++ b/angular-adaptive-motion.js @@ -1,7 +1,7 @@ -/** - * angular-adaptive-motion v0.1.0 +/*! + * angular-adaptive-motion v0.2.0 * The MIT License - * Copyright (c) 2013 Jan Antala http://janantala.com + * Copyright (c) 2013 Jan Antala */ (function () { diff --git a/angular-adaptive-motion.min.js b/angular-adaptive-motion.min.js new file mode 100644 index 0000000..8933b57 --- /dev/null +++ b/angular-adaptive-motion.min.js @@ -0,0 +1,6 @@ +/*! + * angular-adaptive-motion v0.2.0 + * The MIT License + * Copyright (c) 2013 Jan Antala + */ +!function(){"use strict";var a=angular.module("adaptive.motion",[]);!function(){for(var a=0,b=["webkit","moz"],c=0;cb?6:0);break;case b:d=(c-a)/i+2;break;case c:d=(a-b)/i+4}d/=6}return[d,e,h]};a.provider("$motion",[function(){var a,c=document.createElement("video");c.setAttribute("autoplay","true"),c.setAttribute("width","300");var d=document.createElement("canvas"),e=d.getContext("2d");this.treshold={rgb:150,move:2,bright:300},this.hsvFilter={huemin:0,huemax:.1,satmin:0,satmax:1,valmin:.4,valmax:1},this.setTreshold=function(a){angular.extend(this.treshold,a)},this.setHsvFilter=function(a){angular.extend(this.hsvFilter,a)},this.$get=function(f){var g,h,i,j=this.treshold,k=this.hsvFilter,l=5,m=0,n=0,o={x:0,y:0,d:0},p=function(){if(window.URL=window.URL||window.webkitURL,navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia,!navigator.getUserMedia)throw f.$broadcast("adaptive.motion:onError","getUserMedia() is not supported in your browser"),new Error("getUserMedia() is not supported in your browser");navigator.getUserMedia({audio:!1,video:!0},function(b){f.$broadcast("adaptive.motion:onStart"),h=b,c.src=window.URL.createObjectURL(b),c.addEventListener("play",function(){a=window.requestAnimationFrame(r)})},function(){throw f.$broadcast("adaptive.motion:onError","Access denied!"),new Error("Access denied!")})},q=function(){window.cancelAnimationFrame(a),h&&h.stop(),h=void 0,f.$broadcast("adaptive.motion:onStop")},r=function(){d.width!==c.videoWidth&&(m=Math.floor(c.videoWidth/l),n=Math.floor(c.videoHeight/l),d.width=m,d.height=n);try{e.drawImage(c,0,0,m,n),g=e.getImageData(0,0,m,n),f.$broadcast("adaptive.motion:videoData",g);var b=s(g);i=t(b),a=window.requestAnimationFrame(r)}catch(h){if("NScontextERRORcontextNOTcontextAVAILABLE"!==h.name)throw h;a=window.requestAnimationFrame(r)}},s=function(a){for(var c=e.getImageData(0,0,m,n),d=c.width*c.height,g=4*d,h=0,i=0;n>i;i++)for(var j=0;m>j;j++){g=j+i*m;var l=a.data[h],o=a.data[h+1],p=a.data[h+2],q=a.data[h+3],r=b(l,o,p);(r[0]>k.huemin&&r[0].59&&r[0]<1)&&r[1]>k.satmin&&r[1]k.valmin&&r[2]0;){var k=Math.abs(a.data[h]-i.data[h])+Math.abs(a.data[h+1]-i.data[h+1])+Math.abs(a.data[h+2]-i.data[h+2]);k>j.rgb?(b.data[h]=0,b.data[h+1]=0,b.data[h+2]=0,b.data[h+3]=255,g+=1,c+=h/4%m,d+=Math.floor(h/4/b.height)):(b.data[h]=255,b.data[h+1]=255,b.data[h+2]=255,b.data[h+3]=255)}if(g){f.$broadcast("adaptive.motion:edgeData",b);var l={x:c/g,y:d/g,d:g};x(l)}return a},u=function(a){o={x:a.x,y:a.y,d:a.d}},v=0,w=0,x=function(a){v=.9*v+.1*a.d;var b=a.d-v,c=b>j.bright;switch(w){case 0:c&&(w=1,u(a));break;case 2:c||(w=0);break;case 1:var d=a.x-o.x,e=a.y-o.y,g=Math.abs(e)j.move&&f.$broadcast("adaptive.motion:onSwipeLeft"):h&&(e>j.move?f.$broadcast("adaptive.motion:onSwipeDown"):e<-j.move&&f.$broadcast("adaptive.motion:onSwipeUp")),w=2}},y=function(a){f.$on("adaptive.motion:onStart",function(b,c){a(c)})},z=function(a){f.$on("adaptive.motion:onStop",function(b,c){a(c)})},A=function(a){f.$on("adaptive.motion:onError",function(b,c){a(c)})},B=function(a){f.$on("adaptive.motion:onSwipeLeft",function(b,c){a(c)})},C=function(a){f.$on("adaptive.motion:onSwipeRight",function(b,c){a(c)})},D=function(a){f.$on("adaptive.motion:onSwipeUp",function(b,c){a(c)})},E=function(a){f.$on("adaptive.motion:onSwipeDown",function(b,c){a(c)})};return{start:function(){p()},stop:function(){q()},onStart:function(a){y(a)},onStop:function(a){z(a)},onError:function(a){A(a)},onSwipeLeft:function(a){B(a)},onSwipeRight:function(a){C(a)},onSwipeUp:function(a){D(a)},onSwipeDown:function(a){E(a)}}}}]),a.directive("adaptiveMotion",["$rootScope",function(a){return{restrict:"A",link:function(b,c,d){var e=c[0],f=e.getContext("2d");"video"===d.adaptiveMotion?a.$on("adaptive.motion:videoData",function(a,b){f.putImageData(b,0,0)}):"skin"===d.adaptiveMotion?a.$on("adaptive.motion:skinData",function(a,b){f.putImageData(b,0,0)}):a.$on("adaptive.motion:edgeData",function(a,b){f.putImageData(b,0,0)})}}}])}(); \ No newline at end of file diff --git a/bower.json b/bower.json index d24cd6c..e00e1e2 100644 --- a/bower.json +++ b/bower.json @@ -1,11 +1,20 @@ { "name": "angular-adaptive-motion", - "version": "0.1.0", + "version": "0.2.0", "description": "This module allows you to control an angular app using your body.", "author": "https://github.com/angular-adaptive/adaptive-motion/graphs/contributors", "license": "MIT", + "keywords": [ + "angular", + "adaptive", + "motion" + ], "homepage": "http://angular-adaptive.github.io", - "main": "./src/adaptive-motion.js", + "main": "./src/angular-adaptive-motion.js", + "repository": { + "type": "git", + "url": "https://github.com/angular-adaptive/adaptive-motion.git" + }, "ignore": [ "**/.*", "node_modules", diff --git a/package.json b/package.json index d8a1d7e..4afa90e 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,29 @@ { "name": "angular-adaptive-motion", - "version": "0.1.0", + "version": "0.2.0", "description": "This module allows you to control an angular app using your body.", "author": "https://github.com/angular-adaptive/adaptive-motion/graphs/contributors", "license": "MIT", + "keywords": [ + "angular", + "adaptive", + "motion" + ], "homepage": "http://angular-adaptive.github.io", - "main": "./src/adaptive-motion.js", + "main": "./src/angular-adaptive-motion.js", + "repository": { + "type": "git", + "url": "https://github.com/angular-adaptive/adaptive-motion.git" + }, "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", "grunt-karma": "*", "grunt-contrib-jshint": "~0.2.0", "grunt-conventional-changelog": "~1.0.0", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-concat": "~0.3.0", "matchdep": "~0.3.0" }, - "scripts": {}, - "repository": { - "type": "git", - "url": "git://github.com/angular-adaptive/adaptive-motion.git" - } + "scripts": {} } diff --git a/src/angular-adaptive-motion.js b/src/angular-adaptive-motion.js new file mode 100644 index 0000000..8ea019d --- /dev/null +++ b/src/angular-adaptive-motion.js @@ -0,0 +1,446 @@ +(function () { +'use strict'; + +var adaptive = angular.module('adaptive.motion', []); + +// RequestAnimationFrame fallback +(function() { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for(var 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); + }; + } +}()); + +/** + * Converts rgb into hsv + * @param {Integer} r + * @param {Integer} g + * @param {Integer} b + * @return {Array} + */ +var rgb2Hsv = function(r, g, b){ + + r = r/255; + g = g/255; + b = b/255; + + var max = Math.max(r, g, b); + var min = Math.min(r, g, b); + + var h, s, v = max; + + var d = max - min; + + s = max === 0 ? 0 : d / max; + + if (max === min){ + h = 0; // achromatic + } + else{ + switch(max){ + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + + return [h, s, v]; +}; + +adaptive.provider('$motion', [function() { + + var requestId; + var video = document.createElement('video'); + video.setAttribute('autoplay', 'true'); + video.setAttribute('width', '300'); + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + this.treshold = { + 'rgb': 150, + 'move': 2, + 'bright': 300 + }; + + this.hsvFilter = { + 'huemin': 0.0, + 'huemax': 0.1, + 'satmin': 0.0, + 'satmax': 1.0, + 'valmin': 0.4, + 'valmax': 1.0 + }; + + this.setTreshold = function(treshold) { + angular.extend(this.treshold, treshold); + }; + + this.setHsvFilter = function(hsvFilter) { + angular.extend(this.hsvFilter, hsvFilter); + }; + + this.$get = function($rootScope) { + + var treshold = this.treshold; + var hsvFilter = this.hsvFilter; + + var compression = 5; + var width = 0; + var height = 0; + + var draw; + var localMediaStream; + + var lastDraw; + var lastDown = { + x: 0, + y: 0, + d: 0 + }; + + var start = function(){ + + window.URL = window.URL || window.webkitURL; + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + + if (navigator.getUserMedia) { + navigator.getUserMedia({audio: false, video: true}, + function(stream){ + $rootScope.$broadcast('adaptive.motion:onStart'); + localMediaStream = stream; + video.src = window.URL.createObjectURL(stream); + video.addEventListener('play', function() { + requestId = window.requestAnimationFrame(redraw); + }); + }, + function(){ + $rootScope.$broadcast('adaptive.motion:onError', 'Access denied!'); + throw new Error('Access denied!'); + } + ); + } + else { + $rootScope.$broadcast('adaptive.motion:onError', 'getUserMedia() is not supported in your browser'); + throw new Error('getUserMedia() is not supported in your browser'); + } + }; + + var stop = function(){ + window.cancelAnimationFrame(requestId); + if (localMediaStream) { + localMediaStream.stop(); + } + localMediaStream = undefined; + $rootScope.$broadcast('adaptive.motion:onStop'); + }; + + var redraw = function() { + if (canvas.width !== video.videoWidth){ + width = Math.floor(video.videoWidth / compression); + height = Math.floor(video.videoHeight / compression); + canvas.width = width; + canvas.height = height; + } + + try { + context.drawImage(video,0,0,width,height); + draw = context.getImageData(0, 0, width, height); + $rootScope.$broadcast('adaptive.motion:videoData', draw); + var skinFilter = filterSkin(draw); + lastDraw = getMovements(skinFilter); + + requestId = window.requestAnimationFrame(redraw); + } + catch (e) { + if (e.name === 'NScontextERRORcontextNOTcontextAVAILABLE') { + requestId = window.requestAnimationFrame(redraw); + } + else { + throw e; + } + } + }; + + /** + * Filters skin from video image data + * @param {ImageData} video + * @return {ImageData} + */ + var filterSkin = function(video){ + + var skinFilter = context.getImageData(0,0,width,height); + var totalPixels = skinFilter.width * skinFilter.height; + var index = totalPixels * 4; + + var pix = 0; + for (var y=0; y 0.59 && hsv[0] < 1.0) + //Skin Range on HSV values + if(((hsv[0] > hsvFilter.huemin && hsv[0] < hsvFilter.huemax)||(hsv[0] > 0.59 && hsv[0] < 1.0))&&(hsv[1] > hsvFilter.satmin && hsv[1] < hsvFilter.satmax)&&(hsv[2] > hsvFilter.valmin && hsv[2] < hsvFilter.valmax)){ + skinFilter[pix] = r; + skinFilter[pix+1] = g; + skinFilter[pix+2] = b; + skinFilter[pix+3] = a; + } + else{ + skinFilter.data[pix] = 255; + skinFilter.data[pix+1] = 255; + skinFilter.data[pix+2] = 255; + skinFilter.data[pix+3] = 255; + } + + pix = index * 4; + } + } + $rootScope.$broadcast('adaptive.motion:skinData', skinFilter); + return skinFilter; + }; + + /** + * Gets movement data + * @param {ImageData} draw + * @return {ImageData} + */ + var getMovements = function(draw){ + var edge = context.createImageData(width, height); + var totalx = 0; + var totaly = 0; + var changed = 0; + var pix = edge.width * edge.height * 4; + + if (lastDraw){ + + while ((pix -= 4) > 0) { + var rgbaDelta = Math.abs(draw.data[pix] - lastDraw.data[pix]) + + Math.abs(draw.data[pix+1] - lastDraw.data[pix+1]) + + Math.abs(draw.data[pix+2] - lastDraw.data[pix+2]); + + if (rgbaDelta > treshold.rgb){ + edge.data[pix] = 0; + edge.data[pix+1] = 0; + edge.data[pix+2] = 0; + edge.data[pix+3] = 255; + changed += 1; + totalx += (pix/4) % width; + totaly += Math.floor((pix/4) / edge.height); + } + else { + edge.data[pix] = 255; + edge.data[pix+1] = 255; + edge.data[pix+2] = 255; + edge.data[pix+3] = 255; + } + } + } + + if (changed){ + $rootScope.$broadcast('adaptive.motion:edgeData', edge); + + var down = { + x: totalx / changed, + y: totaly / changed, + d: changed + }; + recognizeGesture(down); + } + + return draw; + }; + + /** + * Sets last down + * @param {Object} down + */ + var setLastDown = function(down){ + lastDown = { + x: down.x, + y: down.y, + d: down.d + }; + }; + + var avg = 0; + var state = 0; //States: 0 waiting for gesture, 1 waiting for next move after gesture, 2 waiting for gesture to end + + /** + * Recognizes gesture + * @param {Object} down + */ + var recognizeGesture = function(down){ + avg = 0.9 * avg + 0.1 * down.d; + var davg = down.d - avg; + var foundGesture = davg > treshold.bright; + + switch (state){ + case 0: + if (foundGesture){ //Found a gesture, waiting for next move + state = 1; + setLastDown(down); + } + break; + case 2: //Wait for gesture to end + if (!foundGesture){ //Gesture ended + state = 0; + } + break; + case 1: //Got next move, do something based on direction + var dx = down.x - lastDown.x; + var dy = down.y - lastDown.y; + var dirx = Math.abs(dy) < Math.abs(dx) - treshold.move; + var diry = Math.abs(dx) < Math.abs(dy) - treshold.move; + // console.log(dx, dy, dirx); + + if (dirx) { + if (dx < - treshold.move){ + $rootScope.$broadcast('adaptive.motion:onSwipeRight'); + } + else if (dx > treshold.move){ + $rootScope.$broadcast('adaptive.motion:onSwipeLeft'); + } + } + else if (diry) { + if (dy > treshold.move){ + $rootScope.$broadcast('adaptive.motion:onSwipeDown'); + } + else if (dy < - treshold.move){ + $rootScope.$broadcast('adaptive.motion:onSwipeUp'); + } + } + + state = 2; + break; + } + }; + + var onStart = function(cb){ + $rootScope.$on('adaptive.motion:onStart', function(e, data){ + cb(data); + }); + }; + + var onStop = function(cb){ + $rootScope.$on('adaptive.motion:onStop', function(e, data){ + cb(data); + }); + }; + + var onError = function(cb){ + $rootScope.$on('adaptive.motion:onError', function(e, data){ + cb(data); + }); + }; + + var onSwipeLeft = function(cb){ + $rootScope.$on('adaptive.motion:onSwipeLeft', function(e, data){ + cb(data); + }); + }; + + var onSwipeRight = function(cb){ + $rootScope.$on('adaptive.motion:onSwipeRight', function(e, data){ + cb(data); + }); + }; + + var onSwipeUp = function(cb){ + $rootScope.$on('adaptive.motion:onSwipeUp', function(e, data){ + cb(data); + }); + }; + + var onSwipeDown = function(cb){ + $rootScope.$on('adaptive.motion:onSwipeDown', function(e, data){ + cb(data); + }); + }; + + return { + start: function(){ + start(); + }, + stop: function(){ + stop(); + }, + onStart: function(cb){ + onStart(cb); + }, + onStop: function(cb){ + onStop(cb); + }, + onError: function(cb){ + onError(cb); + }, + onSwipeLeft: function(cb){ + onSwipeLeft(cb); + }, + onSwipeRight: function(cb){ + onSwipeRight(cb); + }, + onSwipeUp: function(cb){ + onSwipeUp(cb); + }, + onSwipeDown: function(cb){ + onSwipeDown(cb); + } + }; + }; +}]); + +adaptive.directive('adaptiveMotion', ['$rootScope', function ($rootScope) { + return { + restrict: 'A', + link: function postLink(scope, element, attrs) { + var canvas = element[0]; + var context = canvas.getContext('2d'); + + if (attrs['adaptiveMotion'] === 'video'){ + $rootScope.$on('adaptive.motion:videoData', function(e, data){ + context.putImageData(data, 0, 0); + }); + } + else if (attrs['adaptiveMotion'] === 'skin'){ + $rootScope.$on('adaptive.motion:skinData', function(e, data){ + context.putImageData(data, 0, 0); + }); + } + else { + $rootScope.$on('adaptive.motion:edgeData', function(e, data){ + context.putImageData(data, 0, 0); + }); + } + } + }; +}]); + +})(); \ No newline at end of file diff --git a/test/test.conf.js b/test/test.conf.js index 6734b50..c4cb038 100644 --- a/test/test.conf.js +++ b/test/test.conf.js @@ -4,7 +4,7 @@ module.exports = function(config) { files: [ 'bower_components/angular/angular.js', 'bower_components/angular-mocks/angular-mocks.js', - 'src/adaptive-motion.js', + 'src/angular-adaptive-motion.js', 'test/*.spec.js' ], frameworks: ['jasmine'],