├── README.md ├── app.html ├── images ├── favicon.ico ├── icon.png └── mediastream.png ├── js ├── app.js └── sample.js ├── styles └── main.css └── videos ├── jsdetection.jpg ├── jsdetection.mp4 ├── jsdetection.ogv └── jsdetection.webm /README.md: -------------------------------------------------------------------------------- 1 | JS Motion Detection 2 | =================== 3 | 4 | This project tries to provide as a basis for other motion-detection apps, by separating the framework from its implementations. 5 | 6 | The app uses WebRTC, HTML5 Canvas and some math, and is refactored out of the [Magic Xylophone](http://www.soundstep.com/blog/experiments/jsdetection/) by [soundstep](https://github.com/soundstep). 7 | You can learn more about the process fom Romuald himself [on Adobe Developer Connection](http://www.adobe.com/devnet/html5/articles/javascript-motion-detection.html) and [his blog post](http://www.soundstep.com/blog/2012/03/22/javascript-motion-detection/). 8 | 9 | ### Usage 10 | Any child element you add inside `
` will automagically work as a hotSpot, which means it will fire `motion` events on itself with extra data upon motion detection. 11 | 12 | You could control its appearance & position with CSS, and hook to the `motion` events with jQuery / plain JS. 13 | 14 | #### Sample Usage & DEMO 15 | 16 | * See some examples in [sample.js](https://github.com/ReallyGood/js-motion-detection/blob/master/js/sample.js) 17 | * Open up the demo **[here](http://reallygood.co.il/plugins/motion/)**. Hit Allow, check the console. 18 | 19 | #### License 20 | [Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License](http://creativecommons.org/licenses/by-nc-sa/3.0/) -------------------------------------------------------------------------------- /app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JavaScript Motion Detection 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 21 | 22 |
23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | Google 42 | 44 |
45 |
46 | 47 |
Loading assets...
48 | 49 |
Back
50 | 51 |
52 |

This demo lets you control stuff using motion detection in your browser.

53 | 54 |

Please allow access to your camera and microphone above.

55 |
56 | 57 |
58 |

This demo lets you control stuff using motion detection in your browser.

59 | 60 |

Install Chrome Beta to play, or watch this video to check it out first.

62 |
63 | 64 | 70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/images/favicon.ico -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/images/icon.png -------------------------------------------------------------------------------- /images/mediastream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/images/mediastream.png -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // config start 4 | var OUTLINES = false; 5 | // config end 6 | 7 | window.hotSpots = []; 8 | 9 | var content = $('#content'); 10 | var video = $('#webcam')[0]; 11 | var canvases = $('canvas'); 12 | 13 | var resize = function () { 14 | var ratio = video.width / video.height; 15 | var w = $(this).width(); 16 | var h = $(this).height() - 110; 17 | 18 | if (content.width() > w) { 19 | content.width(w); 20 | content.height(w / ratio); 21 | } else { 22 | content.height(h); 23 | content.width(h * ratio); 24 | } 25 | canvases.width(content.width()); 26 | canvases.height(content.height()); 27 | content.css('left', (w - content.width()) / 2); 28 | content.css('top', ((h - content.height()) / 2) + 55); 29 | } 30 | $(window).resize(resize); 31 | $(window).ready(function () { 32 | resize(); 33 | $('#watchVideo').click(function () { 34 | $(".browsers").fadeOut(); 35 | $(".browsersWithVideo").delay(300).fadeIn(); 36 | $("#video-demo").delay(300).fadeIn(); 37 | $("#video-demo")[0].play(); 38 | $('.backFromVideo').fadeIn(); 39 | event.stopPropagation(); 40 | return false; 41 | }); 42 | $('.backFromVideo a').click(function () { 43 | $(".browsersWithVideo").fadeOut(); 44 | $('.backFromVideo').fadeOut(); 45 | $(".browsers").fadeIn(); 46 | $("#video-demo")[0].pause(); 47 | $('#video-demo').fadeOut(); 48 | event.stopPropagation(); 49 | return false; 50 | }); 51 | }); 52 | 53 | function hasGetUserMedia() { 54 | // Note: Opera builds are unprefixed. 55 | return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || 56 | navigator.mozGetUserMedia || navigator.msGetUserMedia); 57 | } 58 | 59 | if (hasGetUserMedia()) { 60 | $('.introduction').fadeIn(); 61 | $('.allow').fadeIn(); 62 | } else { 63 | $('.browsers').fadeIn(); 64 | return; 65 | } 66 | 67 | var webcamError = function (e) { 68 | alert('Webcam error!', e); 69 | }; 70 | 71 | if (navigator.getUserMedia) { 72 | navigator.getUserMedia({audio: true, video: true}, function (stream) { 73 | video.src = stream; 74 | initialize(); 75 | }, webcamError); 76 | } else if (navigator.webkitGetUserMedia) { 77 | navigator.webkitGetUserMedia({audio: true, video: true}, function (stream) { 78 | video.src = window.webkitURL.createObjectURL(stream); 79 | initialize(); 80 | }, webcamError); 81 | } else { 82 | //video.src = 'somevideo.webm'; // fallback. 83 | } 84 | 85 | var lastImageData; 86 | var canvasSource = $("#canvas-source")[0]; 87 | var canvasBlended = $("#canvas-blended")[0]; 88 | 89 | var contextSource = canvasSource.getContext('2d'); 90 | var contextBlended = canvasBlended.getContext('2d'); 91 | 92 | // mirror video 93 | contextSource.translate(canvasSource.width, 0); 94 | contextSource.scale(-1, 1); 95 | 96 | var c = 5; 97 | 98 | function initialize() { 99 | $('.introduction').fadeOut(); 100 | $('.allow').fadeOut(); 101 | $('.loading').delay(300).fadeIn(); 102 | start(); 103 | } 104 | 105 | function start() { 106 | $('.loading').fadeOut(); 107 | $('#hotSpots').fadeIn(); 108 | $('body').addClass('black-background'); 109 | $(".instructions").delay(600).fadeIn(); 110 | $(canvasSource).delay(600).fadeIn(); 111 | $(canvasBlended).delay(600).fadeIn(); 112 | $('#canvas-highlights').delay(600).fadeIn(); 113 | $(window).trigger('start'); 114 | update(); 115 | } 116 | 117 | window.requestAnimFrame = (function () { 118 | return window.requestAnimationFrame || 119 | window.webkitRequestAnimationFrame || 120 | window.mozRequestAnimationFrame || 121 | window.oRequestAnimationFrame || 122 | window.msRequestAnimationFrame || 123 | function (callback) { 124 | window.setTimeout(callback, 1000 / 60); 125 | }; 126 | })(); 127 | 128 | function update() { 129 | drawVideo(); 130 | blend(); 131 | checkAreas(); 132 | requestAnimFrame(update); 133 | } 134 | 135 | function drawVideo() { 136 | contextSource.drawImage(video, 0, 0, video.width, video.height); 137 | } 138 | 139 | function blend() { 140 | var width = canvasSource.width; 141 | var height = canvasSource.height; 142 | // get webcam image data 143 | var sourceData = contextSource.getImageData(0, 0, width, height); 144 | // create an image if the previous image doesn’t exist 145 | if (!lastImageData) lastImageData = contextSource.getImageData(0, 0, width, height); 146 | // create a ImageData instance to receive the blended result 147 | var blendedData = contextSource.createImageData(width, height); 148 | // blend the 2 images 149 | differenceAccuracy(blendedData.data, sourceData.data, lastImageData.data); 150 | // draw the result in a canvas 151 | contextBlended.putImageData(blendedData, 0, 0); 152 | // store the current webcam image 153 | lastImageData = sourceData; 154 | } 155 | 156 | function fastAbs(value) { 157 | // funky bitwise, equal Math.abs 158 | return (value ^ (value >> 31)) - (value >> 31); 159 | } 160 | 161 | function threshold(value) { 162 | return (value > 0x15) ? 0xFF : 0; 163 | } 164 | 165 | function difference(target, data1, data2) { 166 | // blend mode difference 167 | if (data1.length != data2.length) return null; 168 | var i = 0; 169 | while (i < (data1.length * 0.25)) { 170 | target[4 * i] = data1[4 * i] == 0 ? 0 : fastAbs(data1[4 * i] - data2[4 * i]); 171 | target[4 * i + 1] = data1[4 * i + 1] == 0 ? 0 : fastAbs(data1[4 * i + 1] - data2[4 * i + 1]); 172 | target[4 * i + 2] = data1[4 * i + 2] == 0 ? 0 : fastAbs(data1[4 * i + 2] - data2[4 * i + 2]); 173 | target[4 * i + 3] = 0xFF; 174 | ++i; 175 | } 176 | } 177 | 178 | function differenceAccuracy(target, data1, data2) { 179 | if (data1.length != data2.length) return null; 180 | var i = 0; 181 | while (i < (data1.length * 0.25)) { 182 | var average1 = (data1[4 * i] + data1[4 * i + 1] + data1[4 * i + 2]) / 3; 183 | var average2 = (data2[4 * i] + data2[4 * i + 1] + data2[4 * i + 2]) / 3; 184 | var diff = threshold(fastAbs(average1 - average2)); 185 | target[4 * i] = diff; 186 | target[4 * i + 1] = diff; 187 | target[4 * i + 2] = diff; 188 | target[4 * i + 3] = 0xFF; 189 | ++i; 190 | } 191 | } 192 | 193 | function checkAreas() { 194 | var data; 195 | for (var h = 0; h < hotSpots.length; h++) { 196 | var blendedData = contextBlended.getImageData(hotSpots[h].x, hotSpots[h].y, hotSpots[h].width, hotSpots[h].height); 197 | var i = 0; 198 | var average = 0; 199 | while (i < (blendedData.data.length * 0.25)) { 200 | // make an average between the color channel 201 | average += (blendedData.data[i * 4] + blendedData.data[i * 4 + 1] + blendedData.data[i * 4 + 2]) / 3; 202 | ++i; 203 | } 204 | // calculate an average between the color values of the spot area 205 | average = Math.round(average / (blendedData.data.length * 0.25)); 206 | if (average > 10) { 207 | // over a small limit, consider that a movement is detected 208 | data = {confidence: average, spot: hotSpots[h]}; 209 | $(data.spot.el).trigger('motion', data); 210 | } 211 | } 212 | } 213 | 214 | function getCoords() { 215 | $('#hotSpots').children().each(function (i, el) { 216 | var ratio = $("#canvas-highlights").width() / $('video').width(); 217 | hotSpots[i] = { 218 | x: this.offsetLeft / ratio, 219 | y: this.offsetTop / ratio, 220 | width: this.scrollWidth / ratio, 221 | height: this.scrollHeight / ratio, 222 | el: el 223 | }; 224 | }); 225 | if (OUTLINES) highlightHotSpots(); 226 | } 227 | 228 | $(window).on('start resize', getCoords); 229 | 230 | function highlightHotSpots() { 231 | var canvas = $("#canvas-highlights")[0]; 232 | var ctx = canvas.getContext('2d'); 233 | canvas.width = canvas.width; 234 | hotSpots.forEach(function (o, i) { 235 | ctx.strokeStyle = 'rgba(0,255,0,0.6)'; 236 | ctx.lineWidth = 1; 237 | ctx.strokeRect(o.x, o.y, o.width, o.height); 238 | }); 239 | } 240 | })(); -------------------------------------------------------------------------------- /js/sample.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | // consider using a debounce utility if you get too many consecutive events 3 | $(window).on('motion', function(ev, data){ 4 | console.log('detected motion at', new Date(), 'with data:', data); 5 | var spot = $(data.spot.el); 6 | spot.addClass('active'); 7 | setTimeout(function(){ 8 | spot.removeClass('active'); 9 | }, 230); 10 | }); 11 | 12 | // example using a class 13 | $('.link').on('motion', function(ev, data){ 14 | console.log('motion detected on a link to', data.spot.el.href); 15 | }); 16 | 17 | // examples for id usage 18 | $('#one').on('motion', function(){ 19 | console.log('touched one'); 20 | }); 21 | 22 | $('#another').on('motion', function(){ 23 | console.log('another'); 24 | }); 25 | })(); -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #FBFBF8; 3 | font-family: 'Open Sans', sans-serif; 4 | font-size: 12px; 5 | padding: 0; 6 | margin: 0; 7 | color: #333; 8 | } 9 | 10 | .black-background { 11 | background-color: #000; 12 | -webkit-transition: background-color 200ms ease 600ms; 13 | -moz-transition: background-color 200ms ease 600ms; 14 | -o-transition: background-color 200ms ease 600ms; 15 | -ms-transition: background-color 200ms ease 600ms; 16 | transition: background-color 200ms ease 600ms; 17 | } 18 | 19 | video { 20 | display: none; 21 | } 22 | 23 | #container { 24 | 25 | } 26 | 27 | #content { 28 | position: absolute; 29 | left: 0; 30 | right: 0; 31 | top: 0; 32 | bottom: 0; 33 | overflow: hidden; 34 | z-index: 0; 35 | } 36 | 37 | .loading { 38 | display: none; 39 | position: absolute; 40 | font-size: 20px; 41 | text-align: center; 42 | width: 200px; 43 | left: 50%; 44 | margin-left: -100px; 45 | top: 50%; 46 | margin-top: -40px; 47 | } 48 | 49 | .browsers, 50 | .introduction { 51 | display: none; 52 | position: absolute; 53 | width: 100%; 54 | text-align: center; 55 | font-size: 20px; 56 | top: 50%; 57 | margin-top: -27px; 58 | } 59 | 60 | .browsers p, 61 | .introduction p { 62 | margin: 0 0 0.1em 0; 63 | padding: 0; 64 | } 65 | 66 | .browsers a { 67 | color: #333; 68 | } 69 | 70 | #header { 71 | position: absolute; 72 | height: 55px; 73 | top: 0; 74 | left: 0; 75 | right: 0; 76 | overflow: hidden; 77 | z-index: 2; 78 | color: #aaa; 79 | font-size: 20px; 80 | } 81 | 82 | .browsersWithVideo, 83 | .instructions, 84 | .allow { 85 | display: none; 86 | line-height: 55px; 87 | margin-left: 20px; 88 | } 89 | 90 | .browsersWithVideo { 91 | font-size: 15px; 92 | } 93 | 94 | .browsersWithVideo a { 95 | color: #AAA; 96 | } 97 | 98 | .backFromVideo { 99 | display: none; 100 | position: absolute; 101 | width: 100%; 102 | bottom: 64px; 103 | text-align: center; 104 | font-size: 14px; 105 | } 106 | 107 | .backFromVideo a { 108 | color: #333; 109 | } 110 | 111 | #footer { 112 | position: absolute; 113 | height: 55px; 114 | bottom: 0; 115 | left: 0; 116 | right: 0; 117 | overflow: hidden; 118 | z-index: 1; 119 | } 120 | 121 | #footer a { 122 | color: #aaa; 123 | text-decoration: underline; 124 | } 125 | 126 | #footer a:hover { 127 | color: #ccd56d; 128 | text-decoration: underline; 129 | } 130 | 131 | .magic { 132 | float: left; 133 | padding-left: 20px; 134 | font-size: 20px; 135 | color: #AAA; 136 | line-height: 55px; 137 | } 138 | 139 | #video-demo { 140 | display: none; 141 | position: absolute; 142 | left: 50%; 143 | top: 50%; 144 | margin-left: -320px; 145 | margin-top: -240px; 146 | } 147 | 148 | #canvas-source { 149 | width: 100%; 150 | } 151 | 152 | #canvas-source, #canvas-highlights { 153 | display: none; 154 | position: absolute; 155 | left: 0; 156 | top: 0; 157 | } 158 | 159 | #canvas-blended { 160 | display: none; 161 | position: absolute; 162 | bottom: 0; 163 | right: 0; 164 | opacity: 0; 165 | } 166 | 167 | #hotSpots { 168 | position: relative; 169 | display: none; 170 | height: 100%; 171 | } 172 | 173 | #hotSpots > * { 174 | position: absolute; 175 | -moz-transition: all 80ms linear; 176 | -webkit-transition: all 80ms linear; 177 | transition: all 80ms linear; 178 | } 179 | 180 | /* Feel free to change this or turn it off altogether*/ 181 | .active { 182 | background: rgba(255, 255, 255, 0.7); 183 | -moz-transition: all 150ms linear; 184 | -webkit-transition: all 150ms linear; 185 | transition: all 150ms linear; 186 | } 187 | 188 | /* Sample hotspots */ 189 | #one { 190 | width: 10%; 191 | height: 20%; 192 | top: 50%; 193 | left: 0; 194 | margin-top: -10%; 195 | } 196 | 197 | #another { 198 | width: 20%; 199 | height: 10%; 200 | top: 0; 201 | left: 50%; 202 | margin-left: -10%; 203 | } 204 | 205 | .link { 206 | font-size: 22px; 207 | color: #fff; 208 | padding: .5%; 209 | } -------------------------------------------------------------------------------- /videos/jsdetection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/videos/jsdetection.jpg -------------------------------------------------------------------------------- /videos/jsdetection.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/videos/jsdetection.mp4 -------------------------------------------------------------------------------- /videos/jsdetection.ogv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/videos/jsdetection.ogv -------------------------------------------------------------------------------- /videos/jsdetection.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReallyGood/js-motion-detection/b9f1a0bce5b3422c591b93c7728ec8b3369fb1a5/videos/jsdetection.webm --------------------------------------------------------------------------------