65.9K
CodeProject 正在变化。 阅读更多。
Home

将 C++ 图形带到 Web

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (11投票s)

2019 年 7 月 26 日

CPOL

7分钟阅读

viewsIcon

30831

downloadIcon

626

在桌面和 Web 上运行您的 C++ 图形。

目录

引言

本文灵感来源于 Google 的 FlutterQt。这两个框架的共同点是它们的用户界面都是独立于原生 UI 控件实现的,因此消除了协调每个底层平台 UI 特性差异的需求。要拥有一个跨平台 UI 库,其前身是一个图形库,而 C++ 标准库目前遗憾地没有提供。

我用 C++ 实现了 90%(52 个中的 47 个)的 HTML 5 Canvas API。从这个意义上讲,本文更恰当地应该命名为“将 HTML 5 Canvas 带到 C++”。C++ 类有两个版本,一个用于 Web,另一个用于桌面。Web 版本使用 Emscripten 实现,桌面版本使用 Cairo 实现。到目前为止,99.999999999999% 的读者在提到 Cairo 时都会关闭浏览器窗口。将 Cairo 纳入 C++ 图形库的提案遭到了严厉的批评和强烈抵制。好吧,无论这种担忧是否有道理,我在这里都不是为了替 Cairo 辩护。没有什么能阻止任何人用 OpenGL、Vulkan 或 Direct2D/Direct3D 等硬件加速技术重新实现这个库,因为实现细节并没有暴露出来。

早在 90 年代,当我还是个青少年时,我制作了一些很酷的图形演示,并想与朋友分享。但他们要运行我的演示,他们首先必须安装语言运行时,而他们并不乐意这样做。想象一下,你有一种语言,可以重新编译到 Web 并与朋友分享 HTML,而不是可执行文件。这就像花一份钱写了两个程序一样!这就是 Emscripten 等当今技术赋能开发人员所做的。甚至 Adobe 也在将 Photoshop 移植到 Web。希望这些 Web 启用技术能为 C++ 带来成功。

要求

这些是我在编写库时使用的 Cairo 教程和 HTML Canvas 参考资料。在此之前,我对 Cairo 或 HTML Canvas 一无所知。

为 Web 编译

要为 Web 编译 C++ 代码,您需要安装 Emscripten,请参阅我的教程了解如何操作。这是执行此操作的命令行。-s WASM=0 告诉 Emscripten 编译器输出一个 asm.js 文件。您可以将其删除。默认是生成 Webassembly 文件,但我的 Visual Studio IIS Express 又出问题了,它无法提供 wasm 文件,所以我又求助于 asm.js

emcc -std=c++11 -s WASM=0 CanvasExample.cpp -o ../WebApplication1/cpp.js

只需构建一个示例 cpp 文件,因为 Canvas 类是纯头文件。根据是否定义了 __EMSCRIPTEN__ 宏,我们包含相应的头文件。严格来说,它们不是纯头文件,因为 Web 版本需要 Emscripten,桌面版本需要 Cairo 和 STB Image 库。

#ifdef __EMSCRIPTEN__
    #include "JsCanvas.h"
#else
    #include "CppCanvas.h"
#endif

在 Visual C++ 上编译

要使用 Visual C++ 编译代码,您需要安装 Microsoft Vcpkg 才能获取 Cairo 和 STB Image 库。Cairo 只支持加载 PNG,这就是我们需要 STB Image 库来加载 JPEG 等其他图像格式的原因。以下是在 32 位中安装和构建这些库的 Vcpkg 命令。

.\vcpkg install cairo stb

实现细节

为了为 Web 编写 Canvas 类,我广泛使用了 EM_ASM_ 与 JavaScript 进行交互。我们可以将参数传递并以 $0$1 等形式作为占位符在 JavaScript 内部接收它们。为了将占位符转换为 string 类型,我们必须调用 preamble.js 提供的 UTF8ToString()。下面的代码显示一个 HTML 消息框,其中包含文本:“I received: Hi5”。

EM_ASM_({
        alert('I received: ' + UTF8ToString($0) + $1);
    }, "Hi", 5);

下面的代码演示了如何在 JsCanvas.h 中使用 EM_ASM_ 实现 lineTo。我们必须传入 canvas 对象名称才能从全局字典中检索它,因此每个函数调用都会有轻微的开销。此名称存储在 JS Canvas 类的 m_Name 成员中。相同类的桌面版本不需要此类信息。此函数将大部分工作委托给 HTML Canvas

void lineTo(double x, double y)
{
    EM_ASM_({
        var ctx = get_canvas(UTF8ToString($0));

        ctx.lineTo($1, $2);
    }, m_Name.c_str(), x, y);
}

对于桌面的 lineTo(),它将调用转发给 Cairo 函数 cairo_line_to()crCairo 对象,它是桌面 Canvas 的实例成员。

void lineTo(double x, double y)
{
    cairo_line_to(cr, x, y);
}

