Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++11

Bring C++ Graphics to the Web

4.87/5 (11 votes)
30 Jul 2019CPOL7 min read 30.1K   590  
Run your C++ Graphics on the Desktop and Web

Table of Contents

Introduction

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++.

Requirements

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.

C++
#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.

C++
.\vcpkg install cairo stb

Implementation Details

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".

C++
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.

C++
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.

C++
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).

HTML
<!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;
            }
            // add your canvas code here!
        }
    </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).

HTML
<!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!

Draw Line

Draw Line demo

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.

JavaScript
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.

C++
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");

Draw Quadratic Curve

Draw Quadratic Curve

Next, we draw a quadratic curve in JavaScript.

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.

C++
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");

Draw Bezier Curve

Draw Bezier demo

Next, we'll draw a Bezier Curve.

JavaScript
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.

C++
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");

Display Text

Display Text demo

Next example, we display text in single color and gradient mode.

JavaScript
var theCanvas = document.getElementById("canvas");
var ctx = theCanvas.getContext("2d");

ctx.font = "20px Georgia";
ctx.fillText("Hello World!", 10, 50);

ctx.font = "30px Verdana";

// Create gradient
var gradient = ctx.createLinearGradient(0, 0, 320, 0);
gradient.addColorStop(0.0,"magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
// Fill with gradient
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.

C++
using namespace canvas;

Canvas ctx("canvas", 320, 280);

ctx.font = "20px Georgia";
ctx.fillText("Hello World!", 10, 50);

ctx.font = "30px Verdana";

// Create gradient
auto gradient = ctx.createLinearGradient("gradient", 0, 0, 320, 0);
gradient.addColorStop(0.0, "magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
// Fill with gradient
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.

C++
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.

C++
gradient.addColorStop(0.0, 0xff00ff));
gradient.addColorStop(0.5, 0x0000ff));
gradient.addColorStop(1.0, 0xff0000));

Display Text Outline

Display Text Outline demo

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.

JavaScript
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";

// Create gradient
var gradient = ctx.createLinearGradient(0, 0, 320, 0);
gradient.addColorStop(0.0,"magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");
// Fill with gradient
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.

C++
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";

// Create gradient
auto gradient = ctx.createLinearGradient("gradient", 0, 0, 320, 0);
gradient.addColorStop(0.0, "magenta");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1.0, "red");

// Fill with gradient
ctx.strokeStyle = gradient;
ctx.strokeText("Big smile!", 10, 90);

ctx.savePng("c:\\temp\\displayTextOutline.png");

Rotate Rectangle

Rotate Rectangle

Next, we draw a rotate rectangle in 20 degrees in JavaScript. The 20 degrees are converted to radians.

JavaScript
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.

C++
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");

Display Image

Display Image demo

In the next pure JavaScript example, we'll display an image from img object ID with drawImage().

JavaScript
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.

HTML
<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.

C++
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");

List of Canvas APIs implemented in C++ Canvas with Cairo in v0.4.0

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)