-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathREADME.html
501 lines (488 loc) · 20.6 KB
/
README.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
<!-- THIS IS A GENERATED FILE - DO NOT EDIT -->
<!doctype html>
<html>
<head>
<style>
/* From https://github.com/johnmdonahue/git_marked */
html {
font-size: 14px;
line-height: 1.6;
font-family: helvetica,arial,freesans,clean,sans-serif;
color: black;
}
body {
box-shadow: 0px 0px 0px 3px #eee;
border: 1px solid #CACACA;
width: 852px;
padding: 30px;
margin: auto;
margin-top: 20px;
}
h1 {
margin: 15px 0;
padding-bottom: 2px;
font-size: 24px;
border-bottom: 1px solid #EEE;
}
h2 {
margin: 20px 0 10px 0;
font-size: 18px;
}
h3 {
margin: 20px 0 10px 0;
padding-bottom: 2px;
font-size: 14px;
border-bottom: 1px solid #DDD;
}
h4 {
font-size: 14px;
line-height: 26px;
padding: 18px 0 4px;
font-weight: bold;
text-transform: uppercase;
}
h5 {
font-size: 13px;
line-height: 26px;
padding: 14px 0 0;
font-weight: bold;
text-transform: uppercase;
}
h6 {
color: #666;
font-size: 14px;
line-height: 26px;
padding: 18px 0 0;
font-weight: normal;
font-variant: italic;
}
br+br {
line-height:0;
height:0;
display:none;
}
p {
margin: 1em 0;
}
blockquote {
margin: 14px 0;
border-left: 4px solid #DDD;
padding-left: 11px;
color: #555;
}
pre, code {
font-family: 'Bitstream Vera Sans Mono','Courier',monospace;
}
pre {
background-color: #F8F8F8;
border: 1px solid #CCC;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px;
color: black;
}
code {
margin: 0 2px;
padding: 2px 5px;
white-space: nowrap;
border: 1px solid #CCC;
background-color: #F8F8F8;
border-radius: 3px;
font-size: 12px !important;
}
pre > code {
margin: 0px;
padding: 0px;
white-space: pre;
border: none;
background-color: transparent;
border-radius: 0;
}
a, a code {
color: #4183C4;
text-decoration:none;
}
a:hover, a code:hover {
text-decoration:underline;
}
table {
border-collapse: collapse;
margin: 20px 0 0;
padding: 0;
}
table tr th, table tr td {
border: 1px solid #CCC;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tbody tr:nth-child(2n-1) {
background-color: #F8F8F8;
}
.c{color:#998;font-style:italic;}
.err{color:#a61717;background-color:#e3d2d2;}
.k{font-weight:bold;}
.o{font-weight:bold;}
.cm{color:#998;font-style:italic;}
.cp{color:#999;font-weight:bold;}
.c1{color:#998;font-style:italic;}
.cs{color:#999;font-weight:bold;font-style:italic;}
.gd{color:#000;background-color:#fdd;}
.gd .x{color:#000;background-color:#faa;}
.ge{font-style:italic;}
.gr{color:#a00;}
.gh{color:#999;}
.gi{color:#000;background-color:#dfd;}
.gi .x{color:#000;background-color:#afa;}
.go{color:#888;}
.gp{color:#555;}
.gs{font-weight:bold;}
.gu{color:#800080;font-weight:bold;}
.gt{color:#a00;}
.kc{font-weight:bold;}
.kd{font-weight:bold;}
.kn{font-weight:bold;}
.kp{font-weight:bold;}
.kr{font-weight:bold;}
.kt{color:#458;font-weight:bold;}
.m{color:#099;}
.s{color:#d14;}
.na{color:#008080;}
.nb{color:#0086B3;}
.nc{color:#458;font-weight:bold;}
.no{color:#008080;}
.ni{color:#800080;}
.ne{color:#900;font-weight:bold;}
.nf{color:#900;font-weight:bold;}
.nn{color:#555;}
.nt{color:#000080;}
.nv{color:#008080;}
.ow{font-weight:bold;}
.w{color:#bbb;}
.mf{color:#099;}
.mh{color:#099;}
.mi{color:#099;}
.mo{color:#099;}
.sb{color:#d14;}
.sc{color:#d14;}
.sd{color:#d14;}
.s2{color:#d14;}
.se{color:#d14;}
.sh{color:#d14;}
.si{color:#d14;}
.sx{color:#d14;}
.sr{color:#009926;}
.s1{color:#d14;}
.ss{color:#990073;}
.bp{color:#999;}
.vc{color:#008080;}
.vg{color:#008080;}
.vi{color:#008080;}
.il{color:#099;}
</style>
</head>
<body>
<h1 id="no-video-no-problem-creating-interactive-video-effects-with-javascript">No video, no problem: Creating interactive video effects with JavaScript</h1>
<p>Matt Campbell shows how JavaScript and a few image layers can produce
impressive video-quality animations you can enhance with interactive elements
like motion controls and head-tracking.</p>
<h2 id="details">Details</h2>
<p>Knowledge Needed: Basic HTML, CSS, JavaScript</p>
<p>Requires: Text editor and browser; reference images</p>
<p>Project Time: 45 minutes</p>
<p>Callout: Download the files! All the files you need for this tutorial can be
found at
<a href="https://github.com/growcode/tutorial-js-lighting">https://github.com/growcode/tutorial-js-lighting</a></p>
<p>ZIP download: <a href="https://github.com/growcode/tutorial-js-lighting/archive/master.zip">https://github.com/growcode/tutorial-js-lighting/archive/master.zip</a></p>
<p>Modern web users have embraced video, but developers often find
video integration problematic due to cross-device/platform limitations (most
mobile browsers do not support auto-play for inline video) and fewer
opportunities to infuse playback with user interactivity. With some fairly
simple methods, you can replicate video-like effects without needing to
transcode large files or fight auto-play restrictions on mobile devices.</p>
<p>This guide will step through some simple techniques to create dynamic lighting
effects by progressively adjusting the opacity of three image layers. The
resulting effect will look like as smooth as video -- check out the demos here:
<a href="https://growcode.github.io/tutorial-js-lighting/demos/">https://growcode.github.io/tutorial-js-lighting/demos/</a>.
As a bonus we'll also show some ways to enhance these animations
with a few fun opportunities for user interactions.</p>
<h2 id="image-setup">Image setup</h2>
<p>First you'll need to create 3 images of the same object/scene with different
lighting configurations. The first image should show the initial state of the
effect, the second image will be the middle transition, and the third image
will be the final state.</p>
<h2 id="stacking-the-images">Stacking the images</h2>
<ol>
<li>Set up a <code><div></code> container element that has <code>position: relative</code> style.</li>
<li>Add each <code><img></code> inside the container with <code>position: absolute</code>.</li>
</ol>
<p>The <code>absolute</code> positioning will allow the images to stack on top of each other
while the container's <code>relative</code> style prevents the images from escaping to
the top of the page.</p>
<p>The HTML and CSS code should look something like:</p>
<pre><code><!doctype html>
<html>
<head>
<style>
#container { position: relative; }
#container img { position: absolute; }
</style>
<body>
<div id="container">
<img src="path/to/image1.jpg"/>
<img src="path/to/image2.jpg"/>
<img src="path/to/image3.jpg"/>
</div>
<script>
// we will be putting more code here
</script>
</body>
</html>
</code></pre><h2 id="tracking-the-images-in-code">Tracking the images in code</h2>
<p>We need a very basic structure in the code to track and fade the images.
This structure can also keep track of the animation's progress. It looks like:</p>
<pre><code>function Shader(images) {
this.images = images;
}
</code></pre><p>The constructor here just accepts a <code>NodeList</code> of images we will be tracking.
Using the handy querySelectorAll function
(<a href="https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll">https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll</a>)
to get the images from the DOM, we can construct our object like so:</p>
<pre><code>var container = document.getElementById('container');
var shader = new Shader(container.querySelectorAll('img'));
</code></pre><h2 id="opacity-changes">Opacity changes</h2>
<p>The lighting effect is achieved by showing the image on the bottom of the
stack first, unmodified. In fact the bottom image is never faded at all -
it is just what the user sees first.</p>
<p>As the animation progresses, we gradually fade in the images from
bottom-to-top, so the user will see the middle image fade in first,
followed by the top image a few moments later.</p>
<table>
<thead>
<tr>
<th><center><img alt="Figure 1" title="Figure 1" src="images/figure01.png" width="687" height="331"/></center></th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Flurry of Fades</b> <ol><li>We start out with our back-most image at full opacity and each of the other two images layered on top with lesser opacity.</li><li>As we progress, the second image layer comes into full opacity, obscuring the back-most layer.</li><li>In our final state, the third image has become fully opaque, representing the final state of our animation.</li></ol></td>
</tr>
</tbody>
</table>
<p>We can do this by tracking the <code>progress</code> of the animation from 0 to 1
(0% to 100%, if you like). Based on that <code>progress</code> variable we can fade the
top images appropriately so that the middle image finishes its fade-in before
the final image:</p>
<pre><code>function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
Shader.prototype.update = function (progress) {
this.images[1].style.opacity = clamp(progress - 0.33, 0, 0.33) / 0.33;
this.images[2].style.opacity = clamp(progress - 0.66, 0, 0.33) / 0.33;
};
</code></pre><p>The <code>clamp</code> function here simply keeps <code>progress</code> within the bounds of <code>0</code>
and <code>0.33</code> (the max it will be divided by). This is necessary because before
the animation is at 66% progress, the final image would calculate
<code>progress - 0.66</code> as a negative number (e.g. <code>-0.66</code>) -- it's
better to just keep it at <code>0</code> in that case since that image is not supposed to
be fading yet. This will delay the fading of that image until the animation
progress is beyond 66% (the only time <code>progress - 0.66</code> would be positive).</p>
<p>In the <code>update</code> function, the first opacity line instructs the middle image to
fade in between 33% and 66% of the animation. The final image starts fading at
66% and finishes at 99% (basically the end).</p>
<h2 id="tweening">Tweening</h2>
<p>Now that our animation is able to react to a <code>progress</code> variable, it's time
to animate that variable. Applying the current time to a sine wave is an easy
way to do this, providing a natural bounce from 0 to 1.</p>
<pre><code>/* Define the animation loop */
function animate() {
/* Get sine for current time */
var angle = Math.sin(Date.now() * 0.002);
/* Normalize sine to 0 through 1 */
var progress = (1 + angle) * 0.5;
/* Ask animation to update the fades */
shader.update(progress);
/* Repeat! */
requestAnimationFrame(animate);
}
/* Begin animation */
animate();
</code></pre><p>First we get the current time in milliseconds using <code>Date.now()</code>. Multiplying
it by <code>0.002</code> will slow down the "bounce" of the sine wave.</p>
<p>Our animation expects a range of <code>0 to 1</code>. Since sine waves are in the
<code>-1 to 1</code> range we have to normalize the range using <code>(1 + angle) * 0.5</code>.</p>
<p>Next, we pass the resulting <code>progress</code> variable to our animation to update
the fades of the images.</p>
<p>Finally we use requestAnimationFrame
(<a href="https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame">https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame</a>)
to call a function the next time the browser paints the
window (usually synced to your monitor's refresh rate). If your browser
doesn't support this, you can just use <code>setTimeout</code> instead:</p>
<pre><code>setTimeout(animate, 16); // 16ms = ~60fps
</code></pre><h2 id="animating-other-properties">Animating other properties</h2>
<p>Animating the opacity is already an interesting effect, but you can animate
any CSS property in the same way. One cool addition is to add a
background image that moves along with the <code>progress</code> variable. This would
create a "parallax" effect, adding even more depth to the scene. Check out
this example: <a href="https://growcode.github.io/tutorial-js-lighting/demos/parallax.html">https://growcode.github.io/tutorial-js-lighting/demos/parallax.html</a></p>
<table>
<thead>
<tr>
<th><center><img alt="Figure 2" title="Figure 2" src="images/figure02.png" width="687" height="500"/></center></th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Title</b> <ol><li>List Item</li><li>List Item</li></ol></td>
</tr>
</tbody>
</table>
<p>First we need to update the CSS to put a background image in the container:</p>
<pre><code>#container {
position: relative;
width:800px;
height:800px;
overflow:hidden;
background: url(images/background.png);
}
</code></pre><p>Since the background image will be scrolling, it must be wider than the image
container. The CSS code forces the container to stay <code>800x800</code> in size and
the <code>overflow:hidden</code> hides the part of the background image that falls
outside that container.</p>
<p>In the code, we are already tracking the <code>container</code> DOM object. We will now be
animating the <code>backgroundPosition</code> style to move the background image.</p>
<p>In the previous <code>animate()</code> function, we can update this property right
after the <code>shader.update(progress)</code> call:</p>
<pre><code>function animate() {
var angle = Math.sin(Date.now() * 0.002);
var progress = (1 + angle) * 0.5;
shader.update(progress);
/* Move the background image */
container.style.backgroundPosition = -50 + progress * 50 + 'px 0';
requestAnimationFrame(animate);
}
</code></pre><p>As the animation progresses, this will will move the background 50 pixels.</p>
<table>
<thead>
<tr>
<th><center><img alt="Figure 3" title="Figure 3" src="images/figure03.png" width="750" height="400"/></center></th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Title</b> <ol><li>List Item</li><li>List Item</li></ol></td>
</tr>
</tbody>
</table>
<h2 id="adding-interactivity">Adding interactivity</h2>
<p>Since the animation reacts to the value of the <code>progress</code> variable,
we can choose to adjust that variable based on any input instead of just
bouncing it with a sine formula. One cool and easy input you should try using
is the <a href="https://github.com/auduno/headtrackr/">https://github.com/auduno/headtrackr/</a> library, which
seamlessly adds headtracking to a webpage.</p>
<table>
<thead>
<tr>
<th><center><img alt="Figure 4" title="Figure 4" src="images/figure04.png" width="687" height="331"/></center></th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Title</b> <ol><li>List Item</li><li>List Item</li></ol></td>
</tr>
</tbody>
</table>
<p>By tying the animation progress to the position of the user's face, we can
create the illusion that the user is "looking around" the object. We'll
capture the value of the user's face from the webcam and adjust the light
shading to mimic a real-world interaction.</p>
<p><a href="https://growcode.github.io/tutorial-js-lighting/demos/headtracking.html">https://growcode.github.io/tutorial-js-lighting/demos/headtracking.html</a></p>
<p>First, download the <a href="https://github.com/auduno/headtrackr/blob/master/headtrackr.min.js">https://github.com/auduno/headtrackr/blob/master/headtrackr.min.js</a>
file and reference it in your <code><head></code> tag:</p>
<pre><code><script src="include/headtrackr.js"></script>
</code></pre><p>This will expose a global <code>htracker</code> object that we'll use later.</p>
<p>The headtrackr library needs a <code><video></code> and <code><canvas></code> element to work its
magic. These are actually invisible and we can just add them beneath the
<code>container</code> div:</p>
<pre><code><canvas id="inputCanvas" width="320" height="240" style="display:none"></canvas>
<video id="inputVideo" autoplay loop style="display:none"></video>
</code></pre><p>Now we'll be able to listen for a <code>headtrackingEvent</code> event. The headtrackr
library fires this event any time it has an updated position for the user's
head. That event will tell us the <code>x</code> (horizontal) position of the user's head,
which we will then simplify to the <code>0..1</code> range and pass on to the animation.</p>
<p>This would replace the <code>animate()</code> function we were using before.</p>
<pre><code>/* Initialize headtrackr with the input video/canvas */
var videoInput = document.getElementById('inputVideo');
var canvasInput = document.getElementById('inputCanvas');
var htracker = new headtrackr.Tracker();
htracker.init(videoInput, canvasInput);
/* Listen for when the user's head moves */
document.addEventListener('headtrackingEvent', function (e) {
/* Restrict value range to -10 .. 10 and divide to -1 .. 1 */
var headX = Math.max(-10, Math.min(10, e.x)) * 0.1;
/* Convert range from -1 .. 1 to 0 .. 1 */
var progress = (1 + headX) * 0.5;
/* Update the animation */
shader.update(progress);
});
/* Tell headtrackr we're ready for the events */
htracker.start();
</code></pre><p>Now after headtrackr finds your head, you should be able to move around to
change the light shading animation.</p>
<p>Laptop users can actually move the laptop instead of their head, making it
appear more like a gyroscope-powered effect.</p>
<h1 id="conclusion">Conclusion</h1>
<p>They say necessity is the mother of invention, but in this case we didn't
need a new shiny library or browser update to solve a problem, just a few pre-existing,
dependable techniques. The code involved is neither new nor complex, though hopefully
you'd agree that the end result is pretty neat. It's always fun to apply
what you already know to newer challenges and it's especially cool
to mix newer web technology (headtracking) with old.</p>
<h1 id="boxout-1-why-not-video-">Boxout #1: Why Not Video?</h1>
<p>Video offers similar functionality that this article describes. Specifically,
videos are animations that are normalized to a 0% - 100% progress. Modern HTML5
browsers even expose an API to update a video's progress with JavaScript.</p>
<p>But the power of HTML5 video comes with its own caveats.</p>
<p>First and foremost is the ever-changing land of codecs. Mobile devices are
close to ubiquitously supporting H.264, but the desktop platforms still have
configurations that don't support it. Firefox in particular relies completely
on the OS to provide support for that codec. These situations call for
transcoding your video to formats like Theora or the newer WebM codec. This
is quite a difference from the dependability of JPEG and PNG image support.</p>
<p>HTML5 video on mobile also suffers from playback limitations. We had a client
who wanted video-quality effects in an HTML5 ad that was targeted to tablets.
But on both Android and iOS, HTML5 video (and audio) cannot autoplay at all. In
fact they can't play unless the user initiates them with a touch action.</p>
<p>The technique described in this article was used to work around this limitation
to provide an effect that animates automatically and can also react to
non-touch interactions such as tilting (accelerometer input).</p>
<h1 id="boxout-2-references-">Boxout #2: (References)</h1>
<h3 id="queryselectorall">querySelectorAll</h3>
<p>querySelectorAll provides the ability to find elements in the DOM using
CSS selectors, similar to how jQuery works.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll">https://developer.mozilla.org/en-US/docs/Web/API/Document.querySelectorAll</a></p>
<h3 id="requestanimationframe">requestAnimationFrame</h3>
<p>You can hook into the browser's rendering loop by passing a callback function
to requestAnimationFrame. The browser will invoke the callback after each
screen paint. Typically this is vsynced to the monitor and also intelligently
stops updating if the tab becomes inactive.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame">https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame</a></p>
<h3 id="headtrackr">headtrackr</h3>
<p>This library makes it easy to integrate head tracking into your webpage. The
works on any WebRTC-compliant browsers by applying computer vision techniques
using a webcam as the video source.</p>
<p><a href="https://github.com/auduno/headtrackr/">https://github.com/auduno/headtrackr/</a></p>
<h3 id="h-264-codec-support">H.264 Codec Support</h3>
<p><a href="http://caniuse.com/mpeg4">http://caniuse.com/mpeg4</a></p>
<h3 id="webm-codec-support">WebM Codec Support</h3>
<p><a href="http://caniuse.com/webm">http://caniuse.com/webm</a></p>
<h1 id="about-the-author">About the author</h1>
<pre><code>IMAGE #5
</code></pre><p>Name: Matt Campbell</p>
<p>Job: Developer at Grow in Norfolk, VA</p>
<p>Web: <a href="https://github.com/codingcampbell/">https://github.com/codingcampbell/</a></p>
<p>Areas of expertise: HTML5, JavaScript, LAMP</p>
</body>
</html>