├── 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 |
28 |
29 |
30 |
31 | Your browser doesn't support the HTML5 video tag.
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Google
42 |
44 |
45 |
46 |
47 |
Loading assets...
48 |
49 |
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
--------------------------------------------------------------------------------