要尝试后续部分中的纯 JavaScript 示例,您可以使用此 HTML(保存为 WebApplication1 文件夹中的“pure_js.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>

要尝试后续部分中的 C++ 示例,您可以使用此 HTML(保存为 WebApplication1 文件夹中的“cpp.html”)。您需要包含“preamble.js”用于 UTF8ToString() 和“jscanvas.js”用于 Canvas 对象的全局字典,最后,示例位于“cpp.js”(asm.js 文件)中。

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

现在我们准备看一些例子了!

画线

Draw Line demo

在接下来的所有示例中,纯 JavaScript 版本先于 C++ 版本显示。在下面的 JavaScript 中,我们构造了一条路径,它是一条线。我们将两个属性 lineWidthlineCap 分别设置为 10roundbeginPathmoveTolineTopath 函数。调用 stroke() 绘制具有圆角的 path

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();

在 C++ 版本中,我们通过提供名称和尺寸来构造 Canvas 对象。名称仅在 Web 版本中用于从全局字典中检索,而尺寸在桌面版本中使用,并在 Web 版本中被忽略,因为 Canvas 尺寸已在 HTML 中指定。我只实现了 lineWidth 等简单属性的访问器,因为 Emscripten 只能返回整数或 double 等基本类型。您可以看到它与纯 JavaScript 版本多么相似。savePng() 用于在桌面上保存图像以便我们可以查看它。savePng() 在 Web 版本中不起作用。如果您的应用程序需要在不通过硬盘存储的情况下显示 Canvas,则可以调用 getImageData() 以 ARGB 颜色格式获取像素数据。

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

接下来,我们在 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();

在桌面版本上,由于 Cairo 没有此函数,quadraticCurveTo 由我实现。

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 demo

接下来,我们将绘制一条贝塞尔曲线。

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();

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 demo

下一个示例,我们以单色和渐变模式显示文本。

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);

Canvas 类的 Web 版本中,渐变也存储在全局字典中。这就是 createLinearGradient 中需要名称的原因。C++ Canvas 类从 v0.4.0 开始可以识别 magentablue 等颜色名称。

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

对于 C++ 版本,提供 fromRGB 以方便指定颜色值。

gradient.addColorStop(0.0, fromRGB(0xff, 0, 0xff));
gradient.addColorStop(0.5, fromRGB(0, 0, 0xff));
gradient.addColorStop(1.0, fromRGB(0xff, 0, 0));

如果愿意,您也可以绕过 fromRGB 直接指定十六进制数字。

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

显示文本轮廓

Display Text Outline demo

下一个示例,我们以单色和渐变模式显示文本。这只需将上述 JavaScript 代码段中的 fillStylefillText 分别替换为 strokeStylestrokeText 即可实现。

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);

上述 C++ 代码段中的 fillStylefillText 也分别被 strokeStylestrokeText 替换。否则,它们是相同的。

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

接下来,我们在 JavaScript 中绘制一个旋转 20 度的矩形。20 度转换为弧度。

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

ctx.rotate(20 * Math.PI / 180);
ctx.fillRect(50, 20, 100, 50);

C++ 版本是相同的,只是我们必须定义 PI 来计算弧度。

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 demo

在下一个纯 JavaScript 示例中,我们将使用 drawImage()img 对象 ID 显示图像。

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

var img = document.getElementById("yes_image");
ctx.drawImage(img, 10, 10);

这是带有 yes_image ID 的 HTML img 元素。

<div style="display: none;"><img src="yes.jpg" id="yes_image"></div>

在 C++ 版本中,我们必须检测 Emscripten 模式。如果是,我们指定 img 对象 ID,否则是图像文件名。桌面版本的 drawImage() 借助 STB Image 库实现。

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

v0.4.0 中用 C++ Canvas 和 Cairo 实现的 Canvas API 列表

颜色、样式和阴影

 fillStyle
 strokeStyle
 shadowColor
 shadowBlur
 shadowOffsetX
 shadowOffsetY
 createLinearGradient()
 createPattern()
 createRadialGradient()
 addColorStop()

线条样式

 lineCap
 lineJoin
 lineWidth
 miterLimit

矩形

 rect()
 fillRect()
 strokeRect()
 clearRect()

路径

 fill()
 stroke()
 beginPath()
 moveTo()
 closePath()
 lineTo()
 clip()
 quadraticCurveTo()
 bezierCurveTo()
 arc()
 arcTo()
 isPointInPath()

转换

 scale()
 rotate()
 translate()
 transform()
 setTransform()

文本

 font
 textAlign
 textBaseline
 fillText()
 strokeText()
 measureText()

图像绘制

 drawImage()

像素操作

 width
 height
 data
 createImageData()
 getImageData()
 putImageData()

合成

 globalAlpha
 globalCompositeOperation

其他

 save()
 restore()

代码托管在 Github。希望您喜欢我的文章。

历史

  • 2019 年 7 月 31 日:初始版本

《将你的...》系列的其他文章

© . All rights reserved.