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