xbintsc 0.5.21 → 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +15 -5
- package/README.zh-CN.md +11 -4
- package/dist/src/cli/hints.js +11 -0
- package/dist/src/cli/hints.js.map +1 -1
- package/dist/src/cli/main.js +1 -1
- package/dist/src/driver/compiler.js +1 -0
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/catalog.js +2 -1
- package/dist/src/extensions/catalog.js.map +1 -1
- package/dist/src/extensions/media/index.d.ts +27 -0
- package/dist/src/extensions/media/index.js +60 -0
- package/dist/src/extensions/media/index.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +12 -0
- package/dist/src/extensions/registry.js +16 -0
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/hints.test.js +15 -0
- package/dist/tests/cli/hints.test.js.map +1 -1
- package/dist/tests/cli/process.test.js +12 -0
- package/dist/tests/cli/process.test.js.map +1 -1
- package/dist/tests/e2e/gui-canvas.test.js +159 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
- package/dist/tests/e2e/gui-example-canvas.test.d.ts +11 -0
- package/dist/tests/e2e/gui-example-canvas.test.js +72 -0
- package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +2 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-media-disabled.test.d.ts +9 -0
- package/dist/tests/e2e/gui-media-disabled.test.js +42 -0
- package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -0
- package/dist/tests/e2e/gui-media.test.d.ts +6 -4
- package/dist/tests/e2e/gui-media.test.js +15 -12
- package/dist/tests/e2e/gui-media.test.js.map +1 -1
- package/dist/tests/e2e/unicode-output.test.d.ts +16 -0
- package/dist/tests/e2e/unicode-output.test.js +65 -0
- package/dist/tests/e2e/unicode-output.test.js.map +1 -0
- package/dist/tests/extensions/media.test.d.ts +1 -0
- package/dist/tests/extensions/media.test.js +58 -0
- package/dist/tests/extensions/media.test.js.map +1 -0
- package/dist/tests/extensions/registry.test.js +8 -0
- package/dist/tests/extensions/registry.test.js.map +1 -1
- package/dist/tests/lexer/identifiers.test.js +12 -0
- package/dist/tests/lexer/identifiers.test.js.map +1 -1
- package/dist/tests/lexer/scanner.test.js +8 -0
- package/dist/tests/lexer/scanner.test.js.map +1 -1
- package/dist/tests/lexer/strings.test.js +30 -0
- package/dist/tests/lexer/strings.test.js.map +1 -1
- package/doc/ai/cli.md +1 -1
- package/doc/ai/extensions.md +35 -22
- package/doc/ai/troubleshooting.md +9 -0
- package/doc/ai/zh-CN/cli.md +1 -1
- package/doc/ai/zh-CN/extensions.md +22 -9
- package/doc/ai/zh-CN/troubleshooting.md +8 -0
- package/doc/gui/build.md +37 -21
- package/doc/gui/canvas.md +47 -21
- package/doc/gui/known-issues.md +7 -6
- package/doc/gui/media.md +26 -11
- package/doc/gui.md +50 -34
- package/doc/zh-CN/gui/build.md +28 -16
- package/doc/zh-CN/gui/canvas.md +36 -17
- package/doc/zh-CN/gui/known-issues.md +6 -5
- package/doc/zh-CN/gui/media.md +21 -10
- package/doc/zh-CN/gui.md +43 -34
- package/package.json +2 -1
- package/runtime/ext_gui/canvas.cpp +114 -199
- package/runtime/ext_gui/canvas.h +101 -2
- package/runtime/ext_gui/canvas_gradient.cpp +112 -0
- package/runtime/ext_gui/canvas_internal.h +6 -6
- package/runtime/ext_gui/canvas_raster.cpp +27 -5
- package/runtime/ext_gui/canvas_stroke.cpp +252 -0
- package/runtime/ext_gui/canvas_style.cpp +240 -0
- package/runtime/ext_gui/dom_api.cpp +1 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +38 -4
- package/runtime/ext_gui/dom_api_canvas_extra.cpp +232 -0
- package/runtime/ext_gui/dom_api_internal.h +31 -0
- package/runtime/ext_gui/dom_api_proto.cpp +24 -0
- package/runtime/ext_gui/gui_media.cpp +69 -2
- package/runtime/ext_gui/media.cpp +34 -488
- package/runtime/ext_gui/media.h +54 -19
- package/runtime/ext_gui/media_api.h +65 -0
- package/runtime/ext_media/media.cpp +507 -0
- package/runtime/ext_media/media_bridge.c +35 -0
- package/scripts/build-gui.ts +6 -72
- package/scripts/build-media.ts +245 -0
- package/src/cli/hints.ts +11 -0
- package/src/cli/main.ts +1 -1
- package/src/driver/compiler.ts +1 -0
- package/src/driver/version.ts +1 -1
- package/src/extensions/catalog.ts +2 -1
- package/src/extensions/media/index.ts +67 -0
- package/src/extensions/registry.ts +24 -0
- package/src/index.ts +1 -0
package/doc/zh-CN/gui.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
> 语言 / Language:[English](../gui.md) | **简体中文**
|
|
4
4
|
|
|
5
5
|
状态:**M16** —— 功能(M1–M15)已全部完成:HTML 解析、CSS 选择器匹配、层叠(cascade)、计算样式与布局(块级、行内、Flexbox 与 CSS 定位)均已就位,引擎还会**绘制**:它构建一个由矩形、图像与排版后的文本 run 组成的显示列表,并通过 SDL_GPU 渲染。输入事件会经过命中测试并投递给原生 TS 处理器,`:hover`/`:focus` 会被动态匹配,`<img>` 依据其固有尺寸确定大小并从纹理绘制——位图(PNG/JPEG/…)与 SVG 文件皆可,后者按绘制尺寸光栅化,因此矢量图保持清晰——CSS transition 会为绘制属性做动画。**M16** 让内联 `<svg>` 子树获得同样的处理:子树被序列化回 SVG 文档,并按绘制设备尺寸光栅化,因此图标不再需要单独的文件。**M8** 增加了交互式 DOM:具有稳定标识的元素句柄、变更操作(`appendChild`、`textContent`、`classList`、`style` 等)以及支持捕获/冒泡的元素级事件。**M9** 会提前编译 `<script>` 主体(内联与 `<script src>`)——参见 `gui-scripts.md`。**M10** 增加 `requestAnimationFrame` 以及若干 DOM 辅助方法。**M11** 增加 `position: relative`/`absolute`/`fixed` 及其偏移,`examples/gui/pelican-bike`(一个可玩的 2D 游戏)就构建在它之上。**M12** 明确了 DOM 节点生命周期(哪些变更释放、哪些保留),并让引擎内存有界:参见[节点生命周期](./gui/dom.md)与[内存与 GC](./gui/runtime.md)。**M13** 真正播放
|
|
6
|
-
`<video>`/`<audio
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
`<video>`/`<audio>`——可选的 `media` 扩展用 vendored 的最小 FFmpeg 在主线程解码,
|
|
7
|
+
视频帧走图像纹理路径、音频进入 SDL 流,DOM 媒体 API 暴露
|
|
8
|
+
`play`/`pause`/`currentTime`/… 与媒体事件。只启用 `gui` 不会链接 FFmpeg。**M14** 增加了 `<canvas>`,配一个自研的 2D 软件光栅器(抗锯齿
|
|
9
9
|
填充、路径、文本与 `drawImage`),并经图像纹理路径上传——参见
|
|
10
10
|
[Canvas](./gui/canvas.md)。**M15** 增加了 CSS `transform` /
|
|
11
11
|
`transform-origin`:每个布局盒一个合成好的 `Matrix2D`,由显示列表一路带到
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
| 4 | 引擎**非自托管**(它是 C/C++,不是 TS),但不得影响编译器的平台无关设计。以按平台预构建的归档形式交付,通过 `nativeObjects` 链接。 |
|
|
53
53
|
| 5 | **事件循环是通用的**:运行时暴露一个通用主循环钩子与一个轮询原语;任何 GUI 特定的东西都不会进入 `runtime/`。 |
|
|
54
54
|
| 6 | 核心对象模型支持**多窗口**。 |
|
|
55
|
-
| 7 |
|
|
55
|
+
| 7 | 第三方底层库**静态链接进扩展归档**,使发布版保持自包含;链接时只额外加入操作系统框架。`gui.a` 携带 SDL3/FreeType/HarfBuzz,可选的 `media.a` 携带 FFmpeg,仅在 `--ext media` 时链接。 |
|
|
56
56
|
|
|
57
57
|
## 架构
|
|
58
58
|
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
窗口 + 输入(SDL3)── 多窗口
|
|
75
75
|
文本(HarfBuzz + FreeType)── 字形图集 / SDF
|
|
76
76
|
图像(stb_image;SVG 经 NanoSVG)
|
|
77
|
+
媒体 DOM + 音频 sink ── 可选的 media.a(FFmpeg)安装解码器
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
引擎从不与编译器通信。编译器只看到一个带有 `nativeObjects()`、`linkerFlags()` 和 `modules()` 的 `Extension`。
|
|
@@ -87,16 +88,16 @@
|
|
|
87
88
|
| 文本整形 | **HarfBuzz** | 正确处理复杂文种的整形 |
|
|
88
89
|
| 字形光栅化 | **FreeType** | 字形轮廓 → GPU 图集 / SDF |
|
|
89
90
|
| 图像 | 位图用 **stb_image**(vendored 头文件),SVG 用 **NanoSVG**(vendored 头文件) | 起步阶段用单头文件;SVG 文件按需光栅化 |
|
|
90
|
-
| 视频 + 音频 | **FFmpeg**(静态 vendored
|
|
91
|
+
| 视频 + 音频 | **FFmpeg**(静态 vendored,最小化构建,可选 `media` 扩展) | 解码常用容器/编码;不依赖 WebView 或平台媒体框架 |
|
|
91
92
|
|
|
92
93
|
固定版本:SDL3 `release-3.2.10`、FreeType `2.13.3`、HarfBuzz `10.1.0`、
|
|
93
|
-
FFmpeg `7.1
|
|
94
|
-
`FFMPEG_VERSION` 覆盖)。
|
|
94
|
+
FFmpeg `7.1`(供可选的 `media` 归档使用;可用 `SDL3_TAG` /
|
|
95
|
+
`FREETYPE_VERSION` / `HARFBUZZ_VERSION` / `FFMPEG_VERSION` 覆盖)。
|
|
95
96
|
|
|
96
97
|
如果否决 SDL3,备选是 **GLFW + OpenGL 3.3**(更简单,但 OpenGL 在 macOS 上
|
|
97
98
|
已弃用,且不提供现代 GPU 抽象)。如果引擎用 Rust 编写,则用 **wgpu-native**。
|
|
98
99
|
|
|
99
|
-
> 这些 vendored 库会**静态链接进 `gui.a`**,因此发布的 xbintsc 保持
|
|
100
|
+
> 这些 vendored 库会**静态链接进 `gui.a` / `media.a`**,因此发布的 xbintsc 保持
|
|
100
101
|
> “下载即用”;`linkerFlags()` 只添加操作系统框架。
|
|
101
102
|
|
|
102
103
|
## 里程碑
|
|
@@ -135,14 +136,15 @@ FFmpeg `7.1`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
|
|
|
135
136
|
`background-color`/`color`/`border-color`/`border-radius` 的动画、重定向
|
|
136
137
|
与 `win.advance(ms)`。`@keyframes` 仍待完成。
|
|
137
138
|
7. **M7 — CI 与发布** ✅(Linux/macOS 构建) / 🚧(运行 + Windows)
|
|
138
|
-
- `compile-examples` 在 Linux 与 macOS 上构建 `gui.a
|
|
139
|
-
Linux 上的 Xvfb + lavapipe 下运行示例与 `tests/e2e/gui-*.test.ts
|
|
140
|
-
macOS 运行在确认 WindowServer
|
|
141
|
-
|
|
142
|
-
- `package` job 在组装发布之前构建 `gui.a
|
|
143
|
-
|
|
144
|
-
- `vendor
|
|
145
|
-
`scripts/build-gui.ts`
|
|
139
|
+
- `compile-examples` 在 Linux 与 macOS 上构建 `gui.a` 与 `media.a`(必需),
|
|
140
|
+
然后在 Linux 上的 Xvfb + lavapipe 下运行示例与 `tests/e2e/gui-*.test.ts`
|
|
141
|
+
(必需)。macOS 运行在确认 WindowServer 之前为试验性;它被保护起来,失败
|
|
142
|
+
只记录日志而不标注运行。
|
|
143
|
+
- `package` job 在组装发布之前构建 `gui.a` 与 `media.a`,因此两者都会随现有
|
|
144
|
+
runtime 归档一起发布(`package-release` 复制整个 `runtime/`)。
|
|
145
|
+
- `vendor/`(`gui.a` 的 SDL3/FreeType/HarfBuzz,以及 `media.a` 的 FFmpeg,
|
|
146
|
+
缓慢的部分)按 OS/arch 缓存,以 `scripts/build-gui.ts` /
|
|
147
|
+
`scripts/build-media.ts` 为键。
|
|
146
148
|
- Windows(构建 + 运行)在 CI 中保持试验性:MSVC 兼容的 `gui.lib` 可在本机
|
|
147
149
|
通过 SDL_GPU 的 Vulkan 后端构建并运行,但托管 runner 没有 GPU/Vulkan
|
|
148
150
|
驱动。失败只记录日志而不标注运行(见[待决问题](./gui/known-issues.md))。
|
|
@@ -189,13 +191,14 @@ FFmpeg `7.1`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
|
|
|
189
191
|
- `win.stats()` 暴露引擎计数器;e2e 覆盖见
|
|
190
192
|
`tests/e2e/gui-retention.test.ts`(见[内存与 GC](./gui/runtime.md))。
|
|
191
193
|
13. **M13 — 视频与音频** ✅
|
|
192
|
-
- `<video>` / `<audio>`
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
`loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error`
|
|
196
|
-
|
|
197
|
-
内置控件。示例见 `examples/gui/media
|
|
198
|
-
`tests/e2e/gui-media.test.ts
|
|
194
|
+
- `<video>` / `<audio>` 通过可选的 `media` 扩展(`runtime/ext_media/media.cpp`)
|
|
195
|
+
使用静态 vendored 的最小 FFmpeg 解码,帧沿用现有图像管线上传到 GPU 纹理,
|
|
196
|
+
音频由 GUI 的音频 sink 在主线程推送进 SDL 流,并在元素句柄上暴露 DOM
|
|
197
|
+
媒体 API 与 `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error`
|
|
198
|
+
事件(`dom_api_media.cpp`、`dom_api_proto.cpp`)。由 TS + CSS 控制;没有
|
|
199
|
+
内置控件。示例见 `examples/gui/media.*`;e2e 见
|
|
200
|
+
`tests/e2e/gui-media.test.ts`(启用 `media`)与
|
|
201
|
+
`tests/e2e/gui-media-disabled.test.ts`(不启用)。
|
|
199
202
|
14. **M14 — `<canvas>` 2D** ✅
|
|
200
203
|
- `<canvas>` 是一个替换元素(`xtgui::Canvas2D`、`layout.*`),
|
|
201
204
|
`getContext("2d")` 句柄与绘制 API 位于 `dom_api_canvas.cpp`,自研的抗锯齿
|
|
@@ -249,10 +252,10 @@ FFmpeg `7.1`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
|
|
|
249
252
|
按绘制设备像素尺寸光栅化并按该尺寸缓存,因此缩放与 HiDPI 保持清晰。
|
|
250
253
|
- **M6b** ✅ CSS transition(在 `style.*` 中解析、在 `document.*` 中维护动画
|
|
251
254
|
时钟与 transition 状态、`advance`/`advance(ms)` 钩子、e2e 覆盖)。
|
|
252
|
-
- **M7** ✅ 在 CI 中于 Linux 与 macOS 上构建 `gui.a
|
|
253
|
-
Xvfb 下运行示例与 GUI e2e 套件;`package`
|
|
254
|
-
`ar -M` 合并在 GNU/Linux 与 macOS 上可用。macOS 运行与所有
|
|
255
|
-
中仍为试验性。
|
|
255
|
+
- **M7** ✅ 在 CI 中于 Linux 与 macOS 上构建 `gui.a`(以及可选的 `media.a`,
|
|
256
|
+
必需),并在 Linux 的 Xvfb 下运行示例与 GUI e2e 套件;`package` 发布两个归档,
|
|
257
|
+
`vendor/` 被缓存,`ar -M` 合并在 GNU/Linux 与 macOS 上可用。macOS 运行与所有
|
|
258
|
+
Windows 在 CI 中仍为试验性。
|
|
256
259
|
- **M8** ✅ 元素/文档句柄(`dom_api.*`)、带延迟重算样式/重新布局的 DOM 变更
|
|
257
260
|
(`document.*`、`xt_gui_flush_dom`)、带捕获/冒泡与 `stopPropagation` 的
|
|
258
261
|
元素事件分发(`dom_api.*`、`gui.cpp`),以及
|
|
@@ -283,11 +286,13 @@ FFmpeg `7.1`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
|
|
|
283
286
|
失效;收集器的触发阈值改为跟随存活集,而不再只升不降。`win.stats()` 暴露
|
|
284
287
|
引擎计数器,`tests/e2e/gui-retention.test.ts` 对其做断言;每帧重写精灵的游戏
|
|
285
288
|
循环现在内存恒定(见[内存与 GC](./gui/runtime.md))。
|
|
286
|
-
- **M13** ✅ `<video>` / `<audio>`
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
- **M13** ✅ `<video>` / `<audio>` 支持。可选的 `media` 扩展 vendored 并构建一个
|
|
290
|
+
最小化的静态 FFmpeg(`scripts/build-media.ts`),合并进 `media.a`;
|
|
291
|
+
`runtime/ext_media/media.cpp` 在主线程把视频解码为 RGBA 帧、把音频解码为
|
|
292
|
+
48 kHz 立体声 PCM,`runtime/ext_gui/gui_media.cpp` 持有播放器、实现 SDL 音频
|
|
293
|
+
sink 并在每帧推进媒体时钟与事件,`renderer_image.cpp` 把每个新视频帧沿图像纹理
|
|
294
|
+
路径上传,`dom_api_media.cpp` 在元素句柄上暴露 `play`/`pause`/`load` 与媒体
|
|
295
|
+
属性。`gui.a` 不含 FFmpeg;只启用 `media` 而不启用 `gui` 会被注册表拒绝。
|
|
291
296
|
`examples/gui/media.*` 演示 TS + CSS 控件的模式,
|
|
292
297
|
`tests/e2e/gui-media.test.ts` 对其做断言。
|
|
293
298
|
- **M14** ✅ `<canvas>` 支持。新的替换元素(`layout.*`)从 `width`/`height`
|
|
@@ -296,8 +301,12 @@ FFmpeg `7.1`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
|
|
|
296
301
|
(4 条子扫描线 + 水平解析覆盖率、非零环绕),`getContext("2d")` 句柄及其 API
|
|
297
302
|
在 `dom_api_canvas.cpp` 绑定,`gui_canvas.cpp` 持有按窗口的生命周期。位图经
|
|
298
303
|
图像纹理路径上传(`renderer_image.cpp`),显示列表携带 `canvas_id`
|
|
299
|
-
(`paint
|
|
300
|
-
`
|
|
304
|
+
(`paint.*`)。此后绘制面又补齐了线性/径向渐变(`canvas_gradient.cpp`)、
|
|
305
|
+
`ellipse`/`roundRect`、线段端点/连接/斜接上限与虚线模式(`canvas_style.cpp`、
|
|
306
|
+
`canvas_stroke.cpp`)以及 `getTransform`;`transform` 改为后乘,因此
|
|
307
|
+
`translate(x, y); rotate(a)` 围绕平移后的原点旋转,而
|
|
308
|
+
`examples/gui/pelican-canvas` 完全用这些 API 画出会动的鹈鹕骑自行车。参见
|
|
309
|
+
[Canvas](./gui/canvas.md);e2e 见 `tests/e2e/gui-canvas.test.ts`。
|
|
301
310
|
- **M15** ✅ CSS `transform` / `transform-origin`。新的翻译单元
|
|
302
311
|
(`transform.{h,cpp}`)把 `translate`/`translateX`/`translateY`/`scale`/
|
|
303
312
|
`scaleX`/`scaleY`/`rotate`/`matrix`(以及 `translate3d`/`scale3d`,其 `z` 参数被丢弃)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xbintsc",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"description": "xbintsc - a TypeScript binary compiler (TypeScript -> LLVM IR -> native binary)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build",
|
|
38
38
|
"runtime": "tsx scripts/build-runtime.ts",
|
|
39
39
|
"gui": "tsx scripts/build-gui.ts",
|
|
40
|
+
"media": "tsx scripts/build-media.ts",
|
|
40
41
|
"package-release": "tsx scripts/package-release.ts",
|
|
41
42
|
"runtime:clean": "node -e \"require('fs').rmSync('build/runtime-obj',{recursive:true,force:true});require('fs').rmSync('runtime/lib',{recursive:true,force:true})\"",
|
|
42
43
|
"xbintsc": "tsx src/cli/main.ts",
|
|
@@ -12,12 +12,10 @@
|
|
|
12
12
|
#include <algorithm>
|
|
13
13
|
#include <atomic>
|
|
14
14
|
#include <cmath>
|
|
15
|
-
#include <cstdlib>
|
|
16
15
|
#include <cstring>
|
|
17
16
|
#include <unordered_map>
|
|
18
17
|
|
|
19
18
|
#include "canvas_internal.h"
|
|
20
|
-
#include "css.h"
|
|
21
19
|
|
|
22
20
|
namespace xtgui {
|
|
23
21
|
|
|
@@ -32,99 +30,12 @@ std::unordered_map<uint64_t, Canvas2D *> &canvas_registry() {
|
|
|
32
30
|
return registry;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
std::string lower_ascii(const std::string &value) {
|
|
36
|
-
std::string out = value;
|
|
37
|
-
for (char &c : out) {
|
|
38
|
-
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
bool parse_font_size_token(const std::string &token, float fallback, float *out) {
|
|
44
|
-
std::string lower = lower_ascii(token);
|
|
45
|
-
struct Suffix {
|
|
46
|
-
const char *text;
|
|
47
|
-
float factor;
|
|
48
|
-
};
|
|
49
|
-
static const Suffix suffixes[] = {{"px", 1.0f}, {"pt", 96.0f / 72.0f}, {"pc", 16.0f},
|
|
50
|
-
{"in", 96.0f}, {"cm", 96.0f / 2.54f}, {"mm", 96.0f / 25.4f},
|
|
51
|
-
{"q", 96.0f / 101.6f}, {"em", 0.0f}, {"rem", 0.0f}};
|
|
52
|
-
for (const Suffix &suffix : suffixes) {
|
|
53
|
-
size_t length = std::strlen(suffix.text);
|
|
54
|
-
if (lower.size() <= length || lower.compare(lower.size() - length, length, suffix.text) != 0)
|
|
55
|
-
continue;
|
|
56
|
-
char *end = nullptr;
|
|
57
|
-
float value = std::strtof(lower.c_str(), &end);
|
|
58
|
-
if (end == lower.c_str()) return false;
|
|
59
|
-
if (suffix.factor == 0.0f) {
|
|
60
|
-
*out = value * fallback;
|
|
61
|
-
} else {
|
|
62
|
-
*out = value * suffix.factor;
|
|
63
|
-
}
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Parse the CSS `font` shorthand into a `FontSpec` (the pieces Canvas2D
|
|
70
|
-
* understands; unknown tokens are ignored). */
|
|
71
|
-
FontSpec parse_font_string(const std::string &text) {
|
|
72
|
-
FontSpec spec;
|
|
73
|
-
spec.family = "sans-serif";
|
|
74
|
-
spec.pixel_size = 10.0f;
|
|
75
|
-
spec.weight = 400;
|
|
76
|
-
spec.italic = false;
|
|
77
|
-
std::vector<std::string> tokens = xt_css_split_whitespace(text);
|
|
78
|
-
std::vector<std::string> family;
|
|
79
|
-
float size = 10.0f;
|
|
80
|
-
bool have_size = false;
|
|
81
|
-
for (const std::string &token : tokens) {
|
|
82
|
-
std::string lower = lower_ascii(token);
|
|
83
|
-
float parsed = 0.0f;
|
|
84
|
-
if (!have_size && parse_font_size_token(lower, 16.0f, &parsed)) {
|
|
85
|
-
size = parsed;
|
|
86
|
-
have_size = true;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (lower == "italic" || lower == "oblique") {
|
|
90
|
-
spec.italic = true;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (lower == "bold") {
|
|
94
|
-
spec.weight = 700;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (lower == "bolder") {
|
|
98
|
-
spec.weight = 700;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (lower == "lighter") {
|
|
102
|
-
spec.weight = 300;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (lower == "normal" || lower == "small-caps") continue;
|
|
106
|
-
if (!lower.empty() && lower.back() == ',') lower.pop_back();
|
|
107
|
-
if (!lower.empty()) family.push_back(lower);
|
|
108
|
-
}
|
|
109
|
-
spec.pixel_size = size;
|
|
110
|
-
if (!family.empty()) spec.family = family[0];
|
|
111
|
-
return spec;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
void parse_color_into(const std::string &css, float color[4], std::string *stored) {
|
|
115
|
-
Color parsed = xt_css_parse_color(css);
|
|
116
|
-
if (!parsed.valid) return;
|
|
117
|
-
color[0] = parsed.r;
|
|
118
|
-
color[1] = parsed.g;
|
|
119
|
-
color[2] = parsed.b;
|
|
120
|
-
color[3] = parsed.a;
|
|
121
|
-
*stored = css;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
33
|
CanvasPoint lerp(const CanvasPoint &a, const CanvasPoint &b, float t) {
|
|
125
34
|
return CanvasPoint{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t};
|
|
126
35
|
}
|
|
127
36
|
|
|
37
|
+
constexpr float kPi = 3.14159265358979323846f;
|
|
38
|
+
|
|
128
39
|
} // namespace
|
|
129
40
|
|
|
130
41
|
Canvas2D* xt_canvas_by_id(uint64_t id) {
|
|
@@ -137,6 +48,20 @@ float CanvasTransform::scale() const {
|
|
|
137
48
|
return std::sqrt(determinant);
|
|
138
49
|
}
|
|
139
50
|
|
|
51
|
+
CanvasTransform CanvasTransform::inverted() const {
|
|
52
|
+
const float determinant = a * d - b * c;
|
|
53
|
+
if (std::fabs(determinant) < 1e-9f) return CanvasTransform();
|
|
54
|
+
const float inv = 1.0f / determinant;
|
|
55
|
+
CanvasTransform result;
|
|
56
|
+
result.a = d * inv;
|
|
57
|
+
result.b = -b * inv;
|
|
58
|
+
result.c = -c * inv;
|
|
59
|
+
result.d = a * inv;
|
|
60
|
+
result.e = (c * f - d * e) * inv;
|
|
61
|
+
result.f = (b * e - a * f) * inv;
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
140
65
|
Canvas2D::Canvas2D(int width, int height) : id_(g_next_canvas_id.fetch_add(1)) {
|
|
141
66
|
resize(width, height);
|
|
142
67
|
canvas_registry()[id_] = this;
|
|
@@ -178,14 +103,17 @@ void Canvas2D::rotate(float radians) {
|
|
|
178
103
|
void Canvas2D::scale(float sx, float sy) { transform(sx, 0.0f, 0.0f, sy, 0.0f, 0.0f); }
|
|
179
104
|
|
|
180
105
|
void Canvas2D::transform(float a, float b, float c, float d, float e, float f) {
|
|
106
|
+
/* Post-multiply: the new matrix applies *after* the current one, so
|
|
107
|
+
* `translate(x, y); rotate(a)` rotates around the translated origin (the
|
|
108
|
+
* canvas rules) instead of rotating the translation itself. */
|
|
181
109
|
const CanvasTransform &m = state_.transform;
|
|
182
110
|
CanvasTransform next;
|
|
183
|
-
next.a = a *
|
|
184
|
-
next.b = b *
|
|
185
|
-
next.c = a *
|
|
186
|
-
next.d = b *
|
|
187
|
-
next.e = a *
|
|
188
|
-
next.f = b *
|
|
111
|
+
next.a = m.a * a + m.c * b;
|
|
112
|
+
next.b = m.b * a + m.d * b;
|
|
113
|
+
next.c = m.a * c + m.c * d;
|
|
114
|
+
next.d = m.b * c + m.d * d;
|
|
115
|
+
next.e = m.a * e + m.c * f + m.e;
|
|
116
|
+
next.f = m.b * e + m.d * f + m.f;
|
|
189
117
|
state_.transform = next;
|
|
190
118
|
}
|
|
191
119
|
|
|
@@ -193,59 +121,6 @@ void Canvas2D::set_transform(float a, float b, float c, float d, float e, float
|
|
|
193
121
|
state_.transform = CanvasTransform{a, b, c, d, e, f};
|
|
194
122
|
}
|
|
195
123
|
|
|
196
|
-
void Canvas2D::set_fill_style(const std::string &css) {
|
|
197
|
-
parse_color_into(css, state_.fill_color, &state_.fill_style);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
void Canvas2D::set_stroke_style(const std::string &css) {
|
|
201
|
-
parse_color_into(css, state_.stroke_color, &state_.stroke_style);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
void Canvas2D::set_global_alpha(float alpha) {
|
|
205
|
-
if (alpha < 0.0f) alpha = 0.0f;
|
|
206
|
-
if (alpha > 1.0f) alpha = 1.0f;
|
|
207
|
-
state_.global_alpha = alpha;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
void Canvas2D::set_font(const std::string &font) {
|
|
211
|
-
state_.font = parse_font_string(font);
|
|
212
|
-
state_.font_string = font;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
void Canvas2D::set_text_align(const std::string &value) {
|
|
216
|
-
std::string lower = lower_ascii(value);
|
|
217
|
-
if (lower == "start" || lower == "left") {
|
|
218
|
-
state_.text_align = 0;
|
|
219
|
-
} else if (lower == "center") {
|
|
220
|
-
state_.text_align = 1;
|
|
221
|
-
} else if (lower == "right" || lower == "end") {
|
|
222
|
-
state_.text_align = 2;
|
|
223
|
-
} else {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
state_.text_align_string = lower;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
void Canvas2D::set_text_baseline(const std::string &value) {
|
|
230
|
-
std::string lower = lower_ascii(value);
|
|
231
|
-
if (lower == "alphabetic") {
|
|
232
|
-
state_.text_baseline = 0;
|
|
233
|
-
} else if (lower == "top") {
|
|
234
|
-
state_.text_baseline = 1;
|
|
235
|
-
} else if (lower == "middle") {
|
|
236
|
-
state_.text_baseline = 2;
|
|
237
|
-
} else if (lower == "bottom") {
|
|
238
|
-
state_.text_baseline = 3;
|
|
239
|
-
} else if (lower == "hanging") {
|
|
240
|
-
state_.text_baseline = 4;
|
|
241
|
-
} else if (lower == "ideographic") {
|
|
242
|
-
state_.text_baseline = 5;
|
|
243
|
-
} else {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
state_.text_baseline_string = lower;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
124
|
void Canvas2D::begin_path() { path_.clear(); }
|
|
250
125
|
|
|
251
126
|
void Canvas2D::move_to(float x, float y) { path_.push_back(CanvasSubpath{CanvasPoint{x, y}}); }
|
|
@@ -287,19 +162,74 @@ void Canvas2D::flatten_arc(float cx, float cy, float radius, float start, float
|
|
|
287
162
|
}
|
|
288
163
|
}
|
|
289
164
|
|
|
290
|
-
void Canvas2D::
|
|
291
|
-
|
|
292
|
-
flatten_arc(cx, cy, radius, start, end, ccw, &arcPoints);
|
|
293
|
-
if (arcPoints.empty()) return;
|
|
165
|
+
void Canvas2D::append_path_points(const CanvasSubpath &points) {
|
|
166
|
+
if (points.empty()) return;
|
|
294
167
|
if (path_.empty() || path_.back().empty()) {
|
|
295
|
-
path_.push_back(CanvasSubpath{
|
|
168
|
+
path_.push_back(CanvasSubpath{points.front()});
|
|
296
169
|
} else {
|
|
297
170
|
CanvasPoint last = path_.back().back();
|
|
298
|
-
if (last.x !=
|
|
299
|
-
path_.back().push_back(
|
|
171
|
+
if (last.x != points.front().x || last.y != points.front().y) {
|
|
172
|
+
path_.back().push_back(points.front());
|
|
300
173
|
}
|
|
301
174
|
}
|
|
302
|
-
for (size_t i = 1; i <
|
|
175
|
+
for (size_t i = 1; i < points.size(); i++) path_.back().push_back(points[i]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void Canvas2D::arc(float cx, float cy, float radius, float start, float end, bool ccw) {
|
|
179
|
+
CanvasSubpath arcPoints;
|
|
180
|
+
flatten_arc(cx, cy, radius, start, end, ccw, &arcPoints);
|
|
181
|
+
append_path_points(arcPoints);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
void Canvas2D::ellipse(float cx, float cy, float rx, float ry, float rotation, float start,
|
|
185
|
+
float end, bool counterclockwise) {
|
|
186
|
+
float sweep = end - start;
|
|
187
|
+
if (counterclockwise) {
|
|
188
|
+
while (sweep > 0.0f) sweep -= 2.0f * kPi;
|
|
189
|
+
while (sweep < -2.0f * kPi) sweep += 2.0f * kPi;
|
|
190
|
+
} else {
|
|
191
|
+
while (sweep < 0.0f) sweep += 2.0f * kPi;
|
|
192
|
+
while (sweep > 2.0f * kPi) sweep -= 2.0f * kPi;
|
|
193
|
+
}
|
|
194
|
+
int segments = (int)std::ceil(std::fabs(sweep) / (kPi / 32.0f));
|
|
195
|
+
if (segments < 1) segments = 1;
|
|
196
|
+
if (segments > 2048) segments = 2048;
|
|
197
|
+
const float cosRotation = std::cos(rotation);
|
|
198
|
+
const float sinRotation = std::sin(rotation);
|
|
199
|
+
CanvasSubpath points;
|
|
200
|
+
points.reserve((size_t)segments + 1);
|
|
201
|
+
for (int i = 0; i <= segments; i++) {
|
|
202
|
+
float angle = start + sweep * ((float)i / (float)segments);
|
|
203
|
+
float px = rx * std::cos(angle);
|
|
204
|
+
float py = ry * std::sin(angle);
|
|
205
|
+
points.push_back(CanvasPoint{cx + px * cosRotation - py * sinRotation,
|
|
206
|
+
cy + px * sinRotation + py * cosRotation});
|
|
207
|
+
}
|
|
208
|
+
append_path_points(points);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
void Canvas2D::round_rect(float x, float y, float width, float height, float radius) {
|
|
212
|
+
if (width < 0.0f) {
|
|
213
|
+
x += width;
|
|
214
|
+
width = -width;
|
|
215
|
+
}
|
|
216
|
+
if (height < 0.0f) {
|
|
217
|
+
y += height;
|
|
218
|
+
height = -height;
|
|
219
|
+
}
|
|
220
|
+
float maxRadius = std::min(width, height) * 0.5f;
|
|
221
|
+
if (radius < 0.0f) radius = 0.0f;
|
|
222
|
+
if (radius > maxRadius) radius = maxRadius;
|
|
223
|
+
move_to(x + radius, y);
|
|
224
|
+
line_to(x + width - radius, y);
|
|
225
|
+
quadratic_curve_to(x + width, y, x + width, y + radius);
|
|
226
|
+
line_to(x + width, y + height - radius);
|
|
227
|
+
quadratic_curve_to(x + width, y + height, x + width - radius, y + height);
|
|
228
|
+
line_to(x + radius, y + height);
|
|
229
|
+
quadratic_curve_to(x, y + height, x, y + height - radius);
|
|
230
|
+
line_to(x, y + radius);
|
|
231
|
+
quadratic_curve_to(x, y, x + radius, y);
|
|
232
|
+
close_path();
|
|
303
233
|
}
|
|
304
234
|
|
|
305
235
|
void Canvas2D::quadratic_curve_to(float cpx, float cpy, float x, float y) {
|
|
@@ -353,6 +283,22 @@ std::vector<CanvasSubpath> Canvas2D::device_subpaths() const {
|
|
|
353
283
|
return result;
|
|
354
284
|
}
|
|
355
285
|
|
|
286
|
+
CanvasPaint Canvas2D::make_paint(uint64_t gradient_id, const float color[4]) const {
|
|
287
|
+
CanvasPaint paint;
|
|
288
|
+
paint.color = color;
|
|
289
|
+
paint.gradient = gradient_id == 0 ? nullptr : find_gradient(gradient_id);
|
|
290
|
+
paint.inverse = state_.transform.inverted();
|
|
291
|
+
return paint;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
CanvasPaint Canvas2D::fill_paint() const {
|
|
295
|
+
return make_paint(state_.fill_gradient, state_.fill_color);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
CanvasPaint Canvas2D::stroke_paint() const {
|
|
299
|
+
return make_paint(state_.stroke_gradient, state_.stroke_color);
|
|
300
|
+
}
|
|
301
|
+
|
|
356
302
|
void Canvas2D::fill() {
|
|
357
303
|
std::vector<CanvasSubpath> subpaths = device_subpaths();
|
|
358
304
|
std::vector<CanvasSubpath> loops;
|
|
@@ -361,50 +307,19 @@ void Canvas2D::fill() {
|
|
|
361
307
|
loops.push_back(std::move(subpath));
|
|
362
308
|
}
|
|
363
309
|
if (loops.empty()) return;
|
|
364
|
-
canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops,
|
|
310
|
+
canvas_detail::fill_loops(image_.pixels, image_.width, image_.height, loops, fill_paint(),
|
|
365
311
|
state_.global_alpha);
|
|
366
312
|
generation_++;
|
|
367
313
|
}
|
|
368
314
|
|
|
369
|
-
void Canvas2D::stroke_subpath(const CanvasSubpath &points, bool closed, const float color[4]) {
|
|
370
|
-
if (points.size() < 2) return;
|
|
371
|
-
float half = state_.line_width * state_.transform.scale() * 0.5f;
|
|
372
|
-
if (half <= 0.0f) half = 0.5f;
|
|
373
|
-
size_t segmentCount = points.size() - 1;
|
|
374
|
-
for (size_t i = 0; i < segmentCount; i++) {
|
|
375
|
-
const CanvasPoint &p = points[i];
|
|
376
|
-
const CanvasPoint &q = points[i + 1];
|
|
377
|
-
float dx = q.x - p.x;
|
|
378
|
-
float dy = q.y - p.y;
|
|
379
|
-
float length = std::sqrt(dx * dx + dy * dy);
|
|
380
|
-
if (length <= 0.0001f) continue;
|
|
381
|
-
float nx = -dy / length * half;
|
|
382
|
-
float ny = dx / length * half;
|
|
383
|
-
CanvasSubpath quad{CanvasPoint{p.x + nx, p.y + ny}, CanvasPoint{q.x + nx, q.y + ny},
|
|
384
|
-
CanvasPoint{q.x - nx, q.y - ny}, CanvasPoint{p.x - nx, p.y - ny}};
|
|
385
|
-
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, quad, color,
|
|
386
|
-
state_.global_alpha);
|
|
387
|
-
}
|
|
388
|
-
(void)closed;
|
|
389
|
-
for (const CanvasPoint &p : points) {
|
|
390
|
-
CanvasSubpath disc;
|
|
391
|
-
const int segments = 16;
|
|
392
|
-
for (int i = 0; i < segments; i++) {
|
|
393
|
-
float angle = 2.0f * 3.14159265358979323846f * ((float)i / (float)segments);
|
|
394
|
-
disc.push_back(CanvasPoint{p.x + half * std::cos(angle), p.y + half * std::sin(angle)});
|
|
395
|
-
}
|
|
396
|
-
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, disc, color,
|
|
397
|
-
state_.global_alpha);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
315
|
void Canvas2D::stroke() {
|
|
402
316
|
std::vector<CanvasSubpath> subpaths = device_subpaths();
|
|
317
|
+
const CanvasPaint paint = stroke_paint();
|
|
403
318
|
for (const CanvasSubpath &subpath : subpaths) {
|
|
404
319
|
if (subpath.size() < 2) continue;
|
|
405
320
|
bool closed = subpath.size() >= 3 && subpath.front().x == subpath.back().x &&
|
|
406
321
|
subpath.front().y == subpath.back().y;
|
|
407
|
-
stroke_subpath(subpath, closed,
|
|
322
|
+
stroke_subpath(subpath, closed, paint);
|
|
408
323
|
}
|
|
409
324
|
generation_++;
|
|
410
325
|
}
|
|
@@ -423,7 +338,7 @@ void Canvas2D::fill_rect(float x, float y, float width, float height) {
|
|
|
423
338
|
to_device(x, y, &x0, &y0);
|
|
424
339
|
to_device(x + width, y + height, &x1, &y1);
|
|
425
340
|
canvas_detail::fill_rect(image_.pixels, image_.width, image_.height, x0, y0, x1, y1,
|
|
426
|
-
|
|
341
|
+
fill_paint(), state_.global_alpha);
|
|
427
342
|
} else {
|
|
428
343
|
CanvasSubpath loop;
|
|
429
344
|
const float corners[4][2] = {
|
|
@@ -433,7 +348,7 @@ void Canvas2D::fill_rect(float x, float y, float width, float height) {
|
|
|
433
348
|
to_device(corner[0], corner[1], &point.x, &point.y);
|
|
434
349
|
loop.push_back(point);
|
|
435
350
|
}
|
|
436
|
-
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop,
|
|
351
|
+
canvas_detail::fill_loop(image_.pixels, image_.width, image_.height, loop, fill_paint(),
|
|
437
352
|
state_.global_alpha);
|
|
438
353
|
}
|
|
439
354
|
generation_++;
|
|
@@ -448,7 +363,7 @@ void Canvas2D::stroke_rect(float x, float y, float width, float height) {
|
|
|
448
363
|
to_device(corner[0], corner[1], &point.x, &point.y);
|
|
449
364
|
loop.push_back(point);
|
|
450
365
|
}
|
|
451
|
-
stroke_subpath(loop, true,
|
|
366
|
+
stroke_subpath(loop, true, stroke_paint());
|
|
452
367
|
generation_++;
|
|
453
368
|
}
|
|
454
369
|
|
|
@@ -456,7 +371,7 @@ float Canvas2D::measure_text(const std::string &text) const {
|
|
|
456
371
|
return xt_text_measure_width(text, state_.font);
|
|
457
372
|
}
|
|
458
373
|
|
|
459
|
-
void Canvas2D::paint_text(const std::string &text, float x, float y, const
|
|
374
|
+
void Canvas2D::paint_text(const std::string &text, float x, float y, const CanvasPaint &paint,
|
|
460
375
|
bool stroke) {
|
|
461
376
|
Font *font = xt_text_resolve(state_.font);
|
|
462
377
|
if (font == nullptr) return;
|
|
@@ -510,7 +425,7 @@ void Canvas2D::paint_text(const std::string &text, float x, float y, const float
|
|
|
510
425
|
GlyphImage image;
|
|
511
426
|
if (xt_text_rasterize(face, glyph.glyph, image) && image.width > 0 && image.height > 0) {
|
|
512
427
|
canvas_detail::blit_glyph(image_.pixels, image_.width, image_.height, image.pixels, image.width,
|
|
513
|
-
image.height, image.left, image.top, deviceX, deviceY,
|
|
428
|
+
image.height, image.left, image.top, deviceX, deviceY, paint,
|
|
514
429
|
state_.global_alpha);
|
|
515
430
|
}
|
|
516
431
|
penX += glyph.x_advance;
|
|
@@ -520,11 +435,11 @@ void Canvas2D::paint_text(const std::string &text, float x, float y, const float
|
|
|
520
435
|
}
|
|
521
436
|
|
|
522
437
|
void Canvas2D::fill_text(const std::string &text, float x, float y) {
|
|
523
|
-
paint_text(text, x, y,
|
|
438
|
+
paint_text(text, x, y, fill_paint(), false);
|
|
524
439
|
}
|
|
525
440
|
|
|
526
441
|
void Canvas2D::stroke_text(const std::string &text, float x, float y) {
|
|
527
|
-
paint_text(text, x, y,
|
|
442
|
+
paint_text(text, x, y, stroke_paint(), true);
|
|
528
443
|
}
|
|
529
444
|
|
|
530
445
|
void Canvas2D::draw_image(const Image &source, float dx, float dy, float dw, float dh, float sx,
|