Edx html5 2 - Advanced
Edx html5 2 - Advanced
Edx html5 2 - Advanced
You will learn by doing, study interactive examples, and have fun with
proposed development projects.
Advanced multimedia features with the Timed Text Track and WebAudio APIs,
HTML5 games techniques,
Persistence techniques for data storage including IndexedDB, File upload and
download using Ajax, Drag and drop,
Web Components, Web Workers, Orientation APIs and others...
Have fun!
Here is a small example of a video with 3 different tracks ("......" masks the
real URL here, as it is too long to fit in this page width!):
And here is how it renders in your current browser (please play the video and
try to show/hide the subtitles/captions):
Notice that (unfortunately), the support for multiple tracks differs significantly
from one browser to another. You can read this article by Ian Devlin: "HTML5
Video Captions – Current Browser Status", written in April 2015, for further
details. Here is a quick summary:
IE 11 and Safari provide a menu you can use to choose which subtitle/caption
track to display. If one of the defined text tracks has the default attribute set,
then it is loaded by default. Otherwise, the default is off.
Chrome and Opera: these browsers don't provide a menu for the user to make
a choice. Instead, they load the text track set that matches the browser
language. If none of the available text tracks match the browser’s language,
then it loads the track with the default attribute, if there is one. Otherwise, it
loads none. Let's say that support is very incomplete (!).
Firefox provides no text track menu at all, but will show the first defined text
track only if it has set. It will load all tracks in memory as soon as the page is
loaded.
So.... how can we do better? Fortunately, there is a Timed Text Track API in the
HTML5/HTML5.1 specificationthat enables us to manipulate <track> contents
from JavaScript. Do you recall that text tracks are associated with WebVTT
files? As a quick reminder, let's look at a WebVTT file:
1. WEBVTT
2.
3. 1
4. 00:00:15.000 --> 00:00:18.000 align:start
5. <v Proog>On the left we can see...</v>
6.
7. 2
8. 00:00:18.167 --> 00:00:20.083 align:middle
9. <v Proog>On the right we can see the...</v>
10.
11. 3
12. 00:00:20.083 --> 00:00:22.000
13. <v Proog>...the <c.highlight>head-snarlers</c></v>
14.
15. 4
16. 00:00:22.000 --> 00:00:24.417 align:end
17. <v Proog>Everything is safe. Perfectly safe.</v>
We cannot access any HTML element before the page has been loaded. This
is why we do all the work in the window.onload listener,
Line 7: we get a pointer to the div with id=trackStatusesDiv, that will be
used to display track statuses,
Line 10: we get all the track elements in the document. They are HTML track
elements,
Line 13: we call a function that will build some HTML to display the track
status in the div we got from line 7.
Lines 16-29: we iterate on the HTML tracks, and for each track we get
the label, the kind and the srclang attribute values. Notice, at line 24, the
use of the readyState attribute, only used from JavaScript, that will give the
current HTML track state.
You can see on the screenshot (or from the JSBin example) that the German
subtitle file has been loaded, and that none of the other tracks have been
loaded.
Possible values for the readyState attribute of HTML tracks:
0 = NONE ; the text track's cues have not been obtained
1 = LOADING ; the text track is loading with no errors yet. Further cues can
still be added to the track by the parser
2 = LOADED ; the text track has been loaded with no errors
3 = ERROR ; the text track was enabled, but when the user agent attempted
to obtain it, something failed. Some or all of the cues are likely missing and will
not be obtained
Now, it's time to look at the twin brother of an HTML track: the
corresponding TextTrack object!
The TextTrack object has different methods and properties for manipulating
track and is associated with different events. But before going into detail, let's
see how to obtain a TextTrack object.
Here is an example that will test if a track has been loaded, and if it hasn't, will
force it to be loaded by setting its mode to "hidden". We could have used
"showing"; in this case, if the file is a subtitle or a caption file, then the subtitles
or captions will be displayed on the video as soon as the track has finished
loading.
1. <button id="buttonLoadFirstTrack"
2. onclick="forceLoadTrack(0);"
3. disabled>
4. Force load track 0
5. </button>
6. <button id="buttonLoadThirdTrack"
7. onclick="forceLoadTrack(2);"
8. disabled>
9. Force load track 2
10. </button>
Here are the additions we made to the JavaScript code of the previous
example:
1. function readContent(track) {
2. console.log("reading content of loaded track...");
3. displayTrackStatuses(htmlTracks); // update document with new track
statuses
4. }
5.
6. function getTrack(htmlTrack, callback) {
7. // TextTrack associated to the htmlTrack
8. var textTrack = htmlTrack.track;
9. if(htmlTrack.readyState === 2) {
10. console.log("text track already loaded");
11. // call the callback function, the track is available
12. callback(textTrack);
13. } else {
14. console.log("Forcing the text track to be loaded");
15.
16. // this will force the track to be loaded
17. textTrack.mode = "hidden";
// loading a track is asynchronous, we must use an event listener
18. htmlTrack.addEventListener('load', function(e) {
19. // the track is arrived, call the callback function
20. callback(textTrack);
21. });
22. }
23. }
24. function forceLoadTrack(n) {
25. // first parameter = track number,
26. // second = a callback function called when the track is loaded,
27. // that takes the loaded TextTrack as parameter
28. getTrack(htmlTracks[n], readContent);
29. }
Explanations:
Lines 26-31: the function called when a button has been clicked. This function
in turn calls the getTrack(trackNumber, callback) function. It passes
the readContent callback function as a parameter. This is typical JavaScript
asynchronous programming: the getTrack() function may force the browser to
load the track and this can take some time (a few seconds), then when the
track has downloaded, we ask the getTrack function to call the function we
passed (the readContent function, which is known as a callback function), with
the loaded track as a parameter.
Line 6: the getTrack function. It first checks if the HTML track is already loaded
(line 10). If it is, it calls the callback function passed by the caller, with the
loaded TextTrack as a parameter. If the TextTrack is not loaded, then it sets its
mode to "hidden". This will instruct the browser to load the track. Because that
may take some time, we must use a load event listener on the HTML track
before calling the callback function. This allows us to be sure that the track is
really completely loaded.
Lines 1-4: the readContent function is only called with a loaded TextTrack.
Here we do nothing special for the moment except that we refresh the different
track statuses in the HTML document.
Reading the content of a TEXTTRACK
A TextTrack object has different properties and
methods:
kind: equivalent to the kind attribute of HTML track elements. Its value is
either "subtitles", "caption", "descriptions", "chapters", or "metadata". We will
see examples of chapters, descriptions and metadata tracks in subsequent
lessons.
label: the label of the track, equivalent of the label attribute of HTML track
elements.
language: the language of the text track, equivalent to the srclang attribute
of HTML track elements (be careful: it's not the same spelling!)
mode: explained earlier. Can have values equal to:
"disabled"|"hidden"|"showing". Can force a track to be loaded (by setting the
mode to "hidden" or "showing").
cues: get a list of cues as a TextTrackCueList object. This is the complete
content of the WebVTT file!
activeCues: used in event listeners while the video is playing. Corresponds to
the cues located in the current time segment. The start and end times of cues
can overlap. In reality this may rarely happen, but this property exists in case it
does, returning a TextTrackCueList object that contains all active tracks at a
given time.
addCue(cue): add a cue to the list of cues.
removeCue(cue): remove a cue from the list of cues.
getCueById(id): returns the cue with a given id (not implemented by all
browsers - a polyfill is given in the examples from the next lessons).
1. function readContent(track) {
2. console.log("reading content of loaded track...");
3. //displayTrackStatuses(htmlTracks);
4. // instead of displaying the track statuses, we display
5. // in the same div, the track content//
6. // first, empty the div
7. trackStatusesDiv.innerHTML = "";
8. // get the list of cues for that track
9. var cues = track.cues;
10. // iterate on them
11. for(var i=0; i < cues.length; i++) {
12. // current cue
13. var cue = cues[i];
14. var id = cue.id + "<br>";
15. var timeSegment = cue.startTime + " =>
" + cue.endTime + "<br>";
16. var text = cue.text + "<P>"
17. trackStatusesDiv.innerHTML += id + timeSegment + text;
18. }
19. }
As you can see, the code is simple: you first get the cues for the
given TextTrack (it must be loaded; this is the case since we took care of it
earlier), then iterate on the list of cues, and use
the id, startTime, endTime and text properties of each cue.
This technique will be used in one of the next lessons, and we will show you
how to make a clickable transcript on the side of the video - something quite
similar to what the edX video player does.
1.enter and exit events fired for cues, (not supported by all browsers as at
October 2015).
2. events fired for TextTrack objects (good support).
Example of cuechange listener on TextTrack:
1. // track is a loaded TextTrack
2. track.addEventListener("cuechange", function(e) {
3. var cue = this.activeCues[0];
4. console.log("cue change");
5. // do something with the current cue
6. });
In the above example, let's assume that we have no overlapping cues for the
current time segment. The above code listens for cue change events: when the
video is being played, the time counter increases. And when this time counter
value reaches time segments defined by one or more cues, the callback is
called. The list of cues that are in the current time
segments in this.activeCues; where this represents the track that fired the
event.
In the following lessons, we show how to deal with overlapping cues (cases
where we have more than one active cue).
1. function readContent(track) {
2. console.log("adding cue change listener to loaded track...");
3. trackStatusesDiv.innerHTML = "";
4. // add a cue change listener to the TextTrack
5. track.addEventListener("cuechange", function(e) {
6. var cue = this.activeCues[0];
7. if(cue !== undefined)
8. trackStatusesDiv.innerHTML += "cue change: text =
" + cue.text + "<br>";
9. });
10. video.play();
11. }
And here is another modified version of this example at JSBin, that shows how
to use enter and exit events on cues:
1. function readContent(track) {
2. console.log("adding enter and exit listeners to all cues of this track");
3. trackStatusesDiv.innerHTML = "";
4. // get the list of cues for that track
5. var cues = track.cues;
6. // iterate on them
7. for(var i=0; i < cues.length; i++) {
8. // current cue
9. var cue = cues[i];
10. addCueListeners(cue);
11. }
12. video.play();
13. }
14.
15. function addCueListeners(cue) {
16. cue.onenter = function(){
17. trackStatusesDiv.innerHTML += 'entered cue id=' + this.id + " "
18. + this.text + "<br>";
19. };
20. cue.onexit = function(){
21. trackStatusesDiv.innerHTML += 'exited cue id=' + this.id + "<br>";
22. };
23. } // end of addCueListeners...
1.Browsers do not load all the tracks at the same time, and the way they decide
when and which track to load differs from one browser to another. So, when we
click on a button to choose the track to display, we need to enforce the loading
of the track, if it has not been loaded yet.
2.When a track file is loaded, then we iterate on the different cues and
generate the transcript as a set of <li>...</li> elements. One <li> per
cue/sentence.
3.We define the id attribute of the <li> to be the same as the cue.id value. In
this way, when we click on a <li> we can get its id and find the corresponding
cue start time, and make the video jump to that time location.
4.We add an enter and an exit listener to each cue. These will be useful for
highlighting the current cue. Note that these listeners are not yet supported by
FireFox (you can use a event listener on a TextTrack instead - the source code
for FireFox is commented in the example).
1. <section id="all">
2. <button disabled id="buttonEnglish"
3. onclick="loadTranscript('en');">
4. Display English transcript
5. </button>
6. <button disabled id="buttonDeutsch"
7. onclick="loadTranscript('de');">
8. Display Deutsch transcript
9. </button>
10. </p>
11. <video id="myVideo" preload="metadata" controls crossOrigin="anon
ymous">
12. <source src="http://...../elephants-dream-medium.mp4"
13. type="video/mp4">
14. <source src="http://...../elephants-dream-medium.webm"
15. type="video/webm">
16. <track label="English subtitles"
17. kind="subtitles"
18. srclang="en"
19. src="http://...../elephants-dream-subtitles-en.vtt" >
20. <track label="Deutsch subtitles"
21. kind="subtitles"
22. srclang="de"
23. src="http://...../elephants-dream-subtitles-de.vtt"
24. default>
25. <track label="English chapters"
26. kind="chapters"
27. srclang="en"
28. src="http://...../elephants-dream-chapters-en.vtt">
29. </video>
30.<div id="transcript"></div>
31. </section>
CSS code:
1. #all {
2. background-color: lightgrey;
3. border-radius:10px;
4. padding: 20px;
5. border:1px solid;
6. display:inline-block;
7. margin:30px;
8. width:90%;
9. }
10.
11. .cues {
12. color:blue;
13. }
14.
15. .cues:hover {
16. text-decoration: underline;
17. }
18.
19. .cues.current {
20. color:black;
21. font-weight: bold;
22. }
23.
24. #myVideo {
25. display: block;
26. float : left;
27. margin-right: 3%;
28. width: 66%;
29. background-color: black;
30. position: relative;
31. }
32.
33. #transcript {
34. padding: 10px;
35. border:1px solid;
36. float: left;
37. max-height: 225px;
38. overflow: auto;
39. width: 25%;
40. margin: 0;
41. font-size: 14px;
42. list-style: none;
43. }
JavaScript code:
Here is an example at JSBin that displays the values of the cues in the different
tracks:
This example, adapted from an example from (now offline) dev.opera.com,
uses some JavaScript code that takes a WebVTT subtitle (or caption) file as an
argument, parses it, and displays the text on , in an element with an id of .
1. ...
2. <video preload="metadata" controls >
3. <source src="https://..../elephants-dream-medium.mp4" type="video/
mp4">
4. <source src="https://..../elephants-dream-medium.webm" type="video/
webm">
5. <track label="English subtitles" kind="subtitles" srclang="en"
6. src="https://..../elephants-dream-subtitles-en.vtt" default>
7. <track label="Deutsch subtitles" kind="subtitles" srclang="de"
8. src="https://..../elephants-dream-subtitles-de.vtt">
9. <track label="English chapters" kind="chapters" srclang="en"
10. src="https://..../elephants-dream-chapters-en.vtt">
11. </video>
12. ...
13. <h3>Video Transcript</h3>
14. <button onclick="loadTranscript('en');">English</button>
15. <button onclick="loadTranscript('de');">Deutsch</button>
16. </div>
17. <div id="transcript"></div>
18. ...
JavaScript code:
1. // Transcript.js, by dev.opera.com
2. function loadTranscript(lang) {
3. var url = "http://mainline.i3s.unice.fr/mooc/" +
4. 'elephants-dream-subtitles-' + lang + '.vtt';
5. // Will download using Ajax + extract subtitles/captions
6. loadTranscriptFile(url);
7. }
8.
9. function loadTranscriptFile(webvttFileUrl) {
10. // Using Ajax/XHR2 (explained in detail in Week 3)
11. var reqTrans = new XMLHttpRequest();
12. reqTrans.open('GET', webvttFileUrl);
13. // callback, called only once the response is ready
14. reqTrans.onload = function(e) {
15. var pattern = /^([0-9]+)$/;
16. var patternTimecode = /^([0-9]{2}:[0-9]{2}:[0-9]{2}[,.]{1}[0-9]
{3}) --\> ([0-9]
17. {2}:[0-9]{2}:[0-9]{2}[,.]{1}[0-9]{3})(.*)$/;
18. var content = this.response; // content of the webVTT file
19. var lines = content.split(/\r?\n/); // Get an array of text lines
20. var transcript = '';
21. for (i = 0; i < lines.length; i++) {
22. var identifier = pattern.exec(lines[i]);
23. // is there an id for this line, if it is, go to next line
24. if (identifier) {
25. i++;
26. var timecode = patternTimecode.exec(lines[i]);
27. // is the current line a timecode?
28. if (timecode && i < lines.length) {
29. // if it is go to next line
30. i++;
31. // it can only be a text line now
32. var text = lines[i];
33.
34. // is the text multiline?
35. while (lines[i] !== '' && i < lines.length) {
36. text = text + '\n' + lines[i];
37. i++;
38. }
39. var transText = '';
40. var voices = getVoices(text);
41. // is the extracted text multi voices ?
42. if (voices.length > 0) {
43. // how many voices ?
44. for (var j = 0; j < voices.length; j++) {
45. transText += voices[j].voice + ': '
46. + removeHTML(voices[j].text)
47. + '<br />';
48. }
49. } else
50. // not a voice text
51. transText = removeHTML(text) + '<br />';
52. transcript += transText;
53. }
54. }
55. var oTrans = document.getElementById('transcript');
56. oTrans.innerHTML = transcript;
57. }
58. };
59. reqTrans.send(); // send the Ajax request
60. }
61.
62. function getVoices(speech) { // takes a text content and check if there
are voices
63. var voices = []; // inside
64. var pos = speech.indexOf('<v'); // voices are like <v Michel> ....
65. while (pos != -1) {
66. endVoice = speech.indexOf('>');
67. var voice = speech.substring(pos + 2, endVoice).trim();
68. var endSpeech = speech.indexOf('</v>');
69. var text = speech.substring(endVoice + 1, endSpeech);
70. voices.push({
71. 'voice': voice,
72. 'text': text
73. });
74. speech = speech.substring(endSpeech + 4);
75. pos = speech.indexOf('<v');
76. }
77. return voices;
78. }
79.
80. function removeHTML(text) {
81. var div = document.createElement('div');
82. div.innerHTML = text;
83. return div.textContent || div.innerText || '';
84. }
Example 2: showing video description
while playing, listening to events,
changing the mode of a track
Each track has a mode property (and a mode attribute) that can
be: "disabled", "hidden" or "showing". More than one track at a time can be in
any of these states. The difference between "hidden" and "disabled" is that
hidden tracks can fire events (more on that at the end of the first example)
whereas disabled tracks do not fire events.
Here is an example at JSBin that shows the use of the property, and how to
listen for cue events in order to capture the current subtitle/caption from
JavaScript. You can change the mode of each track in the video element by
clicking on its button. This will toggle the mode of that track. All tracks with
mode="showing" or mode="hidden" will have the content of their cues
displayed in real time in a small area below the video.
1. <html lang="en">
2. ...
3. <body onload="init();">
4. ...
5. <p>
6. <video id="myVideo" preload="metadata"
7. poster ="https://...../sintel.jpg"
8. crossorigin="anonymous"
9. controls="controls"
10. width="640" height="272">
11. <source src="https://...../sintel.mp4"
12. type="video/mp4" />
13. <source src="https://...../sintel.webm"
14. type="video/webm" />
15. <track src="https://...../sintel-captions.vtt"
16. kind="captions"
17. label="English Captions"
18. default/>
19. <track src="https://...../sintel-descriptions.vtt"
20. kind="descriptions"
21. label="Audio Descriptions" />
22. <track src="https://...../sintel-chapters.vtt"
23. kind="chapters"
24. label="Chapter Markers" />
25. <track src="https://...../sintel-thumbs.vtt"
26. kind="metadata"
27. label="Preview Thumbs" />
28. </video>
29. </p>
30. <p>
31. <div id="currentTrackStatuses"></div>
32. <p>
33. <p>
34. <div id="subtitlesCaptions"></div>
35. </p>
36. <p>
37. <button onclick="clearSubtitlesCaptions();">
38. Clear subtitles/captions log
39. </button>
40. </p>
41.
42. <p>Click one of these buttons to toggle the mode of each track:</p>
43. <button onclick="toggleTrack(0);">
44. Toggle english caption track mode
45. </button>
46. <br>
47. <button onclick="toggleTrack(1);">
48. Toggle audio description track mode
49. </button>
50. <br>
51. <button onclick="toggleTrack(2);">
52. Toggle chapter track mode
53. </button>
54. <br>
55. <button onclick="toggleTrack(3);">
56. Toggle preview thumbnail track modes
57. </button>
58.
59. </body>
60. </html>
JavaScript code:
7. tracks = document.querySelectorAll("track");
8. video.addEventListener('loadedmetadata', function() {
9. console.log("metadata loaded");
10. // defines cue listeners for the active track; we can do this only after
the video metadata have been loaded
11. for(var i=0; i<tracks.length; i++) {
12. var t = tracks[i].track;
13. if(t.mode === "showing") {
14. t.addEventListener('cuechange', logCue, false);
15. }
16. }
17. // display in a div the list of tracks and their
status/mode value
18. displayTrackStatus();
19. });
20. }
21.
22. function displayTrackStatus() {
23. // display the status / mode value of each track.
24. // In red if disabled, in green if showing
25. for(var i=0; i<tracks.length; i++) {
26. var t = tracks[i].track;
27. var mode = t.mode;
28. if(mode === "disabled") {
29. mode = "<span style='color:red'>" + t.mode + "</span>";
30. } else if(mode === "showing") {
31. mode = "<span style='color:green'>" + t.mode + "</span>";
32. }
33. appendToScrollableDiv(statusDiv, "track " + i + ": " + t.label
34. + " " + t.kind+" in "
35. + mode + " mode");
36. }
37. }
38. function appendToScrollableDiv(div, text) {
39. // we've got two scrollable divs. This function
40. // appends text to the div passed as a parameter
41. // The div is scrollable (thanks to CSS overflow:auto)
42. var inner = div.innerHTML;
43. div.innerHTML = inner + text + "<br/>";
44. // Make it display the last line appended
45. div.scrollTop = div.scrollHeight;
46. }
47.
48. function clearDiv(div) {
49. div.innerHTML = '';
50. }
51.
52. function clearSubtitlesCaptions() {
53. clearDiv(subtitlesCaptionsDiv);
54. }
55. function toggleTrack(i) {
56. // toggles the mode of track i, removes the cue listener
57. // if its mode becomes "disabled"
58. // adds a cue listener if its mode was "disabled"
59. // and becomes "hidden"
60. var t = tracks[i].track;
61. switch (t.mode) {
62. case "disabled":
63. t.addEventListener('cuechange', logCue, false);
64. t.mode = "hidden";
65. break;
66. case "hidden":
67. t.mode = "showing";
68. break;
69. case "showing":
70. t.removeEventListener('cuechange', logCue, false);
71. t.mode = "disabled";
72. break;
73. }
74. // updates the status
75. clearDiv(statusDiv);
76. displayTrackStatus();
77. appendToScrollableDiv(statusDiv,"<br>" + t.label+" are now
" +t.mode);
78. }
79.
80. function logCue() {
81. // callback for the cue event
82. if(this.activeCues && this.activeCues.length) {
83. var t = this.activeCues[0].text; // text of current cue
84. appendToScrollableDiv(subtitlesCaptionsDiv, "Active "
85. + this.kind + " changed to: " + t);
86. }
87. }
Read this article by Ian Devlin about the current status of multiple WebVTT
track support by the different browsers, as at May 2015. Note that currently
(July 2016), neither Chrome nor FireFox offers a menu to choose the track to
display.
However, it's easy to implement this feature using the Track API.
Here is a simple example at JSBin: we added two buttons below the video to
enable/disable subtitles/captions and let you choose which track you prefer.
HTML code:
1. ...
2. <body onload="init()">
3. ...
4. <video id="myVideo" preload="metadata" controls crossOrigin="anon
ymous" >
5. <source src="http://...../elephants-dream-medium.mp4"
6. type="video/mp4">
7. <source src="http://...../elephants-dream-medium.webm"
8. type="video/webm">
9. <track label="English subtitles"
10. kind="subtitles"
11. srclang="en"
12. src="http://...../elephants-dream-subtitles-en.vtt"
13. default>
14. <track label="Deutsch subtitles"
15. kind="subtitles"
16. srclang="de"
17. src="http://...../elephants-dream-subtitles-de.vtt">
18. <track label="English chapters"
19. kind="chapters"
20. srclang="en"
21. src="http://...../elephants-dream-chapters-en.vtt">
22. </video>
23. <h3>Current track: <span id="currentLang"></span></h3>
24. <div id="langButtonDiv"></div>
25. </section>
26. ...
JavaScript code:
HTML code:
Currently (July 2016), no browser takes chapter tracks into account. You could
use one of the enhanced video players presented during the HTML5 Part 1
course, but as you will see in this lesson: making your own chapter navigation
menu is not complicated.
There are 7 cues (one for each chapter). Each cue id is the word "chapter-"
followed by the chapter number, then we have the start and end time of the
cue/chapter, and the cue content. In this case: the description of the chapter
("Introduction", "Watch out!", "Let's go", etc...).
Hmm... let's try to open this chapter track with the example we wrote in a
previous lesson - the one that displayed the clickable transcript for
subtitles/captions on the right of the video. We need to modify it a little bit:
1.We add a "show English chapters" button with a click event listener similar to
this :
Here is a new version: in bold are the source code lines we modified.
Look at the JavaScript and HTML tab of the JSBin example to see the source
code. It's the same as in the clickable transcript example, except for the small
changes we explained earlier.
However, we will see how we can do better by using JSON objects as cue
contents. This will be the topic of the next two lessons!
1. WEBVTT
2. Wikipedia
3. 00:01:15.200 --> 00:02:18.800
4. {
5. "title": "State of Wikipedia",
6. "description": "Jimmy Wales talking ...",
7. "src": "http://upload.wikimedia.org/...../120px-Wikipedia-logo-
v2.svg.png",
8. "href": "http://en.wikipedia.org/wiki/Wikipedia"
9. }
This JSON object (in bold green) is a JavaScript object encoded as a text string.
If we listen for cue events or if we read a WebVTT file as done in previous
examples, we can extract this text content using the cue.textproperty. For
example:
This example used only standard plain text content for the cues:
1. WEBVTT
2.
3. chapter-1
4. 00:00:00.000 --> 00:00:26.000
5. Introduction
6.
7. chapter-2
8. 00:00:28.206 --> 00:01:02.000
9. Watch out!
10. ...
We used this example to manually capture the images from the video that
correspond to each of the seven chapters:
We clicked on each chapter link on the right, then paused the video,
then we used a screen capture tool to grab each image that corresponds to
the beginning of chapter,
Finally, we resized the images with Photoshop to approximately 200x400
pixels.
(For advanced users: it's possible to semi-automatize this process using
the command line tool, see for example this and that).
Here are the images which correspond to the seven chapters of the video from
the previous example:
To associate these images with its chapter description, we will use JSON objects
as cue contents:
elephants-dream-chapters-en-JSON.vtt:
1. WEBVTT
2.
3. chapter-1
4. 00:00:00.000 --> 00:00:26.000
5. {
6. "description": "Introduction",
7. "image": "introduction.jpg"
8. }
9.
10.
11. chapter-2
12. 00:00:28.206 --> 00:01:02.000
13. {
14. "description": "Watch out!",
15. "image": "watchOut.jpg"
16. }
17. ...
Before explaining the code, we propose that you try this example at JSBin that
uses this new .vtt file:
HTML code:
1. ...
2. <video id="myVideo" preload="metadata" controls crossOrigin="anon
ymous">
3. <source src="http://...../elephants-dream-medium.mp4"
4. type="video/mp4">
5. <source src="http://...../elephants-dream-medium.webm"
6. type="video/webm">
7. <track label="English subtitles"
8. kind="subtitles"
9. srclang="en" src="http://...../elephants-dream-subtitles-en.vtt" >
10. <track label="Deutsch subtitles"
11. kind="subtitles"
12. srclang="de" src="http://...../elephants-dream-subtitles-
de.vtt"default>
13.<track label="English chapters"
14. kind="chapters"
15. srclang="en" src="http://...../elephants-dream-chapters-en-
JSON.vtt">
16. </video>
17. <h2>Chapter menu</h2>
18.<div id="chapterMenu"></div>
19. ...
It's the same code we had in the first example, except that this time we use a
new WebVTT file that uses JSON cues to describe each chapter. For the sake of
simplicity, we also removed the buttons and all the code for displaying a
clickable transcript of the subtitles/captions on the right of the video.
JavaScript code:
Lines 4-18: when the page is loaded, we assemble all of the track HTML
elements and their corresponding TextTrack objects.
Line 19: using that we can build the chapter navigation menu. All is done in
the window. callback, so nothing happens until the DOM is ready.
Lines 24-43: the buildChapterMenu function first locates the chapter track for
the given language, then checks if this track has been loaded by the browser.
Once it has been confirmed that the track is loaded, the function
displayChapters is called.
Lines 45-65: the displayChapters(track) function will iterate over all of the
cues within the chapter track passed as its parameter. For each cue, the JSON
content is back into a JavaScript object (line 55) and the image filename and
description of the chapter/cue are extracted (lines 56-57). Then an HTML
description for the chapter is built and added to the div element with
id=chapterMenu. Here is the HTML code for one menu marker:
1.<figure class="">
2.
<img onclick="jumpTo(0);" class="thumb"src="http://...../introduction.jpg">
3. <figcaption class="desc">
4. Introduction
5. </figcaption>
6.</figure>
Notice that we add a click listener to each thumbnail image. Clicking a chapter
thumbnail will cause the video to jump to the chapter time location (the
example above is for the first chapter with start time = 0).
We also added CSS classes "", "thumb" and "", which make it easy to style and
position the thumbnails using CSS.
1. #chapterMenuSection {
2. background-color: lightgrey;
3. border-radius:10px;
4. padding: 20px;
5. border:1px solid;
6. display:inline-block;
7. margin:0px 30px 30px 30px;
8. width:90%;
9. }
10.
11. figure.img {
12. margin: 2px;
13. float: left;
14. }
15.
16. figcaption.desc {
17. text-align: center;
18. font-weight: normal;
19. margin: 2px;
20. }
21.
22. .thumb {
23. height: 75px;
24. border: 1px solid #000;
25. margin: 10px 5px 0 0;
26. box-shadow: 5px 5px 5px grey;
27. transition: all 0.5s;
28. }
29.
30. .thumb:hover {
31. box-shadow: 5px 5px 5px black;
32. }
A sample menu marker is shown below (it's also animated - hover your mouse
over the thumbnail to see its animated shadow):
Try it at JSBin
Creating tracks on the fly: example
with sound sprites
INTRODUCTION
In this lesson we show:
The addTextTrack method for adding a TextTrack to an
html <track> element,
The VTTCue constructor, for creating cues programmatically, and
the addCue method for adding cues on the fly to a TextTrack etc.
These methods will allow us to create TextTrack objects and cues on the fly,
programatically. The presented example shows how we can create "sound
sprites": small sounds that are parts of a mp3 file, and that can be played
separately. Each sound will be defined as a cue in a track associated with
the <audio> element.
The idea is to create a track on the fly, then add cues within this track. Each
cue will be created with the id, the start and end time taken from the above
JavaScript object. In the end, we will have a track with individual cues
located at the time location where an animal sound is in the mp3 file.
Then we generate buttons in the HTML document, and when the user clicks on
a button, the getCueById method is called, then the start and end time
properties of the cue are accessed and the sound is played (using
the currentTime property of the audio element).
Polyfill for getCueById: Note that this method is not available on all browsers
yet. A simple polyfill is used in the examples
presented. If the getCueById method is not implemented (this is the case in
many browsers, as at November 2015), it's easy to use this small polyfill:
Techniques
To add a TextTrack to a track element, use the addTextTrack method (of the
audio or video element). The function's signature is addTextTrack( kind [,
label [, language ] ] ) where kind is our familiar choice
between subtitles, captions, chapters, etc; the optional label is any
text you'd like to use describing the track; and the optional language is from
our usual list of BCP-47 abbreviations, eg 'de', 'en', 'es', '' (etc).
1. window.onload = function() {
2. // Create an audio element programmatically
3.
var audio = newAudio("http://mainline.i3s.unice.fr/mooc/animalSounds.mp3")
;
4.
5. audio.addEventListener("loadedmetadata", function() {
6. // When the audio file has its metadata loaded, we can add
7. // a new track to it, with mode = hidden. It will fire events
8. // even if it is hidden
9. var track = audio.addTextTrack("metadata", "sprite track", "en");
10. track.mode = "hidden";
11.
12. // for browsers that do not implement the getCueById() method
13. if (typeof track.getCueById !== "function") {
14. track.getCueById = function(id) {
15. var cues = track.cues;
16. for (var i = 0; i != track.cues.length; ++i) {
17. if (cues[i].id === id) {
18. return cues[i];
19. }
20. }
21. };
22. }
23.
24. var sounds = [
25. {
26. id: "purr",
27. startTime: 0.200,
28. endTime: 1.800
29. },
30. {
31. id: "meow",
32. startTime: 2.300,
33. endTime: 3.300
34. },
35. ...
36. ];
37.
38. for (var i = 0; i !== sounds.length; ++i) {
39. // for each animal sound, create a cue with id, start and end time
40. var sound = sounds[i];
41.
var cue = new VTTCue(sound.startTime, sound.endTime, sound.id);
42. cue.id = sound.id;
43. // add it to the track
44. track.addCue(cue);
45. // create a button and add it to the HTML document
46. document.querySelector("#soundButtons").innerHTML +=
47. "<button class='playSound' id="
48. + sound.id + ">" +sound.id
49. + "</button>";
}
50.
51. var endTime;
52. audio.addEventListener("timeupdate", function(event) {
53. // When we play a sound, we set the endtime var.
54. // We need to listen when the audio file is being played,
55. // in order to pause it when endTime is reached.
56. if (event.target.currentTime > endTime)
57. event.target.pause();
58. });
59.
60. function playSound(id) {
61. // Plays the sound corresponding to the cue with id equal
62. // to the one passed as a parameter. We set the endTime var
63. // and position the audio currentTime at the start time
64. // of the sound
65. var cue = track.getCueById(id);
66. audio.currentTime = cue.startTime;
67. endTime = cue.endTime;
68. audio.play();
69. };
70. // create listeners for all buttons
71. var buttons = document.querySelectorAll("button.playSound");
72. for(var i=; i < buttons.length; i++) {
73. buttons[i].addEventListener("click", function(e) {
74. playSound(this.id);
75. });
76. }
77. });
78. };
Updating the document in sync with a
media playing
Mixing JSON cue content with track and cue events, makes the synchronization
of elements in the HTML document (while the video is playing) much easier.
Example of track event listeners that use JSON cue contents
Here is a small code extract that shows how we can capture the JSON content
of a cue when the video reaches its start time. We do this within a listener
attached to a TextTrack:
1. textTrack.oncuechange = function (){
2. // "this" is the textTrack that fired the event.
3. // Let's get the first active cue for this time segment
4. var cue = this.activeCues[0];
5. var obj = JSON.parse(cue.text);
6. // do something
7. }
Here is a very impressive demo by Sam Dutton that uses JSON cues containing
the latitude and longitude of the camera used for filming the video, to
synchronize two map views: every time the active cue changes, the
Google and equivalent Google street view are updated.
Example of a cue content from this demonstration:
We can acquire a cue DOM object using the techniques we have seen
previously, or by using the new HTML5 TextTrack getCueById() method.
And once we have a cue object, it is possible to add event listeners to it:
1. cue.onenter = function(){
2. // display something, play a sound, update any DOM element...
3. };
4. cue.onexit = function(){
5. // do something else
6. };
JavaScript code:
1. window.onload = function() {
2. var videoElement = document.querySelector("#myVideo");
3. var myIFrame = document.querySelector("#myIframe");
4. var currentURLSpan = document.querySelector("#currentURL");
5. var textTracks = videoElement.textTracks; // one for each track element
6. var textTrack = textTracks[0]; // corresponds to the first track element
7.
8. // change mode so we can use the track
9. textTrack.mode = "hidden";
10. // Default position on the google map
11. var centerpos = new google.maps.LatLng(48.579400,7.7519);
12.
13. // default options for the google map
14. var optionsGmaps = {
15. center:centerpos,
16. navigationControlOptions: {style:
17. google.maps.NavigationControlStyle.SMALL},
18. mapTypeId: google.maps.MapTypeId.ROADMAP,
19. zoom: 15
20. };
21.
22. // Init map object
23. var map = new google.maps.Map(document.getElementById("map"),
24. optionsGmaps);
25.
26. // cue change listener, this is where the synchronization between
27. // the HTML document and the video is done
28. textTrack.oncuechange = function (){
29. // we assume that we have no overlapping cues
30. var cue = this.activeCues[0];
31. if(cue === undefined) return;
32. // get cue content as a JavaScript object
33. var cueContentJSON = JSON.parse(cue.text);
34. // do different things depending on the type of sync (wikipedia, gmap)
35. switch(cueContentJSON.type) {
36. case'WikipediaPage':
37. var myURL = cueContentJSON.url;
38. var myLink = "<a
href=\"" + myURL + "\">" + myURL + "</a>";
39. currentURLSpan.innerHTML = myLink;
40. myIFrame.src = myURL; // assign url to src property
41. break;
42. case 'LongLat':
43. drawPosition(cueContentJSON.long, cueContentJSON.lat);
44. break;
45. }
46. };
47.
48. function drawPosition(long, lat) {
49. // Make new object LatLng for Google Maps
50. var latlng = new google.maps.LatLng(lat, long);
51.
52. // Add a marker at position
53. var marker = new google.maps.Marker({
54. position: latlng,
55. map: map,
56. title:"You are here"
57. });
58. // center map on longitude and latitude
59. map.panTo(latlng);
60. }
61. };
All the critical work is done by the event listener, lines 27-50. We have only
the one track, so we set its mode to "hidden" (line 10) in order to be sure that
it will be loaded, and that playing the video will fire events on it. The rest is
just Google map code and classic DOM manipulation for updating HTML content
(a span that will display the current URL, line 42).
You also learned that it's possible to write a custom player: to make your own
controls and use the JavaScript API of the <audio> and <video> elements; to
call play() and pause(); to read/write properties such as currentTime; to
listen to events (ended, error, , etc.); and to manage a playlist, etc.
The Web Audio API will fulfill such missing parts, and much more.
In this course, we will not cover the whole Web Audio API specification. Instead,
we focus on the parts of the API that can be useful for writing enhanced
multimedia players (that work with streamed audio or video), and on parts that
will be useful for games (i.e. parts that work with small sound samples loaded
in memory). There is a whole part of the API that specializes in music synthesis
and scheduling notes, that will not be covered here.
Here's a screenshot from one example we will study: an audio player with
animated waveform and volume meters that 'dance' with the music:
Web Audio concepts
The audio context
The canvas used a graphic context for drawing shapes and handling properties
such as colors and line widths.
The Web Audio API takes a similar approach, using an AudioContext for all its
operations.
Using this context, the first thing we'll do when using this API is to build an
"audio routing graph" made of "audio nodes" which are linked together (most
of the time in the course, we will call it the "audio graph"). Some node types
are for "audio sources", another built-in node is for the speakers, and many
other types exist, that correspond to audio effects (delay, reverb, filter, stereo
panner, etc.), audio analysis (useful for creating fancy visualizations of
the signal). Others, which are specialized for music synthesis, will not be
covered in this course.
Step 3 - open the and go to the Web Audio tab, reload the
page if needed
Audio nodes are linked via their inputs and outputs, forming a chain that starts
with one or more sources, goes through one or more nodes, then ends up at a
destination (although you don't have to provide a destination if you just want to
visualize some audio data, for example).
Explanations:
As soon as the page is loaded: initialize the audio context (line 11). Here we
use a trick so that the code works on all browsers: Chrome, FF, Opera, Safari,
Edge. The trick at line 3 is required for Safari, as it still needs the WebKit
prefixed version of the AudioContext constructor.
Then we build a graph (line 17).
The build graph function first builds the nodes, then connects them to build
the audio graph. Notice the use of audioContext.destination for the
speakers (line 32). This is a built-in node. Also, the MediaElementSource node
"" which is the HTML's audio element.
External resource
Introducing the Web Audio Editor in Firefox Developer Tools
Example of bigger graphs
Web Audio nodes are implemented natively in the browser. The Web Audio
framework has been designed to handle a very large number of nodes. It's
common to encounter applications with several dozens of nodes: some, such
as this vocoder app, use hundreds of nodes:
Working with streamed content:
the MediaSourceElement node
In the previous lesson, we encountered the MediaElementSource node that is
used for routing the sound from a <video> or <audio> element stream. The
above video shows how to make a simple example step by step, and how to
setup FireFox for debugging Web Audio applications and visualize the audio
graph.
Typical use:
Example at JSBin
HTML:
JavaScript:
In the following lessons, we will see the different nodes that are useful with
streamed audio and with the MediaElementSource node. Adding them in the
audio graph will enable us to change the sound in many different ways.
uency slider (that changes the frequency value property of the node). The
meaning of the different properties (frequency, detune and Q) differs
depending on the type of the filter you use (click on the menu to see the
different types available). Look at this documentation for details on the
different filters and how the frequency, detune and Q properties are used with
each of these filter types.
Here is a nice graphic application that shows the frequency responses the
various filters, you can choose the type of filters and play with the different
property values using sliders:
Multiple filters are often used together. We will make a equalizer in a next
lesson, and use six filters with type=peaking.
at JSBin, THIS EXAMPLE DOES NOT WORK IN YOUR BROWSER as the edX
platforms Ajax loading in its HTML pages. Try it at JSBin!
Try this demo to see the difference between different impulse files!
So before building the audio graph, we need to download the impulse. For this,
we use an Ajax request (this will be detailed during Week 3), but for the
moment, just take this function as it is... The Web Audio API requires that
impulse files should be decoded in memory before use. Accordingly, once the
requested file has downloaded, we call the decodeAudioData method. Once the
impulse is decoded, we can build the graph. So typical use is as follows:
Now let's consider the function which builds the graph. In order to set the
quantity of reverb we would like to apply, we need two separate routes for the
signal:
1.One "dry" route where we directly connect the audio source to the
destination,
2.One "wet" route where we connect the audio source to the convolver node
(that will add a reverb effect), then to the destination,
3.At the end of both routes, before the destination, we add a gain node, so that
we can specify the quantity of dry and wet signal we're going to send to the
speakers.
1. function buildAudioGraphConvolver() {
2. // create the nodes
3. var source = audioContext.createMediaElementSource(playerConvolver);
4. convolverNode = audioContext.createConvolver();
5. // Set the buffer property of the convolver node with the decoded
impulse
6. convolverNode.buffer = decodedImpulse;
7. convolverGain = audioContext.createGain();
8. convolverGain.gain.value = 0;
9. directGain = audioContext.createGain();
10. directGain.gain.value = 1;
11. // direct/dry route source -> directGain -> destination
12. source.connect(directGain);
13. directGain.connect(audioContext.destination);
14. // wet route with convolver: source -> convolver
15. // -> convolverGain -> destination
16. source.connect(convolverNode);
17. convolverNode.connect(convolverGain);
18. convolverGain.connect(audioContext.destination);
19. }
Note that at line 6 we use the decoded impulse. We could not have done this
before the impulse was loaded and decoded.
It's usually a good idea to insert a compressor in your audio graph to give a
louder, richer and fuller sound, and to prevent clipping.
In this we set the gain to a very high value that will make a saturated sound.
To prevent clipping, it is sufficient to add a compressor right at the end of the
graph! Here we use the compressor with all default settings.
NB This course does not go into detail about the different properties of the
compressor node, as they are largely for musicians with the purpose of
enabling the user to set subtle effects such as release, attack, etc.
1. <audio src="http://mainline.i3s.unice.fr/mooc/guitarRiff1.mp3"
1. id="compressorExample" controls loop
1. crossorigin="anonymous"></audio>
2. <br>
3. <label for="gainSlider1">Gain</label>
4. <input type="range" min="0" max="10" step="0.01"
5. value="8" id="gainSlider1" />
6. <button id="compressorButton">Turn compressor On</button>
If you read the description of this filter type: "Frequencies inside the range get
a boost or an attenuation; frequencies outside it are unchanged." This is
exactly what we need to write a equalizer! We're going to use several sliders,
each of which one range of frequency values.
the frequency property value of a filter will indicate the middle of the
frequency range getting a boost or an attenuation, each slider corresponds to a
filter whose frequency will be set to 60Hz, 170Hz, 350Hz, 1000Hz, 3500Hz, or
10000Hz.
the gain property value of a filter corresponds to the boost, in dB, to be
applied; if negative, it will be an attenuation. We will code the sliders' event
listeners to change the gain value of the corresponding filter.
the Q property values control the width of the frequency band. The greater the
Q value, the smaller the frequency band. We'll ignore it for the purposes of this
example.
7. </audio>
8. <div class="controls">
9. <label>60Hz</label>
10. <input type="range"
11. value="0" step="1" min="-30" max="30"
12. oninput="changeGain(this.value, 0);">
13. </input>
14. <output id="gain0">0 dB</output>
15. </div>
16. <div class="controls">
17. <label>170Hz</label>
18. <input type="range"
19. value="0" step="1" min="-30" max="30"
20. oninput="changeGain(this.value, 1);">
21. </input>
22. <output id="gain1">0 dB</output>
23. </div>
24. <div class="controls">
25. <label>350Hz</label>
26. <input type="range"
27. value="0" step="1" min="-30" max="30"
28. oninput="changeGain(this.value, 2);">
29. </input>
30. <output id="gain2">0 dB</output>
31. </div>
32. ...
33. </div>
JavaScript code:
And the example works in the same way, but this time with a video. Try moving
the sliders to change the sound!
Example at JSBin:
2D real time visualizations: waveforms
Introduction
WebAudio offers an Analyser node that provides real-time frequency and time-
domain analysis information. It leaves the audio stream unchanged from the
input to the output, but allows us to acquire data about the sound signal being
played. This data is easy for us to process since complex computations such as
Fast Fourier Transforms are being executed, behind the scenes,.
HTML code:
1. <audio src="http://mainline.i3s.unice.fr/mooc/guitarRiff1.mp3"
2. id="player" controls loop crossorigin="anonymous">
3. </audio>
4. <canvas id="myCanvas" width=300 height=100></canvas>
JavaScript code:
1. function buildAudioGraph() {
2. var mediaElement = document.getElementById('player');
3. var sourceNode = audioContext.createMediaElementSource(mediaElement);
4. // Create an analyser node
5. analyser = audioContext.createAnalyser();
6. // set visualizer options, for lower precision change 1024 to 512,
7. // 256, 128, 64 etc. bufferLength will be equal to fftSize/2
8. analyser.fftSize = 1024;
9. bufferLength = analyser.frequencyBinCount;
10. dataArray = new Uint8Array(bufferLength);
11. sourceNode.connect(analyser);
12. analyser.connect(audioContext.destination);
13. }
With the exception of lines 8-12, where we set the analyser options (explained
later), we build the following graph:
Step 2 - write the animation loop
The visualization itself depends on the options which we set for the analyser
node. we set the FFT size to 1024 (FFT is a kind of accuracy setting: the bigger
the value, the more accurate the analysis will be. 1024 is common for
visualizing waveforms, while lower values are preferred for visualizing
frequencies). Here is what we set in this example:
1. analyser.fftSize = 1024;
2. bufferLength = analyser.frequencyBinCount;
3. dataArray = new Uint8Array(bufferLength);
Here is the code that is run 60 times per second to draw the waveform:
1. function visualize() {
2. // 1 - clear the canvas
3. // like this: canvasContext.clearRect(0, 0, width, height);
4. // Or use rgba fill to give a slight blur effect
5. canvasContext.fillStyle = 'rgba(0, 0, 0, 0.5)';
6. canvasContext.fillRect(0, 0, width, height);
7. // 2 - Get the analyser data - for waveforms we need time domain
data
8. analyser.getByteTimeDomainData(dataArray);
9.
10. // 3 - draws the waveform
11. canvasContext.lineWidth = 2;
12. canvasContext.strokeStyle = 'lightBlue';
13.
14. // the waveform is in one single path, first let's
15. // clear any previous path that could be in the buffer
16. canvasContext.beginPath();
17. var sliceWidth = width / bufferLength;
18. var x = 0;
19.
20. for(var i = 0; i < bufferLength; i++) {
21. // dataArray values are between 0 and 255,
22. // normalize v, now between 0 and 1
23. var v = dataArray[i] / 255;
24. // y will be in [0, canvas height], in pixels
25. var y = v * height;
26.
27. if(i === 0) {
28. canvasContext.moveTo(x, y);
29. } else {
30. canvasContext.lineTo(x, y);
31. }
32.
33. x += sliceWidth;
34. }
35.
36. canvasContext.lineTo(canvas.width, canvas.height/2);
37. // draw the path at once
38. canvasContext.stroke();
39. // once again call the visualize function at 60 frames/s
40. requestAnimationFrame(visualize);
41. }
42.
Explanations:
Example at JSBin:
Example 3: both examples, this time with the
graphic equalizer
Adding the graphic equalizer to the graph changes nothing, we visualize the
sound that goes to the speakers. Try lowering the slider values - you should
see the waveform changing.
Example at JSBin
Example at JSBin:
The frequency range depends upon the sample rate of the signal (the audio
source) and on the FFT size. While the sound is being played, the values
change and the bar chart is animated.
The number of bars is equal to the FFT size / 2 (left screenshot with size
= 512, right screenshot with size = 64).
In the example above, the Nth bar (from left to right) corresponds to the
frequency range N *(/fftSize). If we have a sample rate equal to 44100 Hz
and size equal to 512, then the first bar represents frequencies between 0 and
44100/512 = 86.12Hz. etc. As the amount of data returned by the analyser
node is half the size, we will only be able to plot the to half the sample rate.
You will see that this is generally enough as frequencies in the second half of
the sample rate are not relevant.
The height of each bar shows the strength of that specific frequency bucket.
It's just a representation of how much of each frequency is present in the signal
(i.e. how "loud" the frequency is).
You do not have to master the signal processing 'plumbing' summarised above
- just plot the reported values!
Enough said! Let's study some extracts from the source code.
This code is very similar to Example 1 at the top of this page. We've set the FFT
size to a lower value, and rewritten the animation loop to plot frequency bars
instead of a waveform:
1. function buildAudioGraph() {
2. ...
3. // Create an analyser node
4. analyser = audioContext.createAnalyser();
5. // Try changing to lower values: 512, 256, 128, 64...
6. // Lower values are good for frequency visualizations,
7. // try 128, 64 etc.?
8. analyser.fftSize = 256;
9. ...
10. }
This time, when building the audio graph, we have used a smaller FFT size.
Values between 64 and 512 are very common here. Try them in the JSBin
example! Apart from the lines in bold, this function is exactly the same as in
Example 1.
1. function visualize() {
2. // clear the canvas
3. canvasContext.clearRect(0, 0, width, height);
4. // Get the analyser data
5. analyser.getByteFrequencyData(dataArray);
6.
7. var barWidth = width / bufferLength;
8. var barHeight;
9. var x = 0;
10. // values go from 0 to 255 and the canvas heigt is 100. Let's rescale
11. // before drawing. This is the scale factor
12. heightScale = height/128;
13. for(var i = 0; i < bufferLength; i++) {
14. // between 0 and 255
15. barHeight = dataArray[i];
16.
17. // The color is red but lighter or darker depending on the value
18. canvasContext.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';
19. // scale from [0, 255] to the canvas height [0, height] pixels
Explanations:
Here are the two functions we will call from the animation loop (borrowed and
adapted from http://www.smartjava.org/content/exploring-html5-web-audio-
visualizing-sound):
1. function drawVolumeMeter() {
2. canvasContext.save();
3. analyser.getByteFrequencyData(dataArray);
4. var average = getAverageVolume(dataArray);
5. // set the fill style to a nice gradient
6. canvasContext.fillStyle=gradient;
7. // draw the vertical meter
8. canvasContext.fillRect(0,height-average,25,height);
9. canvasContext.restore();
10. }
11.
12. function getAverageVolume(array) {
13. var values = 0;
14. var average;
15. var length = array.length;
16. // get all the frequency amplitudes
17. for (var i = 0; i < length; i++) {
18. values += array[i];
19. }
20. average = values / length;
21. return average;
22. }
Note that we are measuring intensity (line 4) and once the frequency analysis
data is copied into the , we call the getAverageVolume function(line 5) to
compute the average value which we will draw as the volume meter.
And here is what the new animation loop looks like (for the sake of clarity, we
have moved the code that draws the signal waveform to a separate function):
1. function visualize() {
2. clearCanvas();
3. drawVolumeMeter();
4. drawWaveform();
5.
6. // call again the visualize function at 60 frames/s
7. requestAnimationFrame(visualize);
8. }
Notice that we used the best practices seen in week 3 of the HTML5 part 1
course: we saved and restored the context in all functions that change
something in the canvas context (see
function drawVolumeMeter and drawWaveForm in the source code).
We added a stereoPanner node right after the source and a left/right balance
slider to control its panproperty. Use this slider to see how the left and right
volume meter react.
In order to isolate the left and the right channel (for creating individual volume
meters), we used a new node called a Channel Splitter node. From this node,
we created two routes, each going to a separate analyser (lines 46 and 47 of
the example below)
Use the connect method with extra parameters to connect the different
outputs of the channel splitter node:
Example at JSBin:
This is the audio graph we've built:
As you can see there are two routes: the one on top sends the output signal to
the speakers and uses an analyser node to animate the waveform, meanwhile
the one at the bottom splits the signal and send its left and right parts to
separate analyser nodes which draw the two volume meters. Just before the
split, we added a stereoPanner to enable adjustment of the left/right balance
with a slider.
1. function buildAudioGraph() {
2. var mediaElement = document.getElementById('player');
3. var sourceNode = audioContext.createMediaElementSource(mediaElement);
4. // connect the source node to a stereo panner
5. stereoPanner = audioContext.createStereoPanner();
6. sourceNode.connect(stereoPanner);
7. // Create an analyser node for the waveform
8. analyser = audioContext.createAnalyser();
9. // Use FFT value adapted to waveform drawing
10. analyser.fftSize = 1024;
11. bufferLength = analyser.frequencyBinCount;
12. dataArray = new Uint8Array(bufferLength);
13. // Connect the stereo panner to the analyser
14. stereoPanner.connect(analyser);
15. // and the analyser to the destination
16. analyser.connect(audioContext.destination);
17. // End of route 1. We start another route from the
18. // stereoPanner node, with two analysers for the meters
19. // Two analysers for the stereo volume meters
20. // Here we use a small FFT value as we're gonna work with
21. // frequency analysis data
22. analyserLeft = audioContext.createAnalyser();
23. analyserLeft.fftSize = 256;
24. bufferLengthLeft = analyserLeft.frequencyBinCount;
25. dataArrayLeft = new Uint8Array(bufferLengthLeft);
26. analyserRight = audioContext.createAnalyser();
27. analyserRight.fftSize = 256;
28. bufferLengthRight = analyserRight.frequencyBinCount;
29. dataArrayRight = new Uint8Array(bufferLengthRight);
30.
31. // Split the signal
32. splitter = audioContext.createChannelSplitter();
33. // Connect the stereo panner to the splitter node
34. stereoPanner.connect(splitter);
35. // Connect each of the outputs from the splitter to
36. // the analysers
37. splitter.connect(analyserLeft,0,0);
38. splitter.connect(analyserRight,1,0);
39. // No need to connect these analysers to something, the sound
40. // is already connected through the route that goes through
41. // the analyser used for the waveform
42. }
And here is the new function for drawing the two volume meters:
1. function drawVolumeMeters() {
2. canvasContext.save();
3. // set the fill style to a nice gradient
4. canvasContext.fillStyle=gradient;
5. // left channel
6. analyserLeft.getByteFrequencyData(dataArrayLeft);
7. var averageLeft = getAverageVolume(dataArrayLeft);
8. // draw the vertical meter for left channel
9. canvasContext.fillRect(0,height-averageLeft,25,height);
10. // right channel
11. analyserRight.getByteFrequencyData(dataArrayRight);
12. var averageRight = getAverageVolume(dataArrayRight);
13. // draw the vertical meter for left channel
14. canvasContext.fillRect(26,height-averageRight,25,height);
15. canvasContext.restore();
16. }
The code is very similar to the previous one. We draw two rectangles ,
corresponding to the two analyser nodes - instead of the single display in the
previous example.
These features are useful in video games: where a library of sounds may need
to ready to be played. By changing the playback rate or the effects, many
different sounds can be created, even with a limited number of samples (for
instance, an explosion played at different speed, with different effects).
Try this impressive DAW that uses free sound samples from freesound.org. Its
author calls it "Band in a browser" (more info on the Web site)! Each
instrument is a small audio file that contains all the notes played on a real
instrument. When you play a song (midi file) the app will play-along, selecting
the same musical note from the corresponding instrument audio sample. This
is all done with Web Audio and samples loaded in memory:
The author of this course wrote a multitrack audio player: it loads different mp3
files corresponding to different instruments and play/loop them in sync.
You can try it or get the sources on GitHub. The documentation is in the help
menu.
Try also this small demonstration that uses the Howler.js library for loading
sound samples in memory and playing them using WebAudio (we'll discuss this
library later). Click on the main window and notice how fast the sound effects
are played. Click as fast as you can!
Notice in the code that each time we click on the button, we rebuild the audio
graph.
But don't worry, Web Audio is optimized for handling thousands of nodes...
HTML code extract:
1. var ;
2.
3. var soundURL =
4. 'http://mainline.i3s.unice.fr/mooc/shoot2.mp3';
5. var decodedSound;
6.
7. window.onload = function init() {
8. // The page has been loaded
9. // To make it work even on browsers like Safari, that still
10. // do not recognize the non prefixed version of AudioContext
11.
var audioContext = window.AudioContext || window.webkitAudioContext;
12.
13. ctx = new audioContext();
14.
15. loadSoundUsingAjax(soundURL);
16. // By default the button is disabled, it will be
17. // clickable only when the sound sample will be loaded
18. playButton.onclick = function(evt) {
19. playSound(decodedSound);
20. };
21. };
22.
23. function loadSoundUsingAjax(url) {
24. var request = new XMLHttpRequest();
25. request.open('GET', url, true);
26. // Important: we're loading binary data
27. request.responseType = 'arraybuffer';
28.
29. // Decode asynchronously
30. request.onload = function() {
31. console.log("Sound loaded");
32. // Let's decode it. This is also asynchronous
33. ctx.decodeAudioData(request.response,
34. function(buffer) { // success
35. console.log("Sound decoded");
36. decodedSound = buffer;
37. // we enable the button
38. playButton.disabled = false;
39. },
40. function(e) { // error
41. console.log("error");
42. }
43. ); // end of decodeAudioData callback
44. }; // end of the onload callback
45. // Send the request. When the file will be loaded,
46. // the request.onload callback will be called (above)
47. request.send();
48. }
49.
50.function playSound(buffer){
51. // builds the audio graph, then start playing the source
52. var bufferSource = ctx.createBufferSource();
53. bufferSource.buffer = buffer;
54. bufferSource.connect(ctx.destination);
55. bufferSource.start(); // remember, you can start() a source only
once!
56.}
Explanations:
When the page is loaded, we first call the loadSoundUsingAjax function for
loading and decoding the sound sample (line 16), then we define a click
listener for the play button. Loading and decoding the sound can take some
time, so it's an asynchronous process. This means that the call
to loadSoundUsingAjaxwill return while the downloading and decoding is still
in progress. We can define a click listener on the button anyway, as it disabled
by default (see the HTML code). Once the sample has been loaded and
decoded, only then will the button be enabled (line 42).
The loadSoundUsingAjax function will first create using the "new version of
Ajax called XhR2" (described in detail during week 3). we create the request
(lines 26-30): notice the use of 'arrayBuffer' as a responseType for the
request. This has been introduced by Xhr2 and is necessary for binary file
transfer. Then the request is sent (line 52).
Ajax is an asynchronous process: once the browser receives the requested
file, the request.callback will be called (it is defined at line 33), and we can
decode the file ( the content of which must be uncompressed in memory). This
is done by calling .decodeAudioData(file, successCallback,
errorCallback). When the file is decoded, the success callback is called (line
38-43). We store the decoded buffer in the variable decodedSound, and we
enable the button.
Now, when someone clicks on the button, the playSound function will be
called (lines 55-61). This function builds a simple audio graph: it creates
an AudioBufferSourceNode (line 57), sets its buffer property with the
decoded sample, connects this source to the speakers (line 59) and plays the
sound. Source nodes can only be used once (a "fire and forget"
philosophy), so to play the sound again, we have to rebuild a source
node and connect that to the destination. This seems strange when
you learn Web Audio, but don't worry - it's a very fast operation, even
with hundreds of nodes.
because we will never know exactly when all the sounds have finished being
loaded and decoded. All these calls will run operations in the background yet
return instantly.
The BufferLoader utility object: useful for preloading sound
and image assets
There are different approaches for dealing with this problem. During the HTML5
Part 1 course, we presented utility functions for loading multiple images. Here
we use the same approach and have packaged the code into an object called
the BufferedLoader.
HTML code:
JavaScript code extract (does not contain the BufferLoader utility code):
1. var listOfSoundSamplesURLs = [
2. 'http://mainline.i3s.unice.fr/mooc/shoot1.mp3',
3. 'http://mainline.i3s.unice.fr/mooc/shoot2.mp3'
4. ];
5.
6. window.onload = function init() {
7. // To make it work even on browsers like Safari, that still
8. // do not recognize the non prefixed version of AudioContext
9. var audioContext = window.AudioContext || window.webkitAudioContext;
10.
11. ctx = new audioContext();
12. loadAllSoundSamples();
13. };
14.
15. function playSampleNormal(buffer){
16. // builds the audio graph and play
17. var bufferSource = ctx.createBufferSource();
18. bufferSource.buffer = buffer;
19. bufferSource.connect(ctx.destination);
20. bufferSource.start();
21. }
22.
23.
24. function onSamplesDecoded(buffers){
25. console.log("all samples loaded and decoded");
26. // enables the buttons
27. shot1Normal.disabled=false;
28. shot2Normal.disabled=false;
29. // creates the click listeners on the buttons
30. shot1Normal.onclick = function(evt) {
31. playSampleNormal(buffers[0]);
32. };
33. shot2Normal.onclick = function(evt) {
34. playSampleNormal(buffers[1]);
35. };
36. }
37.
38. function loadAllSoundSamples() {
39. // onSamplesDecoded will be called when all samples
40. // have been loaded and decoded, and the decoded sample will
41. // be its only parameter (see function above)
42.
oded);
After the call to loadAllSoundSamples() (line 13), when all the sound sample
files have been loaded and decoded, a callback will be initiated
to onSamplesDecoded(decodedSamples), located at line 25. The array of
decoded samples is the parameter of the onSamplesDecoded function.
The BufferLoader utility object is created at line 45 and takes as parameters:
1) the audio context, 2) an array listing the URLs of the different audio files to
be loaded and decoded, and 3) the callback function which is to be called once
all the files have been loaded and decoded. This callback function should
accept an array as its parameter: the array of decoded sound files.
To study the source of the BufferLoaded object, look at the JavaScript tab in the
example at JSBin.
makeSource(buffer)
1. function makeSource(buffer) {
2. // build graph source -> gain -> compressor -> speakers
3. // We use a compressor at the end to cut the part of the signal
4. // that would make peaks
5. // create the nodes
6. var source = ctx.createBufferSource();
7. var compressor = ctx.createDynamicsCompressor();
8. var gain = ctx.createGain();
9. // set their properties
10. // Not all shots will have the same volume
11. gain.gain.value = 0.2 + Math.random();
12. source.buffer = buffer;
13. // Build the graph
14. source.connect(gain);
15. gain.connect(compressor);
16. compressor.connect(ctx.destination);
17. return source;
18. }
And this is the function that plays different sounds in a row, eventually
creating random time intervals between them and random pitch variations:
Explanations:
Lines 11-15: we make a loop for building multiple routes in the graph. The
number of routes corresponds to the number of times that we want the same
buffer to be played. Note that the random2 parameter enables us to randomize
the playback rate of the source node that corresponds to the pitch of the
sound.
Line 14: this is where the sound is being played. Instead of calling
source.start(), we call source.start(delay), this tells the Web Audio to play the
sound after a certain time.
The makeSource function builds a graph from one decoded sample to the
speakers. is added that is also randomized in order to generate sounds with
different volumes (between 0.2 and 1.2 in the example). A compressor node is
added in order to limit the max intensity of the signal in case the gain makes it
peak.
Useful libraries
It's best practice to know the Web Audio API itself. Many of the examples
demonstrated during this course may be hard to write using high-level
libraries. However, if you don't have too many custom needs, such libraries can
make your life simpler! Also, some libraries use sound synthesis that we did not
cover in the course and are fun to use - for example, adding 8-bit sounds to
your HTML5 game!
HowlerJS: useful for loading and playing sound sample in video games. Can
handle audio sprites (multiple sounds in a single audio file), loops,
spatialization. Very simple to use. Try this very simple example we prepared for
you at JsBin that uses HowlerJS!
, and in particular a helper built with this library, for adding 8-bit procedural
sounds to video games, without the need to load audio files. Try the demo!
There is also a sound generator you can try. When you find a sound you like,
just copy and paste the parameter values into your code.
For writing musical applications, take a look at ToneJS !
Week 2
During the late 1990s and early 2000s, JavaScript increased in popularity, and
the community coined the term 'DHTML' (Dynamic HTML), which was to be
the first umbrella term describing a collection of technologies used together to
create interactive and animated Web sites. Developers of the DHTML era
hadn't forgotten about Porter's 'Game Lib', and within a couple of years, Brent
Silby presented 'Game Lib 2'. It is still possible to play many games created
with that library on his Web site.
You can have multiple canvas elements on one page, even stacked one on top
of another, like transparent layers. Each will be visible in the DOM tree and
has own state independent of the others. It behaves like a regular DOM
element.
The canvas has a rich JavaScript API for drawing all kinds of shapes; we can
draw wireframe or filled shapes and set several properties such as color, line
width, patterns, gradients, etc. It also supports transparency and pixel level
manipulations. It is supported by all browsers, on or mobile phones, and on
most it will take advantage of hardware acceleration.
Using the WebSockets technology (which is not part of HTML5 but comes from
the W3C WebRTC specification - "Real-time Communication Between
Browsers"), you can create two-way communication sessions between multiple
browsers and a server. The WebSocket and useful libraries built on top of it
such as .io, provide the means for sending messages to a server and receiving
event-driven responses without having to poll the server for a reply.
Introduction
The "game loop" is the main component of any
game. It separates the game logic and the visual
layer from a user's input and actions.
Try an example at JSBin : open the HTML, and output tabs to see the code.
1. setInterval(‘addStarToTheBody()’, 200);
2. setInterval(‘document.body.innerHTML += “*”;’, 200);
GOOD:
1. setInterval(function(){
2. document.body.innerHTML += “*”;
3. }, 200);
Reminder from HTML5 part 1 course, week 4: with setInterval - if we set the
number of milliseconds at, say, 200, it will call our game loop function EACH
200ms, even if the previous one is not yet finished. Because of this
disadvantage, we might prefer to use another function, better suited to our
goals.
The setTimeout function works like setInterval but with one little difference:
it calls your function AFTER a given amount of time.
Try an example at JSBin: open the HTML, and output tabs to see the code. This
example does the same thing as the previous example by adding a "*" to the
document every 200ms.
For several years, setTimeout was the best and most popular JavaScript
implementation of game loops. This changed when Mozilla presented the
requestAnimationFrame API, which became the reference W3C standard API for
game animation.
When we use timeouts or intervals in our animations, the browser doesn’t have
any information about our intentions -- do we want to repaint the DOM
structure or a canvas during every loop? Or maybe we just want to make some
calculations or send requests a couple of times per second? For this reason, it
is really hard for the browser’s engine to optimize the loop.
And since we want to repaint the game (move the characters, animate sprites,
etc.) every frame, and other contributors/developers introduced a new
approach which they called requestAnimationFrame.
This approach helps the browser to optimize all the animations on the screen,
no matter whether Canvas, DOM or WebGL. Also, if the animation loop is
running in a browser tab that is not currently visible, the browser won't keep it
running.
This target may be hard to reach; the animation loop content may
take longer than this, or the scheduler may be a bit early or late.
The timestamp parameter of the function is useful for exactly that: it gives
a time.
The most famous has been written by Paul Irish from the jQuery team. He
wrote this shim to simplify the usage of requestAnimationFrame in different
browsers (look at WikiPedia for the meaning of "shim").
The support for the standard API is very good with modern browsers, so we will
not use this shim in our future examples. If you would like to target "old
browsers" as well, just adapt your code to this polyfill - it's just a matter of
changing two lines of code and inserting the JS shim.
Current support:
Up to date version of this table.
Introduction
We are going to develop a game - not all at once, let's divide the whole job into
a series of smaller tasks. The first step is to create a foundation or basic
structure.
Let's start by building the skeleton of a small game framework, based on the
Black Box Driven Development in JavaScript methodology. In other words: a
game framework skeleton is a simple object-based model that uses
encapsulation to expose only useful methods and properties.
We will evolve this framework throughout the lessons in this course, and cut it
in different files once it becomes too large to fit one single file.
1. var GF = function(){
2. var mainLoop = function(time){
3. //Main function, called each frame
4. requestAnimationFrame(mainLoop);
5. };
6. var start = function(){
7. requestAnimationFrame(mainLoop);
8. };
9. // Our GameFramework returns a public API visible from outside its scope
10. // Here we only expose the start method, under the "start" property
name.
11. return {
12. start: start
13. };
14. };
With this skeleton, it's very easy to create a new game instance:
Quick glossary: the word delta is the name of a Greek letter (uppercase Δ,
lowercase δ or ?). The upper-case version is used in mathematics as an
abbreviation for measuring the change in some object, over time - in our case,
how quickly the is running. This dictates the maximum speed at which the
game display will be updated. This maximum speed could be referred to as
the rate of change. We call what is displayed at a single point-in-time, a frame.
Thus the rate of change can be measured in frames per second
(fps). Accordingly, our game's determines the achievable frame rate
- the shorter the delta (measured in mS), the faster the possible rate of
change (in fps).
Now we can call the measureFPS function from inside the animation loop,
passing it the current time, given by the timer that comes with
the requestAnimationFrame API:
And the <div> element used to display FPS on the screen is created in this
example by the start() function:
My favorite hack uses the onerror callback on an <img> element like this:
1. function mainloop(){
2. var img = new Image;
3. img.onerror = mainloop;
4. img.src = 'data:image/png,' + Math.random();
5. }
What we are doing here, is creating a new image on each frame and providing
invalid data as a source of the image. The image cannot be displayed properly,
so the browser calls the event handler that is the function itself, and so on.
Funny right? Please try this and check the number of FPS displayed with this
JSBin example.
Source code extract of this example:
Introduction
[Note: drawing within a canvas was studied in detail during the W3C HTML5
Part 1 course, in week 3.]
Good news! We will add graphics to our game engine in this lesson! To-date
we have talked of "basic concepts"; so without further ado, let's draw
something, animate it, and move shapes around the screen :-)
Let's do this by including into our framework the same "monster" we used
during the HTML5 Part 1 course.
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="utf-8">
5. <title>Draw a monster in a canvas</title>
6. </head>
7. <body>
8. <canvas id="myCanvas" width="200" height="200"></canvas>
9. </body>
10. </html>
Let's use CSS to reveal the canvas, for example, add a 1px black border around
it:
1. canvas {
2. border: 1px solid black;
3. }
1.Use a function that is called AFTER the page is fully loaded (and the DOM is
ready), set a pointer to the canvas node in the DOM.
2.Then, get a 2D graphic context for this canvas ( is an object we will use to
draw on the canvas, to set global properties such as color, gradients, patterns
and line width).
3.Only then can you can draw something,
4.Do not forget to use global variables for the canvas and context objects. I
also recommend keeping the width and height of the canvas somewhere.
These might be useful later.
5.For each function that will change the context (color, line width, coordinate
system, etc.), start by saving the context, and end by restoring it.
In this small example, we used the context object to draw a monster using the
default color (black) and wireframe and filled modes:
HTML code:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="utf-8">
5. <title>Trembling monster in the Game Framework</title>
6. </head>
7. <body>
8. <canvas id="myCanvas" width="200" height="200"></canvas>
9. </body>
10. </html>
1. // Inits
2. window.onload = function init() {
3. var game = new GF();
4. game.start();
5. };
6.
7.
8. // GAME FRAMEWORK STARTS HERE
9. var GF = function(){
10. // Vars relative to the canvas
11. var canvas, , w, h;
12.
13. ...
14. var measureFPS = function(newTime){
15. ...
16. };
17. // Clears the canvas content
18. function clearCanvas() {
19. ctx.clearRect(0, 0, w, h);
20. }
21. // Functions for drawing the monster and perhaps other objects
22. function drawMyMonster(x, y) {
23. ...
24. }
25. var mainLoop = function(time){
26. // Main function, called each frame
27. measureFPS(time);
28. // Clear the canvas
29. clearCanvas();
30. // Draw the monster
31. drawMyMonster(10+Math.random()*10, 10+Math.random()*10);
32. // Call the animation loop every 1/60th of second
33. requestAnimationFrame(mainLoop);
34. };
35.
36. var start = function(){
37. ...
38. // Canvas, context etc.
39. canvas = document.querySelector("#myCanvas");
40. // often useful
41. w = canvas.width;
42. h = canvas.height;
43. // important, we will draw with this object
44. ctx = canvas.getContext('2d');
45.
46. // Start the animation
47. requestAnimationFrame(mainLoop);
48. };
49.
50. //our GameFramework returns a public API visible from outside its scope
51. return {
52. start: start
53. };
54. };
Explanations:
Note that we now start the game engine in a window. callback (line 2), so only
after the page has been loaded.
We also moved 99% of the init() method from the previous example into
the start() method of the game and added the canvas, , w, h variables as
global variables to the game framework object.
Finally, in the main loop we added a call to the drawMonster() function,
injecting randomicity through the parameters: the monster is drawn with an
x,y offset of between 0 and 10, in successive frames of the animation.
And we clear the previous canvas content before drawing the current frame
(line 35).
If you try the example, you will see a trembling monster. The canvas is cleared
and the monster drawn in random positions, at around 60 times per second!
In the next part of this week's course, we'll see how to interact with it using the
mouse or the keyboard.
Input & output: how events work in Web apps & games?
In any case, the events are called DOM events, and we use the DOM APIs to
create event handlers.
This method is very easy to use, but it is not the recommended way to handle
events. Indeed, It works today but is deprecated (will probably be abandoned
in the future). Mixing 'visual layer' (HTML) and 'logic layer' (JavaScript) in one
place is really bad practice and causes a host of problems during development.
1. document.getElementById('someDiv').onclick = function() {
2. alert(!');
3. }
1. document.getElementById('someDiv').addEventListener('click', function() {
2. alert('clicked!');
3. }, false);
Note that the third parameter describes whether the callback has to be called
during the captured phase. This is not important for now, just set it to false.
Depending on the type of event you are listening to, you will consult different
properties from the event object in order to obtain useful information such as:
"which keys are pressed down?", "what is the location of the mouse cursor?",
"which mouse button has been clicked?", etc.
In the following lessons, we will remind you now how to deal with the keyboard
and the mouse (previously covered during the HTML5 Part 1 course) in the
context of a game engine (in particular, how to manage multiple events at the
same time), and also demonstrate how you can accept input from a using the
new Gamepad API.
Further reading
In Method 1 (above) we mentioned that "Mixing 'visual layer' (HTML) and 'logic
layer' (JavaScript) ... bad practice", and this is similarly reflected in many style
features being deprecated in HTML5 and moved into CSS3. The management
philosophy at play here is called "the separation of concerns" and applies in
several ways to software development - at the code level, through to the
management of staff. It's not part of the course, but professionals may find the
following references useful:
Separation of concerns - Wikipedia, the free encyclopedia
Chapter 5. Separation of Concerns from Programming JavaScript
Applications, by Eric Elliott, O'Reilly, 2013.
The Art of Separation of Concerns by , January 3, 2008
After a keyboard-related event (eg keydown or ), the code of the key that fired
the event will be passed to the listener function. It is possible to test which key
has been pressed or released, like this:
1. window.addEventListener(', function(event) {
2. if (event.keyCode === 37) {
3. // Left arrow was pressed
4. }
5. }, false);
You can try key codes with this interactive example, and here is a list of
keyCodes (from CSS Tricks):
Game requirements:
managing multiple keypress / events
In a game, we often need to check which keys are being used, at a very high
frequency - typically from inside the game loop that is looping at up to 60 times
per second.
If a spaceship is moving left, chances are you are keeping the left arrow down,
and if it's firing missiles at the same time you must also be pressing the space
bar like a maniac, and maybe pressing the shift key to release smart bombs.
Sometimes these three keys might be down at the same time, and the game
loop will have to take these three keys into account: move the ship left, release
a new missile if the previous one is out of the screen or if it reached a target,
launch a smart bomb if conditions are met, etc.
We will update its content inside the different input event listeners, and later
check its values inside the game loop to make the game react accordingly.
So, these are the changes to our small game engine prototype (which is far
from finished yet):
1. // Inits
2. window.onload = function init() {
3. var game = new GF();
4. game.start();
5. };
6. // GAME FRAMEWORK STARTS HERE
7. var GF = function(){
8. ...
9. // vars for handling inputs
10. var inputStates = {};
11. var measureFPS = function(newTime){
12. ...
13. };
14. // Clears the canvas content
15. function clearCanvas() {
16. ctx.clearRect(0, 0, w, h);
17. }
18. // Functions for drawing the monster and perhaps other objects
19. function drawMyMonster(x, y) {
20. ...
21. }
22. var mainLoop = function(time){
23. // Main function, called each frame
24. measureFPS(time);
25. // Clears the canvas
26. clearCanvas();
27. // Draws the monster
28. drawMyMonster(10+Math.random()*10, 10+Math.random()*10);
29.
30. // check inputStates
31. if (inputStates.left) {
32. ctx.fillText("left", 150, 20);
33. }
34. if (inputStates.up) {
35. ctx.fillText("up", 150, 50);
36. }
37. if (inputStates.right) {
38. ctx.fillText("right", 150, 80);
39. }
40. if (inputStates.down) {
41. ctx.fillText("down", 150, 120);
42. }
43. if (inputStates.space) {
44. ctx.fillText("space bar", 140, 150);
45. }
46. // Calls the animation loop every 1/60th of second
47. requestAnimationFrame(mainLoop);
48. };
49. var start = function(){
50. ...
51. // Important, we will draw with this object
52. ctx = canvas.getContext('2d');
53. // Default police for text
54. ctx.font="20px Arial";
55. // Add the listener to the main, window object, and update the
states
56. window.addEventListener('keydown', function(event){
57. if (event.keyCode === 37) {
58. inputStates.left = true;
59. } else if (event.keyCode === 38) {
60. inputStates.up = true;
61. } else if (event.keyCode === 39) {
62. inputStates.right = true;
63. } else if (event.keyCode === 40) {
64. inputStates.down = true;
65. } else if (event.keyCode === 32) {
66. inputStates.space = true;
67. }
68. }, false);
69. // If the key is released, change the states object
70. window.addEventListener('keyup', function(event){
71. if (event.keyCode === 37) {
72. inputStates.left = false;
73. } else if (event.keyCode === 38) {
74. inputStates.up = false;
75. } else if (event.keyCode === 39) {
76. inputStates.right = false;
77. } else if (event.keyCode === 40) {
78. inputStates.down = false;
79. } else if (event.keyCode === 32) {
80. inputStates.space = false;
81. }
82. }, false);
83. // Starts the animation
84. requestAnimationFrame(mainLoop);
85. };
86. // our GameFramework returns a public API visible from outside its scope
87. return {
88. start: start
89. };
90. };
You may notice that on some computers / operating systems, it is not possible
to simultaneously press the up and down arrow keys, or left and right
arrow because they are mutually exclusive. space + up + right should work in
combination.
Dealing with mouse events
Special care must be taken when acquiring mouse coordinates because the
HTML5 canvas has (or directed) CSS properties which could produce false
coordinates. The trick to the right x and y mouse cursor coordinates is to use
this method from the canvas API:
The width and height of the rect object must be taken into account. These
dimensions correspond to the padding / margins / borders of the canvas. See
how we deal with them in the getMousePos() function in the next example.
Move the mouse over the canvas and press or release mouse buttons. Notice
that we keep the state of the mouse (position, buttons up or down) as part of
the inputStates object, just as we do with the keyboard (per lesson).
Source code:
Explanations:
line 25 calculates the angle between mouse cursor and the rectangle,
lines 27-28 move the rectangle v pixels along a line between the rectangle's
current position and the mouse cursor,
Lines 41-46 translate the rectangle, rotate it, and recenter the rotational
point to the center of the rectangle (in its new position).
The new online version of the game engine can be tried at JSBin:
Try pressing arrows and space keys, moving the mouse, and pressing the
buttons, all at the same time. You'll see that the game framework handles all
these events simultaneously because the global variable
namedinputStates is updated by keyboard and mouse events, and consulted
to direct movements every 1/60th second.
1. // Inits
2. window.onload = function init() {
3. var game = new GF();
4. game.start();
5. };
6. // GAME FRAMEWORK STARTS HERE
7. var GF = function(){
8. ...
9. // Vars for handling inputs
10. var inputStates = {};
11. var measureFPS = function(newTime){
12. ...
13. };
14. // Clears the canvas content
15. function clearCanvas() {
16. ctx.clearRect(0, 0, w, h);
17. }
18. // Functions for drawing the monster and perhaps other objects
19. function drawMyMonster(x, y) {
20. ...
21. }
22. var mainLoop = function(time){
23. // Main function, called each frame
24. measureFPS(time);
25. // Clears the canvas
26. clearCanvas();
27. // Draws the monster
28. drawMyMonster(10+Math.random()*10, 10+Math.random()*10);
29. // Checks inputStates
30. if (inputStates.left) {
31. ctx.fillText("left", 150, 20);
32. }
33. if (inputStates.up) {
34. ctx.fillText("up", 150, 40);
35. }
36. if (inputStates.right) {
37. ctx.fillText("right", 150, 60);
38. }
39. if (inputStates.down) {
40. ctx.fillText("down", 150, 80);
41. }
42. if (inputStates.space) {
43. ctx.fillText("space bar", 140, 100);
44. }
45. if (inputStates.mousePos) {
46. ctx.fillText("x = " + inputStates.mousePos.x + " y = " +
47. inputStates.mousePos.y, 5, 150);
48. }
49. if (inputStates.mousedown) {
50. ctx.fillText("mousedown b" + inputStates.mouseButton, 5, 180);
51. }
52. // Calls the animation loop every 1/60th of second
53. requestAnimationFrame(mainLoop);
54. };
55. function getMousePos(evt) {
56. // Necessary to take into account CSS boudaries
57. var rect = canvas.getBoundingClientRect();
58. return {
59. x: evt.clientX - rect.left,
60. y: evt.clientY - rect.top
61. };
62. }
63. var start = function(){
64. ...
65. // Adds the listener to the main window object, and updates the states
66. window.addEventListener('keydown', function(event){
67. if (event.keyCode === 37) {
68. inputStates.left = true;
69. } else if (event.keyCode === 38) {
70. inputStates.up = true;
71. } else if (event.keyCode === 39) {
72. inputStates.right = true;
73. } else if (event.keyCode === 40) {
74. inputStates.down = true;
75. } else if (event.keyCode === 32) {
76. inputStates.space = true;
77. }
78. }, false);
79. // If the key is released, changes the states object
80. window.addEventListener('keyup', function(event){
81. if (event.keyCode === 37) {
82. inputStates.left = false;
83. } else if (event.keyCode === 38) {
84. inputStates.up = false;
85. } else if (event.keyCode === 39) {
86. inputStates.right = false;
87. } else if (event.keyCode === 40) {
88. inputStates.down = false;
89. } else if (event.keyCode === 32) {
90. inputStates.space = false;
91. }
92. }, false);
93. // Mouse event listeners
94. canvas.addEventListener('mousemove', function (evt) {
95. inputStates.mousePos = getMousePos(evt);
96. }, false);
97. canvas.addEventListener('mousedown', function (evt) {
98. inputStates.mousedown = true;
99. inputStates.mouseButton = evt.button;
100. }, false);
101. canvas.addEventListener('mouseup', function (evt) {
102. inputStates.mousedown = false;
103. }, false);
104. // Starts the animation
105. requestAnimationFrame(mainLoop);
106. };
107. // Our GameFramework returns a public API visible from outside its
scope
108. return {
109. start: start
110. };
111. };
Introduction
Some games, mainly arcade/action games, are designed to be used with a
gamepad:
The Gamepad API is currently (as at July 2016) supported by all major browsers
(including Microsoft Edge), except Safari and Internet Explorer. Note that the
API is still a draft and may change in the future (though it has changed very
little since 2012). We recommend using a Wired Xbox 360 Controller or a PS2
controller, both of which should work out of the box on Windows XP, Windows
Vista, Windows, and Linux desktop distributions. Wireless controllers are
supposed to work too, but we haven't tested the API with them. You may find
someone who has managed but they've probably needed to install an
operating system driver to make it work.
See the up to date version of this table.
Detecting gamepads
Events triggered when the gamepad is plugged in or
unplugged
Let's start with a 'discovery' script to check that the GamePad is connected,
and to see the range of facilities it can offer to JavaScript.
If the user interacts with a controller (presses a button, moves a stick) a event
will be sent to the page. NB the page must be visible! The event object passed
to the listener has a gamepad property which describes the connected device.
Example on JSBin
1. window.addEventListener("gamepadconnected", function(e) {
2. var gamepad = e.gamepad;
3. var index = gamepad.index;
4. var id = gamepad.id;
5. var nbButtons = gamepad.buttons.length;
6. var nbAxes = gamepad.axes.length;
7. console.log("Gamepad No " + index +
8. ", with id " + id + " is connected. It has " +
9. nbButtons + " buttons and " +
10. nbAxes + " axes");
11. });
1. window.addEventListener("", function(e) {
2. var gamepad = e.gamepad;
3. var index = gamepad.index;
4. console.log("Gamepad No " + index + " has been disconnected");
5. });
1. var gamepad;
2.
3. function mainloop() {
4. ...
5. scangamepads();
6.
7. // test gamepad status: buttons, joysticks etc.
8. ...
9. requestAnimationFrame(mainloop);
10. }
11.
12. function scangamepads() {
13. // function called 60 times/s
14. // the gamepad is a "snapshot", so we need to set it
15. // 60 times / second in order to have an updated status
16. var gamepads = navigator.getGamepads();
17. for (var i = 0; i < gamepads.length; i++) {
18. // current gamepad is not necessarily the first
19. if(gamepads[i] !== undefined)
20. gamepad = gamepads[i];
21. }
22. }
In this code, we check every 1/60 second for newly or re-connected gamepads,
and we update the gamepadglobal var with the first gamepad object returned
by the browser. We need to do this so that we have an accurate "snapshot" of
the gamepad state, with fixed values for the buttons, axes, etc. If we want to
check the current button and joystick , we must poll the browser at a high
frequency and call for an updated snapshot.
To keep things simple, the above code works with a single gamepad - here's a
good example of managing multiple gamepads.
Example on JSBin. You might also give a look at at this demo that does the
same thing but with multiple gamepads.
Code for checking if a button is pressed:
1. function checkButtons(gamepad) {
2. for (var i = 0; i < gamepad.buttons.length; i++) {
3. // do nothing is the gamepad is not ok
4. if(gamepad === undefined) return;
5. if(!gamepad.connected) return;
6. var b = gamepad.buttons[i];
7. if(b.pressed) {
8. console.log("Button " + i + " is pressed.");
9. if(b.value !== undefined)
10. // analog trigger L2 or R2, value is a float in [0, 1]
11. console.log("Its value:" + b.val);
12. }
13. }
14. }
In line 11, notice how we detect whether the current button is an analog trigger
(L2 or R2 on Xbox360 or PS2/PS3 gamepads).
Next, we'll integrate it into the code. Note that we also need to call
the function from the loop, to generate fresh "snapshots" of the gamepad with
updated properties. Without this call, the gamepad.buttons will return the
same states every time.
1. function () {
2. // clear, draw objects, etc...
3. ...
4. scangamepads();
5. // Check gamepad button states
6. checkButtons(gamepad);
7. // animate at 60 frames/s
8. requestAnimationFrame(mainloop);
9. }
JSBin example:
Traditionally Linux has been described as 'for work only' or 'no games', so it was a
pleasant surprise to see how easy things were - no "driver" to install (it seems important to
uninstall any existing connection between a device and the x-server), installed "joystick"
testing and calibration tool, and the "-" configuration and testing tool; and that was 'it' - no
actual configuration was necessary!
External resources
THE BEST resource (December 2015): this paper from smashingmagazine.com
tells you everything about the GamePad API. Very complete, explains how to
set a dead zone, a keyboard fallback, etc.
Good article about using the gamepad API on the Mozilla Developer Network
site
An interesting article on the gamepad support, published on the HTML5 Rocks
Web site
gamepad.js is a Javascript library to enable the use of gamepads and joysticks
in the browser. It smoothes over the differences between browsers, platforms,
APIs, and a wide variety of gamepad/joystick devices.
Another library we used in our team for controlling a mobile robot (good
support from the authors)
Gamepad Controls for HTML5 Games
Move the monster with keyboard and mouse
Check this online example at JSBin: we've changed very few lines of code from
the previous evolution!
Note: this is not the best way to animate objects in a game; we will look at a far
better solution - "animation" - in another lesson.
1. function updateMonsterPosition() {
2. monster.speedX = monster.speedY = 0;
3. // Checks inputStates
4. if (inputStates.left) {
5. ctx.fillText("left", 150, 20);
6. monster.speedX = -monster.speed;
7. }
8. if (inputStates.up) {
9. ctx.fillText("up", 150, 40);
10. monster.speedY = -monster.speed;
11. }
12. if (inputStates.right) {
13. ctx.fillText("right", 150, 60);
14. monster.speedX = monster.speed;
15. }
16. if (inputStates.down) {
17. ctx.fillText("down", 150, 80);
18. monster.speedY = monster.speed;
19. }
20. if (inputStates.space) {
21. ctx.fillText("space bar", 140, 100);
22. }
23. if (inputStates.mousePos) {
24. ctx.fillText("x = " + inputStates.mousePos.x + " y = " +
25. inputStates.mousePos.y, 5, 150);
26. }
27. if (inputStates.mousedown) {
28. ctx.fillText("mousedown b" + inputStates.mouseButton, 5, 180);
29. monster.speed = 5;
30. } else {
31. // Mouse up
32. monster.speed = 1;
33. }
34. monster.x += monster.speedX;
35. monster.y += monster.speedY;
36. }
Explanations:
Notice that two arrow keys and a mouse button can be pressed down at the
same time. In this situation, the monster will take a diagonal direction and
accelerate. This is why it is important to keep all the input states up-to-date,
and not to handle single events individually, as we did in week 4 of the HTML5
Part 1 course!
gamepad enhancements
Let's add the gamepad utility functions from the previous lesson (we tidied
them a bit too, removing the code for displaying the progress bars, buttons,
etc.), added a gamepad property to the game framework, and added one new
call in the game loop for updating the gamepad status:
And here is the updateGamePadStatus function (the inner function calls are to
gamepad utility functions detailed in the previous lesson):
1. function updateGamePadStatus() {
2. // get new snapshot of the gamepad properties
3. scangamepads();
4. // Check gamepad button states
5. checkButtons(gamepad);
6. // Check joysticks
7. checkAxes(gamepad);
8. }
The checkAxes function updates the left, right, up, down properties of
the inputStates object we previously used with key events. Therefore, without
changing any code in the updatePlayerPositionfunction, the monster moves
by joystick command!
Time-based animation
Introduction
Let's study an important technique known as "time-based animation", that
is used by nearly all "real" video games.
The way to address this is to run at a lower frame-rate on the phone. This will
enable the car to race around the track in the same amount of (real) time as it
does on a powerful desktop computer.
Solution: you need to compute the amount of time that has elapsed between
the last frame that was drawn and the current one; and depending on this delta
of time, adjust the distance the car must move across the screen. We will see
several examples of this later.
You want to perform some animations only a few times per
second. For example, in sprite-based animation (drawing different images as a
character moves, for example), you will not change the images of the
animation 60 times/s, but only ten times per second. Mario will walk on the
screen in a 60 fps animation, but his posture will not change every 1/60th of .
You may also want to accurately set the framerate, leaving some CPU
time for other tasks. Many games consoles limit the frame-rate to 1/30th of a
second, in order to allow time for other sorts of computations (physics engine,
artificial intelligence, etc.)
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset=utf-8 />
5. <title>Small animation example</title>
6. <script>
7. var canvas, ctx;
8. var width, height;
9. var x, y;
10. var speedX;
11. // Called after the DOM is ready (page loaded)
12. function init() {
13. // init the different variables
14. canvas = document.querySelector("#mycanvas");
15. ctx = canvas.getContext('2d');
16. width = canvas.width;
17. height = canvas.height;
18. x=10; y = 10;
19. // Move 3 pixels left or right at each frame
20. speedX = 3;
21. // Start animation
22. animationLoop();
23. }
24. function animationLoop() {
25. // an animation involves: 1) clear canvas and 2) draw shapes,
26. // 3) move shapes, 4) recall the loop with requestAnimationFrame
27. // clear canvas
28. ctx.clearRect(0, 0, width, height);
29. ctx.strokeRect(x, y, 10, 10);
30. // move rectangle
31. x += speedX;
32. // check collision on left or right
33. if(((x+5) > width) || (x <= 0)) {
34. // cancel move + inverse speed
35. x -= speedX;
36. speedX = -speedX;
37. }
38. // animate.
39. requestAnimationFrame(animationLoop);
40. }
41. </script>
42. </head>
43. <body onload="init();">
44. <canvas id="mycanvas" width="200" height="50" style="border: 2px
solid black">
45. </canvas>
46. </body>
47. </html>
If you try this example on a low-end smartphone (use this URL for the example
in stand-alone mode: http://jsbin.com/dibuze) and if you run it at the same time
on a desktop PC, it is obvious that the rectangle moves faster on the desktop
computer screen than on your phone.
This is because the frame rate differs between the computer and the
smartphone: perhaps 60 fps on the computer and 25 fps on the phone. As we
only move the rectangle in the animationLoop, in one second the rectangle will
be moved 25 times on the smartphone compared with 60 times on the
computer! Since we move the rectangle the same number of pixels each time,
the rectangle moves faster on the computer!
Try it on JsBin and notice that the square moves much slower on the screen.
Indeed, its speed is a direct consequence of the extra time spent in the
animation loop.
1. function animationLoop() {
2. ...
3. for(var i = 0; i < 50000000; i++) {
4. // slow down artificially the animation
5. }
6. ...
7. requestAnimationFrame(animationLoop);
8. }
Measuring time between frames to achieve a constant speed on screen, even when the
frame rate changes
So, if we measure the time at the beginning of each animation loop, and store
it, we can then compute the delta of times elapsed between two consecutive
loops.
We then apply some simple maths to compute the number of pixels we need to
move the shape to achieve a given speed (in pixels/s).
First example that uses time based animation: the bouncing square
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset=utf-8 />
5. <title>Move rectangle using time based animation</title>
6. <script>
7. var canvas, ctx;
8. var width, height;
9. var x, y, incX; // incX is the distance from the previously drawn
10. // rectangle to the new one
11. var speedX; // speedX is the target speed of the rectangle, in
pixels/s
12. // for time based animation
13. var now, delta;
14. var then = new Date().getTime();
15. // Called after the DOM is ready (page loaded)
16. function init() {
17. // Init the different variables
18. canvas = document.querySelector("#mycanvas");
19. ctx = canvas.getContext('2d');
20. width = canvas.width;
21. height = canvas.height;
22. x=10; y = 10;
23. // Target speed in pixels/second, try with high values, 1000, 2000...
24. speedX = 200;
25. // Start animation
26. animationLoop();
27. }
28. function animationLoop() {
29. // Measure time
30. now = new Date().getTime();
31.
32. // How long between the current frame and the previous one?
33. delta = now - then;
34. //console.log(delta);
35. // Compute the displacement in x (in pixels) in function of the time
elapsed and
36. // in function of the wanted speed
37. incX = calcDistanceToMove(delta, speedX);
38. // an animation involves: 1) clear canvas and 2) draw shapes,
39. // 3) move shapes, 4) recall the loop with requestAnimationFrame
40. // clear canvas
41. ctx.clearRect(0, 0, width, height);
42. ctx.strokeRect(x, y, 10, 10);
43. // move rectangle
44. x += incX;
45. // check collision on left or right
46. if((x+10 >= width) || (x <= 0)) {
47. // cancel move + inverse speed
48. x -= incX;
49. speedX = -speedX;
50. }
51. // Store time
52. then = now;
53. requestAnimationFrame(animationLoop);
54. }
55. // We want the rectangle to move at a speed given in pixels/second
56. // (there are 60 frames in a second)
57. // If we are really running at 60 frames/s, the delay between
58. // frames should be 1/60
59. // = 16.66 ms, so the number of pixels to move = (speed * del)/1000.
60. // If the delay is twice as
61. // long, the formula works: let's move the rectangle for twice as long!
62. var calcDistanceToMove = function(delta, speed) {
63. return (speed * delta) / 1000;
64. }
65. </script>
66. </head>
67. <body onload="init();">
68. <canvas id="mycanvas" width="200" height="50" style="border: 2p
x solid black"></canvas>
69. </body>
70. </html>
In this example, we only added a few lines of code for measuring the time and
computing the time elapsed between two consecutive frames (see line 38).
Normally, requestAnimationFrame(callback) tries to call the callback
function every 16.66 ms (this corresponds to 60 frames/s)... but this is never
exactly the case. If you do a console.log(delta)in the animation loop, you
will see that even on a very powerful computer, the delta is "very close" to
16.6666 ms, but 99% of the time it will be slightly different.
The function calcDistanceToMove(delta, speed) takes two parameters: 1)
the time elapsed in ms, and 2) the target speed in pixels/s.
Or you can try the next example that simulates a complex animation loop that
takes a long time to draw each frame...
We added a long loop in the middle of the animation loop. This time, the
animation should be very jerky. However, notice that the apparent speed of the
square is the same as in the previous example: the animation adapts itself!
1. function animationLoop() {
2. // Measure time
3. now = new Date().getTime();
4.
5. // How long between the current frame and the previous one ?
6. delta = now - then;
7. //console.log(delta);
8. // Compute the displacement in x (in pixels) in function of the time elapsed
and
9. // in function of the wanted speed
10. incX = calcDistanceToMove(delta, speedX);
11. // an animation is : 1) clear canvas and 2) draw shapes,
12. // 3) move shapes, 4) recall the loop with requestAnimationFrame
13. // clear canvas
14. ctx.clearRect(0, 0, width, height);
15. for(var i = 0; i < 50000000; i++) {
16. // just to slow down the animation
17. }
18. ctx.strokeRect(x, y, 10, 10);
19. // move rectangle
20. x += incX;
21. // check collision on left or right
22. if((x+10 >= width) || (x <= 0)) {
23. // cancel move + inverse speed
24. x -= incX;
25. speedX = -speedX;
26. }
27. // Store time
28. then = now;
29. requestAnimationFrame(animationLoop);
30. }
From this article that explains the Time API: "The only method exposed
is now(), which returns a DOMHighResTimeStamp representing the current
time in milliseconds. The timestamp is very accurate, with precision to a
thousandth of a millisecond. Please note that while Date.now() returns the
number of milliseconds elapsed since 1 January 1970 00:00:00
UTC, performance.now() returns the number of milliseconds, with
microseconds in the fractional part, from performance.timing.navigationStart(),
the start of navigation of the document, to the performance.now() call.
Another important difference between Date.now() and performance.now() is
that the latter is monotonically increasing, so the difference between two calls
will never be negative."
To sum up:
performance.now() returns the time since the load of the document (it is
called a DOMHighResTimeStamp), with a sub accuracy, as a floating point value,
with very high accuracy.
Date.now() returns the number of since the Unix epoch, as an integer value.
Here is a version on JSBin of the previous example with the bouncing rectangle,
that uses the timer.
1. ...
2. <script>
3. ...
4. var speedX; // speedX is the target speed of the rectangle in pixels/s
5. // for time based animation
6. var now, delta;
7. // High resolution timer
8. var then = performance.now();
9. // Called after the DOM is ready (page loaded)
10. function init() {
11. ...
12. }
13. function animationLoop() {
14. // Measure time, with high resolution timer
15. now = performance.now();
16.
17. // How long between the current frame and the previous one?
18. delta = now - then;
19. //console.log(delta);
20. // Compute the displacement in x (in pixels) in function
21. // of the time elapsed and
22. // in function of the wanted speed
23. incX = calcDistanceToMove(delta, speedX);
24. //console.log("dist = " + incX);
25. // an animation involves: 1) clear canvas and 2) draw shapes,
26. // 3) move shapes, 4) recall the loop with requestAnimationFrame
27. // clear canvas
28. ctx.clearRect(0, 0, width, height);
29. ctx.strokeRect(x, y, 10, 10);
30. // move rectangle
31. x += incX;
32. // check collision on left or right
33. if((x+10 >= width) || (x <= 0)) {
34. // cancel move + inverse speed
35. x -= incX;
36. speedX = -speedX;
37. }
38. // Store time
39. then = now;
40. // call the animation loop again
41. requestAnimationFrame(animationLoop);
42. }
43. ...
44. </script>
Only two lines have changed but the accuracy is much higher, if you
uncomment the console.log(...) calls in the main loop. You will see the
difference.
Method 3 - using the optional timestamp parameter
of the callback function of requestAnimationFrame
Here is a running example of the animated rectangle, that uses this timestamp
parameter.
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset=utf-8 />
5. <title>Time based animation using the parameter of the
requestAnimationFrame callback</title>
6. <script>
7. var canvas, ctx;
8. var width, height;
9. var x, y, incX; // incX is the distance from the previously drawn rectangle
10. // to the new one
11. var speedX; // speedX is the target speed of the rectangle in
pixels/s
12. // for time based animation
13. var now, delta=0;
14. // High resolution timer
15. var oldTime = 0;
16. // Called after the DOM is ready (page loaded)
17. function init() {
18. // init the different variables
19. canvas = document.querySelector("#mycanvas");
20. ctx = canvas.getContext('2d');
21. width = canvas.width;
22. height = canvas.height;
23. x=10; y = 10;
24. // Target speed in pixels/second, try with high values, 1000, 2000...
25. speedX = 200;
26. // Start animation
27. requestAnimationFrame(animationLoop);
28. }
29. function animationLoop(currentTime) {
30. // How long between the current frame and the previous one?
31. delta = currentTime - oldTime;
32. // Compute the displacement in x (in pixels) in function of the time
elapsed and
33. // in function of the wanted speed
34. incX = calcDistanceToMove(delta, speedX);
35. // clear canvas
36. ctx.clearRect(0, 0, width, height);
37. ctx.strokeRect(x, y, 10, 10);
38. // move rectangle
39. x += incX;
40. // check collision on left or right
41. if(((x+10) > width) || (x < 0)) {
42. // inverse speed
43. x -= incX;
44. speedX = -speedX;
45. }
46. // Store time
47. oldTime = currentTime;
48. // asks for next frame
49. requestAnimationFrame(animationLoop);
50. }
51. var calcDistanceToMove = function(delta, speed) {
52. return (speed * delta) / 1000;
53. }
54. </script>
55. </head>
56. <body onload="init();">
57. <canvas id="mycanvas" width="200" height="50" style="border: 2p
x solid black"></canvas>
58. </body>
59. </html>
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset=utf-8 />
5. <title>Set framerate using a high resolution timer</title>
6. </head>
7. <body>
8. <p>This example measures and sums deltas of time between consecutive
frames of animation. It includes
a <code>setFrameRateInFramesPerSecond</code> function you can use
to reduce the number of frames per second of the main animation.</p>
9.
10. <canvas id="myCanvas" width="700" height="350">
11. </canvas>
12. <script>
13. var canvas = document.querySelector("#myCanvas");
14. var ctx = canvas.getContext("2d");
15. var width = canvas.width, height = canvas.height;
16. var lastX = width * Math.random();
17. var lastY = height * Math.random();
18. var hue = 0;
19.
20. // Michel Buffa: set the target frame rate. TRY TO CHANGE THIS
VALUE AND SEE
21. // THE RESULT. Try 2 frames/s, 10 frames/s, 60 frames/s Normally
there
22. // should be a limit of 60 frames/s in the browser's
implementations.
23. setFrameRateInFramesPerSecond(60);
24.
25. // for time based animation. DelayInMS corresponds to the target
framerate
26. var now, delta, delayInMS, totalTimeSinceLastRedraw = 0;
27.
28. // High resolution timer
29. var then = performance.now();
30.
31. // start the animation
32. requestAnimationFrame(mainloop);
33.
34. function setFrameRateInFramesPerSecond(frameRate) {
35. delayInMs = 1000 / frameRate;
36. }
37.
38. // each function that is going to be run as an animation should end by
39. // asking again for a new frame of animation
40. function (time) {
41. // Here we will only redraw something if the time we want between
frames has
42. // elapsed
43. // Measure time with timer
44. now = time;
45.
46. // How long between the current frame and the previous one?
47. delta = now - then;
48. // TRY TO UNCOMMENT THIS LINE AND LOOK AT THE CONSOLE
49. // console.log("delay = " + delayInMs + " delta = " + delta + " total
time = " +
50. // totalTimeSinceLastRedraw);
51.
52. // If the total time since the last redraw is > delay corresponding to the
wanted
53. // framerate, then redraw, else add the delta time between the last call
to line()
54. // by requestAnimFrame to the total time..
55. if (totalTimeSinceLastRedraw > delayInMs) {
56. // if the time between the last frame and now is > delay then we
57. // clear the canvas and redraw
58.
59. ctx.save();
60.
61. // Trick to make a blur effect: instead of clearing the canvas
62. // we draw a rectangle with a transparent color. Changing the 0.1
63. // for a smaller value will increase the blur...
64. ctx.fillStyle = "rgba(0,0,0,0.1)";
65. ctx.fillRect(0, 0, width, height);
66.
67. ctx.translate(width / 2, height / 2);
68. ctx.scale(0.9, 0.9);
69. ctx.translate(-width / 2, -height / 2);
70.
71. ctx.beginPath();
72. ctx.lineWidth = 5 + Math.random() * 10;
73. ctx.moveTo(lastX, lastY);
74. lastX = width * Math.random();
75. lastY = height * Math.random();
76.
77. ctx.bezierCurveTo(width * Math.random(),
78. height * Math.random(),
79. width * Math.random(),
80. height * Math.random(),
81. lastX, lastY);
82.
83. hue = hue + 10 * Math.random();
84. ctx.strokeStyle = "hsl(" + hue + ", 50%, 50%)";
85. ctx.shadowColor = "white";
86. ctx.shadowBlur = 10;
87. ctx.stroke();
88.
89. ctx.restore();
90.
91. // reset the total time since last redraw
92. totalTimeSinceLastRedraw = 0;
93. } else {
94. // sum the total time since last redraw
95. totalTimeSinceLastRedraw += delta;
96. }
97.
98. // Store time
99. then = now;
100.
101. // request new frame
102. requestAnimationFrame(mainloop);
103. }
104. </script>
105. </body>
106. </html>
Source code:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset=utf-8 />
5. <title>Bouncing rectangle with high resolution timer and adjustable
frame rate</title>
6. <script>
7. var canvas, ctx;
8. var width, height;
9. var x, y, incX; // incX is the distance from the previously drawn rectangle
10. // to the new one
11. var speedX; // speedX is the target speed of the rectangle in pixels/s
12. // for time based animation, DelayInMS corresponds to the target frame
rate
13. var now, delta, delayInMS, totalTimeSinceLastRedraw=0;
14. // High resolution timer
15. var then = performance.now();
16. // Michel Buffa: set the target frame rate. TRY TO CHANGE THIS VALUE
AND SEE
17. // THE RESULT. Try 2 frames/s, 10 frames/s, 60, 100 frames/s Normally
there
18. // should be a limit of 60 frames/s in the browser's implementations, but
you can
19. // try higher values
20. setFrameRateInFramesPerSecond(25);
21. function setFrameRateInFramesPerSecond(framerate) {
22. delayInMs = 1000 / framerate;
23. }
24. // Called after the DOM is ready (page loaded)
25. function init() {
26. // init the different variables
27. canvas = document.querySelector("#mycanvas");
28. ctx = canvas.getContext('2d');
29. width = canvas.width;
30. height = canvas.height;
31. x=10; y = 10;
32. // Target speed in pixels/second, try with high values, 1000, 2000...
33. speedX = 2000;
34. // Start animation
35. requestAnimationFrame(animationLoop)
36. }
37. function animationLoop(time) {
38. // Measure time with high resolution timer
39. now = time;
40.
41. // How long between the current frame and the previous one?
42. delta = now - then;
43. if(totalTimeSinceLastRedraw > delayInMs) {
44. // Compute the displacement in x (in pixels) in function of the time
elapsed
45. // since the last draw and
46. // in function of the wanted speed. This time, instead of delta we
47. // use totalTimeSinceLastRedraw as we're not always drawing at
48. // each execution of mainloop
49. incX = calcDistanceToMove(totalTimeSinceLastRedraw, speedX);
50. // an animation involves: 1) clear canvas and 2) draw shapes,
51. // 3) move shapes, 4) recall the loop with requestAnimationFrame
52. // clear canvas
53. ctx.clearRect(0, 0, width, height);
54. ctx.strokeRect(x, y, 10, 10);
55. // move rectangle
56. x += incX;
57. // check collision on left or right
58. if((x+10 >= width) || (x <= 0)) {
59. // cancel move + inverse speed
60. x -= incX;
61. speedX = -speedX;
62. }
63. // reset the total time since last redraw
64. totalTimeSinceLastRedraw = delta;
65. } else {
66. // sum the total time since last redraw
67. totalTimeSinceLastRedraw += delta;
68. }
69. // Store time
70. then = now;
71.
72. // animate.
73. requestAnimationFrame(animationLoop);
74. }
75. var calcDistanceToMove = function(delta, speed) {
76. return (speed * delta) / 1000;
77. }
78. </script>
79. </head>
80. <body onload="init();">
81. <canvas id="mycanvas" width="200" height="50" style="border: 2p
x solid black"></canvas>
82. </body>
83. </html>
To animate a monster at 60 fps but blinking his eyes once per second, you
would use a with requestAnimationFrame and target a 60 fps animation, but
you would also have a call to setInterval(changeEyeColor, 1000); and the
changeEyeColor function will update a global variable, eyeColor, every
second, which will be taken into account within the drawMonster function,
called 60 times/s from the .
Here is an online example of the game framework at JSBin: this time, the
monster has a speed in pixels/s and we use time-based animation. Try it and
verify the smoothness of the animation; the FPS counter on a Mac Book Pro
core i7 shows 60 fps.
Now try this slightly modified version in which we added a delay inside the
animation loop. This should slow down the frame rate. On a Mac Book Pro +
core i7, the frame-rate drops down to 37 fps. However, if you move the
monster using the arrow keys, its speed on the screen is the same, excepting
that it's not as smooth as in the previous version, which ran at 60 fps.
Here are the parts we changed
Declaration of the monster object - now the speed is in
pixels/s instead of in pixels per frame
1. // The monster !
2. var monster = {
3. x:10,
4. y:10,
5. speed:100, // pixels/s this time !
6. };
Introduction
In this section we will see how we can animate and control not only the player
but also other objects on the screen.
Let's study a simple example: animating a few balls and detecting collisions
with the surrounding walls. For the sake of simplicity, we will not use time-
based animation in the first examples.
Each ball has an x and y position, and in this example, instead of working with
angles, we defined two "speeds" - horizontal and vertical speeds - in the form
of the increments we will add to the x positions at each frame of animation.
We also added a variable for adjusting the size of the balls: the radius.
1. b1.draw();
2. b1.move();
We will call these methods from inside the mainLoop, and as you'll see, we will
create many balls. This object-oriented design makes it easier to handle large
quantities.
Notice that:
Note that we just changed the way we designed the balls and computed the
angles after they rebound from the walls. The changes are highlighted in bold:
example at JSBin.
Try to move the monster with arrow keys and use the mouse button while
moving to change the monster's speed. Look at the source code and change
the parameters controlling the creation of the balls: number, speed, radius, etc.
Also, try changing the monster's default speed. See the results.
For this version, we copied and pasted some code from the previous example
and we also modified the mainLoop to make it more readable. In a next lesson,
we will split the game engine into different files and clean the to make it more
manageable. But for the moment, jsbin.com is a good playground to and test
things...
As you can see, we draw the player/monster, we update its position; and we
call an updateBalls function to do the same for the balls: draw and update
their position.
1. function updateMonsterPosition(delta) {
2. monster.speedX = monster.speedY = 0;
3. // check inputStates
4. if (inputStates.left) {
5. monster.speedX = -monster.speed;
6. }
7. if (inputStates.up) {
8. monster.speedY = -monster.speed;
9. }
10. ...
11. // Compute the incX and incY in pixels depending
12. // on the time elapsed since last redraw
13. monster.x += calcDistanceToMove(delta, monster.speedX);
14. monster.y += calcDistanceToMove(delta, monster.speedY);
15. }
16. function updateBalls(delta) {
17. // for each ball in the array
18. for(var i=0; i < ballArray.length; i++) {
19. var ball = ballArray[i];
20. // 1) move the ball
21. ball.move();
22. // 2) test if the ball collides with a wall
23. testCollisionWithWalls(ball);
24. // 3) draw the ball
25. ball.draw();
26. }
27. }
Now, in order to turn this into a game, we need to create some interactions
between the player (the monster) and the obstacles/enemies (balls, walls)... It's
time to take a look at collision detection.
Collision detection
Introduction
In this chapter, we explore some techniques for detecting collisions between
objects. This includes moving and static objects. We will first present three
"classic" collision tests, and follow them with brief sketches of more complex
algorithms.
Imagine there is a line running between those two center points. The distances
from the center points to the edge of each circle is, by definition, equal to their
respective radii. So:
if the edges of the circles touch, the distance between the centers is r1+r2;
any greater distance and the circles don't touch or collide; whereas
any less and they do collide or overlay.
In other words: if the distance between the center points is less than the
sum of the radii, then the circles collide.
This could be optimized a little averting the need to compute a square root:
Which yields:
This technique is attractive because a "bounding circle" can often be used with
graphic objects of other shapes, providing they are not too elongated
horizontally or vertically.
The famous game Gran Turismo 4 on the PlayStation 2 uses bounding spheres
for detecting collisions between cars:
Rectangle - rectangle (aligned along X and Y axis)
detection test
Let's look at simple illustration:
From this:
1. ...
1. // The monster!
2. var monster = {
3. x: 80,
4. y: 80,
5. width: 100,
6. height: 100,
7. speed: 1,
8. boundingCircleRadius: 70
9. };
10.
11. var player = {
12. x: 0,
13. y: 0,
14. boundingCircleRadius: 20
15. };
16. ...
17.
18. function updatePlayer() {
19. // The player is just a square drawn at the mouse position
20. // Just to test rectangle/rectangle collisions.
21.
22. if (inputStates.mousePos) {
23. player.x = inputStates.mousePos.x;
24. player.y = inputStates.mousePos.y;
25.
26. // draws a rectangle centered on the mouse position
27. // we draw it as a square.
28. // We remove size/2 to the x and y position at drawing time in
29. // order to recenter the rectangle on the mouse pos (normally
30. // the 0, 0 of a rectangle is at its top left corner)
31. var size = player.boundingCircleRadius;
32. ctx.fillRect(player.x - size / 2, player.y - size / 2, size, size);
33. }
34. }
35.
36. function checkCollisions() {
37. // Bounding rect position and size for the player. We need to
translate
38. // it to half the player's size
39. var playerSize = player.boundingCircleRadius;
40. var playerXBoundingRect = player.x - playerSize / 2;
41. var playerYBoundingRect = player.y - playerSize / 2;
42. // Same with the monster bounding rect
43. var monsterXBoundingRect = monster.x - monster.width / 2;
44. var monsterYBoundingRect = monster.y - monster.height / 2;
45.
46. if (rectsOverlap(playerXBoundingRect, playerYBoundingRect,
47. playerSize, playerSize,
48. monsterXBoundingRect, monsterYBoundingRect,
49. monster.width, monster.height)) {
50. ctx.fillText("Collision", 150, 20);
51. ctx.strokeStyle = ctx.fillStyle = 'red';
52. } else {
53. ctx.fillText("No collision", 150, 20);
54. ctx.strokeStyle = ctx.fillStyle = 'black';
55. }
56. }
57.
58.// Collisions between aligned rectangles
59.function rectsOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
60.
61. if ((x1 > (x2 + w2)) || ((x1 + w1) < x2))
62. return false; // No horizontal axis projection overlap
63. if ((y1 > (y2 + h2)) || ((y1 + h1) < y2))
64. return false; // No vertical axis projection overlap
65. return true; // If previous tests failed, then both axis projections
66. // overlap and the rectangles intersect
67.}
You could also try the free shoot'em up (Windows only) that retraces the
history of the genre over its different levels (download here). Press the G key to
see the bounding rectangles used for collision test. Here is a screenshot:
These games run at 60 fps and can have hundreds of bullets moving at the
same time. Collisions have to be tested: did the player's bullets hit an enemy,
AND did an enemy bullet (for one of the many enemies) hit the player? These
examples demonstrate the efficiency of such collision test techniques.
The solution for computing the resulting velocities is to swap the components
between the two balls (as we move from step 2 to step 3), then finally
recombine the velocities for each ball to achieve the result (step 4):
The above picture has been borrowed from this interesting article about how to
implement in C# pool like collision detection.
Of course, we will only compute these steps if the balls collide, and for that we
will have used the basic circle collision test outlined earlier.
To illustrate the here is an example at JSBin that displays the different vectors
in real time, with only two balls. The maths for the collision test have also been
expanded in the source code to make computations clearer. Note that this is
not for beginners: advanced maths and physics are involved!
Introduction
Our previous exercise enabled us to animate balls in the game framework (this
example).
Now we can add the functionality presented in the last lesson, to perform
collision tests between a circle and a rectangle. It will be called 60 times/s
when we update the position of the balls. If there is a collision between a ball
(circle) and the monster (rectangle), we set the ball color to red.
1. function updateBalls(delta) {
2. // for each ball in the array
3. for(var i=0; i < ballArray.length; i++) {
4. var ball = ballArray[i];
5. // 1) move the ball
6. ball.move();
7. // 2) test if the ball collides with a wall
8. testCollisionWithWalls(ball);
9.
10. // 3) Test if the monster collides
11. if(circRectsOverlap(monster.x, monster.y,
12. monster.width, monster.height,
13. ball.x, ball.y, ball.radius)) {
14. //change the color of the ball
15. ball.color = 'red';
16. }
17. // 3) draw the ball
18. ball.draw();
19. }
20. }
Sprite-based animation
Introduction
In this lesson, we learn how to animate images - which are known as "sprites".
This technique utilises components from a collection of animation frames. By
drawing different component-images, rapidly, one-after-the-other, we obtain an
animation effect.
So, when we think about writing a "sprite engine", we need to consider how to
support different layouts of sheet.
Sprite extraction and animation
Principle
Before doing anything interesting with the sprites, we need to:
1.Load the sprite sheet(s),
2.Extract the different postures and store them in an array of sprites,
3.Choose the appropriate one, and draw it within the animation loop, taking
into account elapsed time. We cannot draw a different image of the woman
walking at 60 updates per second. We will have to create a realistic "delay"
between each change of sprite image.
Sprite extraction
In this example, we'll move the slider to extract the sprite indicated by the
slider value. See the red rectangle? This is the sprite image currently selected!
When you move the slider, the corresponding sprite is drawn in the small
canvas. As you move the slider from one to the next, see how the animation is
created?
Try it at JSBin:
HTML code:
1. <html lang="en">
2. <head>
3. <title>Extract and draw sprite</title>
4. <style>
5. canvas {
6. border: 1px solid black;
7. }
8. </style>
9. </head>
10. <body>
11. Sprite width: 48, height: 92, rows: 8, sprites per posture: 13<p>
12. <label for="x">x: <input id="x" type="number" min=0><br/>
13. <label for="y">y: <input id="y" type="number" min=0><br/>
14. <label for="width">width: <input
id="width" type="number" min=0><br/>
15. <label for="height">height: <input
id="height" type="number" min=0><p>
16. Select current sprite: <input type=range
id="spriteSelect" value=0> <output id="spriteNumber">
17. <p/>
18. <canvas id="canvas" width="48" height="92" />
19. </p>
20. <canvas id="spritesheet"></canvas>
21. </body>
22. </html>
Notice that we use an <input type="range"> to select the current sprite, and
we have two canvases: a small one for displaying the sprite, and a larger one
that contains the sprite sheet and in which we draw a red square to highlight
the selected sprite.
Here's an extract from the JavaScript. You don't have to understand all the
details, just look at the part in bold which extracts the individual sprites:
34. spriteNumber.innerHTML=0;
35. // Load the spritesheet
36. spritesheet = new Image();
37. spritesheet.src="http://i.imgur.com/3VesWqx.png";
38. // Called when the spritesheet has been loaded
39. spritesheet.onload = function() {
40. // enable slider
41. spriteSelect.disabled = false;
42. // Resize big canvas to the size of the sprite sheet image
43. canvasSpriteSheet.width = spritesheet.width;
44. canvasSpriteSheet.height = spritesheet.height;
45. // Draw the whole spritesheet
46. ctx2.drawImage(spritesheet, 0, 0);
47. // Draw the first sprite in the big canvas, corresponding to sprite 0
48. // wireframe rectangle in the sprite sheet
49.
drawWireFrameRect(ctx2, 0 , 0, SPRITE_WIDTH, SPRITE_HEIGHT, 'red', 3)
;
50.
// small canvas, draw sub image corresponding to sprite 0
51. ctx1.drawImage(spritesheet, 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT,
52. 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT);
53. };
54. // input listener on the slider
55. spriteSelect.oninput = function(evt) {
56. // Current sprite number from 0 to NB_FRAMES_PER_POSTURE *
NB_ROWS
57. var index = spriteSelect.value;
58. // Computation of the x and y position that corresponds to the
sprite
59. // number index as selected by the slider
60. var x = index * SPRITE_WIDTH % spritesheet.width;
61.
var y = Math.floor(index / NB_FRAMES_PER_POSTURE) * SPRITE_H
EIGHT;
62. // Update fields
63. xField.value = x;
64. yField.value = y;
65. // Clear big canvas, draw wireframe rect at x, y, redraw stylesheet
66.
ctx2.clearRect(0, 0, canvasSpriteSheet.width, canvasSpriteSheet.height);
67. ctx2.drawImage(spritesheet, 0, 0);
68.
drawWireFrameRect(ctx2, x , y, SPRITE_WIDTH, SPRITE_HEIGHT, 'red', 3);
69.
70. // Draw the current sprite in the small canvas
71. ctx1.clearRect(0, 0, SPRITE_WIDTH, SPRITE_HEIGHT);
72. ctx1.drawImage(spritesheet, x, y, SPRITE_WIDTH, SPRITE_HEIGHT,
73. 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT);
74.
75. // Update output elem on the right of the slider
76. spriteNumber.innerHTML = index;
77. };
78. };
79.
80. function drawWireFrameRect(ctx, x, y, w, h, color, lineWidth) {
81. ctx.save();
82. ctx.strokeStyle = color;
83. ctx.lineWidth = lineWidth;
84. ctx.strokeRect(x , y, w, h);
85. ctx.restore();
86. }
87.
Explanations:
Lines 1-4: characteristics of the sprite sheet. How many rows, i.e., how many
sprites per row, etc.
Lines 11-39: initializations that run just after the page has been loaded. We
first get the canvas and contexts. Then we set the minimum and maximum
values of the slider (an <input type=range>) at lines and disable it at line
34 (we cannot slide it before the sprite sheet image has been loaded). We
display the current sprite number 0 in the <output> field to the right of the
slider (line 35). Finally, in lines 37-39, we load the sprite sheet image.
Lines 42-58: this callback is run once the sprite sheet image has been loaded.
We enable the slider, set the big canvas to the size of the loaded image, and
then draw it (line 51). We also draw the first sprite from the sprite sheet in the
small canvas and draw a red wireframe rectangle around the first sprite in the
sprite sheet (lines 52-58).
Lines 61-87: the input listener callback, called each time the slider
moves. Lines 65-68 are the most important ones here: we compute the
x position of the sprite selected with the slider. We take into account the
number of sprites per posture, the number of rows, and the dimensions of each
sprite. Then, as in the previous step, we draw the current sprite in the small
canvas and highlight the current sprite with a red rectangle in the sprite sheet.
The code is generic enough to work with different kinds of sprite sheets. Adjust
the global parameters in bold at lines 1-5 and try the extractor.
Example 2: here is the same application with another sprite sheet. We just
changed these parameter values: try the same code but with another sprite
sheet (the one with the robot) - see on JSBin:
Now it's time to see how we can make a small sprite animation framework!
Introduction
Now that we have presented the principle of sprite extraction (sprites as sub-
images of a single composite image), let's write a small sprite animation
framework.
1. var robot;
2.
3. window.onload = function() {
4. canvas = document.getElementById("canvas");
5. ctx = canvas.getContext("2d");
6. // Load the spritesheet
7. spritesheet = new Image();
8. spritesheet.src = SPRITESHEET_URL;
9. // Called when the spritesheet has been loaded
10. spritesheet.onload = function() {
11. ...
12. robot = new Sprite();
13. // 1 is the posture number in the sprite sheet. We have
14. // only one with the robot.
15. robot.extractSprites(spritesheet, NB_POSTURES, 1,
16. NB_FRAMES_PER_POSTURE,
17. SPRITE_WIDTH, SPRITE_HEIGHT);
18. robot.setNbImagesPerSecond(20);
19. requestAnimationFrame(mainloop);
20. }; // onload
21. };
22.
23. function mainloop() {
24. // Clear the canvas
25. ctx.clearRect(0, 0, canvas.width, canvas.height);
26. // draw sprite at 0, 0 in the small canvas
27. robot.draw(ctx, 0, 0, 1);
28. requestAnimationFrame(mainloop);
29. }
Try the example on JSBin that uses this framework first! Experiment by
editing line 20: robot.setNbImagesPerSecond(20); changing the value of
the parameter and observing the result.
THE SPRITEIMAGE object AND SPRITE MODELS
In this small framework we use "SpriteImage ", a JS object we build to
represent one sprite image. Its properties the global sprite sheet to which it
belongs, its position in the sprite sheet, and its size. It also has a draw method
for drawing the sprite image at an xPos, yPos position, and at size.
1. function SpriteImage(, x, y, width, height) {
2. this.img = img; // the whole image that contains all sprites
3. this.x = x; // x, y position of the sprite image in the whole image
4. this.y = y;
5. this.width = width; // width and height of the sprite image
6. this.height = height;
7.
8. this.draw = function(ctx, xPos, yPos, scale) {
9. ctx.drawImage(this.img,
10. this.x, this.y, // x, y, width and height of img to extract
11. this.width, this.height,
12. xPos, yPos, // x, y, width and height of img to draw
13. this.width*scale, this.height*scale);
14. };
15. }
We define the Sprite model. This is the one we used to create the small robot
in the previous example.
1. function Sprite() {
2. this.spriteArray = [];
3. this.currentFrame = 0;
4. this.delayBetweenFrames = 10;
5. this.extractSprites = function(spritesheet,
6. nbPostures, postureToExtract,
7. nbFramesPerPosture,
8. spriteWidth, spriteHeight) {
9. // number of sprites per row in the spritesheet
10. var nbSpritesPerRow = Math.floor(spritesheet.width / spriteWidth);
11. // Extract each sprite
12. var startIndex = (postureToExtract -1) * nbFramesPerPosture;
13. var endIndex = startIndex + nbFramesPerPosture;
14. for(var index = startIndex; index < maxIndex; index++) {
15. // Computation of the x and y position that corresponds to the sprite
16. // index
17. // x is the rest of index/nbSpritesPerRow * width of a sprite
18. var x = (index % nbSpritesPerRow) * spriteWidth;
19. // y is the divisor of index by nbSpritesPerRow * height of a sprite
20. var y = Math.floor(index / nbSpritesPerRow) * spriteHeight;
21. // build a spriteImage object
22.
var s = new SpriteImage(spritesheet, x, y, spriteWidth, spriteHeight);
23. this.spriteArray.push(s);
24. }
25. };
26. this.then = performance.now();
27. this.totalTimeSinceLastRedraw = 0;
28. this.draw = function(ctx, x, y) {
29. // Use time based animation to draw only a few images per second
30. var now = performance.now();
31. var delta = now - this.then;
32. // Draw currentSpriteImage
33. var currentSpriteImage = this.spriteArray[this.currentFrame];
34. // x, y, scale. 1 = size unchanged
35. currentSpriteImage.draw(ctx, x, y, 1);
36. // if the delay between images is elapsed, go to the next one
37. if (this.totalTimeSinceLastRedraw > this.delayBetweenFrames) {
38. // Go to the next sprite image
39. this.currentFrame++;
40. this.currentFrame %= this.spriteArray.length;
41. // reset the total time since last image has been drawn
42. this.totalTimeSinceLastRedraw = 0;
43. } else {
44. // sum the total time since last redraw
45. this. totalTimeSinceLastRedraw += delta;
46. }
47. this.then = now;
48. };
49. this.setNbImagesPerSecond = function(nb) {
50. // delay in ms between images
51. this.delayBetweenFrames = 1000 / nb;
52. };
53. }
Same example but with the walking woman sprite
sheet
Try this JsBin
we have changed the parameters of the sprites and sprite sheet. Now you can
select the index of the posture to extract: the woman sprite sheet has 8
different postures, so you can call:
1. womanDown.extractSprites(, NB_POSTURES, 1,
2. NB_FRAMES_PER_POSTURE,
3. SPRITE_WIDTH, SPRITE_HEIGHT);
4.
5. womanDiagonalBottomLeft.extractSprites(spritesheet, NB_POSTURES, 2,
6. NB_FRAMES_PER_POSTURE,
7. SPRITE_WIDTH, SPRITE_HEIGHT);
8.
9. womanLeft.extractSprites(spritesheet, NB_POSTURES, 3,
10. NB_FRAMES_PER_POSTURE,
11. SPRITE_WIDTH, SPRITE_HEIGHT);
12. // etc...
Moving the sprites, stopping the sprites
Example at JsBin
As usual, we used key listeners, an inputStates global object, and this time
we created 8 woman sprites, one for each direction.
Object states
With our game framework handling the basics, we can make things more
exciting by causing something to happen when a collision occurs - maybe the
player 'dies', balls are removed, or we should add to your score? Usually, we
add extra properties to the player and enemy objects. For example, a
boolean dead property that will record if an object is dead or alive: if a ball is
marked "dead", do not draw it! If all balls are dead: go to the next level with
more balls, faster balls, etc...
Let's try adding a dead property to balls and consult it before drawing them.
We could also test to see if all the balls are dead, in which case we recreate
them and add one more ball. Let's update the score whenever the monster
eats a ball. And finally, we should add a test in the createBalls function to
ensure that no balls are created on top of the monster.
Ah!... you think that the game has been too easy? Let's reverse the game: now
you must survive without being touched by a ball!!!
Also, every five seconds a next level will start: the set of balls is re-created, and
each level has two more than before. How long can you survive?
Try this JsBin, then look at the source code. Start from the mainloop!
currentGameState = gameStates.gameOver:
currentGameState = gameStates.gamerunning:
Game state management in the JavaScript code:
1. ...
2. // game states
3. var gameStates = {
4. mainMenu: 0,
5. gameRunning: 1,
6. gameOver: 2
7. };
8.
9. var currentGameState = gameStates.gameRunning;
10. var currentLevel = 1;
11. var TIME_BETWEEN_LEVELS = 5000; // 5 seconds
12. var currentLevelTime = TIME_BETWEEN_LEVELS;
13. ...
14. var mainLoop = function (time) {
15. ...
16. // number of ms since last frame draw
17. delta = timer(time);
18.
19. // Clear the canvas
20. clearCanvas();
21.
22. // monster.dead is set to true in updateBalls when there
23. // is a collision
24. if (monster.dead) {
25. currentGameState = gameStates.gameOver;
26. }
27.
28. switch (currentGameState) {
29. case gameStates.gameRunning:
30. // draw the monster
31. drawMyMonster(monster.x, monster.y);
32.
33. // Check inputs and move the monster
34. updateMonsterPosition(delta);
35.
36. // update and draw balls
37. updateBalls(delta);
38.
39. // display Score
40. displayScore();
41.
42. // decrease currentLevelTime. Survive 5s per level
43. // When < 0 go to next level
44. currentLevelTime -= delta;
45.
46. if (currentLevelTime < 0) {
47. goToNextLevel();
48. }
49. break;
50. case gameStates.mainMenu:
51. // TO DO! We could have a main menu with high scores etc.
52. break;
53. case gameStates.gameOver:
54. ctx.fillText("GAME OVER", 50, 100);
55. ctx.fillText("Press SPACE to start again", 50, 150);
56. ctx.fillText("Move with arrow keys", 50, 200);
57. ctx.fillText("Survive 5 seconds for next level", 50, 250);
58.
59. if (inputStates.space) {
60. startNewGame();
61. }
62. break;
63. }
64. ...
65. };
66. ...
And below are the functions for starting a new level, starting a new game, and
the updateBalls function that determines when a player loses and changes
the current game-state to GameOver:
1. function startNewGame() {
2. monster.dead = false;
3. currentLevelTime = 5000;
4. currentLevel = 1;
5. nbBalls = 5;
6. createBalls(nbBalls);
7. currentGameState = gameStates.gameRunning;
8. }
9.
10. function goToNextLevel() {
11. // reset time available for next level
12. // 5 seconds in this example
13. currentLevelTime = 5000;
14. currentLevel++;
15. // Add two balls per level
16. nbBalls += 2;
17. createBalls(nbBalls);
18. }
19.
20. function updateBalls(delta) {
21. // Move and draw each ball, test collisions,
22. for (var i = 0; i < ballArray.length; i++) {
23. ...
24. // Test if the monster collides
25. if (circRectsOverlap(monster.x, monster.y,
26. monster.width, monster.height,
27. ball.x, ball.y, ball.radius)) {
28.
29. //change the color of the ball
30. ball.color = 'red';
31. monster.dead = true;
32. // Here, a sound effect greatly improves
33. // the experience!
34. plopSound.play();
35. }
36.
37. // 3) draw the ball
38. ball.draw();
39. }
40. }
Introduction
JSBin is a great tool for sharing code, for experimenting, etc. But as soon as the
size of your project increases, you'll find that the tool is not suited for
developing large systems.
Review the different functions and isolate those that have no dependence on
the framework. Obviously, the sprite utility functions, the collision detection
functions, and the ball constructor function can be separated from the game
framework, and could easily be reused in other projects. Key and mouse
listeners also can be isolated, gamepad code too...
Look at what you could change to reduce dependencies: add a parameter in
order to make a function independent from global variables, for example.
In the end, try to limit the game.js file to the core of the game framework
(init function, , game states, score, levels), and separate the rest into
functional groupings, eg utils.js, sprites.js, collision.js,
listeners.js, etc.
game.html:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="utf-8">
5. <title>Nearly a real game</title>
6. <!-- External JS libs -->
7. <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/howler/1.1.25/
howler.min.js"></script>
8. <!-- CSS files for your game -->
9. <link rel="stylesheet" href="css/game.css">
10.<!-- Include here all game JS files-->
11. <script src="js/game.js"></script>
12. </head>
13. <body>
14. <canvas id="myCanvas" width="400" height="400"></canvas>
15. </body>
16. </html>
1. canvas {
2. border: 1px solid black;
3. }
If you remember all the way back to the HTML5.0 course, a development
project structure was proposed to help you organise multiple files by directory
and sub-directories ("folders" if you are an MS-Windows user). So let's take the
JavaScript code from the last JSBin example, save it to a file called game.js,
and locate it in a subdirectory js under the directory where the game.html file
is located. we'll keep the CSS file in a subdirectory:
Try the game: open the game.html file in your browser. If the game does not
work, open , look at the console, fix the errors, try again, etc. You may have to
do this several times when you split your files and encounter errors.
Ball.js:
Just for fun, let's try the game without fixing this, and look at the console:
Fix: extract the utility functions related to time-based animation and add
a parameter to the draw method of ball.js. Don't forget to add it
in game.js where ball.draw() is called. The call should be
now ball.draw(); instead of .draw() without any parameter.
timeBasedAnim.js:
fps.js:
listeners.js:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="utf-8">
5. <title>Nearly a real game</title>
6. <link rel="stylesheet" href="css/game.css">
7. <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/howler/1.1.25/
howler.min.js"></script>
8. <!-- Include here all JS files -->
9. <script src="js/game.js"></script>
10.<script src="js/ball.js"></script>
11.<script src="js/timeBasedAnim.js"></script>
12.<script src="js/fps.js"></script>
13.<script src="js/listeners.js"></script>
14.<script src="js/collisions.js"></script>
15. </head>
16. <body>
17. <canvas id="myCanvas" width="400" height="400"></canvas>
18. </body>
19. </html>
We could go further by defining a monster.js file, turning all the code related
to the monster/player into a well-formed object, and move methods, etc. There
are many potential improvements you could make. JavaScript experts are
welcome to make a much fancier version of this little game :-)
Download the zip for this version, just open the game.html file in your browser!
Our intent this week was to show you the primary techniques/approaches for
dealing with animation, interactions, collisions, managing with game states,
etc.
The quizzes for this week are not so important. We're keen to see you write
your own game - you can freely re-use the examples presented during lectures
and modify them, improve the code structure, playability, add sounds, better
graphics, more levels, etc. We like to give points for style and flair, but most
especially because we've been (pleasantly) surprised!
Week 3
Ajax appeared around 2005 with Google and is now widely used. We are not
going to teach you Ajax but instead focus on the relationships between "the
new version of Ajax", known as XHR2 (for XmlHttpRequest level 2) and the File
API (seen in the HTML5 Part 1 course). Also, you will discover that the
HTML5 <progress> element is of great use for monitoring the progress of file
uploads (or downloads).
With XHR2, you can ask the browser to decode the file you send/receive
natively. To do this, when you use an XMLHttpRequest to send or receive a
file, you need to specify the type of file with a value equal toarrayBuffer.
Note: 1) the simple and concise syntax, and 2) the use of the
new arrayBuffer type for the expected response (line 5):
The above function is used, and we modified an example from the HTML5 part
1 course that shows how to read a binary file from disk using the File API's
method readAsArrayBuffer. In this example, instead of reading the file from
disk, we download it using XHR2.
Complete source code:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <title>XHR2 and binary files + Web Audio API</title>
5. </head>
6. <body>
7. <p>Example of using XHR2 and <code>xhr.responseType =
'arraybuffer';</code> to download a binary sound file
8. and start playing it on user-click using the Web Audio API.</p>
9. <p>
10. <h2>Load file using Ajax/XHR2 and the arrayBuffer response type</h2>
11.<button onclick="downloadSoundFile('http://myserver.com/
song.mp3');">
12. Download and play example song.
13. </button>
14. <button onclick="playSound()" disabled>Start</button>
15. <button onclick="stopSound()" disabled>Stop</button>
16. <script>
17. // WebAudio context
18. var context = new window.AudioContext();
19. var source = null;
20. var audioBuffer = null;
21.
22. function stopSound() {
23. if (source) {
24. source.stop();
25. }
26. }
27.
28. function playSound() {
29. // Build a source node for the audio graph
30. source = context.createBufferSource();
31. source.buffer = audioBuffer;
32. source.loop = false;
33. // connect to the speakers
34. source.connect(context.destination);
35. source.start(0); // Play immediately.
36. }
37.
38. function initSound(audioFile) {
39. // The audio file may be an mp3 - we must decode it before playing it
from memory
40. context.decodeAudioData(audioFile, function(buffer) {
41. console.log("Song decoded!");
42. // audioBuffer the decoded audio file we're going to work with
43. audioBuffer = buffer;
44. // Enable all buttons once the audio file is
45. // decoded
46. var buttons = document.querySelectorAll('button');
47. buttons[1].disabled = false; // play
48. buttons[2].disabled = false; // stop
49. alert("Binary file has been loaded and decoded, use play / stop
buttons!")
50. }, function(e) {
51. console.log('Error decoding file', e);
52. });
53. }
54.
55. // Load a binary file from a URL as an ArrayBuffer.
56. function downloadSoundFile(url) {
57. var xhr = new XMLHttpRequest();
58. xhr.open('GET', url, true);
59.
60. xhr.responseType = 'arraybuffer'; // THIS IS NEW WITH HTML5!
61. xhr.onload = function(e) {
62. console.log("Song downloaded, decoding...");
63. initSound(this.response); // this.response is an ArrayBuffer.
64. };
65. xhr.onerror = function(e) {
66. console.log("error downloading file");
67. }
68.
69. xhr.send();
70. console.log("Ajax request sent... wait until it downloads completely");
71. }
72. </script>
73. </body>
74. </html>
Explanations:
Line 12: a click on this button will call the downloadSoundFile function,
passing it the URL of a sample mp3 file.
Lines 58-73: this function sends the Ajax request, and when the file has
arrived, the xhr.onload callback is called (line 63).
Lines 39-55: The initSound function decodes the mp3 into memory using the
WebAudio API, and enables the play and stop buttons.
When the play button is enabled and clicked (line 15) it calls
the playSound function. This builds a minimal Web Audio graph with
a BufferSource node that contains the decoded sound (lines 31-32), connects
it to the speakers (line 35), and then plays it.
Monitoring uploads or downloads using
a progress event
The syntax for declaring progress event handlers is slightly different depending
on the type of operation: a download (using the GET HTTP method), or an
upload (using POST).
Notice that the only difference is the "upload" added after the name of the
request object: with we use xhr.onprogress and with POST we
use xhr.upload.onprogress.
HTML:
JavaScript:
1. // progress element
2. var progress = document.querySelector('#downloadProgress');
3.
4. function downloadSoundFile(url) {
5. var xhr = new XMLHttpRequest();
6. xhr.open('GET', url, true);
7.
8. ...
9. xhr.onprogress = function(e) {
10. progress.value = e.loaded;
11. progress.max = e.total;
12. }
13. xhr.send();
14. }
For example, with a file that is 10,000 bytes long, if the current number of
bytes downloaded is 1000, then <progress value=1000 max=10000> will look
like this:
Try it on JSBin - look at the code, which includes the previous source code
extract.
Notice that the URL of the server is fake, so the request will fail. However, the
simulation takes time, and it is interesting to see how it works.
We will show full examples of real working code with server-side PHP source,
during the “File API, drag and drop and XHR2” lecture, later this
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="utf-8" />
5. <title>File upload with XMLHttpRequest level 2 and HTML5</title>
6. </head>
7.
8. <body>
9. <h1>Example of XHR2 file upload</h1>
10. Choose a file and wait a little until it is uploaded (on a fake
11. server). A message should pop up once the file is uploaded 100%.
12. <p>
13. <input id="file" type="file" />
14. </p>
15. <script>
16. var fileInput = document.querySelector('#file');
17.
18. fileInput.onchange = function() {
19. var xhr = new XMLHttpRequest();
20. xhr.open('POST', 'upload.html'); // With FormData,
21. // POST is mandatory
22.
23. xhr.onload = function() {
24. alert('Upload complete !');
25. };
26.
27. var form = new FormData();
28. form.append('file', fileInput.files[0]);
29. // send the request
30. xhr.send(form);
31. };
32. </script>
33. </body>
34. </html>
Explanations:
1. xhr.upload.onprogress = function(e) {
2. progress.value = e.loaded; // number of bytes uploaded
3. progress.max = e.total; // total number of bytes in the file
4. };
Code from this example (nearly the same as previous example's code):
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <meta charset="utf-8" />
5. <title>HTML5 file upload with monitoring</title>
6. </head>
7.
8. <body>
9. <h1>Example of XHR2 file upload, with progress bar</h1>
10. Choose a file and wait a little until it is uploaded (on a fake server).
11. <p>
12. <input id="file" type="file" />
13. <br/><br/>
14.<progress id="progress" value=0></progress>
15.
16. <script>
17. var fileInput = document.querySelector('#file'),
18. progress = document.querySelector('#progress');
19.
20. fileInput.onchange = function() {
21. var xhr = new XMLHttpRequest();
22. xhr.open('POST', 'upload.html');
23.
24. xhr.upload.onprogress = function(e) {
25. progress.value = e.loaded;
26. progress.max = e.total;
27. };
28. xhr.onload = function() {
29. alert('Upload complete!');
30. };
31.
32. var form = new FormData();
33. form.append('file', fileInput.files[0]);
34.
35. xhr.send(form);
36. };
37. </script>
38. </body>
39. </html>
The only difference between these two is the listener which updates the
progress bar's value and max attributes.
The drag and drop API
Introduction
From the W3C HTML 5.1 specification: "the drag and drop API defines an event-
based drag-and-drop mechanism, it does not define exactly what a drag-and-
drop operation actually is".
We will start by presenting the API itself, and then we will focus on the
particular case of drag and dropping files.
External resources
Article from opera dev channel, lots of demos included
Article about drag and drop in HTML5 at html5Rocks.com
Nice shopping cart
demo: http://nettutsplus.s3.amazonaws.com/64_html5dragdrop/demo/index.ht
ml
Drag detection
This is a very simple example that allows HTML elements to be moved using
the mouse. In order to make any visible element draggable, add
the draggable="true" attribute to any visible HTML5 element. Notice that
some elements are draggable by default, such as <img> elements. In order to
detect a drag, add an event listener for the
In order to detect a drag, add an event listener for the dragstart event:
1. <ol ondragstart="dragStartHandler(event)">
2. <li draggable="true" data-value="fruit-apple">Apples</li>
3. <li draggable="true" data-value="fruit-orange">Oranges</li>
4. <li draggable="true" data-value="fruit-pear">Pears</li>
5. </ol>
In the above code, we made all of the <li> elements draggable, and we detect
a event occurring to any item within the ordered
list: <ol ondragstart="dragStarthandler(event)">.
Try the following interactive example in your browser (just click and drag one
of the list items) or play with it at CodePen.
Screenshot:
Complete code from the example:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <script>
5. function dragStartHandler(event) {
6. alert('dragstart event, target: ' + event.target.innerHTML);
7. }
8. </script>
9. </head>
10. <body>
11. <p>What fruits do you like? Try to drag an element!</p>
12. <ol ondragstart="dragStartHandler(event)">
13. <li draggable="true" data-value="fruit-apple">Apples</li>
14. <li draggable="true" data-value="fruit-orange">Oranges</li>
15. <li draggable="true" data-value="fruit-pear">Pears</li>
16. </ol>
17. <p>Drop your favorite fruits below:</p>
18. <body>
19. <html>
In this script, the event handler will only display an alert showing the name of
the target element that launched the event.
How to detect a drop and do something with the dragged elements
Let's continue to develop the example. We show how to drag an element and
detect a drop, receiving a value which corresponds to the dragged element.
Then we change the page content accordingly.
When data is copied to this clipboard, a key/value pair must be given. The data
copied to the clipboard is associated with this name.
The variable event.target at line 5 below is the <li> element that has been
dragged, and event.target.dataset.value is the value of its data-value attribute
(in our case "apples", "oranges" or "pears"):
1. function dragStartHandler(event) {
2. console.log('dragstart event, target: ' + event.target.innerHTML);
3. // Copy to the drag'n'drop clipboard the value of the
4. // data* attribute of the target,
5. // with a type "Fruit".
6. event.dataTransfer.setData("Fruit", event.target.dataset.value);
7. }
Typically, in the drop handler, we need to acquire data about the element that
has been dropped (we get this from the clipboard at lines 6-8, the data was
copied there during step 1 in the handler).
Complete example
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <script>
5. function dragStartHandler(event) {
6. console.log('dragstart event, target: ' +
7. event.target.innerHTML);
8. // Copy to the drag'n'drop clipboard the value
9. // of the data* attribute of
10. // the target, with a type "Fruits".
11. event.dataTransfer.setData("Fruit",
12. event.target.dataset.value);
13. }
14. function dropHandler(event) {
15. console.log('drop event, target: ' +
16. event.target.innerHTML);
17. var li = document.createElement('li');
18. // get the data from the drag'n'drop clipboard,
19. // with a type="Fruit"
20. var data = event.dataTransfer.getData("Fruit");
21. if (data == 'fruit-apple') {
22. li.textContent = 'Apples';
23. } else if (data == 'fruit-orange') {
24. li.textContent = 'Oranges';
25. } else if (data == 'fruit-pear') {
26. li.textContent = 'Pears';
27. } else {
28. li.textContent = 'Unknown Fruit';
29. }
30. // add the dropped data as a child of the list.
31. document.querySelector("#droppedFruits").appendChild(li);
32. }
33. </script>
34. </head>
35. <body>
36. <p>What fruits do you like? Try to drag an element!</p>
37. <ol ondragstart="dragStartHandler(event)">
38. <li draggable="true" data-value="fruit-apple">Apples</li>
39. <li draggable="true" data-value="fruit-orange">Oranges</li>
40. <li draggable="true" data-value="fruit-pear">Pears</li>
41. </ol>
42. <div ondragover="return false" ondrop="dropHandler(event);">
43. Drop your favorite fruits below:
44. <ol id="droppedFruits"></ol>
45. </div>
46. <body>
47. <html>
Line 44: we define the drop zone (ondrop=...), and when a drag enters the
zone we prevent event propagation (ondragover="return false")
When we enter the listener (line 5), we copy the data-value attribute to
the clipboard with a name/key equal to "Fruit" (line 11),
When a drop occurs in the "drop zone" (the <div> at line 44),
the dropHandler(event) function is called. This always occurs after a call to
the handler. In other words: when we enter the drop handler, there must
always be something on the clipboard! We use
an event.dataTransfer.setData(...) in the handler, and
an event.dataTransfer.getData(...) in the drop handler.
The dropHandler function is called (line 15), we get the object with a
name/key equal to "Fruit" (line 21) from the clipboard , we create
a <li> element (line 18), and set its value according to the value in that
clipboard object (lines 23-31),
Finally we add the <li> element to the <ol> list within the drop zone <div>.
Notice that we use some CSS to set aside some screen-space for the drop zone
(not presented in the source code above, but available in the online example):
1. div {
2. height: 150px;
3. width: 150px;
4. float: left;
5. border: 2px solid #666666;
6. background-color: #ccc;
7. margin-right: 5px;
8. border-radius: 10px;
9. box-shadow: inset 0 0 3px #000;
10. text-align: center;
11. cursor: move;
12. }
13. li:hover {
14. border: 2px dashed #000;
15. }
However with HTML5 we may add attributes that start with data- followed by
any string literal (WITH NO UPPERCASE) and it will be treated as a storage area
for private data. This can later be accessed in your JavaScript code.
Valid HTML5 code: <img src="photo.jpg" data-photographer="Michel
Buffa"date="14July2016">. You can set the data- attribute to any value.
The reason for this addition is in a bid to keep the HTML code valid, some
classic attributes like alt, reland title have often been misused for storing
arbitrary data. The data-*attributes of HTML5 are an "official" way to add
arbitrary data to HTML elements that also valid HTML code.
The specification says: "Custom data attributes are intended to store custom
data private to the page or application, for which there are no
more appropriate attributes or elements."
In this example, when you click on the sentence that starts with "John Says",
the end of the sentence changes, and the values displayed are taken from
data-* attributes of the <li> element.
1. <script>
2. var user = document.getElementsByTagName("li")[0];
3. var pos = 0, span = user.getElementsByTagName("span")[0];
4. var phrases = [
5. {name: "city", prefix: "I am from "},
6. {name: "food", prefix: "I like to eat "},
7. {name: "lang", prefix: "I like to program in "}
8. ];
9. user.addEventListener( "click", function(){
10. // Pick the first, second or third phrase
11. var phrase = phrases[ pos++ % 3 ];
12. // Use the .dataset property depending on the value of phrase.name
13. // phrase.name is "city", "food" or "lang"
14. span.innerHTML = phrase.prefix + user.dataset[ phrase.name ];
15. // could be replaces by old way..
16. // span.innerHTML = phrase.prefix + user.getAttribute("data-" +
phrase.name );
17. }, false);
18. </script>
All data‐ attributes are accessed using the dataset property of the HTML
element: in this example, user.dataset[phrase.name] is
either user.dataset.city, user.dataset.food, or user.dataset.lang.
1. <script>
2. var input = document.querySelector('input');
3.
4. input.dataset.myvaluename = input.value; // Set an initial value.
5.
6. input.addEventListener('change', function(e) {
7. this.dataset.myvaluename = this.value;
8. });
9. </script>
1. <style>
2. input::after {
3. color:red;
4. content: attr(data-myvaluename) '/' attr(max);
5. position: relative;
6. left: 100px; top: -15px;
7. }
8. </style>
The attr() function takes an attribute name as a parameter and returns its
value. Here we used the name of the attribute we added on the fly.
Add visual feedback when you drag something, when the mouse enters a drop zone, etc.
We can associate some CSS styling with the lifecycle of a drag and drop. This is
easy to do as the drag and drop API provides many events we can listen to, and
can be used on the draggable elements as well as in the drop zones:
: this event, which we discussed in a previous section, is used draggable
elements. We used it to get a value from the element that was dragged, and
copied it the clipboard. It's a good time to add some visual feedback - for
example, by adding a CSS class to the draggable object.
: this event is launched when the drag has ended (on a drop or if the user
releases the mouse button outside a drop zone). In both cases, it is a best
practice to reset the style of the draggable object to default.
The next screenshot shows the use of CSS styles (green background + dashed
border) triggered by the start of a drag operation. As soon as the drag ends
and the element is dropped, we reset the style of the dragged object to its
default. The full runnable online example is a bit further down the page (it
includes, in addition, visual feedback on the drop zone):
1. ...
2. <style>
3. .dragged {
4. border: 2px dashed #000;
5. background-color: green;
6. }
7. </style>
8. <script>
9. function dragStartHandler(event) {
10. // Change CSS class for visual feedback
11. event.target.style.opacity = '0.4';
12. event.target.classList.add('dragged');
13. console.log('dragstart event, target: ' + event.target);
14. // Copy to the drag'n'drop clipboard the value of the data* attribute of
the target,
15. // with a type "Fruits".
16. event.dataTransfer.setData("Fruit", event.target.dataset.value);
17. }
18. function dragEndHandler(event) {
19. console.log("drag end");
20. // Set draggable object to default style
21. event.target.style.opacity = '1';
22. event.target.classList.remove('dragged');
23. }
24. </script>
25. ...
26. <ol
ondragstart="dragStartHandler(event)" ondragend="dragEndHandler(event)"
>
27. <li draggable="true" data-value="fruit-apple">Apples</li>
28. <li draggable="true" data-value="fruit-orange">Oranges</li>
29. <li draggable="true" data-value="fruit-pear">Pears</li>
30. </ol>
Notice at lines 12 and 24 the use of the property that has been
introduced with HTML5 in order to allow CSS class manipulation from
JavaScript.
: usually we bind this event to the drop zone. The event occurs when a
dragged object enters a drop zone. So, we could change the look of the drop
zone.
: this event is also used in relation to the drop zone. When a dragged element
leaves the drop zone (maybe the user changed his mind?), we must set the
look of the drop zone back to normal.
: this event is also generally bound to elements that correspond to a drop
zone. A best practice here is to prevent the propagation of the event, and also
to prevent the default behavior of the browser (i.e. if we drop an image, the
default behavior is to display its full size in a new page, etc.)
drop: also on the drop zone. This is when we actually process the drop (get
the value from the clipboard, etc). It's also necessary to reset the look of the
drop zone to default.
Complete source code (for clarity's sake, we put the CSS and JavaScript into a
single HTML page):
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <style>
5. div {
6. height: 150px;
7. width: 150px;
8. float: left;
9. border: 2px solid #666666;
10. background-color: #ccc;
11. margin-right: 5px;
12. border-radius: 10px;
13. box-shadow: inset 0 0 3px #000;
14. text-align: center;
15. cursor: move;
16. }
17. .dragged {
18. border: 2px dashed #000;
19. background-color: green;
20. }
21. .draggedOver {
22. border: 2px dashed #000;
23. background-color: green;
24. }
25. </style>
26. <script>
27. function dragStartHandler(event) {
28. // Change css class for visual feedback
29. event.target.style.opacity = '0.4';
30. event.target.classList.add('dragged');
31. console.log('dragstart event, target: ' + event.target.innerHTML);
32. // Copy in the drag'n'drop clipboard the value of the data* attribute
of the target,
33. // with a type "Fruits".
34. event.dataTransfer.setData("Fruit", event.target.dataset.value);
35. }
36. function dragEndHandler(event) {
37. console.log("drag end");
38. event.target.style.opacity = '1';
39. event.target.classList.remove('dragged');
40. }
41. function dragLeaveHandler(event) {
42. console.log("drag leave");
43. event.target.classList.remove('draggedOver');
44. }
45. function dragEnterHandler(event) {
46. console.log("Drag enter");
47. event.target.classList.add('draggedOver');
48. }
49. function dragOverHandler(event) {
50. //console.log("Drag over a droppable zone");
51. event.preventDefault(); // Necessary. Allows us to drop.
52. }
53. function dropHandler(event) {
54. console.log('drop event, target: ' + event.target);
55. // reset the visual look of the drop zone to default
56. event.target.classList.remove('draggedOver');
57. var li = document.createElement('li');
58. // get the data from the drag'n'drop clipboard, with a type="Fruit"
59. var data = event.dataTransfer.getData("Fruit");
60. if (data == 'fruit-apple') {
61. li.textContent = 'Apples';
62. } else if (data == 'fruit-orange') {
63. li.textContent = 'Oranges';
64. } else if (data == 'fruit-pear') {
65. li.textContent = 'Pears';
66. } else {
67. li.textContent = 'Unknown Fruit';
68. }
69. // add the dropped data as a child of the list.
70. document.querySelector("#droppedFruits").appendChild(li);
71. }
72. </script>
73. </head>
74. <body>
75. <p>What fruits do you like? Try to drag an element!</p>
76. <ol ondragstart="dragStartHandler(event)" ondragend="dragEndHandl
er(event)" >
77. <li draggable="true" data-value="fruit-apple">Apples</li>
78. <li draggable="true" data-value="fruit-orange">Oranges</li>
79. <li draggable="true" data-value="fruit-pear">Pears</li>
80. </ol>
81. <div id="droppableZone" ondragenter="dragEnterHandler(event)"ondr
op="dropHandler(event)"
82.
ondragover="dragOverHandler(event)"ondragleave="dragLeaveHandler(event
)">
83. Drop your favorite fruits below:
84. <ol id="droppedFruits"></ol>
85. </div>
86. <body>
87. <html>
More feedback using the dropEffect property: changing the cursor's shape
It is possible to change the cursor's shape during the drag process. The cursor
will turn into a "copy", "move" or "link" icon, depending on the semantic of your
drag and drop, when you enter a drop zone during a drag. For example, if you
"copy" a fruit into the drop zone, as we did in the previous example, a "copy"
cursor like the one below would be appropriate:
And if you are making a "link" or a "shortcut", a cursor would be looking like
this:
Alternatively, you could use any custom image/icon you like:
Here is an extract of the code we can add to the example we saw earlier:
1. function dragStartHandler(event) {
2. // Allow a "copy" cursor effect
3. event.dataTransfer.effectAllowed = 'copy';
4. ...
5. }
1. function dragEnterHandler(event) {
2. // change the cursor shape to a "+"
3. event.dataTransfer.dropEffect = 'copy';
4. ...
5. }
1. function dragStartHandler(event) {
2. // allowed cursor effects
3. event.dataTransfer.effectAllowed = 'copy';
4. // Load and create an image
5. var dragIcon = document.createElement('img');
6. dragIcon.src = 'anImage.png';
7. dragIcon.width = 100;
8. // set the cursor to this image, with an offset in X, Y
9. event.dataTransfer.setDragImage(dragIcon, -10, -10);
10. ...
11. }
Here are the various possible values for cursor states (your browser will not
necessarily support all of these; we noticed that copyMove, etc. had no effect
with Chrome, for example). The values of "move", "copy", and "link" are widely
supported.
1. <html lang="en">
2. <head>
3. <style>
4. .box {
5. border: silver solid;
6. width: 256px;
7. height: 128px;
8. margin: 10px;
9. padding: 5px;
10. float: left;
11. }
12. </style>
13. <script>
14. function drag(target, evt) {
15. evt.dataTransfer.setData("Text", target.id);
16. }
17. function drop(target, evt) {
18. var id = evt.dataTransfer.getData("Text");
19. target.appendChild(document.getElementById(id));
20. // prevent default behavior
21. evt.preventDefault();
22. }
23. </script>
24. </head>
25. <body>
26. Drag and drop browser images in a zone:<br/>
27.
<img src="http://html5demo.braincracking.org/img/logos/chrome1.png"id="c
r"
28. ondragstart="drag(this, event)" alt="Logo Chrome">
29.
<img src="http://html5demo.braincracking.org/img/logos/firefox1.png"id="ff"
30. ondragstart="drag(this, event)" alt="Logo Firefox">
31.
<img src="http://html5demo.braincracking.org/img/logos/ie1.png" id="ie"
32. ondragstart="drag(this, event)" alt="Logo IE">
33.
<img src="http://html5demo.braincracking.org/img/logos/opera1.png" id="o
p"
34. ondragstart="drag(this, event)" alt="Logo Opera">
35.
<img src="http://html5demo.braincracking.org/img/logos/safari1.png" id="sf"
36. ondragstart="drag(this, event)" alt="Logo Safari"><br/>
37.
<div class="box" ondragover="return false" ondrop="drop(this, event)">
38. <p>Good web browsers</p>
39. </div>
40.
<div class="box" ondragover="return false" ondrop="drop(this, event)">
41. <p>Bad web browsers</p>
42. </div>
43. </body>
44. </html>
The trick here is to only work on the DOM directly. We used a variant of the
event handler proposed by the DOM API. This time, we used handlers with two
parameters (the first parameter, target, is the element that triggered the
event, and the second parameter is the event itself). In the we copy just
the id of the element in the DOM (line 15).
In the drop handler, we just move the element from one part of the DOM tree
to another (under the <div> defined at line 38, that is the drop zone). This
occurs at line 18 (get back the id from the clipboard), and line 19 (make it a
child of the div. Consequently, it is no longer a child of the <body>, and indeed
we have "moved" one <img> from its initial position to another location in the
page).
1. <html lang="en">
2. <head>
3. <style>
4. .box {
5. border: silver solid;
6. width: 256px;
7. height: 128px;
8. margin: 10px;
9. padding: 5px;
10. float: left;
11. }
12. .notDraggable {
13. user-select: none;
14. }
15. </style>
16. <script>
17. function drop(target, event) {
18. event.preventDefault();
19. target.innerHTML = event.dataTransfer.getData('text/plain');
20. };
21. </script>
22. </head>
23. <body>
24. <p id="text">
25. <b>Drag and drop a text selection from this paragraph</b>. Drag and
drop any
26. part of this text to
27. the drop zone. Notice in the code: there is no need for a dragstart
handler in case of
28. text selection:
29. the text is added to the clipboard when dragged with a key/name
equal to "text/plain".
30. Just write a
31. drop handler that will do an event.dataTransfer.getData("text/plain")
and you are
32. done!
33. </p>
34.<p class="notDraggable">
35. This paragraph is not however. Look at the CSS in the source code.
36. </p>
37. <div class="box" ondragover="return false" ondrop="drop(this, event
)">
38. <p>Drop some text selection here.</p>
39. </div>
40. </body>
41. </html>
In the next chapter of Week 3, we will see how to drag and drop files!
Indeed, the main work will be done in the drop handler, where we will use
the files property of the dataTransfer object (aka the clipboard). This is where
the browser will copy the files that have been dragged from the desktop.
This files object is the same one we saw in the chapter about the File API in
the "HTML5 part 1" course: it is a collection of file objects (sort of file
descriptors). From each file object, we will be able to extract the name of the
file, its type, size, last modification date, read it, etc.
In this source code extract we have a drop handler that works on files which
have been dragged and dropped from the desktop to a drop zone
associated with this handler with an ondrop=dropHandler(event);attribute:
1. function dropHandler(event) {
2. // Do not propagate the event
3. event.stopPropagation();
4. // Prevent default behavior, in particular when we drop images or links
5. event.preventDefault();
6. // get the dropped files from the clipboard
7. var files = event.dataTransfer.files;
8. var filenames = "";
9. // do something with the files...here we iterate on them and log the filenames
10. for(var i = 0 ; i < files.length ; i++) {
11. filenames += '\n' + files[i].name;
12. }
13. console.log(files.length + ' file(s) have been dropped:\n' + filenames);
14. }
At the beginning of the drop handler in the previous piece of code, you can
see the lines of code (lines 2-5) that stop the propagation of the drop event and
prevent the default behavior of the browser. Normally when we drop an image
or an HTTP link onto a web page, the browser will display the image or the web
page pointed by the link, in a new tab/window. This is not what we would like in
an application the drag and drop process. These two lines are necessary to
prevent the default behavior of the browser:
1. function dragOverHandler(event) {
2. // Do not propagate the event
3. event.stopPropagation();
4. // Prevent default behavior, in particular when we drop images or links
5. event.preventDefault();
6. ...
7. }
8. function dropHandler(event) {
9. // Do not propagate the event
10. event.stopPropagation();
11. // Prevent default behavior, in particular when we drop images or links
12. event.preventDefault();
13. ...
14. }
External resources
Article from HTML5 rocks about
Article from thecssninja.com about dragging files from browser to desktop
Drag and drop files to a drop zone, display file details in a list
Try the example below directly in your browser (just drag and drop files to the
greyish drop zone), or play with it at CodePen:
Complete source code from the example:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <style>
5. div {
6. height: 150px;
7. width: 350px;
8. float: left;
9. border: 2px solid #666666;
10. background-color: #ccc;
11. margin-right: 5px;
12. border-radius: 10px;
13. box-shadow: inset 0 0 3px #000;
14. text-align: center;
15. cursor: move;
16. }
17. .dragged {
18. border: 2px dashed #000;
19. background-color: green;
20. }
21. .draggedOver {
22. border: 2px dashed #000;
23. background-color: green;
24. }
25.
26. </style>
27. <script>
28. function dragLeaveHandler(event) {
29. console.log("drag leave");
30. // Set style of drop zone to default
31. event.target.classList.remove('draggedOver');
32. }
33. function dragEnterHandler(event) {
34. console.log("Drag enter");
35. // Show some visual feedback
36. event.target.classList.add('draggedOver');
37. }
38. function dragOverHandler(event) {
39. //console.log("Drag over a droppable zone");
40. // Do not propagate the event
41. event.stopPropagation();
42. // Prevent default behavior, in particular when we drop images or
links
43. event.preventDefault();
44. }
45. function dropHandler(event) {
46. console.log('drop event');
47. // Do not propagate the event
48. event.stopPropagation();
49. // Prevent default behavior, in particular when we drop images or
links
50. event.preventDefault();
51. // reset the visual look of the drop zone to default
52. event.target.classList.remove('draggedOver');
53. // get the files from the clipboard
54. var files = event.dataTransfer.files;
55. var filesLen = files.length;
56. var filenames = "";
57. // iterate on the files, get details using the file API
58. // Display file names in a list.
59. for(var i = 0 ; i < filesLen ; i++) {
60. filenames += '\n' + files[i].name;
61. // Create a li, set its value to a file name, add it to the ol
62. var li = document.createElement('li');
63.
li.textContent = files[i].name; document.querySelector("#droppedFiles").app
endChild(li);
64. }
65. console.log(files.length + ' file(s) have been dropped:\
n' + filenames);
66. }
67. </script>
68. </head>
69. <body>
70. <h2>Drop your files here!</h2>
71. <div id="droppableZone" ondragenter="dragEnterHandler(event)"ond
rop="dropHandler(event)"
72. ondragover="dragOverHandler(event)"
ondragleave="dragLeaveHandler(event)">
73. Drop zone
74. <ol id="droppedFiles"></ol>
75. </div>
76. <body>
77. <html>
Note that:
We prevented the browser default behavior in the drop and handlers
Otherwise, if we dropped a media file (an image, an audio of video file), the
browser would try to display/play it in a new window/tab. We also stop the
propagation for performance reasons, because when we drag an object it can
raise many events within the parents of the drop zone element as well.
Lines 73-74 create a <li> element. Its value is initialized with the of the
current file in the and added to the <ol> list.
We have reproduced the example here - please review the source code to
refresh your memory (click on the JS tab or look at the example at CodePen).
Click the "Choose files" button (an <input type="file"> element), select one
or more images -- and you should see image thumbnails displayed in the open
space beneath it:
Source code extract (the part that reads the image file content and displays
the thumbnails):
1. function readFilesAndDisplayPreview(files) {
2. // Loop through the FileList and render image files
3. // as thumbnails.
4. for (var i = 0, f; f = files[i]; i++) {
5. // Only process image files.
6. if (!f.type.match('image.*')) {
7. continue;
8. }
9. var reader = new FileReader();
10. //capture the file information.
11. reader.onload = function(e) {
12. // Render thumbnail.
13. var span = document.createElement('span');
14. span.innerHTML = "<img class='thumb' src='" +
15. e.target.result + "'/>";
16. document.getElementById('list').insertBefore(span, null);
17. };
18. // Read the image file as a data URL. Will trigger
19. // a call to the onload callback above
20. // only once the image is completely loaded
21. reader.readAsDataURL(f);
22. }
23. }
At line7, there is a test that will avoid processing files. The "!" is the NOT
operator in JavaScript. The call to continue at line 8 will make the for loop go to
its end and process the next file. See the HTML5 part 1 course about the file
API for more details (each file has a name, type,
lastModificationDate and sizeattribute. The call to match(...) here is a
standard way in JavaScript to match a string value with a regular expression).
At line 19, we insert the <img> element that was created and initialized with
the dataURL of the image file, into the HTML list with an id of "list".
So, let's add this method to our code example, to display file details once
dropped, and also add an <output id="list"></output> to the HTML of this
example.
Complete example of drag and drop + thumbnails of
images
Try it below in your browser ( image files into the drop zone) or play with it at
CodePen:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <style>
5. div {
6. height: 150px;
7. width: 350px;
8. border: 2px solid #666666;
9. background-color: #ccc;
10. margin-right: 5px;
11. border-radius: 10px;
12. box-shadow: inset 0 0 3px #000;
13. text-align: center;
14. cursor: move;
15. }
16. .dragged {
17. border: 2px dashed #000;
18. background-color: green;
19. }
20. .draggedOver {
21. border: 2px dashed #000;
22. background-color: green;
23. }
24. </style>
25. <script>
26. function dragLeaveHandler(event) {
27. console.log("drag leave");
28. // Set style of drop zone to default
29. event.target.classList.remove('draggedOver');
30. }
31. function dragEnterHandler(event) {
32. console.log("Drag enter");
33. // Show some visual feedback
34. event.target.classList.add('draggedOver');
35. }
36. function dragOverHandler(event) {
37. //console.log("Drag over a droppable zone");
38. // Do not propagate the event
39. event.stopPropagation();
40. // Prevent default behavior, in particular when we drop images or
links
41. event.preventDefault();
42. }
43. function dropHandler(event) {
44. console.log('drop event');
45. // Do not propagate the event
46. event.stopPropagation();
47. // Prevent default behavior, in particular when we drop images or
links
48. event.preventDefault();
49. // reset the visual look of the drop zone to default
50. event.target.classList.remove('draggedOver');
51. // get the files from the clipboard
52. var files = event.dataTransfer.files;
53. var filesLen = files.length;
54. var filenames = "";
55. // iterate on the files, get details using the file API
56. // Display file names in a list.
57. for(var i = 0 ; i < filesLen ; i++) {
58. filenames += '\n' + files[i].name;
59. // Create a li, set its value to a file name, add it to the ol
60. var li = document.createElement('li');
61. li.textContent = files[i].name;
62. document.querySelector("#droppedFiles").appendChild(li);
63. }
64. console.log(files.length + ' file(s) have been dropped:\
n' + filenames);
65. readFilesAndDisplayPreview(files);
66. }
67. function readFilesAndDisplayPreview(files) {
68. // Loop through the FileList and render image files as thumbnails.
69. for (var i = 0, f; f = files[i]; i++) {
70. // Only process image files.
71. if (!f.type.match('image.*')) {
72. continue;
73. }
74. var reader = new FileReader();
75. //capture the file information.
76. reader.onload = function(e) {
77. // Render thumbnail.
78. var span = document.createElement('span');
79. span.innerHTML = "<img class='thumb' width='100'
src='" + e.target.result + "'/>";
80. document.getElementById('list').insertBefore(span, null);
81. };
82. // Read the image file as a data URL. Will trigger the call to the
above callback when
83. // the image file is completely loaded
84. reader.readAsDataURL(f);
85. }
86. }
87. </script>
88. </head>
89. <body>
90. <h2>Drop your files here!</h2>
91. <div id="droppableZone" ondragenter="dragEnterHandler(event)"ondr
op="dropHandler(event)"
92. ondragover="dragOverHandler(event)"
ondragleave="dragLeaveHandler(event)">
93. Drop zone
94. <ol id="droppedFiles"></ol>
95. </div>
96. <br/>
97. <output id="list"></output>
98. <body>
99. <html>
Just for fun, we also added an experimental "directory chooser" that is thus
far only implemented by Google Chrome (notice, <input type="file"
webkitdirectory> is not in the HTML5 specification. Drag and drop
functionality will work through a file chooser in any modern browser, but the
directory chooser will only work with Google Chrome).
Complete interactive example with source code
Try it in your browser below (use all three functions: firstly using the file
selector, secondly the directory selector, and finally to drag and drop image
files into the drop zone), or play with it at CodePen:
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. <style>
5. div {
6. height: 150px;
7. width: 350px;
8. border: 2px solid #666666;
9. background-color: #ccc;
10. margin-right: 5px;
11. border-radius: 10px;
12. box-shadow: inset 0 0 3px #000;
13. text-align: center;
14. cursor: move;
15. }
16.
17. .dragged {
18. border: 2px dashed #000;
19. background-color: green;
20. }
21. .draggedOver {
22. border: 2px dashed #000;
23. background-color: green;
24. }
25.
26. </style>
27. <script>
28. function dragLeaveHandler(event) {
29. console.log("drag leave");
30. // Set style of drop zone to default
31. event.target.classList.remove('draggedOver');
32. }
33. function dragEnterHandler(event) {
34. console.log("Drag enter");
35. // Show some visual feedback
36. event.target.classList.add('draggedOver');
37. }
38. function dragOverHandler(event) {
39. //console.log("Drag over a droppable zone");
40. // Do not propagate the event
41. event.stopPropagation();
42. // Prevent default behavior, in particular when we drop
43. // images or links
44. event.preventDefault();
45. }
46. function dropHandler(event) {
47. console.log('drop event');
48. // Do not propagate the event
49. event.stopPropagation();
50. // Prevent default behavior, in particular when we drop
51. // images or links
52. event.preventDefault();
53. // reset the visual look of the drop zone to default
54. event.target.classList.remove('draggedOver');
55. // get the files from the clipboard
56. var files = event.dataTransfer.files;
57. var filesLen = files.length;
58. var filenames = "";
59. // iterate on the files, get details using the file API
60. // Display file names in a list.
61. for(var i = 0 ; i < filesLen ; i++) {
62. filenames += '\n' + files[i].name;
63. // Create a li, set its value to a file name, add it to the ol
64. var li = document.createElement('li');
65. li.textContent = files[i].name;
66. document.querySelector("#droppedFiles").appendChild(li);
67. }
68. console.log(files.length + ' file(s) have been dropped:\
n' + filenames);
69. readFilesAndDisplayPreview(files);
70. }
71. function readFilesAndDisplayPreview(files) {
72. // Loop through the FileList and render image files as
73. // thumbnails.
74. for (var i = 0, f; f = files[i]; i++) {
75. // Only process image files.
76. if (!f.type.match('image.*')) {
77. continue;
78. }
79. var reader = new FileReader();
80. //capture the file information.
81. reader.onload = function(e) {
82. // Render thumbnail.
83. var span = document.createElement('span');
84. span.innerHTML = "<img class='thumb' width='100' src='" +
85. e.target.result + "'/>";
86. document.getElementById('list').insertBefore(span, null);
87. };
88. // Read the image file as a data URL.
89. reader.readAsDataURL(f);
90. }
91. }
92. function handleFileSelect(evt) {
93. var files = evt.target.files; // FileList object
94. // do something with files... why not call
95. // readFilesAndDisplayPreview!
96. readFilesAndDisplayPreview(files);
97. }
98. </script>
99. </head>
100. <body>
101. <h2>Use one of these input fields for selecting files</h2>
102. <p>Beware, the directory choser works only in Chrome and may
overload
103. your browser memory if there are too many big images in the
104. directory you choose.</p>
105. Choose multiple files : <input type="file" id="files" multiple
106. onchange="handleFileSelect(event)"/>
107. </p>
108. <p>Choose a directory (Chrome only): <input type="file"
109. id="dir" webkitdirectory
110. onchange="handleFileSelect(event)"/>
111. </p>
112.
113. <h2>Drop your files here!</h2>
114. <div id="droppableZone" ondragenter="dragEnterHandler(event)"
115. ondrop="dropHandler(event)"
116. ondragover="dragOverHandler(event)"
117. ondragleave="dragLeaveHandler(event)">
118. Drop zone
119. <ol id="droppedFiles"></ol>
120. </div>
121. <br/>
122. <output id="list"></output>
123. <body>
124. <html>
The parts that we have added are in bold. As you can see, all methods share
the same code for previewing the images.
example
Try this interactive example at JSBin (this example does not work on CodePen.
We are using a fake remote server and it cancels the connection as soon as we
try to connect):
We build (line 75) an object of type FormData (this comes from the standard
JavaScript DOM API level 2), we fill this object with the file contents (line 77),
then we send the Ajax request (line 81), and monitor the upload progress (lines
66-69).
Instead of uploading all the files at once, it might be interesting to upload one
file at a time with visual feedback, such as: "uploading file
MichaelJackson.jpg.....". We will leave this exercise up to you.
Dragging out files from the browser to the desktop is supported by most
desktop browsers, except Internet Explorer (version <= 10). Note that you can
drag and drop not only from browser to desktop but also from browser to
browser. Further that we are discussing drag and drop operations and NOT the
right-click "Context menu" options browsers offer to save or copy images (for
example).
When discussing drag and drop, the W3C specification makes no reference to
dragging files out of the browser; it defines a drag and drop model based on
events, but does not define the way the data that is drag and dropped will be
handled.
Browser vendors have defined a de facto standard for dragging files out of the
browser.
1.A standard way to copy a file to the clipboard during a drag, if we want this
file to be draggable out of the browser.
2.The download code for copying the contents of the clipboard if the drop is on
the desktop.
For definition 1, the file copied to the clipboard must have a key/name equal
to DownloadURL, and the data itself should follow this format:
where the filename is the name the file will use once the download is
complete.
1. ondragstart="event.dataTransfer.setData('DownloadURL',
'image/png:logo.png:http://www.w3devcampus.com/logo.png');"
1. <!DOCTYPE html>
2. <html lang="en">
3. <head>
4. < meta charset=utf-8 />
5. <title>Example of drag out from browser to desktop</title>
6. </head>
7. <body>
8. <a href="http://www.w3devcampus.com/wp-content/uploads/2013/01/
michelbuffa.png"
9. draggable="true"
10. ondragstart="event.dataTransfer.setData('DownloadURL',
11.
'image/png:michelbuffaDownloaded.png:http://www.w3devcampus.co
m/wp-
12. content/uploads/2013/01/michelbuffa.png');"
13. onclick="return false";
14. >
15. I'm a link to Michel Buffa's picture, drag me to the desktop!
16. </a>
17. </body>
18. </html>
Notes:
You need to indicate the MIME type of the file, followed by ":", then the
filename of the file that will be copied to the desktop, and finally the URL of the
file.
1. <html lang="en">
2. <head>
3. <script
src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
4. <script>
5. function dragStartHandler(e) {
6. console.log("drag start");
7. var element = e.target;
8. var src;
9. if (element.tagName === "IMG" &&
10. element.src.indexOf("data:") === 0) {
11. src = element.src;
12. }
13. if (element.tagName === "CANVAS") {
14. src = element.toDataURL();
15. }
16.
17. if (src) {
18. var name = element.getAttribute("alt") || "download";
19. var mime = src.split(";")[0].split("data:")[1];
20. var ext = mime.split("/")[1] || "png";
21. var download = mime + ":" + name + "." + ext + ":" + src;
22. // Prepare file content to be draggable to the desktop
23. e.dataTransfer.setData("DownloadURL", download);
24. }
25. }
26.
27. function drawCanvas(){
28. var canvas = document.getElementById('mycanvas');
29. var ctx = canvas.getContext('2d');
30. var lingrad = ctx.createLinearGradient(0,0,0,150);
31. lingrad.addColorStop(0, '#000');
32. lingrad.addColorStop(0.5, '#669');
33. lingrad.addColorStop(1, '#fff');
34. ctx.fillStyle = lingrad;
35. ctx.fillRect(0, 0, canvas.width, canvas.height);
36. // Create an image from the canvas
37. var img = new Image();
38. img.src = canvas.toDataURL("image/png");
39. img.alt = 'downloaded-from-image';
40. //img.draggable='true' is not necessary, images are draggable
41. // by default
42. img.addEventListener('dragstart', dragStartHandler, false);
43. // Add the image to the document
44. document.querySelector("body").appendChild(img);
45. }
46. </script>
47. </head>
48. <body onload="drawCanvas()">
49. <h2>Drag out a canvas or an image to the desktop</h2>
50. <p>Demo adapted by M.Buffa
from: : <ahref="http://jsfiddle.net/bgrins/xgdSC/"/>http://jsfiddle.net/bgrins/
xgdSC/</a> (courtesy of TheCssNinja & Brian Grinstead)</p>
51. Drag out the image or the canvas. The filenames will be different on
desktop. This example is interesting as it works with canvas data, img (in the
form of a data URL or classic internal/external URL).
52. <br/>
53. <br/>The Canvas:<br/>
54. <canvas id='mycanvas' alt='downloaded-from-canvas' draggabl
e='true'
55. ondragstart="dragStartHandler(event)"></canvas>
56. <br/>
57. <br/>
58. The Image<br/>
59. </body>
60. </html>
Notice the way we build the data is common for the canvas and the image (lines 20-23).
Many solutions proposed on the Web rely on jQuery plugins. However, coding
such a behavior using only HTML5 APIs is easy AND faster AND has a
lower page weight, as we will see.
This part of the course will describe different approaches for implementing file
uploads associated with a form.
We have included PHP server-side code: this course focuses on HTML5 and
front-end development - so, the PHP code is given "as is".
The problem
Imagine that we have a regular HTML5 form, but as well as the input fields for
entering a name, address, age, etc., we also want to select and upload multiple
files (which might include images).
Serial approach: upload the files as soon as they are
selected or dragged and dropped
Let's design an XHR2/Ajax, a form with an <input type=file multiple> input
field, and one or more <progress> elements for monitoring file uploads. The
form will also have input fields of different types.
An example of this kind of form is shown below: when the user drags and drops
files, they will start being uploaded immediately. However, the form will only
be sent when all the fields are valid.
This approach is similar to Gmail's behavior when you compose a message and
add an attachment. The attachments are uploaded as soon as they are
selected or dropped into the message window, but the message will only be
sent when you press the "send" button. An empty field with
a required attribute, if left empty, will cause an error message pop up in a
bubble, and the form will not be submitted. Nice!
However, on the server side, we need a way to "join" the files that have been
asynchronously uploaded with the rest of the form's values. This is easier to do
than it sounds. Look at the provided PHP code provided with each of the
examples.
The difference between this and the first approach is that we are
sending everything at the same time using Ajax/JavaScript: the regular input
field content and the selected files.
The next page provides the source code of several examples, as well as the
server-side PHP code.
However, we also provide complete source code for these examples, including
the server-side PHP code. The course is about HTML5, not PHP, so we provide
this code "as is": it is only a few lines long per example, and has been tested
with the latest version of PHP. It should run out of the box with most WAMP,
LAMP, and MAMP distributions (Apache / PHP).
Unzip the archive and follow the included READMEs. These examples
propose different implementations of the two approaches presented in
the previous lecture, and both with an <input type=file> and drag and drop.
The HTML part of the examples is also using a technique we saw during the
HTML5 Part 1 course, that saves the input fields' content as you type, using
LocalStorage. You can reload the page any time without losing what you typed.
Initially, the examples all used a FormData object but at the time we
encountered some incompatibilities with older versions of PHP, so we had to
manually set a component of the HTTP header.
This part of the lesson is optional and is mainly useful for students who are also
involved in the server side of the Web development. Ask in the forum if you
are examples that use Java instead of PHP for handling multipart forms sent
using Ajax.
We could have merged file selector + drag and drop, as we did in examples
earlier in the course, but the code would have been longer and more difficult to
follow.
Auto-loading of the files, regular form submission,
benefits of the HTML5 form validation system
Example using a file selector (<input type="file">)
Try the online example at JSBin (this one does not have the PHP code running,
but works anyway, even if the files are not uploaded - it " the upload"). Look at
the online example for the code and the following explanations.
In this example, the "send" button is disabled and becomes enabled as soon as
all the files are completely uploaded. Also, note that the form is saved as the
user types, by using localStorage. Accordingly, it can be restored on page
reload, as in the example from the localStorage topic of the HTML5 Part 1
course.
Note that the full working source code of this example corresponds to
"example 1" in the zip archive that contains all examples.
Explanations:
Examples that use the single-package approach: upload files when the form is submitted
We will use the previous two examples as a basis for two further examples:
1.one that uses only a file selector,
2.one that uses drag and drop.
You can try this example at JSBin, and look at the source code and comments
for details.
Example using drag and drop
Same behavior as example 3, but this time using drag and drop.
Try the example at JSBin and look at source code and comments.
IndexedDB
Introduction
IndexedDB is presented as an alternative to
the WebSQL Database, which the W3C
deprecated on November 18, 2010 (while
still available in some browsers, it is no
longer in the HTML5 specification). Both are
solutions for storage, but they do not offer
the same functionalities. WebSQL Database
is a relational database access system,
whereas IndexedDB is an indexed table system.
From the W3C specification about IndexedDB: "User agents (apps running in
browsers) may need to store large numbers of objects locally in order to satisfy
off-line data requirements of Web applications. Where WebStorage (as seen in
the HTML5 part 1 course -localStorage and sessionStorage) is useful for storing
pairs of keys and their corresponding values, IndexedDB provides in-order
retrieval of keys, efficient searching of values, and the storage of duplicate
values for a key".
To sum up:
1. IndexedDB is a transactional Object Store in which you will be
offline.
data.
External resources
Much of this chapter either builds
on or is an adaptation of articles
posted on the Mozilla Developer Network site (IndexedDB, Basic
Concepts of IndexedDB and Using IndexedDB).
W3C specification about IndexedDB
Mozilla developer's page about IndexedDB
Getting started with IndexedDB article from codeproject.com
Example that runs using the new version of the specification, (read the
explanations)
Current Support
Current support is excellent both on mobile and desktop browsers.
Introduction
IndexedDB is very different from SQL databases, but don't be afraid if you've
only used SQL databases: IndexedDB might seem complex at first sight, but it
really isn't.
Let's quickly look at the main concepts of IndexedDB, as we will go into detail
later on:
Example of a transaction:
This transaction model is really useful when you consider what might happen if
a user opened two instances of your web app in two different tabs
simultaneously. Without transactional operations, the two instances might
stomp all over each others' modifications.
The IndexedDB API is mostly asynchronous. The API doesn't give you data
by returning values; instead, you have to pass a callback function. You don't
"store" a value in the database, or "retrieve" a value out of the database
through synchronous means. Instead, you "request" that a database operation
happens. You are notified by a DOM event when the operation finishes, and the
type of event lets you know if the operation succeeded or failed. This may
sound a little complicated at first, but there are some sanity measures baked-
in. After all, you are a JavaScript programmer, aren't you? ;-)
IndexedDB uses requests all over the place. Requests are objects that
receive the success or failure DOM events mentioned previously. They
have and properties, and you can
call addEventListener() and removeEventListener() on them. They also
have readyState, result, and errorCode properties which advise the status of
a request.
In a traditional relational data store, you would have a table that stores a
collection of rows of data and columns of named types of data. IndexedDB, on
the other hand, requires you to create an object store for a type of data and
simply persist JavaScript objects to that store. Each object store can have a
collection of indexes (corresponding to the properties of the JavaScript object
you store in the store) that enable efficient querying and iteration.
The concept of " origin" is defined by the combination of all three components
mentioned earlier (domain, protocol, port). For example, an app in a page with
the URL http://www.example.com/app/, and app
at http://www.example.com/dir/ may both access the same IndexedDB
database because they have the same origin (, example.com, and 80).
Whereas apps at http://www.example.com:8080/dir/ (different
port) or https://www.example.com/dir/(different protocol), do not satisfy
the same origin criteria (port or protocol differ from http://www.example.com)
See this article from MDN about the same origin policy for further details and
examples.
IndexedDB: definitions
These definitions come from the W3C specification. Read this page to
familiarize yourself with the terms.
DATABASE
Each origin (you may consider as "each application") has an associated set
of databases. A databasecomprises one or more object stores which hold the
data stored in the database.
Every database has a name that identifies it within a specific origin. The name
can be any string value, including the empty string, and stays constant for the
lifetime of the database.
Each database also has a current version. When a database is first created,
its version is 0, if not specified otherwise. Each database can only have one
version at any given time. A database can't exist in multiple versions at once.
The act of opening a database creates a connection. There may be
multiple connections to a given database at any given time.
OBJECT STORE
An object store is by which data is stored in the database.
Every object store has a name. The name is unique within the database to
which it belongs.
The object store persistently holds records (JavaScript objects), which are key-
value pairs. One of these keys is a kind of "primary key" in the SQL database
sense. This "key" is a property that every object in the datastore must contain.
Values in the object store are structured, but this structure may vary between
objects (i.e., if we store persons in a database, and use the email as "the key
all objects must define", some may have first name and last name, others may
have an address or no address at all, etc.)
Records within an object store are sorted according to keys, in ascending
order.
Optionally, an object store may also have a key generator and a key path. If
the object store has a key path, it is said to use in-line keys. Otherwise, it is
said to use out-of-line keys.
The object store can derive the key from one of three sources:
1.A key generator. A key generator generates a monotonically increasing
number every time a key is needed. This is somewhat similar to auto-
incremented primary keys in database.
2.Keys can be derived via a key path.
3.Keys can also be explicitly specified when a value is stored in the object store.
VERSION
When a database is first created, its version is the integer 0. Each database
has one version at a time; a database can't exist in multiple versions at once.
The only way to change the version is by opening it with a higher version
number than the current one. This will start a transaction and fire an event.
The only place where the schema of the database can be updated is inside the
handler of that event.
TRANSACTION
From the specification: "A transaction is used to interact with the data in a
database. Whenever data is read or written to the database, this is done by
using a transaction.
So, for example, if a database connection already has a writing transaction with
a scope that covers only the flyingMonkey object store, you can start a second
transaction with a scope of the unicornCentaur and unicornPegasus object
stores. As for reading transactions, you can have several of them, and they
may even overlap. A "" transaction never runs concurrently with other
transactions (reminder: we usually use such transactions when we create the
object store or when we modify the schema).
Generally speaking, the above requirements mean that "" transactions which
have overlapping scopes always run in the order they were created, and never
run in parallel. A "" transaction is automatically created when a database
version number is provided that is greater than the current database version.
This transaction will be active inside the event handler, allowing the creation
of new object stores and indexes.
REQUEST
The operation by which reading and writing on a database is done. Every
request represents one read or one write operation. Requests are always run
within a transaction. The example below adds a customer to the object store
named "customers".
1. // Use the transaction to add data...
2. var objectStore = transaction.objectStore("customers");
3. for (var i in customerData) {
4. var request = objectStore.add(customerData[i]);
5. request.onsuccess = function(event) {
6. // event.target.result == customerData[i].ssn
7. };
8. }
INDEX
It is sometimes useful to retrieve records from an object store through
means other than their key.
An index allows the user to look up records in an object store using the
properties of the values in the object store's records. Indexes are a common
concept in databases. Indexes can speed up object retrieval and allow multi-
criteria searches. For example, if you store persons in your object store, and
add an index on the "email" property of each person, then searching for some
person using his/her email address will be much faster.
An index is a list of records which holds the data stored in the index. The
records in an index are automatically populated whenever records in the
referenced object store are inserted, updated or deleted. There may be several
indexes referencing the same object store, in which case changes to the object
store cause all such indexes to update.
An index contains a unique flag. When this flag is set to true, the index
enforces the rule that no two records in the index may have the same key. If a
user attempts to insert or modify a record in the index's referenced object
store, such that an indexed attribute's value already exists in an index, then
the attempted modification to the object store fails.
The key must be of a data type that has a number that is greater than the one
before. Each record in an object store must have a key that is unique the same
store, so you cannot have multiple records with the same key in a given object
store.
A key can be one of the following types: string, date, float, and array.
For arrays, the key can range from an empty value to infinity. And you can
include an array within an array.
Alternatively, you can also look up records in an object store using an index.
KEY GENERATOR
A mechanism for producing new keys in an ordered sequence. If an object store
does not have a key generator, then the application must provide keys for
records being stored. Similar to auto-generated primary keys in SQL databases.
IN-LINE KEY
A key that is stored as part of the stored value. Example: the email of a person
or a student number in an object representing a student in a student store. It is
found using a key path. An in-line key could be generated using a generator.
After the key has been generated, it can then be stored in the value using the
key path, or it can also be used as a key.
OUT-OF-LINE KEY
A key that is stored separately from the value being stored, for instance, an
auto-incremented id that is not part of the object. Example: you
store {name:Buffa, firstName:Michel} and {name:Gandon, firstName:
Fabien}, each will have a key (think of it as a primary key, an id...) that can be
auto-generated or specified, but that is not part of the object stored.
KEY PATH
Defines where the browser should extract the key from a value in the object
store or index. A valid key path can include one of the following: an
empty string, a JavaScript identifier, or multiple JavaScript identifiers
separated by periods. It cannot include spaces.
VALUE
Each record has a value, which could include anything that can be expressed in
JavaScript, boolean, number, string, date, object, array, regexp, undefined,
and null.
When an object or an array is stored, the properties and values in that object or
array can also be anything that is a valid value.
Blobs and files can be stored, (supported by all major browsers, IE > 9). The
example in the next chapter stores images using blobs.
Range and scope
SCOPE
The set of object stores and indexes to which a transaction applies. The scopes
of read-only transactions can overlap and execute at the same time. On the
other hand, the scopes of writing transactions cannot overlap. You can still
start several transactions with the same scope at the same time, but they just
queue up and execute one after another.
CURSOR
A mechanism for iterating over multiple records within a key range. The cursor
has a source defining which index or object store it is iterating. It has a position
within the and retrieves records sequentially according to the value of their
keys in either increasing or decreasing order. For the reference documentation
on cursors, see IDBCursor.
KEY RANGE
A continuous interval over some data used for keys. Records can be retrieved
from object stores and indexes using keys or a range of keys. You can limit or
filter the range using lower and upper bounds. For example, you can iterate
over all the values of a key between x .
In the "Using IndexedDB" pages of this course, you will learn about:
External resources
Additional information is available on these Web sites. Take a look at these!
Using IndexedDB from the Firefox Developer's site
Storing images and files in IndexedDB
How to see IndexedDB content in browsers other than Chrome: the
linq2indexedDB tool
How to view IndexedDB content in Firefox
Creating a database
Our online example at JSBin shows how to create and populate an object store
named "CustomerDB". This example should work on both mobile and desktop
versions of all major post-2012 browsers.
With Chrome, please open the JSBin example and activate the Developer tool
console (F12 or cmd-alt-i). Open the JavaScript and HTML tabs on JSBin.
Then, click the "Create CustomerDB" button in the HTML user interface of the
example: it will call the createDB() JavaScript function that:
This message comes from the JavaScript request. callback. Indeed, the first
time we open the database we ask for a specific version (in this example:
version 2) with:
...and if there is no version "2" of the database, then we enter the callback
where we actually create the database.
You can try to click again on the button if database version "2" exists,
this the request. callback will be called. This is where we will
add/remove/search data (you should see a message on the console).
Notice that the version number cannot be a float: "1.2" and "1.4" will
automatically be rounded to "1".
Explanations:
All the "creation" process is done in the onupgradeneeded callback (lines 26-
50):
Line 30: get the database created in the result of the dom event: db =
event.target.result;
Line 35: create an object store named "customers" with the primary key being
the social security number ("ssn" property of the JavaScript objects we want to
store in the object store): var objectStore =
db.createObjectStore("customers", {keyPath: "ssn"});
Lines 39 and 44: create indexes on the "name" and "email" properties of
JavaScript objects:objectStore.createIndex("name", "name", {unique:
false});
Lines 48-50: populate the database: objectStore.add(...).
Note that we did not create any transaction, as the callback on a create
database request is always in a default transaction that cannot overlap
with another transaction at the same time.
If we try to open a database version that exists, then the request. callback is
called. This is where we are going to work with data. The DOM event result
attribute is the database itself, so it is wise to store it in a variable for later
use: = event.target.result;
Deleting a database
You can delete a database simply by running this command:
1. indexedDB.deleteDatabase("databaseName");
While the creation of the database occurred in a transaction that ran "under
the hood" without explicit use of the "transaction" keyword, for
adding/removing/updating/retrieving data, explicit use of a
transaction is required.
We generate a transaction object from the database, indicate with which object
store the transaction will be associated, and specify an access mode .
Source code example for creating a transaction associated with the object
store named "customers":
Transactions, when created, must have a mode set that is either , or (this last
mode is only for creating a new database or for modifying its schemas: i.e.
changing the primary key or the indexes).
When you can, use mode. Concurrent read transactions will become
possible.
In the following pages, we will explain how to insert, search, remove, and
update data. A final example that merges all examples together will also be
shown at the end of this section.
Execute this example and look at the IndexedDB object store content from the
Chrome dev tools (F12 or cmd-alt-i). One more customer should have been
added.
We just added a single function into the example from the previous section -
the function AddACustomer()that adds one customer:
1. { : "123-45-6789", name: "Michel Buffa", age: 47, email:
"buffa@i3s.unice.fr" }
1. function addACustomer() {
2. // 1 - get a transaction on the "customers" object store
3. // in readwrite, as we are going to insert a new object
4. var transaction = db.transaction(["customers"], "readwrite");
5. // Do something when all the data is added to the database.
6. // This callback is called after transaction has been completely
7. // executed (committed)
8. transaction.oncomplete = function(event) {
9. alert("All done!");
10. };
11. // This callback is called in case of error (rollback)
12. transaction.onerror = function(event) {
13. console.log("transaction.onerror
errcode=" + event.target.error.name);
14. };
15. // 2 - Init the transaction on the objectStore
16. var objectStore = transaction.objectStore("customers");
17. // 3 - Get a request from the transaction for adding a new object
18. var request = objectStore.add({ ssn: "123-45-6789",
19. name: "Michel Buffa",
20. age: 47,
21. email: "buffa@i3s.unice.fr" });
22. // The insertion was ok
23. request.onsuccess = function(event) {
24. console.log("Customer with ssn= " + event.target.result + "
25. added.");
26. };
27. // the insertion led to an error (object already in the store,
28. // for example)
29. request.onerror = function(event) {
30. console.log("request.onerror, could not insert customer,
31. errcode = " + event.target.error.name);
32. };
33. }
Explanations:
In the code above, lines 4, 19 and 22 show the main calls you have to perform
in order to add a new object to the store:
1.Create a transaction.
2.Map the transaction onto the object store.
3.Create an "add" request that will take part in the transaction.
The different callbacks are in lines 9 and 14 for the transaction, and in lines 28
and 35 for the request.
You may have several requests for the same transaction. Once all requests
have the transaction. callback is called. In any other case
the transaction. callback is called, and the remains unchanged.
This time, we added some tests for checking that the database is open before
trying to insert an element, and we added a small form for entering a new
customer.
Notice that the insert will fail and display an alert with an error message if:
The is already present in the database. This property has been declared as
the keyPath (a sort of primary key) in the object store schema, and it should be
unique:db.createObjectStore("customers", { keyPath: "" });
The email address is already present in the object store. Remember that in
our , the emailproperty is an index that we declared as
unique: objectStore.createIndex("email", "email", { unique: true });
Try to insert the same customer twice, or different customers with the same .
An alert like this should pop up:
1. <fieldset>
2. SSN: <input type="text" id="ssn" placeholder="444-44-4444"
3. required/><br>
4. Name: <input type="text" id="name"/><br>
5. Age: <input type="number" id="age" min="1" max="100"/><br>
6. Email:<input type="email" id="email"/> reminder, email must be
7. unique (we declared it as a "unique" index)<br>
8. </fieldset>
9. <button onclick="addACustomer();">Add a new Customer</button>
1. function addACustomer() {
2. if(db === null) {
3. alert('Database must be opened, please click the Create
4. CustomerDB Database first');
5. return;
6. }
7. var transaction = db.transaction(["customers"], "readwrite");
8. // Do something when all the data is added to the database.
9. transaction.oncomplete = function(event) {
10. console.log("All done!");
11. };
12. transaction.onerror = function(event) {
13. console.log("transaction.onerror
errcode=" + event.target.error.name);
14. };
15. var objectStore = transaction.objectStore("customers");
16. // adds the customer data
17. var newCustomer={};
18. newCustomer.ssn = document.querySelector("#ssn").value;
19. newCustomer.name = document.querySelector("#name").value;
20. newCustomer.age = document.querySelector("#age").value;
21. newCustomer.email = document.querySelector("#email").value;
22. alert('adding customer ssn=' + newCustomer.ssn);
23.
24. var request = objectStore.add(newCustomer);
25. request.onsuccess = function(event) {
26. console.log("Customer with ssn= " + event.target.result + "
27. added.");
28. };
29.
30. request.onerror = function(event) {
31. alert("request.onerror, could not insert customer, errcode = "
32. + event.target.error.name +
33. ". Certainly either the ssn or the email is already
34. present in the Database");
35. };
36. }
It is also possible to shorten the code of the above function by chaining the
different operations using the "." operator (getting a transaction from the ,
opening the store, adding a new customer, etc.).
The above code does not perform all the tests, but you may encounter such a
way of coding (!).
Indeed, entering an empty value for the keyPath or for indexes is a valid value
(in the IndexedDB sense). In order to avoid this, you should add more
JavaScript code. We will let you do this as an exercise.
Using IndexedDB: removing data from an object store
Let's move to the next online example at JSBin:
See the changes in Chrome dev. tools: refresh the view (right click/refresh) or
press F12 or cmd-alt-i twice. There is a bug in the refresh feature with some
versions of Google Chrome.
1.Be sure to click the "create database button" to open the existing database.
2.Then use Chrome dev tools to check that the customer with ssn=444-44-
444 exists. If it's not there, just insert into the database like we did earlier
in the course.
3.Right click on indexDB in the Chrome dev tools and refresh the display of the
IndexedDB's content if necessary if you cannot see with ssn=444-44-444.
Then click on the "Remove Customer ssn=444-44-4444(Bill)" button. Refresh
the display of the database. The 'Bill' object should have disappeared!
1. function removeACustomer() {
6. }
9. transaction.oncomplete = function(event) {
11. };
14. event.target.error.name);
15. };
21. };
26. };
27. }
Notice that after the deletion of the Customer (line 23), the request. callback
is called. And if you try to print the value of the event.target.result variable,
it is "undefined".
The above screenshot shows how we added an empty customer with , (we just
clicked on the open database button, then on the "add a new customer button"
with an empty form).
Now, we fill the name, age and email input fields to update the object
with " and click on the "update data about an existing customer" button. This
updates the data in the object store, as shown in this screenshot:
Here is the new code added to our example:
1. function updateACustomer() {
2. if(db === null) {
3. alert('Database must be opened first, please click the Create
4. CustomerDB Database first');
5. return;
6. }
7. var transaction = db.transaction(["customers"], "readwrite");
8. // Do something when all the data is added to the database.
9. transaction.oncomplete = function(event) {
10. console.log("All done!");
11. };
12. transaction.onerror = function(event) {
13. console.log("transaction.onerror
errcode=" + event.target.error.name);
14. };
15. var objectStore = transaction.objectStore("customers");
16. var customerToUpdate={};
17. customerToUpdate.ssn = document.querySelector("#ssn").value;
18. customerToUpdate.name = document.querySelector("#name").value;
19. customerToUpdate.age = document.querySelector("#age").value;
20. customerToUpdate.email = document.querySelector("#email").value;
21.
22. alert('updating customer ssn=' + customerToUpdate.ssn);
23. var request = objectStore.put(customerToUpdate);
24. request.onsuccess = function(event) {
25. console.log("Customer updated.");
26. };
27.
28. request.onerror = function(event) {
29. alert("request.onerror, could not update customer, errcode= " +
30. event.target.error.name + ". The ssn is not in the
31. Database");
32. };
33. }
1. function searchACustomer() {
2. if(db === null) {
3. alert('Database must be opened first, please click the Create
4. CustomerDB Database first');
5. return;
6. }
7. var transaction = db.transaction(["customers"], "readwrite");
8. // Do something when all the data is added to the database.
9. transaction.oncomplete = function(event) {
10. console.log("All done!");
11. };
12. transaction.onerror = function(event) {
13. console.log("transaction.onerror
errcode=" + event.target.error.name);
14. };
15. var objectStore = transaction.objectStore("customers");
16. // Init a customer object with just the ssn property initialized
17. // from the form
18. var customerToSearch={};
19. customerToSearch.ssn = document.querySelector("#ssn").value;
20. alert('Looking for customer ssn=' + customerToSearch.ssn);
21. // Look for the customer corresponding to the ssn in the object
22. // store
23. var request = objectStore.get(customerToSearch.ssn);
24. request.onsuccess = function(event) {
25. console.log("Customer found" + event.target.result.name);
26. document.querySelector("#name").value=event.target.result.name;
27. document.querySelector("#age").value = event.target.result.age;
28. document.querySelector("#email").value
29. =event.target.result.email;
30. };
31.
32. request.onerror = function(event) {
33. alert("request.onerror, could not find customer, errcode = " +
event.target.error.name + ".
34. The ssn is not in the Database");
35. };
36. }
The search is at line 30, and the callback in the case of success ., lines 32-
38. event.target with as the retrieved object (lines 33 to 36).
Well, this is a lot of code, isn't it? We can considerably abbreviate this function
(though, admittedly it won't take care of all possible errors). Here is the
shortened version:
1. function searchACustomerShort() {
2. db.transaction("customers").objectStore("customers")
3. .get(document.querySelector("#ssn").value).onsuccess =
4. function(event) {
5. document.querySelector("#name").value =
6. event.target.result.name;
7. document.querySelector("#age").value =
8. event.target.result.age;
9. document.querySelector("#email").value =
10. event.target.result.email;
11. }; // end of onsuccess callback
12. }
You can try it on JSBin: a version of the online example using this shortened
version (the function is at the end of the JavaScript code):
1. function searchACustomerShort() {
2. if(db === null) {
3. alert('Database must be opened first, please click the Create
4. CustomerDB Database first');
5. return;
6. }
7. db.transaction("customers").objectStore("customers")
8. .get(document.querySelector("#ssn").value)
9. .onsuccess =
10. function(event) {
11. document.querySelector("#name").value =
12. event.target.result.name;
13. document.querySelector("#age").value =
14. event.target.result.age;
15. document.querySelector("#email").value =
16. event.target.result.email;
17. };
18. }
Explanations:
Since there's only one object store, you can avoid passing a list of object
stores that you need in your transaction and just pass the name as a string
(line 8),
We are only reading from the database, so we don't need a "" transaction.
Calling transaction() with no mode specified gives a "" transaction (line 8),
We don't actually save the request object to a variable. Since the DOM event
has the request as its target we can use the event to get to the result property
(line 9).
1. function listAllCustomers() {
2. var objectStore =
3. db.transaction("customers").objectStore("customers");
4. objectStore.openCursor().onsuccess = function(event) {
5. // we enter this callback for each object in the store
6. // The result is the cursor itself
7. var cursor = event.target.result;
8. if (cursor) {
9. alert("Name for SSN " + cursor.key + " is " +
10. cursor.value.name);
11. // Calling continue on the cursor will result in this callback
12. // being called again if there are other objects in the store
13. cursor.continue();
14. } else {
15. alert("No more entries!");
16. }
17. }; // end of onsuccess...
18. } // end of listAllCustomers()
In the above example, we're iterating over all objects in ascending order.
The callback for cursors is a little special. The cursor object itself is
the result property of the request (above we're using the shorthand, so
it's event.target.result). Then the actual key and value can be found
on the key and value properties of the cursor object. If you want to keep
going, then you have to call cursor.continue()on the cursor.
When you've reached the end of the data (or if there were no entries that
matched your openCursor()request) you still get a success callback, but
the result property is undefined.
One common pattern with cursors is to retrieve all objects in an object store
and add them to an array, like this:
1. function listAllCustomersArray() {
2. var objectStore =
3. db.transaction("customers").objectStore("customers");
4. var customers = []; // the array of customers that will hold
5. // results
6. objectStore.openCursor().onsuccess = function(event) {
7. var cursor = event.target.result;
8. if (cursor) {
9. customers.push(cursor.value); // add a customer in the
10. // array
11. cursor.continue();
12. } else {
13. alert("Got all customers: " + customers);
14. }
15. }; // end of onsuccess
16. } // end of listAllCustomersArray()
Here is a function that examines by name the in the object store, and returns
the first one it finds with a name equal to "Bill":
1. function getCustomerByName() {
2. if(db === null) {
3. alert('Database must be opened first, please click the Create
4. CustomerDB Database first');
5. return;
6. }
7. var objectStore =
8. db.transaction("customers").objectStore("customers");
9. var index = objectStore.index("name");
10. index.get("Bill").onsuccess = function(event) {
11. alert("Bill's SSN is " + event.target.result.ssn +
12. " his email is " + event.target.result.email);
13. };
14. }
The search by index occurs at lines 11 and 13: line 11 creates an "index" object
that corresponds to the "name" property. Line 13 calls the get() method on this
index-object to retrieve all of the from the dataStore which have a name equal
to "Bill".
In order to get all the "Bills", once again we have to use a cursor.
When we work with indexes, we can open two different types of cursors on
indexes:
1.A normal cursor which maps the index property to the object in the object
store, or,
2.A key cursor which maps the index property to the key used to store the
object in the object store.
1. index.openCursor().onsuccess = function(event) {
2. var cursor = event.target.result;
3. if (cursor) {
4. // cursor.key is a name, like "Bill", and cursor.value is the
5. // whole object.
6. alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ",
7. email: " + cursor.value.email);
8. cursor.continue();
9. }
10. };
Key cursor:
1. index.openKeyCursor().onsuccess = function(event) {
2. var cursor = event.target.result;
3. if (cursor) {
4. // cursor.key is a name, like "Bill", and cursor.value is the
5. // SSN (the key).
6. // No way to directly get the rest of the stored object.
7. alert("Name: " + cursor.key + ", "SSN: " + cursor.value);
8. cursor.continue();
9. }
10. };
You can try an online example at JSBin that uses the above methods:
How to try this example:
1. function getAllCustomersByName() {
2. if(db === null) {
3. alert('Database must be opened first, please click the Create
4. CustomerDB Database first');
5. return;
6. }
7. var objectStore =
8. db.transaction("customers").objectStore("customers");
9. var index = objectStore.index("name");
10. // Only match "Bill"
11. var singleKeyRange = IDBKeyRange.only("Bill");
12. index.openCursor(singleKeyRange).onsuccess = function(event) {
13. var cursor = event.target.result;
14. if (cursor) {
15. // cursor.key is a name, like "Bill", and cursor.value is the
16. // whole object.
17. alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn ",
18. + email: " + cursor.value.email);
19. cursor.continue();
20. }
21. };
22. }
Complete example
Adapted from this example on gitHub
Try the online example at JsBin (enter "Gaming", "Batman" etc. as key range
values):
Conclusion
Hmmm... The two courses, HTML5 Part 1 and HTML5 Part 2, have covered a lot
of material, and you may have trouble identifying which of the different
techniques you have learned best suits your needs.
We have borrowed two tables from HTML5Rocks.com that summarize the pros
and cons of various approaches.
To sum up:
If you need to work with transactions (in the database sense: protect data
against concurrent access, etc.), or do some searches on a large amount of
data, if you need indexes, etc., then use IndexedDB
If you need a way to store simple strings or JSON objects, then use
localStorage/sessionStorage.Example: store HTML form content as you
type, store a game's hi-scores, preferences of an application, etc.
If you need to cache files for faster access or for making your
application run offline, then use the cache API. This is true for today, but
in future, look for the Service Workers API. This topic will be presented in future
versions of this course, once browser support has improved and the API has
stabilized.
If you need to manipulate files (read or download/upload), then use
the File API and XHR2.
If you need to manipulate a file system, there is a FileSystem and a FileWriter
API which are very poorly supported and will certainly be replaced in HTML 5.1.
We decided not to discuss these in the course because they lack agreement
within the community and browser vendors.
If you need an SQL database client-side: No! Forget this idea, please! There
was once a WebSQL API, but it disappeared rapidly.
Note that the above points may be used in any combination: you
may have a Web app that uses localStorage and IndexedDB to cache its
resources so that it can run in offline mode.
Web Storage
IndexedDB
Week4
lecture VideoUsingWebComponents.zip
Introduction
Web components enable you to use custom HTML elements in your HTML
... will render in your document an animated GIF, and it will loop forever in
ping-pong mode: the order of the animation will be reversed when the last
image is reached and again when the animation goes back to the first
image.
Click on the image to run the animated GIF demo, or visit this Web site.
If you look at the source of the demo page, you will note the following at
This is new! It's called an "HTML import"! If your browser supports HTML
imports, you can now import another HTML document, that will come with
its own HTML, CSS, and JavaScript code-base, into your HTML page . The
code for the animated GIF player, rendered when the browser encounters
the custom HTML element <x-gif>, is located in the imported HTML file
(and this HTML file can in turn include or define CSS and JavaScript
content).
Even more impressive: if you use the devtools or the right click context
menu to view the source of the page, you will not see the DOM of this
...and your document will still be valid. Looking at the source code or at
the DOM with the devtool's inspector will not reveal the source code
components. Usually, you will need to import the HTML file that defines the
components you want to use, and maybe also a polyfill if you want to use
input fields with voice recognition, and a text area that could vocalize the
text:
project, also offer huge sets of components for creating rich UIs with a
Current support
a single API - rather it's what we call an "umbrella" API, built on top of
Currently (as at December 2015), only Google Chrome and Opera natively
support these four APIs. Other browsers support only some of them, or
Mozilla include a polyfill, that adds support for most modern browsers (>
2013).
mobile browsers:
Shadow DOM is supported by Chrome and Opera, and FireFox offers
partial support:
partial support:
HTML Imports is supported by Chrome and Opera, and FireFox offers
partial support:
HTML templates
HTML templates are an important building-block of Web components. When
you use a custom element like <x-gif....>, the browser will (before rendering
the document) clone and add some HTML/CSS/JS code to your document,
thanks to the HTML template API that is used behind the scenes.
HTML templates define fragments of code (HTML, JavaScript and CSS styles)
that can be reused.
These parts of are inert (i.e., CSS will not be applied, JavaScript will not be
executed, images will not be loaded, videos will not be played, etc.) until the
template is used.
1. <template id="">
2. <img src="" alt="great image">
3. <div class="comment"></div>
4. </template>
Note that it's ok to have the src attribute empty here, we will initialize it when
the template is activated.
1. var t = document.querySelector('#mytemplate');
2. // Populate the src at runtime.
3. t.content.querySelector('img').src ='http://webcomponents.github.io/
img/logo.svg';
4. // Clone the template, sort of "instantiation"!
5. var clone = document.importNode(t.content, true);
6. document.body.appendChild(clone);
Explanations:
In this example, line 1 assigns the DOM node corresponding to the template
we defined to variable t.
t.content (line 3) is the root of the subtree in the template (in other words,
the lines of HTML code inside the template element)
Note that we set the value of the src attribute of the image inside the
template at line 3, using a CSS selector on the template's content.
Lines 5 and 6 the template's content and add it to the <body> of the
document.
Online Example
Here is an online example at JSBin that uses exactly the code presented:
1. <template id="mytemplate">
2. <img src="" alt="great image">
3. <div class="comment">hello</div>
4. </template>
5. <body>
6. <button onclick="instantiate()">Instantiate the template</button><br>
7. </body>
1. function instantiate() {
2. var t = document.querySelector('#mytemplate');
3. // Populate the src at runtime.
4. t.content.querySelector('img').src =
5. 'http://webcomponents.github.io/img/logo.svg';
6. var clone = document.importNode(t.content, true);
7. document.body.appendChild(clone);
8. }
Introduction
The Shadow DOM API provides DOM encapsulation: it serves to hide what is not
necessary to see!
aspect
shadow host.
NB: Because other browsers do not offer the tool-set, all of the examples we
discuss on this subject use Google Chrome or Chromium.
Open this JSBin example in your browser, and fire up the console (F12 on
Windows/Linux, Cmd-Alt-i on Mac OS):
Click on the "Elements" tab in the , or use the magnifying glass and click on the
video, to look at DOM view of the video element. You will see the exact HTML
code that is in this example, but you cannot see the elements that compose
the control bar. You don't have access to the play button, etc.
Let's take a look behind the scenes, and see the Shadow DOM associated with
the <video> element.
First, click on the Settings icon (three vertical dots) and select Settings in the
drop down menu:
Then scroll down until you see the "Show user agent shadow DOM" option and
check it. Close the panel.
Now, look for the video element again and within the DOM view you should see
something new:
Open this shadow root by clicking on it, and move the mouse pointer over the
different elements:
Chrome developers are already using the shadow DOM to define their own Web
Components, such as <video> or <audio> elements! And they use the Shadow
DOM to hide the internal plumbing.
Browser developers have been using Web Components for a while, and
now it's available to every Web developer!
a Simple example of Shadow Dom usage
Let's have a look at a very simple example:
1. <button>Hello, world (not rendered)!</button>
2. <script>
3. var host = document.querySelector('button');
4. var root = host.createShadowRoot();
5. root.textContent = 'the shadow root node is rendered';
6. </script>
Lines 3-5 show how to associate a shadow root with an existing HTML element.
In this example, the <button>defined at line 1 is a shadow host, and it is
associated with the shadow root which contains five words of text (line 5).
This example illustrates the three rules of the shadow DOM. Let's look at them
again:
The three rules of Shadow DOM:
1.With Shadow DOM, elements are associated with a new kind of node: a
shadow root.
2.An element in the HTML which has a shadow root associated with it is called a
shadow host.
3.The content of a shadow host doesn’t appear; the content of the shadow root
is rendered instead.
And indeed, the above example (try the online version here at JSBin) renders
the content of the shadow root, not the content of the button. In the online
example, try to change the text of the button (line 1), and you will notice that
nothing changes. Then modify the text at line 5 and observe the result!
Note that once again, the content shown is the shadow root + the styles
applied. The styles applied are those defined in the template's content that has
been cloned and put inside the shadow root.
NB a little bit of French squeezed past our filters. "" in French (and other
languages) means "Instantiate" in English. We hope you'll translate, as
appropriate; but if you seek definitions or use the word in web-searches, then
the English spelling will help!
Look at this example at JSBin that uses two H1s in the document: one is
associated with a shadow root (defined in a template with an embedded CSS
that selects H1 elements and makes them white on red); whereas the other is
located in the body of the document and is not affected by the CSS within the
Web Component.
Beware: the included polyfill will not emulate CSS encapsulation. To see the
real behavior, try with Chrome or Opera!
1. <template id="mytemplate">
2. <style>
3. h1 {color:white; background:red}
4. </style>
5. <h1>This is a shadowed H1</h1>
6. </template>
7. <body>
8. <h1 id="withShadowDom">This is a text header</h1>
9. <h1>Normal header with no shadow DOM associated.</h1>
10. </body>
JavaScript code:
The second H1 is not affected by the CSS defined in the template used by the
first H1.
Insert content from the host element within the Shadow DOM
Generic injection
USING <content></content> inSIDE THE HTML5
template
It is possible to define a part of the template into which external HTML content
will be "injected". For this, we use the <content>...</content> element, as
shown below:
1. <template id="mytemplate">
2. <h1 part='heading'>This is a shadowed H1</h1>
3. <p part="paragraph">
4. <content></content>
5. </p>
6. </template>
7. <body>
8. <H1 class="myWidget">Injected content</h1>
9. </body>
Explanations:
Explanations:
The content of this page uses examples adapted from the excellent "Shadow
DOM 201, CSS and Styling" article at HTML5rocks.
example: see how CSS style encapsulation works
Example at JSBin:
HTML code:
1. <head>
2. ...
3. <style>
4. h3 {
5. color:lightgreen;
6. background-color:blue;
7. }
8. </style>
9. </head>
10. <body>
11. ...
12. <div>
13. <h3>Note, I'm a H3 in a div, the div is a shadow host</h3>
14. </div>
15. <h3>This is a normal h3, affected by the CSS style included in this
page</h3>
16. ...
JavaScript code:
Explanations:
We injected a style color:red into the CSS of the Shadow DOM (line 4 of the
JavaScript code). Only the H3 in the Shadow DOM became red, even with a
"global" rule that says that all H3s should be light green with a blue
background (lines 4-8 of the HTML code). Again, styles apply only within their
own scope (by default).
Other style rules defined on the HTML page that H3s don't affect the elements
in the Shadow DOM. This is because selectors don't cross the shadow
boundary.
1. ...
2. <template id="">
3. <h1 part='heading'>This is a shadowed H1</h1>
4. <p part="paragraph">
5. <content></content>
6. </p>
7. </template>
8.
9. <body>
10. <p class="myWidget"><span>This is an injected span, it's just
"moved" inside the Shadow DOM, but belongs to the main document, and can
be styled with the external CSS file.</span>
11. </p>
12. </body>
13. ...
CSS source code:
JavaScript code:
Explanations:
In the HTML, the content between <p class="myWidget"> and </p> is injected
into the template (HTML code, line 10, and in the template line 5), then the
template is cloned and added to the body of the document (JavaScript
code, lines 3, 4 and 11). This content still belongs to the main HTML page,
where it has been defined. Global styles apply on this content: the span
{ color:red; } will change the color of the injected <span>.
In The JavaScript code, at line 14, we add new elements in the shadow DOM.
These elements are created directly in the shadow DOM and are encapsulated.
External CSS styles will not apply! The div { color:red; } will not change
its color!
Example at JBin:
HTML code:
1. ...
2. <style>
3. h3 {
4. color:lightgreen;
5. background-color:blue;
6. }
7. </style>
8. </head>
9. <body>
10. ...
11. <div>
12. <h3>This content is injected</h3>
13. </div>
14. ...
JavaScript code:
Explanations:
The code at line 5 contains a CSS rule that selects the shadow host content
(the H3 content at line 12 of the HTML code). This content is injected at line
7 of the JS code.
This rule says "make it uppercase", and indeed, as there is no conflict with
another CSS rule, the text is rendered in uppercase.
This rule also says "make the background color red!". This time, the external,
global, stylesheet has a rule that is in conflict with this one (at line 5 of the
HTML code). The external CSS has higher priority, so the text background color
will be blue.
One common use of the :host selector: reacting to mouse
events
You can use the :host(:hover), :host(:active), :host(:focus) etc.
selectors. Notice the use of parenthesis that are not necessary with regular CSS
selectors.
Try this example at JSBin: move the mouse over the shadow host
We just replaced line 5 in the JavaScript code of the previous example with this
one:
You can use :host-context(.different) to style the host <x-foo> only when
it's a descendant of an element with the class .different:
1. :host-context(.different) {
2. color: red;
3. }
This enables you to encapsulate style rules in an element's Shadow DOM that
uniquely style if its context matches certain constraints (here: has the CSS
class "different").
Example at JSBin:
HTML code:
1. <style>
2. #host::shadow span{
3. color:red;
4. }
5. </style>
6. </head>
7. <body>
8. <div>
9. <h3 id="host">Injected content</h3>
10. </div>
JavaScript code:
Explanations:
The selector in the HTML code first selected the element with id="host" ->
the H3 in the page,
Then ::shadow selected its shadow DOM,
Then span selected all spans in the shadow DOM of the element.
Basic usage:
1. document.registerElement('element-name', {
2. prototype: proto
3. })
1. <body>
2. <my-widget>
3. <span id="titleToInject">Title injected</span>
4. <span id="partToInject">Paragraph injected</span>
5. </my-widget>
6. </body>
HTML code for the declaration of the template (the same as in one of the
previous examples):
1. <template id="mytemplate">
2. <style>
3. h1 {
4. color:white;
5. background:red;
6. }
7. </style>
8. <h1 part='heading'>
9. <content select="#titleToInject"></content>
10. </h1>
11. <p part="paragraph">
12. <content select="#partToInject">
13. </content>
14. </p>
15. </template>
JavaScript code:
Explanations:
To sum up, it's an example similar to one we saw earlier in the course, except
that the shadow host is not the custom element itself. And the code that
instantiates the template and puts it in a shadow DOM and this shadow DOM
with the custom element, is located in a createCallback function which is
called when the browser needs to render the custom element.
Complete example
Now, we can use the newly created element and inject content. The template
used here is the last one we studied in a previous lesson about HTML
templates. Check the complete online example at JSBin:
External resource
This lesson is only an introduction to custom elements. More advanced users,
who would like to see how a custom element can inherit from another custom
element, are welcome to read this article on HTML5Rocks.
HTML Imports
This is the simplest of the APIs from Web components :-)
Add a <link rel="imports" href="your_html_file"> and all the html/css/js
that defines a Web component you plan to use will be imported:
It is as simple as:
1. <head>
2. <link rel="imports" href="components/myComponents.html">
3. </head>
4. <body>
5. <my-widget>
6. <span id="titleToInject">Title injected</span>
7. <span id="partToInject">Paragraph injected</span>
8. </my-widget>
9. </body>
Look at line 2: this is where the importation of the HTML, and JS code of new
"components" is done. The HTML+JS+CSS code that defines templates,
attachment to a shadow host, CSS, and registering of new custom HTML
elements is located in myComponents.html.
You could create a my-widget.html file, add the HTML template and the
JavaScript code to that file, and import my-widget.html into your document
and use <my-widget>...</my-widget> from the last lesson directly!
External resource
This lesson is a basic introduction to HTML imports. You can add functionality
such as and callbacks, manage dependencies if you create Web components
that use other components and you import the same components more than
once, etc. This will interest advanced users who plan to create large libraries of
Web components. For creating simple Web components, this lesson explained
everything you'll need for most cases.
Article on HTML imports at HTML5Rocks
Web Workers
Introduction
In the browser, 'normal' JavaScript code is run in a single thread (a thread is a
light-weight CPU process, see this Wikipedia page for details). This means that
the browser GUI, the JavaScript, and other tasks are competing for processor
time. If you run an intensive CPU task, everything else is blocked, including the
user interface. You have no doubt observed something like this during your
Web browsing experiences:
Or maybe:
Terminology check: if the terms background and foreground and the concept
of multi-tasking are new to you, please review PC Mag's definition
of foreground and background.
an example that does not use Web Workers
This example will block the user interface unless you close the tab. Try it at
JSBin but DO NOT CLICK ON THE BUTTON unless you are prepared to kill your
browser/tab, because this routine will consume 100% of CPU time, completely
blocking the user interface:
1. <!DOCTYPE HTML>
2. <html>
3. <head>
4. <title>Worker example: One-core computation</title>
5. </head>
6. <body>
7. <button id="startButton">Click to start discovering prime
numbers</button><p> Note that this will make the page unresponsive, you
will have to close the tab in order to get back your CPU!
8. <p>The highest prime number discovered so far
is: <output id="result"></output></p>
9. <script>
10. function computePrime() {
11. var n = 1;
12. search: while (true) {
13. n += 1;
14. for (var i = 2; i <= Math.sqrt(n); i += 1)
15. if (n % i == 0)
16. continue search;
17. // found a prime!
18. document.getElementById('result').textContent = n;
19. }
20. }
21.
document.querySelector("#startButton").addEventListener('click', computePri
me);
22. </script>
23. </body>
24. </html>
Notice the infinite loop in the function computePrime (line 12, in bold). This is
guaranteed to block the user interface. If you are brave enough to click on the
button that calls the computePrime() function, you will notice that the line
18 execution (that should normally modify the DOM of the page and display the
prime number that has been found) does nothing visible. The UI is
unresponsive. This is really, really, bad JavaScript programming - and should
be avoided at all costs.
Shortly we will see a "good version" of this example that uses Web Workers.
With Web Workers, the carefully controlled communication points with other
threads mean that it's actually very hard to cause concurrency problems.
There's no access in a worker to non-thread safe components or to the DOM.
We pass specific data into and out of a thread through serialized objects. The
separate threads share different copies so the problem with the four bytes
variable, explained in the previous paragraph, cannot occur.
There are two different kinds of Web Workers described in the specification:
1.Dedicated Web Workers: threads that are dedicated to one single
page/tab. Imagine a page with a given URL that runs a Web Worker that counts
in the background 1-2-3- etc. It will be duplicated if you open the same URL in
two different tabs. So each independent thread will start counting from 1 at
startup time (when the tab/page is loaded).
2.Shared Web Workers: these are threads which may be shared between
different pages of tabs (they must conform to the same-origin policy) on the
same client/browser. These threads will be able to communicate, exchange
messages, etc. For example, a shared worker, that counts in the background 1-
2-3- etc. and communicates its current value. All the pages/tabs which share
its communication channel will display the same value! Also, if you
refresh each of those pages, they will return displaying the same value as each
other. The pages don't need to be the same (with the same URL). However,
they must conform to the "same origin" policy.
Shared Web Workers will not be studied in this course. They are not yet
supported by major browser vendors, and a proper study would require a whole
week's worth of material. We may cover this topic in a future version of this
course when implementations are more stable/available.
External resources
W3C specification about Web Workers
Using Web Workers, article in the Mozilla Developer Network
Current Support
Dedicated Web Workers
Support as at December 2015:
Up to date version of this table: http://caniuse.com/#feat=webworkers
More than one worker can be created/loaded by a parent page. This is parallel
computing after all :-)
(1) Messages can be sent by the parent page to a worker using this kind of
code:
(2) Messages (like the object message example, above) are received from a
worker using this method (code located in the JavaScript file of the worker):
(4) And the parent page can listen to messages from a worker like this:
1. = function(event){
2. // do something with event.data
3. };
3 - a complete example
The "Parent HTML page" of a simplistic example using a dedicated Web
Worker:
1. <!DOCTYPE HTML>
2. <html>
3. <head>
4. <title>Worker example: One-core computation</title>
5. </head>
6. <body>
7. <p>The most simple example of Web Workers</p>
8. <script>
9. // create a new worker (a thread that will be run in the background)
10. var worker = new Worker("worker0.js");
11. // Watch for messages from the worker
12. worker.onmessage = function(e){
13. // Do something with the message from the client: e.data
14. alert("Got message that the background work is finished...")
15. };
16. // Send a message to the worker
17. worker.postMessage("start");
18. </script>
19. </body>
20. </html>
1. onmessage = function(e){
2. if ( e.data === "start" ) {
3. // Do some computation that can last a few seconds...
4. // alert the creator of the thread that the job is finished
5. done();
6. }
7. };
8. function done(){
9. // Send back the results to the parent page
10. postMessage("done");
11. }
4 - Handling errors
The parent page can handle errors that may occur inside its workers, by
listening for event from a worker object:
1. var worker = new Worker('worker.js');
2. worker.onmessage = function (event) {
3. // do something with event.data
4. };
5. worker.onerror = function (event) {
6. console.log(event.message, event);
7. };
8. }
This is the example we tried earlier, without Web Workers, and it froze the
page. This time, we'll use a Web Worker. Now you will notice that the prime
numbers it in the background are displayed as soon as the next prime number
is found.
Try this example online (we cannot put it on JsBin as Workers need to be
defined in a separate JavaScript file) :
Arghhh! They seem to have moved the site around a bit. If the link (above)
does not work please try this instead. =dn
The HTML5 page code from this example that uses a Web Worker:
1. <!DOCTYPE HTML>
2. <html>
3. <head>
4. <title>Worker example: One-core computation</title>
5. </head>
6. <body>
7. <p>The highest prime number discovered so far
is: <output id="result"></output></p>
8. <script>
9. var worker = new Worker('worker.js');
10. worker.onmessage = function (event) {
11. document.getElementById('result').textContent = event.data;
12. };
13. </script>
14. </body>
15. </html>
In this source code:
Workers can only communicate with their parent page using messages. See
the code of the worker below to see how the message has been sent.
1. var n = 1;
2. search: while (true) {
3. n += 1;
4. for (var i = 2; i <= Math.sqrt(n); i += 1)
5. if (n % i == 0)
6. continue search;
7. // found a prime!
8. postMessage(n);
9. }
1.There is an infinite loop in the code at line 2 (while true...). This is not a
problem as it runs in the background.
2.When a prime number is found, it is posted to the creator of the Web Worker
(aka the parent HTML page), using the postMessage(...) function (line 8).
3.Computing prime numbers using such a weak algorithm is very CPU
intensive. However, the Web page is still responsive: you can refresh it and the
"script not responding" error dialog box will not appear, etc. There is a demo in
the next section of this course chapter in which some graphic animation has
been added to this example, and you can verify that the animation is not
affected by the computations in the background.
This occurs with Opera, and Firefox. With Chrome, Safari or Chromium, you
can run the browser using some command line options to override these
security constraints. Read, for example, this blog post that explains this
method in detail.
Ok, back to our improved version! This time, we test if the browser supports
Web Workers, and we also use a modified version of the worker.js code for
displaying a message and have it wait 3 seconds before starting the
computation of prime numbers.
HTML code:
1. <!DOCTYPE HTML>
2. <html>
3. <head>
4. <title>Worker example: One-core computation</title>
5. </head>
6. <body>
7. <p>The highest prime number discovered so far
is: <output id="result"></output></p>
8. <script>
9. if(window.Worker){
10. // web workers supported by the browser
11. var worker=new Worker("worker1.js");
12. worker.onmessage=function(event){
13. document.getElementById('result').textContent = event.data;
14. };
15. }else{
16. // the browser does not support web workers
17. alert("Sorry, your browser does not support Web Workers");
18. }
19. </script>
20. </body>
21. </html>
Line 9 shows how to test if the browser can run JavaScript code that uses the
HTML5 Web Workers API.
worker1.js code:
In this example, we just added a message that is sent to the "parent page"
(line 1) and we use the standard JavaScript method setTimeout() to delay the
beginning of the prime number computation by 3s.
example: how to stop/kill a worker after a given
amount of time
So far we have created and used a worker. Now we will see how to kill it!
A worker is a thread, and a thread uses resources. If you no longer need its
services, it is best practice to release the used resources, especially since some
browsers may run very badly when excessive memory consumption
occurs. Even if we the variable that was used to create the worker, the worker
itself continues to live - it does not stop! Worse: the worker continues in its
task (therefore memory and other resources are still allocated) but it becomes
inaccessible. In this situation, we cannot do anything but close the
tab/page/browser.
The Web Worker API provides a terminate() method that we can use on any
worker, to end its life. After a worker has been killed, it is not possible to undo
its termination. The only option is to create a new worker.
HTML code:
1. <!DOCTYPE HTML>
2. <html>
3. <head>
4. <title>Worker example: One-core computation</title>
5. </head>
6. <body>
7. <p>The highest prime number discovered so far
is: <output id="result"></output></p>
8. <script>
9. if(window.Worker){
10. // web workers supported by the browser
11. var worker=new Worker("worker2.js");
12. worker.onmessage=function(event){
13. document.getElementById('result').textContent = event.data;
14. };
15. }else{
16. // the browser does not support web workers
17. alert("Sorry, your browser does not support Web Workers");
18. }
19. setTimeout(function(){
20. // After 10 seconds, we kill the worker
21. worker.terminate();
22. document.body.appendChild(document.createTextNode("Worker
killed, 10 seconds elapsed !")
23. );}, 10000);
24. </script>
25. </body>
26. </html>
Notice at line 22 the call to worker.terminate(), that kills the worker after
10000ms.
A Web worker can also kill itself by calling the close() method in the worker's
JavaScript file:
worker.js:
1. importScripts('script1.js');
2. importScripts('script2.js');
3. // Other possible syntax
4. importScripts('script1.js', 'script2.js');
We borrowed these two figures from blog post (in French), as they illustrate
this very well:
Note that:
1.Chrome has already implemented a new way for transferring objects from/to
Web Workers by reference, in addition to the standard "by copy" method. This
is in the HTML 5.1 draft specification from the W3C - look for "transferable"
objects!
2.The canvas is not usable from Web Workers, however, HTML 5.1 proposes a
canvas proxy.
Debugging Web Workers
Like other multi-threaded applications, debugging Web Workers can be a tricky
task, and having a good makes this process much easier.
Chrome provides tools for debugging Web Workers: read this post for details.
When you open a page with Web Workers, open the Chrome Dev Tools (F12),
look on the right at the Workers tab, check the radio box and reload the page.
This will pop-up a small window for tracing the execution of each worker. In
these windows, you can set breakpoints, inspect variables, log messages, etc.
Here is a screenshot of a debugging session with the prime numbers example:
See this MSDB blog post for details. Load a page with Web Workers, press F12
to show the debugging tools. Here is a screenshot of a debugging session with
Web Workers in IE11:
FireFox has similar tools, see https://developer.mozilla.org/en-US/docs/Tools.
For Opera, look at Dragonfly documentation page, Opera's debugger.
DEMO 1:
A variation of the prime number example (previous lecture) which shows that
an animation in the parent page is not affected by the background computation
of prime numbers. Try it online: http://html5demos.com/worker
Move the blue square with up and down arrows, it moves smoothly. Click the
"start worker" button: this will run the code that computes prime numbers in a
Web Worker. Now, try to move the square again: the animation hasn't even
slowed down...
Demo 2
Do ray tracing using a variable number of Workers, and try it
online: http://nerget.com/rayjs-mt/rayjs.html (if you've not heard of it
before, here's an explanation that will tell you more than you will ever want to
know about ray tracing!)
In this demo, you can select the number of Web Workers which will compute
parts of the image (pixels). If you use too many Web Workers, the performance
decreases because too much time is spent exchanging data between workers
and their creator, instead of computing in parallel.
Other demos
Many impressive demos at the Mozilla Developer Network
Try them online at the MDN demo repository!
There are also many impressive demos at Chrome
Experiments
Try them!
Introduction
This section covers the HTML5 orientation API: a way to use angle measures
provided by accelerometers from mobile devices or laptops such as MacBooks.
Beware: all examples must be run on a device with an orientation sensor
and/or with an accelerometer. Furthermore, Chrome/Safari and Mozilla support
this API, but Opera mobile doesn't (yet) at the time of writing.
If it provides a "mobile device emulation mode", you can use the of a desktop
browser to fake the orientation values (see the support table below, the
columns for desktop versions of browsers are about the support for this
emulation mode).
Here is a table that shows support for this API as of December 2015:
Notice that all major browsers support the API in their mobile version.
External resources:
W3C specification
Article on html5Rocks.com about device orientation
The coordinate system and Euler angles
Transformations between the Earth coordinate frame and the device
coordinate frame uses the following system of rotations:-
Rotations use the right-hand convention, such that positive rotation around an
axis is clockwise when viewed along the positive direction of the axis. When
considering rotations, always think in terms of looking down: either at your feet
on the ground or at a device (or map) lying flat on a table. We
count clockwise rotation as apositive number, and anti-clockwise as negative.
In this case, rotations are measured in degrees.
Terminology check: Here (on the left) is a photo of my compass showing the
(upwards) counting of degrees in a clockwise direction:
On the right (click on the thumbnail to view a larger image) is a demonstration
using the compass to plot a course due East from my local out to Kawau
Island. Notice that the compass and the grid lines on the chart point to True
North. Whereas the chart's printed compass rose points to Magnetic North. It is
as well to remember that GPS-based systems refer to True North, which can
differ markedly from directions taken off magnetic compasses. If you are not
familiar with using a compass to navigate, here is an illustrated explanation
of relating compass readings to the real-world/a map.
As well as the (2D) left/right, forward/backward directions on a map (or HTML5
canvas!), we need to consider other types of movements.
Another direction that is not apparent when we assume our world is as flat as a
map, is to tilt. If you've ever ridden a you will know that it is easier to change
its direction by leaning over, than by trying to turn the handlebar. Imagine
then, tilting or twisting your device to steer your character in a motorcycle or
airplane game!
Get the different angles using the JavaScript HTML5 orientation API
Typical use
The use of this API is very straightforward:
1.Test if your browser supports the orientation API
(window.DeviceOrientationEvent is not null),
2.Define a listener for the 'deviceorientation' event as
follows: window.addEventListener('deviceorientation', callback,
false); with the callback function accepting the event object as its single
input parameter,
3.Extract the angles from the event (use its properties: alpha, beta, gamma).
(If using a mobile device, open the page in standalone mode (without the JsBin
editor) )
The above screenshot came from an iPad laying immobile on a desk.
Theoretically, all the angle values will be zero when the device is laid flat,
providing it has not been moved since the page loaded. However, depending
on the hardware, these values may change even if the device is stationary: a
very sensitive sensor might report constantly changing values. This is why, in
the example, we round the returned values with Math.round() at display time
(see code).
If we change the orientation of the device here are the results:
Typical use / code from the above example:
1. ...
2. <h2>Device Orientation with HTML5</h2>
3. You need to be on a mobile device or use a laptop with
accelerometer/orientation
4. device.
5. <p>
6. <div id="LR"></div>
7. <div id="FB"></div>
8. <div id="DIR"></div>
9. <script type="text/javascript">
10. if (window.DeviceOrientationEvent) {
11. console.log("DeviceOrientation is supported");
12. window.addEventListener('deviceorientation', function(eventData)
{
13. // gamme is for left/right inclination
14. var LR = eventData.gamma;
15. // beta is for front/back inclination
16. var FB = eventData.beta;
17. // alpha is for orientation
18. var DIR = eventData.alpha;
19. // display values on screen
20. deviceOrientationHandler(LR, FB, DIR);
21. }, false);
22. } else {
23. alert("Device orientation not supported on your device or browser.
Sorry.");
24. }
25. function deviceOrientationHandler(LR, FB, DIR) {
26. document.querySelector("#LR").innerHTML = "gamma :
" + Math.round(LR);
27. document.querySelector("#FB").innerHTML = "beta :
" + Math.round(FB);
28. document.querySelector("#DIR").innerHTML = "alpha :
" + Math.round(DIR);
29. }
30. </script>
31. ...
Another example that shows how to orient the HTML5 logo using the
orientation API + CSS3 3D rotations
This is just a variation of the previous example, try it at JsBin - if you're using a
mobile device, it'll be better in standalone mode:
Results on the iPad: the logo rotates when we change the iPad's orientation.
This is a good "visual feedback" for an orientation controlled ...
This example as a Youtube video: http://www.youtube.com/watch?
v=OrNLhOAGSdE
1. ...
2. <h2>Device Orientation with HTML5</h2>
3. You need to be on a mobile device or use a laptop with
accelerometer/orientation
4. device.
5. <p>
6. <div id="LR"></div>
7. <div id="FB"></div>
8. <div id="DIR"></div>
9. <img src="http://www.html5
10. rocks.com/en/tutorials/device/orientation/html5_logo.png" id="imgLogo"
11. class="logo">
12. <script type="text/javascript">
13. if (window.DeviceOrientationEvent) {
14. console.log("DeviceOrientation is supported");
15. window.addEventListener('deviceorientation', function(eventData) {
16. var LR = eventData.gamma;
17. var FB = eventData.beta;
18. var DIR = eventData.alpha;
19. deviceOrientationHandler(LR, FB, DIR);
20. }, false);
21. } else {
22. alert("Not supported on your device or browser. Sorry.");
23. }
24. function deviceOrientationHandler(LR, FB, DIR) {
25. // USE CSS3 rotations for rotating the HTML5 logo
26. //for webkit browser
27. document.getElementById("imgLogo").style.webkitTransform =
28. "rotate(" + LR + "deg) rotate3d(1,0,0, " + (FB * -1) + "deg)";
29. //for HTML5 standard-compliance
30. document.getElementById("imgLogo").style.transform =
31. "rotate(" + LR + "deg) rotate3d(1,0,0, " + (FB * -1) + "deg)";
32. document.querySelector("#LR").innerHTML = "gamma :
" + Math.round(LR);
33. document.querySelector("#FB").innerHTML = "beta :
" + Math.round(FB);
34. document.querySelector("#DIR").innerHTML = "alpha :
" + Math.round(DIR);
35. }
36. </script>
37. ...
This example works in Firefox, Chrome, and IOS Safari. Created by Derek
Anderson @ Media Upstream. Original source code available GitHub.
We adapted the source code so that you can tweak it in JsBin, or test it in
standalone mode (using a mobile device).
You can imagine the above example that sends the current orientation of the
device to a server using WebSockets. The server in turn updates the logo and
position on a PC screen. If multiple devices connect, they can chat together
and take control of the 3D Logo.
This video shows one of the above examples slightly modified: the JavaScript
code running in the Web page on the iPad sends in real time the device
orientation using the Web Sockets API to a server that in turns the orientation
to a client running on a desktop browser. In this way the tablet "controls" the
HTML5 logo that is shown on the desktop browser:
Introduction
This section presents the Device Motion API which is used in a similar manner
to the device orientation API discussed earlier.
Basic Usage
1. function handleMotionEvent(event) {
2. var x = event.accelerationIncludingGravity.x;
3. var y = event.accelerationIncludingGravity.y;
4. var z = event.accelerationIncludingGravity.z;
5. // Process ...
6. }
7. window.addEventListener("devicemotion", handleMotionEvent, true);
Why are there two different values? Because some devices have the capability
of excluding the effects of gravity, eg if equipped with a gyroscope.
Indeed there is acceleration due implicitly to gravity, see also this: Acceleration
of Gravity on Earth...
So, the device motion event is a superset of the device orientation event; it
returns rotation as well as accelerationinformation from the device.
Common steps
The principles same as for the orientation API:
1.Test if the API is supported by the browser,
2.Add a listener for '' events,
3.Get the acceleration values from the DOM event that has been passed to the
listener,
4.Process the data.
Common processing with acceleration values
Test the value of the acceleration.z property: If > 0 then the device is facing
up, otherwise it is facing down. This would be useful if you wanted to
play heads or tails with your phone ;-)
1. // For example, if acceleration.z is > 0 then the phone is facing up
2. var facingUp = -1;
3. if (acceleration.z > 0) {
4. facingUp = +1;
5. }
Compute the angle corresponding to the Left / Right and Front / Back tilts. This
example
was taken from http://www.html5rocks.com/en/tutorials/device/orientation and
uses the accelerationIncludingGravity property of the event.
1. function deviceMotionHandler(eventData) {
2. // Grab the acceleration including gravity from the results
3. var acceleration = eventData.accelerationIncludingGravity;
4. // Convert the value from acceleration to degrees
5. // acceleration.x|y is the acceleration according
6. // to gravity, we'll assume we're on Earth and divide
7. // by 9.81 (earth gravity) to get a percentage value,
8. // and then multiply that by 90 to convert to degrees.
9. var tiltLR = Math.round(((acceleration.x) / 9.81) * -90);
10. var tiltFB = Math.round(((acceleration.y + 9.81) / 9.81) * 90 * facin
gUp);
11. // ... do something
12. }
Compute the vertical (direction of the sky) - this extract comes from a
complete example further down this page...
1. ...
2. var angle = Math.atan2(accel.y,accel.x);
3. var canvas = document.getElementById('myCanvas');
4. var ctx = canvas.getContext('2d');
5.
6. ctx.moveTo(50,50);
7. // Draw sky direction in the canvas
8. ctx.lineTo(50-50*Math.cos(angle),50+50*Math.sin(angle));
9. ctx.stroke();
Use acceleration values to move a ball on the screen of a tablet when the
tablet is tilted front / back or left / right (complete example later on)...
1. ...
2. ball.x += acceleration.x;
3. ball.y += acceleration.y;
4. ...
Complete examples
Move the HTML5 logo
example at JsBin. If using a mobile device use this URL.
Code from this example:
1. <!doctype html>
2. <html>
3. <head></head>
4. <body>
5. <h2>Device Orientation with HTML5</h2>
6. You need to be on a mobile device or use a laptop with
accelerometer/orientation
7. device.
8. <p>
9. <div id="rawAccel"></div>
10. <div id="tiltFB"></div>
11. <div id="tiltLR"></div>
12. <div id="upDown"></div>
13. <imgsrc="http://www.html5rocks.com/en/tutorials/device/
orientation/html5_logo.png"id="imgLogo" class="logo">
14. <script type="text/javascript">
15. if (window.DeviceMotionEvent != undefined) {
16. console.log("DeviceMotion is supported");
17. window.addEventListener('devicemotion', function(eventData) {
18. // Grab the acceleration including gravity from the results
19. var acceleration = eventData.accelerationIncludingGravity;
20. // Display the raw acceleration data
21. var rawAcceleration = "[" + Math.round(acceleration.x) + ",
" +Math.round(acceleration.y)
22. + ", " + Math.round(acceleration.z) + "]";
23. // Z is the acceleration in the Z axis, and if the device
24. // is facing up or down
25. var facingUp = -1;
26. if (acceleration.z > 0) {
27. facingUp = +1;
28. }
29. // Convert the value from acceleration to degrees
30. // acceleration.x|y is the acceleration according to gravity,
31. // we'll assume we're on Earth and divide
32. // by 9.81 (earth gravity) to get a percentage value,
33. // and then multiply that by 90 to convert to degrees.
34. var tiltLR = Math.round(((acceleration.x) / 9.81) * -90);
35. var tiltFB = Math.round(((acceleration.y + 9.81) / 9.81) * 90 *
facingUp);
36. document.querySelector("#rawAccel").innerHTML =
37. "Raw acceleration" + rawAcceleration;
38. document.querySelector("#tiltFB").innerHTML =
39. "Tilt front/back : " + tiltFB;
40. document.querySelector("#tiltLR").innerHTML =
41. "Tilt left/right : " + tiltLR;
42. document.querySelector("#upDown").innerHTML =
43. "Face Up:Down : " + facingUp;
44. updateLogoOrientation(tiltLR, tiltFB);
45. }, false);
46. } else {
47. alert("Not supported on your device or browser. Sorry.");
48. }
49. function updateLogoOrientation(tiltLR, tiltFB) {
50. // USE CSS3 rotations for rotating the HTML5 logo
51. //for webkit browser
52. document.getElementById("imgLogo").style.webkitTransform =
53. "rotate(" + tiltLR + "deg) rotate3d(1,0,0, " + (tiltFB * -
1) +"deg)";
54. //for HTML5 standard-compliance
55. document.getElementById("imgLogo").style.transform =
56. "rotate(" + tiltLR + "deg) rotate3d(1,0,0, " + (tiltFB * -
1) +"deg)";
57. }
58. </script>
59. </body>
60. </html>
This example has been adapted and put on jsbin.com so that you can tweak
it: http://jsbin.com/uyuqek/4/edit
1. <html>
2. <head>
3. <meta http-equiv="content-type" content="text/html; charset=utf-8">
4. <meta name="viewport" content="user-scalable=no, width=device-
width" />
5. <link rel="stylesheet"
6. href="http://code.jquery.com/mobile/1.0b2/jquery.mobile-
1.0b2.min.css" />
7. <script type="text/javascript"
8. src = "http://code.jquery.com/jquery-1.6.2.min.js">
9. </script>
10. <script type="text/javascript"
11. src = "http://code.jquery.com/mobile/1.0b2/jquery.mobile-
1.0b2.min.js">
12. </script>
13. <script type="text/javascript">
14. $(document).ready(function(){
15. window.addEventListener("devicemotion",onDeviceMotion,false);
16. });
17. function onDeviceMotion(event){
18. var ctx = document.getElementById("c").getContext("2d");
19. var accel = event.accelerationIncludingGravity;
20. $("#sliderX").val(Math.round(accel.x)).slider("refresh");
21. $("#sliderY").val(Math.round(accel.y)).slider("refresh");
22. $("#sliderZ").val(Math.round(accel.z)).slider("refresh");
23. // sky direction
24. var angle = Math.atan2(accel.y,accel.x)
25. ctx.clearRect(0,0,100,100);
26. ctx.beginPath();
27. ctx.arc(50,50,5,0,2*Math.PI,false);
28. ctx.moveTo(50,50);
29. // Draw sky direction
30. ctx.lineTo(50-50*Math.cos(angle),50+50*Math.sin(angle));
31. ctx.stroke();
32. }
33. </script>
34. </head>
35. <body>
36. <div data-role="page" id = "intropage">
37. <div data-role="header">
38. <h1>Accelerometer</h1>
39. </div>
40. <div data-role="content">
41. <label for="sliderX">X Acceleration (Roll)</label>
42. <input type="range" name="sliderX" id="sliderX"
43. value="0" min="-10" max="10" data-theme="a" />
44. <label for="sliderY">Y Acceleration (Pitch)</label>
45. <input type="range" name="sliderY" id="sliderY"
46. value="0" min="-10" max="10" data-theme="b" />
47. <label for="sliderZ">Z Acceleration (<strike>Yaw</strike>
48. Face up/down)
49. </label>
50. <input type="range" name="sliderZ" id="sliderZ"
51. value="0" min="-10" max="10" data-theme="c" />
52. </div>
53. <p style = "text-align:center">SKY direction:
54. follow this line:</p>
55. <div style = "text-align:center;margin-top:10px;">
56. <canvas id="c" width="100" height="100"></canvas>
57. </div>
58. </div>
59. </body>
60. </html>
Try this example at JsBin. If using a mobile device, use this URL instead!
External resources:
From the W3C specification: http://dev.w3.org/geo/api/spec-source-
orientation.html#devicemotion
Article on html5Rocks.com about device orientation
Article on dev.opera.com about device motion and orientation