Table of Contents
This article is inspired by Google's Flutter and Qt. What these 2 frameworks have in common is that their user interface is implemented independently of native UI controls, so the requirement to reconcile the UI idiosyncrasies of each underlying platform is eliminated. The precursor to having a cross platform UI library, is a graphics library which C++ Standard Library, regrettably, does not have, at this moment.
I have implemented 90% (47 out of 52) of the HTML 5 Canvas APIs in C++. This article would more aptly be named "Bring HTML 5 Canvas to C++" in that sense. There are 2 versions of the C++ class, one for the web and other for the desktop. The web version is implemented with Emscripten and the desktop version with Cairo. At this point, 99.999999999999% readers close their browser window at the mention of Cairo. The C++ graphics library proposal to include Cairo is met with vitriol and strong resistance. Well, whether that concern is well-found or not, I am not here to defend Cairo. There is nothing to prevent anyone from reimplementing this library with a hardware-accelerated technology like OpenGL, Vulkan or Direct2D/Direct3D because the implementation detail is not exposed.
Back in 90s, when I was a teenager, I made some cool graphics demo and wanted to share with my friends. But for them to run my demo, they have to first install the language runtime which they are not keen to do. Imagine you have a language where you can recompile to the web and share the HTML, instead of the executable, with your friends. It is like writing 2 programs for the price of 1! That's what today's technology like Emscripten empowers developers to do. Even Adobe is porting Photoshop to the web. Hopefully, these web-enabling technologies take off for C++.
These are the Cairo tutorial and HTML Canvas reference material I used when writing the library. Prior to this, I did not have any knowledge of Cairo or HTML Canvas.
Compiling for the Web
To compile the C++ code for the web, you need to install Emscripten, refer to my tutorial on how to do it. This is the commandline to do it. -s WASM=0
tells the Emscripten compiler to output an asm.js file. You can remove it. The default is to generate Webassembly file but my Visual Studio IIS Express is acting up on me again, it cannot serve wasm file, so I resort to asm.js again.
emcc -std=c++11 -s WASM=0 CanvasExample.cpp -o ../WebApplication1/cpp.js
Only 1 example cpp file needs to be built because the Canvas
class is header only. Depending on if the __EMSCRIPTEN__
macro is defined, we include the respective header. Strictly speaking, these are not header only, since we need the Emscripten for the web version and, Cairo and STB Image library for the desktop version.
#ifdef __EMSCRIPTEN__
#include "JsCanvas.h"
#else
#include "CppCanvas.h"
#endif
Compiling on Visual C++
To compile the code with Visual C++, you need to install Microsoft Vcpkg in order to get the Cairo and STB Image library. Cairo only supports loading PNG, this is the reason we need the STB image library to load other image formats like JPEG. The Vcpkg
command to install and build these libraries in 32-bit is below.
.\vcpkg install cairo stb
In order to write the Canvas
class for the web, I use EM_ASM_
extensively to interact with JavaScript. We can pass the parameters and receive them as placeholders in the form of $0
, $1
and so on, inside the JavaScript. For it to convert the placeholder into string
type, we have to call UTF8ToString()
which is provided by preamble.js. The code below displays an HTML message box with the text: "I received: Hi5
".
EM_ASM_({
alert('I received: ' + UTF8ToString($0) + $1);
}, "Hi", 5);
The below code demonstrates how EM_ASM_
is used to implement lineTo
in JsCanvas.h. We have to pass in the canvas
object name to retrieve it from a global dictionary, so there is a slight overhead in every function call. This name is stored in the m_Name
member of the JS Canvas
class. Desktop version of the same class has no need for such information. This function delegates to the HTML Canvas
for the grunt of work.
void lineTo(double x, double y)
{
EM_ASM_({
var ctx = get_canvas(UTF8ToString($0));
ctx.lineTo($1, $2);
}, m_Name.c_str(), x, y);
}
For desktop's lineTo()
, it forwards the call to the Cairo
function, cairo_line_to()
. cr
is Cairo
object which is an instance member of desktop's Canvas
.
void lineTo(double x, double y)
{
cairo_line_to(cr, x, y);
}
To try out the pure JavaScript examples that follow in later sections, you can use this HTML (saved as "pure_js.html" in WebApplication1 folder).
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Canvas App</title>
<script src="modernizr-canvas.js"></script>
<script type="text/javascript">
window.addEventListener("load", eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasApp() {
if (!Modernizr.canvas) {
return;
}
}
</script>
</head>
<body>
<div style="position: absolute; top: 0px; left: 0px;">
<canvas id="canvas" width="320" height="280">
Your browser does not support HTML5 Canvas. </canvas>
</div>
<div style="display: none;"><img src="yes.jpg" id="yes_image"></div>
</body>
</html>
To try out the C++ examples that follow in later sections, you can use this HTML (saved as "cpp.html" in WebApplication1 folder). You need to include "preamble.js" for the UTF8ToString()
and "jscanvas.js" for the global dictionary of Canvas
objects and lastly, the examples are in "cpp.js"(asm.js file).
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>C++ Canvas App</title>
</head>
<body>
<div style="position: absolute; top: 0px; left: 0px;">
<canvas id="canvas" width="320" height="280">
Your browser does not support HTML5 Canvas. </canvas>
</div>
<div style="display: none;"><img src="yes.jpg" id="yes_image"></div>
<script async type="text/javascript" src="preamble.js"></script>
<script async type="text/javascript" src="jscanvas.js"></script>
<script async type="text/javascript" src="cpp.js"></script>
</body>
</html>
Now we are ready to see some examples!
In all the examples that follow, the pure JavaScript version is shown before the C++ version. In the JavaScript below, we construct a path which is a line. We set the 2 properties, lineWidth
and lineCap
to be 10
and round
respectively. beginPath
, moveTo
and lineTo
are the path
functions. stroke()
is called to draw the path
which is a line with round cap.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
ctx.beginPath();
ctx.lineWidth = 10.0;
ctx.lineCap = "round";
ctx.moveTo(20, 20);
ctx.lineTo(200, 20);
ctx.stroke();
In C++ version, we construct the Canvas
object by giving it a name and dimensions. The name is only used in the web version to retrieve it from global dictionary while the dimensions are used in the desktop version and ignored in the web version since the Canvas
dimensions are already specified inside the HTML. I only implemented the accessor for the simple property like lineWidth
because Emscripten can only return basic types like integer or double
. You can see how similar to the pure JavaScript version. savePng()
is to save the image on the desktop so that we can view it. savePng()
does nothing on the web version. If your application needs to display the Canvas
without going through the hard disk storage, getImageData()
can be called to get the pixel data in ARGB color format.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
ctx.beginPath();
ctx.lineWidth = 10.0;
ctx.lineCap = LineCap::round;
ctx.moveTo(20, 20);
ctx.lineTo(200, 20);
ctx.stroke();
ctx.savePng("c:\\temp\\drawLine.png");
Next, we draw a quadratic curve in JavaScript.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.quadraticCurveTo(20, 100, 200, 20);
ctx.stroke();
On the desktop version, quadraticCurveTo
is implemented by me since Cairo did not have this function.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.quadraticCurveTo(20, 100, 200, 20);
ctx.stroke();
ctx.savePng("c:\\temp\\drawQuadraticCurve.png");
Next, we'll draw a Bezier Curve.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 100, 200, 100, 200, 20);
ctx.stroke();
The C++ version is identical.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.bezierCurveTo(20, 100, 200, 100, 200, 20);
ctx.stroke();
ctx.savePng("c:\\temp\\drawBezier.png");
Next example, we display text in single color and gradient mode.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
ctx.font = "20px Georgia";
ctx.fillText("Hello World!", 10, 50);
ctx.font = "30px Verdana";
var gradient = ctx.createLinearGradient(0, 0, 320, 0);
gradient.addColorStop(0.0,"magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
ctx.fillStyle = gradient;
ctx.fillText("Big smile!", 10, 90);
In the web version of the Canvas
class, the gradient is stored in the global dictionary as well. This is why the name is needed in the createLinearGradient
. The C++ Canvas
class can recognize color names like magenta
and blue
as of version 0.4.0.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
ctx.font = "20px Georgia";
ctx.fillText("Hello World!", 10, 50);
ctx.font = "30px Verdana";
auto gradient = ctx.createLinearGradient("gradient", 0, 0, 320, 0);
gradient.addColorStop(0.0, "magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
ctx.fillStyle = gradient;
ctx.fillText("Big smile!", 10, 90);
ctx.savePng("c:\\temp\\displayText.png");
For the C++ version, fromRGB
is provided for convenience to specify a color value.
gradient.addColorStop(0.0, fromRGB(0xff, 0, 0xff));
gradient.addColorStop(0.5, fromRGB(0, 0, 0xff));
gradient.addColorStop(1.0, fromRGB(0xff, 0, 0));
You can also bypass the fromRGB
to state hexadecimal numbers directly if you so please.
gradient.addColorStop(0.0, 0xff00ff));
gradient.addColorStop(0.5, 0x0000ff));
gradient.addColorStop(1.0, 0xff0000));
Next example, we display text in single color and gradient mode. This is simply accomplished by replacing the fillStyle
and fillText
in the above JavaScript code segment with strokeStyle
and strokeText
respectively.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
ctx.font = "20px Georgia";
ctx.lineWidth = 1.0;
ctx.strokeText("Hello World!", 10, 50);
ctx.font = "30px Verdana";
var gradient = ctx.createLinearGradient(0, 0, 320, 0);
gradient.addColorStop(0.0,"magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
ctx.strokeStyle = gradient;
ctx.strokeText("Big smile!", 10, 90);
The fillStyle
and fillText
in the above C++ code segment are likewise replaced with strokeStyle
and strokeText
respectively. Otherwise, they are identical.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
ctx.font = "20px Georgia";
ctx.lineWidth = 1.0;
ctx.strokeText("Hello World!", 10, 50);
ctx.font = "30px Verdana";
auto gradient = ctx.createLinearGradient("gradient", 0, 0, 320, 0);
gradient.addColorStop(0.0, "magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
ctx.strokeStyle = gradient;
ctx.strokeText("Big smile!", 10, 90);
ctx.savePng("c:\\temp\\displayTextOutline.png");
Next, we draw a rotate
rectangle in 20 degrees in JavaScript. The 20 degrees are converted to radians.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
ctx.rotate(20 * Math.PI / 180);
ctx.fillRect(50, 20, 100, 50);
The C++ version is identical, except we have to define PI
for calculating the radians.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
double PI = 3.14159265359;
ctx.rotate(20 * PI / 180);
ctx.fillRect(50, 20, 100, 50);
ctx.savePng("c:\\temp\\rotateRect.png");
In the next pure JavaScript example, we'll display an image from img
object ID with drawImage()
.
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");
var img = document.getElementById("yes_image");
ctx.drawImage(img, 10, 10);
This is the HTML img
element with yes_image
ID.
<div style="display: none;"><img src="yes.jpg" id="yes_image"></div>
In the C++ version, we have to detect Emscripten mode. If it is, we specify the img
object ID, else it is the image filename. Desktop version of drawImage()
is implemented with help of STB Image library.
using namespace canvas;
Canvas ctx("canvas", 320, 280);
#ifdef __EMSCRIPTEN__
ctx.drawImage("yes_image", 10.0, 10.0);
#else
ctx.drawImage("C:\\Users\\shaov\\Pictures\\yes.jpg", 10.0, 10.0);
#endif
ctx.savePng("c:\\temp\\displayImage.png");
Colors, Styles, and Shadows
fillStyle
strokeStyle
shadowColor
shadowBlur
shadowOffsetX
shadowOffsetY
createLinearGradient()
createPattern()
createRadialGradient()
addColorStop()
Line Styles
lineCap
lineJoin
lineWidth
miterLimit
Rectangles
rect()
fillRect()
strokeRect()
clearRect()
Paths
fill()
stroke()
beginPath()
moveTo()
closePath()
lineTo()
clip()
quadraticCurveTo()
bezierCurveTo()
arc()
arcTo()
isPointInPath()
Transformations
scale()
rotate()
translate()
transform()
setTransform()
Text
font
textAlign
textBaseline
fillText()
strokeText()
measureText()
Image Drawing
drawImage()
Pixel Manipulation
width
height
data
createImageData()
getImageData()
putImageData()
Compositing
globalAlpha
globalCompositeOperation
Other
save()
restore()
The code is hosted on Github. I hope you like my article.
History
- 31st July, 2019: Initial version
Other Articles in the Bring Your... Series