Adding a canvas to the Emscripten template
In an earlier part of this chapter, we discussed making calls to the Emscripten WebAssembly app from a shell template. Now that you know how to make the interaction work between JavaScript and WebAssembly, we can add a canvas element back into the template and start to manipulate that canvas using the WebAssembly module. We are going to create a new .c file that will call a JavaScript function passing it an x and y coordinate. The JavaScript function will manipulate a spaceship image, moving it around the canvas. We will also create a brand new shell file called canvas_shell.html.
As we did for the previous version of our shell, we will start by breaking this file down into four sections to discuss it at a high level. We will then discuss the essential parts of this file a piece at a time.
- The beginning of the HTML file starts with the opening HTML tag and the head element:
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Canvas Shell</title>
<link href="canvas.css" rel="stylesheet" type="text/css">
</head>
- After that, we have the opening body tag, and we have removed many of the HTML elements that we had in the earlier version of this file:
<body>
<canvas id="canvas" width="800" height="600" oncontextmenu="event.preventDefault()"></canvas>
<textarea class="em_textarea" id="output" rows="8"></textarea>
<img src="spaceship.png" id="spaceship">
- Next, there is the opening script tag, a few global JavaScript variables, and a few new functions that we added:
<script type='text/javascript'>
var img = null;
var canvas = null;
var ctx = null;
function ShipPosition( ship_x, ship_y ) {
if( img == null ) {
return;
}
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 800, 600);
ctx.save();
ctx.translate(ship_x, ship_y);
ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.restore();
}
function ModuleLoaded() {
img = document.getElementById('spaceship');
canvas = document.getElementById('canvas');
ctx = canvas.getContext("2d");
}
- After the new JavaScript functions, we have the new definition of the Module object:
var Module = {
preRun: [],
postRun: [ModuleLoaded],
print: (function() {
var element = document.getElementById('output');
if (element) element.value = ''; // clear browser cache
return function(text) {
if (arguments.length > 1) text =
Array.prototype.slice.call(arguments).join(' ');
// uncomment block below if you want to write
to an html element
/*
text = text.replace(/&/g, "&");
text = text.replace(/</g, "<");
text = text.replace(/>/g, ">");
text = text.replace('\n', '<br>', 'g');
*/
console.log(text);
if (element) {
element.value += text + "\n";
element.scrollTop = element.scrollHeight;
// focus on bottom
}
};
})(),
printErr: function(text) {
if (arguments.length > 1) text =
Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
canvas: (function() {
var canvas = document.getElementById('canvas');
canvas.addEventListener("webglcontextlost",
function(e) {
alert('WebGL context lost. You will need to
reload the page.');
e.preventDefault(); },
false);
return canvas;
})(),
setStatus: function(text) {
if (!Module.setStatus.last) Module.setStatus.last =
{ time: Date.now(), text: '' };
if (text === Module.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+
(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
// if this is a progress update, skip it if too
soon
if (m && now - Module.setStatus.last.time < 30)
return;
Module.setStatus.last.time = now;
Module.setStatus.last.text = text;
if (m) {
text = m[1];
}
console.log("status: " + text);
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies =
Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' +
(this.totalDependencies-left) +
'/' + this.totalDependencies + ')' : 'All
downloads complete.');
}
};
Module.setStatus('Downloading...');
window.onerror = function() {
Module.setStatus('Exception thrown, see JavaScript
console');
Module.setStatus = function(text) {
if (text) Module.printErr('[post-exception status]
' + text);
};
};
The last few lines close out our tags and include the {{{ SCRIPT }}} Emscripten tag:
</script>
{{{ SCRIPT }}}
</body>
</html>
Those previous four blocks of code define our new canvas_shell.html file. If you would like to download the file, you can find it on GitHub at the following address: https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly/blob/master/Chapter02/canvas.html.
Now that we have looked at the code at a high level, we can look at the source in more detail. In the head section of the HTML, we are changing the title and the name of the CSS file that we are linking. Here is the change in the HTML head:
<title>Canvas Shell</title>
<link href="canvas.css" rel="stylesheet" type="text/css">
We do not need most of the elements that were in the previous <body> tag. We need a canvas, which we had removed from the shell_minimal.html file provided by Emscripten, but now we need to add it back in. We are keeping the textarea that was initially in the minimal shell, and we are adding a new img tag that has a spaceship image taken from a TypeScript canvas tutorial on the embed.com website at https://www.embed.com/typescript-games/draw-image.html. Here are the new HTML tags in the body element:
<canvas id="canvas" width="800" height="600" oncontextmenu="event.preventDefault()"></canvas>
<textarea class="em_textarea" id="output" rows="8"></textarea>
<img src="spaceship.png" id="spaceship">
Finally, we need to change the JavaScript code. The first thing we are going to do is add three variables at the beginning to hold a reference to the canvas element, the canvas context, and the new spaceship img element:
var img = null;
var canvas = null;
var ctx = null;
The next thing we are adding to the JavaScript is a function that renders the spaceship image to the canvas at a given x and y coordinate:
function ShipPosition( ship_x, ship_y ) {
if( img == null ) {
return;
}
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 800, 600);
ctx.save();
ctx.translate(ship_x, ship_y);
ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.restore();
}
This function first checks to see whether the img variable is a value other than null. That will let us know if the module has been loaded or not because the img variable starts set to null. The next thing we do is clear the canvas with the color black using the ctx.fillStyle = “black” line to set the context fill style to the color black, before calling ctx.fillRect to draw a rectangle that fills the entire canvas with a black rectangle. The next four lines save off the canvas context, translate the context position to the ship's x and y coordinate value, and then draw the ship image to the canvas. The last one of these four lines performs a context restore to set our translation back to (0,0) where it started.
After defining this function, the WebAssembly module can call it. We need to set up some initialization code to initialize those three variables when the module is loaded. Here is that code:
function ModuleLoaded() {
img = document.getElementById('spaceship');
canvas = document.getElementById('canvas');
ctx = canvas.getContext("2d");
}
var Module = {
preRun: [],
postRun: [ModuleLoaded],
The ModuleLoaded function uses getElementById to set img and canvas to the spaceship and canvas HTML elements, respectively. We will then call canvas.getContext(”2d”) to get the 2D canvas context and set the ctx variable to that context. All of this gets called when the Module object finishes loading because we added the ModuleLoaded function to the postRun array.
We have also added back the canvas function that was on the Module object in the minimum shell file, which we had removed along with the canvas in an earlier tutorial. That code watches the canvas context and alerts the user if that context is lost. Eventually, we will want this code to fix the problem, but, for now, it is good to know when it happens. Here is that code:
canvas: (function() {
var canvas = document.getElementById('canvas');
// As a default initial behavior, pop up an alert when webgl
context is lost. To make your
// application robust, you may want to override this behavior
before shipping!
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvas.addEventListener("webglcontextlost", function(e) {
alert('WebGL context lost. You will need to reload the page.');
e.preventDefault(); }, false);
return canvas;
})(),
To go along with this new HTML shell file, we have created a new canvas.c file to compile into a WebAssembly module. Be aware that, in the long run, we will be doing a lot less in our JavaScript and a lot more inside our WebAssembly C/C++ code. Here is the new canvas.c file:
#include <emscripten.h>
#include <stdlib.h>
#include <stdio.h>
int ship_x = 0;
int ship_y = 0;
void MoveShip() {
ship_x += 2;
ship_y++;
if( ship_x >= 800 ) {
ship_x = -128;
}
if( ship_y >= 600 ) {
ship_y = -128;
}
EM_ASM( ShipPosition($0, $1), ship_x, ship_y );
}
int main() {
printf("Begin main\n");
emscripten_set_main_loop(MoveShip, 0, 0);
return 1;
}
To start, we create a ship_x and ship_y variable to track the ship's x and y coordinates. After that, we create a MoveShip function. This function increments the ship's x position by 2 and the ship's y position by 1 each time it is called. It also checks to see whether the ship's x coordinates have left the canvas on the right side, which moves it back to the left side if it has, and does something similar if the ship has moved off the canvas on the bottom. The last thing this function does is call our JavaScript ShipPosition function, passing it the ship's x and y coordinates. That final step is what will draw our spaceship to the new coordinates on the HTML5 canvas element.
In the new version of our main function, we have the following line:
emscripten_set_main_loop(MoveShip, 0, 0);
This line turns the function passed in as the first parameter into a game loop. We will go into more detail about how emscripten_set_main_loop works in a later chapter, but for the moment, know that this causes the MoveShip function to be called every time a new frame is rendered to our canvas.
Finally, we will create a new canvas.css file that keeps the code for the body and #output CSS and adds a new #canvas CSS class. Here are the contents of the canvas.css file:
body {
margin-top: 20px;
}
#output {
background-color: darkslategray;
color: white;
font-size: 16px;
padding: 10px;
margin-left: auto;
margin-right: auto;
display: block;
width: 60%;
}
#canvas {
width: 800px;
height: 600px;
margin-left: auto;
margin-right: auto;
display: block;
}
After everything is complete, we will use emcc to compile the new canvas.html file as well as canvas.wasm and the canvas.js glue code. Here is what the call to emcc will look like:
emcc canvas.c -o canvas.html --shell-file canvas_shell.html
Immediately after emcc, we pass in the name of the .c file, canvas.c, which will be used to compile our WASM module. The -o flag tells our compiler that the next argument will be the output. Using an output file with a .html extension tells emcc to compile the WASM, JavaScript, and HTML files. The next flag passed in is --shell-file, which tells emcc that the argument to follow is the name of the HTML shell file, which will be used to create the HTML file of our final output.
The following is a screenshot of canvas.html: