将 C++ 图形带到 Web






4.87/5 (11投票s)
在桌面和 Web 上运行您的 C++ 图形。
目录
引言
本文灵感来源于 Google 的 Flutter 和 Qt。这两个框架的共同点是它们的用户界面都是独立于原生 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()
。cr
是 Cairo
对象,它是桌面 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>
现在我们准备看一些例子了!
画线
在接下来的所有示例中,纯 JavaScript 版本先于 C++ 版本显示。在下面的 JavaScript 中,我们构造了一条路径,它是一条线。我们将两个属性 lineWidth
和 lineCap
分别设置为 10
和 round
。beginPath
、moveTo
和 lineTo
是 path
函数。调用 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");
绘制二次曲线
接下来,我们在 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");
绘制贝塞尔曲线
接下来,我们将绘制一条贝塞尔曲线。
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");
显示文本
下一个示例,我们以单色和渐变模式显示文本。
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 开始可以识别 magenta
和 blue
等颜色名称。
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));
显示文本轮廓
下一个示例,我们以单色和渐变模式显示文本。这只需将上述 JavaScript 代码段中的 fillStyle
和 fillText
分别替换为 strokeStyle
和 strokeText
即可实现。
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++ 代码段中的 fillStyle
和 fillText
也分别被 strokeStyle
和 strokeText
替换。否则,它们是相同的。
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");
旋转矩形
接下来,我们在 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");
显示图像
在下一个纯 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 日:初始版本