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.
Files changed (98) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +15 -5
  3. package/README.zh-CN.md +11 -4
  4. package/dist/src/cli/hints.js +11 -0
  5. package/dist/src/cli/hints.js.map +1 -1
  6. package/dist/src/cli/main.js +1 -1
  7. package/dist/src/driver/compiler.js +1 -0
  8. package/dist/src/driver/compiler.js.map +1 -1
  9. package/dist/src/driver/version.d.ts +1 -1
  10. package/dist/src/driver/version.js +1 -1
  11. package/dist/src/extensions/catalog.js +2 -1
  12. package/dist/src/extensions/catalog.js.map +1 -1
  13. package/dist/src/extensions/media/index.d.ts +27 -0
  14. package/dist/src/extensions/media/index.js +60 -0
  15. package/dist/src/extensions/media/index.js.map +1 -0
  16. package/dist/src/extensions/registry.d.ts +12 -0
  17. package/dist/src/extensions/registry.js +16 -0
  18. package/dist/src/extensions/registry.js.map +1 -1
  19. package/dist/src/index.d.ts +1 -0
  20. package/dist/src/index.js +1 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/tests/cli/hints.test.js +15 -0
  23. package/dist/tests/cli/hints.test.js.map +1 -1
  24. package/dist/tests/cli/process.test.js +12 -0
  25. package/dist/tests/cli/process.test.js.map +1 -1
  26. package/dist/tests/e2e/gui-canvas.test.js +159 -0
  27. package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
  28. package/dist/tests/e2e/gui-example-canvas.test.d.ts +11 -0
  29. package/dist/tests/e2e/gui-example-canvas.test.js +72 -0
  30. package/dist/tests/e2e/gui-example-canvas.test.js.map +1 -0
  31. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  32. package/dist/tests/e2e/gui-helpers.js +2 -0
  33. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  34. package/dist/tests/e2e/gui-media-disabled.test.d.ts +9 -0
  35. package/dist/tests/e2e/gui-media-disabled.test.js +42 -0
  36. package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -0
  37. package/dist/tests/e2e/gui-media.test.d.ts +6 -4
  38. package/dist/tests/e2e/gui-media.test.js +15 -12
  39. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  40. package/dist/tests/e2e/unicode-output.test.d.ts +16 -0
  41. package/dist/tests/e2e/unicode-output.test.js +65 -0
  42. package/dist/tests/e2e/unicode-output.test.js.map +1 -0
  43. package/dist/tests/extensions/media.test.d.ts +1 -0
  44. package/dist/tests/extensions/media.test.js +58 -0
  45. package/dist/tests/extensions/media.test.js.map +1 -0
  46. package/dist/tests/extensions/registry.test.js +8 -0
  47. package/dist/tests/extensions/registry.test.js.map +1 -1
  48. package/dist/tests/lexer/identifiers.test.js +12 -0
  49. package/dist/tests/lexer/identifiers.test.js.map +1 -1
  50. package/dist/tests/lexer/scanner.test.js +8 -0
  51. package/dist/tests/lexer/scanner.test.js.map +1 -1
  52. package/dist/tests/lexer/strings.test.js +30 -0
  53. package/dist/tests/lexer/strings.test.js.map +1 -1
  54. package/doc/ai/cli.md +1 -1
  55. package/doc/ai/extensions.md +35 -22
  56. package/doc/ai/troubleshooting.md +9 -0
  57. package/doc/ai/zh-CN/cli.md +1 -1
  58. package/doc/ai/zh-CN/extensions.md +22 -9
  59. package/doc/ai/zh-CN/troubleshooting.md +8 -0
  60. package/doc/gui/build.md +37 -21
  61. package/doc/gui/canvas.md +47 -21
  62. package/doc/gui/known-issues.md +7 -6
  63. package/doc/gui/media.md +26 -11
  64. package/doc/gui.md +50 -34
  65. package/doc/zh-CN/gui/build.md +28 -16
  66. package/doc/zh-CN/gui/canvas.md +36 -17
  67. package/doc/zh-CN/gui/known-issues.md +6 -5
  68. package/doc/zh-CN/gui/media.md +21 -10
  69. package/doc/zh-CN/gui.md +43 -34
  70. package/package.json +2 -1
  71. package/runtime/ext_gui/canvas.cpp +114 -199
  72. package/runtime/ext_gui/canvas.h +101 -2
  73. package/runtime/ext_gui/canvas_gradient.cpp +112 -0
  74. package/runtime/ext_gui/canvas_internal.h +6 -6
  75. package/runtime/ext_gui/canvas_raster.cpp +27 -5
  76. package/runtime/ext_gui/canvas_stroke.cpp +252 -0
  77. package/runtime/ext_gui/canvas_style.cpp +240 -0
  78. package/runtime/ext_gui/dom_api.cpp +1 -0
  79. package/runtime/ext_gui/dom_api_canvas.cpp +38 -4
  80. package/runtime/ext_gui/dom_api_canvas_extra.cpp +232 -0
  81. package/runtime/ext_gui/dom_api_internal.h +31 -0
  82. package/runtime/ext_gui/dom_api_proto.cpp +24 -0
  83. package/runtime/ext_gui/gui_media.cpp +69 -2
  84. package/runtime/ext_gui/media.cpp +34 -488
  85. package/runtime/ext_gui/media.h +54 -19
  86. package/runtime/ext_gui/media_api.h +65 -0
  87. package/runtime/ext_media/media.cpp +507 -0
  88. package/runtime/ext_media/media_bridge.c +35 -0
  89. package/scripts/build-gui.ts +6 -72
  90. package/scripts/build-media.ts +245 -0
  91. package/src/cli/hints.ts +11 -0
  92. package/src/cli/main.ts +1 -1
  93. package/src/driver/compiler.ts +1 -0
  94. package/src/driver/version.ts +1 -1
  95. package/src/extensions/catalog.ts +2 -1
  96. package/src/extensions/media/index.ts +67 -0
  97. package/src/extensions/registry.ts +24 -0
  98. 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>`——一个 vendored 的最小 FFmpeg 在主线程解码,视频帧走图像
7
- 纹理路径、音频进入 SDL 流,DOM 媒体 API 暴露 `play`/`pause`/`currentTime`/…
8
- 与媒体事件。**M14** 增加了 `<canvas>`,配一个自研的 2D 软件光栅器(抗锯齿
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 | 第三方底层库**静态链接进 `gui.a`**,使发布版保持自包含;链接时只额外加入操作系统框架。 |
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,最小化构建) | 解码常用容器/编码;不依赖 WebView 或平台媒体框架 |
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`(可用 `SDL3_TAG` / `FREETYPE_VERSION` / `HARFBUZZ_VERSION` /
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`,因此它会随现有 runtime 归档
143
- 一起发布(`package-release` 复制整个 `runtime/`)。
144
- - `vendor/`(SDL3/FreeType/HarfBuzz,缓慢的部分)按 OS/arch 缓存,以
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>` 通过静态 vendored 的最小 FFmpeg 解码
193
- (`media.*`、`gui_media.cpp`),帧沿用现有图像管线上传到 GPU 纹理,
194
- 音频在主线程推送进 SDL 流,并在元素句柄上暴露 DOM 媒体 API 与
195
- `loadedmetadata`/`play`/`pause`/`ended`/`timeupdate`/`error` 事件
196
- (`dom_api_media.cpp`、`dom_api_proto.cpp`)。由 TS + CSS 控制;没有
197
- 内置控件。示例见 `examples/gui/media.*`,e2e 见
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`(必需),并在 Linux 的
253
- Xvfb 下运行示例与 GUI e2e 套件;`package` 发布 `gui.a`,`vendor/` 被缓存,
254
- `ar -M` 合并在 GNU/Linux 与 macOS 上可用。macOS 运行与所有 Windows 在 CI
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>` 支持。一个最小化的静态 FFmpeg 被 vendored
287
- 并合并进 `gui.a`(`scripts/build-gui.ts`),`media.*` 在主线程把视频解码为
288
- RGBA 帧、把音频解码为 48 kHz 立体声 PCM,`gui_media.cpp` 持有播放器并在每帧
289
- 推进媒体时钟与事件,`renderer_image.cpp` 把每个新视频帧沿图像纹理路径上传,
290
- `dom_api_media.cpp` 在元素句柄上暴露 `play`/`pause`/`load` 与媒体属性。
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.*`)。参见 [Canvas](./gui/canvas.md);e2e 见
300
- `tests/e2e/gui-canvas.test.ts`。
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.21",
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 * m.a + c * m.b;
184
- next.b = b * m.a + d * m.b;
185
- next.c = a * m.c + c * m.d;
186
- next.d = b * m.c + d * m.d;
187
- next.e = a * m.e + c * m.f + e;
188
- next.f = b * m.e + d * m.f + f;
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::arc(float cx, float cy, float radius, float start, float end, bool ccw) {
291
- CanvasSubpath arcPoints;
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{arcPoints.front()});
168
+ path_.push_back(CanvasSubpath{points.front()});
296
169
  } else {
297
170
  CanvasPoint last = path_.back().back();
298
- if (last.x != arcPoints.front().x || last.y != arcPoints.front().y) {
299
- path_.back().push_back(arcPoints.front());
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 < arcPoints.size(); i++) path_.back().push_back(arcPoints[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, state_.fill_color,
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, state_.stroke_color);
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
- state_.fill_color, state_.global_alpha);
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, state_.fill_color,
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, state_.stroke_color);
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 float color[4],
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, color,
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, state_.fill_color, false);
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, state_.stroke_color, true);
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,