xbintsc 0.5.41 → 0.5.43

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 (43) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/tests/e2e/gui-data-url.test.d.ts +10 -0
  5. package/dist/tests/e2e/gui-data-url.test.js +127 -0
  6. package/dist/tests/e2e/gui-data-url.test.js.map +1 -0
  7. package/dist/tests/e2e/gui-effects.test.js +144 -4
  8. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  9. package/dist/tests/e2e/gui-paint.test.js +77 -0
  10. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  11. package/doc/gui/api.md +1 -1
  12. package/doc/gui/canvas.md +6 -2
  13. package/doc/gui/known-issues.md +18 -10
  14. package/doc/gui/media.md +11 -2
  15. package/doc/gui/rendering.md +48 -19
  16. package/doc/gui.md +12 -8
  17. package/doc/zh-CN/gui/api.md +1 -1
  18. package/doc/zh-CN/gui/canvas.md +6 -4
  19. package/doc/zh-CN/gui/known-issues.md +12 -8
  20. package/doc/zh-CN/gui/media.md +9 -2
  21. package/doc/zh-CN/gui/rendering.md +37 -15
  22. package/doc/zh-CN/gui.md +7 -6
  23. package/package.json +7 -7
  24. package/runtime/ext_gui/dom_api_canvas.cpp +42 -24
  25. package/runtime/ext_gui/gradient_values.cpp +44 -48
  26. package/runtime/ext_gui/gui_events.cpp +16 -13
  27. package/runtime/ext_gui/image.cpp +180 -25
  28. package/runtime/ext_gui/image.h +6 -4
  29. package/runtime/ext_gui/paint.cpp +69 -4
  30. package/runtime/ext_gui/paint.h +57 -1
  31. package/runtime/ext_gui/paint_background.cpp +197 -41
  32. package/runtime/ext_gui/paint_effects.cpp +2 -1
  33. package/runtime/ext_gui/paint_internal.h +5 -1
  34. package/runtime/ext_gui/renderer.cpp +81 -13
  35. package/runtime/ext_gui/renderer.h +19 -5
  36. package/runtime/ext_gui/renderer_gradient.cpp +156 -0
  37. package/runtime/ext_gui/renderer_image.cpp +6 -1
  38. package/runtime/ext_gui/renderer_internal.h +48 -0
  39. package/runtime/ext_gui/renderer_layer.cpp +552 -0
  40. package/runtime/ext_gui/style.h +29 -1
  41. package/runtime/ext_gui/style_print.cpp +10 -2
  42. package/runtime/ext_gui/style_values.cpp +10 -0
  43. package/src/driver/version.ts +1 -1
@@ -15,12 +15,14 @@ up to two indices into the display list's triangulated `clip-path` polygons, so
15
15
  `overflow`/`clip-path` clipping and `z-index` ordering survive into the renderer.
16
16
  `DisplayList::dump()` feeds `paintList()`/`paintCount()`.
17
17
 
18
- `opacity` is honoured only at its extremes: a box whose computed `opacity` is
19
- `0` (and its whole subtree) paints nothing, and anything above `0` paints fully
20
- opaque. A shape carries its colour's alpha only, so there is no compositing of a
21
- partly transparent box — the `opacity` transitions the CSS parser accepts are
22
- therefore not visible either. Hiding and revealing a layer with `opacity: 0` on
23
- the hidden state is what `examples/gui/pelican-bike`'s game-over overlay does.
18
+ `opacity` multiplies the alpha of every colour painted by the box and its
19
+ descendants (the values multiply down the paint chain), so fractional opacity and
20
+ the `opacity` transitions the CSS parser accepts are visible. It is a
21
+ per-shape approximation, not an offscreen group: overlapping children of a
22
+ translucent box composite individually, whereas a browser composes the group
23
+ once and then applies the alpha. `opacity: 0` still skips the whole subtree.
24
+ Hiding and revealing a layer with `opacity: 0` on the hidden state is what
25
+ `examples/gui/pelican-bike`'s game-over overlay does.
24
26
 
25
27
  `runtime/ext_gui/renderer.{h,cpp}` turns that list into batched vertex buffers
26
28
  per window (shapes, glyph quads and images) and merges consecutive ops that
@@ -80,18 +82,31 @@ The window background (`setBackground`) is the render-pass clear colour.
80
82
  comma-separated layer list is painted bottom-up (the first declaration layer is
81
83
  topmost), and both gradient kinds are supported:
82
84
 
83
- - a **linear** gradient is sampled at 18 points and emitted as solid rectangles;
84
- `to <side>`, `deg`/`grad`/`rad`/`turn`, percentage and multi-stop colours work
85
- (the band walk is axis-aligned, so an oblique angle uses the nearest axis);
86
- - a **radial** gradient is 32 concentric ellipses. Each is a unit-radius rounded
87
- rectangle mapped through the shape pipeline's transform slot, so a circle or
88
- ellipse keeps the distance-field antialiasing. Shape/size keywords
85
+ - a **linear** gradient is sampled at 18 points (64 when repeating) and emitted
86
+ as solid rectangles; `to <side>`, `deg`/`grad`/`rad`/`turn`, percentage or
87
+ length (`px`/`em`/…) offsets, the `color <start> <end>` shorthand and
88
+ multi-stop colours work (the band walk is axis-aligned, so an oblique angle
89
+ uses the nearest axis);
90
+ - a **radial** gradient is resolved per pixel in `renderer_gradient.cpp` into one
91
+ RGBA texture that is drawn as a single image quad (`PaintGradient`). Nesting the
92
+ bands instead would composite their alpha once per band — since every band
93
+ covers the whole interior — so a vignette whose stops fade to `transparent`
94
+ painted an opaque disc over the scene. Shape/size keywords
89
95
  (`circle`/`ellipse`, `closest-side`/`farthest-side`/`closest-corner`/
90
- `farthest-corner`, explicit radii) and `at <position>` are parsed.
96
+ `farthest-corner`, explicit radii) and `at <position>` are parsed; a
97
+ keyword-sized ellipse keeps the box's aspect ratio and is scaled until it meets
98
+ the side or corner, as CSS does. Stops interpolate in premultiplied alpha, and
99
+ the last stop extends past the ending shape over the rest of the box.
100
+
101
+ `repeating-linear-gradient()` and `repeating-radial-gradient()` tile the stop
102
+ pattern from 0 to the last stop's offset across the whole box; the stop list is
103
+ not forced to end at 100%, so `red 0%, blue 10%` repeats every 10%. A normal
104
+ gradient whose last stop is below 100% holds that colour for the rest of the box,
105
+ as in CSS.
91
106
 
92
107
  The background colour sits under every layer. Gradients are partially
93
- transparent if their stops are, and the band approximation can show banding and
94
- alpha stacking on a very translucent multi-stop gradient.
108
+ transparent if their stops are; a radial gradient is exact per pixel, while a
109
+ linear one still samples 18 bands and can show banding on a large soft ramp.
95
110
 
96
111
  **Generated content** needs no new display-list kind: `::before`/`::after` are
97
112
  ordinary layout boxes owned by the per-restyle arena in `style.cpp`, so their
@@ -117,10 +132,24 @@ nesting keeps the outermost two.
117
132
  colour, so a box and its descendants see the filter. `blur()` and
118
133
  `drop-shadow()` are approximations at the display-list level: blur spreads nine
119
134
  translucent copies per shape, drop-shadow is a box/text silhouette. This keeps
120
- the single render pass — there is no offscreen render target — at the cost of
121
- group semantics: overlapping descendants of a blurred/filtered group can darken,
122
- and `backdrop-filter` cannot sample what is behind the element, so it parses and
123
- cascades but paints nothing.
135
+ the ordinary painter single-pass at the cost of group semantics: overlapping
136
+ descendants of a blurred/filtered group can darken, and `backdrop-filter` cannot
137
+ sample what is behind the element, so it parses and cascades but paints nothing.
138
+
139
+ **`mix-blend-mode`** is the exception that does need an offscreen layer. A box
140
+ with a non-`normal` mode is wrapped in `LayerBegin`/`LayerEnd` markers (visible
141
+ in `paintList()` as `layer blend=…` / `end-layer`); the renderer draws that group
142
+ to a full-viewport offscreen texture and composites it over the scene with a
143
+ small GLSL blend shader. All sixteen CSS modes (`multiply`, `screen`, `overlay`,
144
+ `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`,
145
+ `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`) are
146
+ implemented with the compositing-spec formulas. The shader is compiled at run
147
+ time by the bundled glslang (SPIR-V for Vulkan) and cross-compiled to MSL by
148
+ SPIRV-Cross (Metal); the two scene textures per nesting level are reused across
149
+ frames and ping-pong so the backdrop stays sampleable while it is written.
150
+ Backends that consume neither format — the D3D12/DXIL path, which has no shader
151
+ compiler here — keep painting the group with ordinary source-over blending.
152
+ `backdrop-filter` still paints nothing.
124
153
 
125
154
  ## Text stack (M4b)
126
155
 
package/doc/gui.md CHANGED
@@ -40,10 +40,11 @@ them) can be translated, scaled and rotated without reflowing anything — and
40
40
  [Animation](./gui/animation.md#css-transforms-m15).
41
41
  **M17** adds real **WebGL 1.0**, and **M18** adds **WebGL 2.0**: `<canvas>.getContext("webgl2")` accepts `#version 300 es` shaders, vertex-array objects, instanced draws and sampler objects, all compiled to SPIR-V by the bundled glslang (and to MSL by the bundled SPIRV-Cross on Metal) and rendered through SDL_GPU. Geometry, lighting, texture sampling and blending run on the GPU and the page composites the colour texture without a readback — see [WebGL](./gui/webgl.md).
42
42
  **M19** fills in the decorative CSS layer: `@media` queries, generated content
43
- (`::before`/`::after`), multi-layer linear/radial backgrounds, `text-shadow`,
44
- `clip-path` and the `filter` colour/shadow effects. `backdrop-filter` and true
45
- group blur need an offscreen compositor and remain documented approximations or
46
- unsupported — see [Rendering](./gui/rendering.md#backgrounds-generated-content-and-effects-m19).
43
+ (`::before`/`::after`), multi-layer linear/radial/repeating backgrounds,
44
+ `text-shadow`, `clip-path`, `mix-blend-mode` (through an offscreen compositor)
45
+ and the `filter` colour/shadow effects. `backdrop-filter` still needs a backdrop
46
+ snapshot, and true group blur remains an approximation — see
47
+ [Rendering](./gui/rendering.md#backgrounds-generated-content-and-effects-m19).
47
48
  **M20** makes large documents interactive: `@media` is evaluated once per rule,
48
49
  layout and paint share a bounded shaping cache, and a frame only does (and
49
50
  presents) the work a change requires — see
@@ -335,8 +336,9 @@ engine were Rust.
335
336
  - Generated content: `::before`/`::after` selectors, inherited pseudo styles,
336
337
  `content` strings/escapes/`attr()`, and synthetic layout boxes that never
337
338
  leak into DOM traversal.
338
- - Multi-layer `background-image` with linear and radial gradients, painted
339
- bottom-up as solid bands (18 strips / 32 concentric ellipses).
339
+ - Multi-layer `background-image` with linear gradients painted bottom-up as
340
+ solid bands (18 strips) and radial gradients rasterised per pixel into one
341
+ image texture (`renderer_gradient.cpp`).
340
342
  - `text-shadow` as shadow text runs, including a nine-tap blur approximate.
341
343
  - `clip-path` polygons (`inset`/`circle`/`ellipse`/`polygon`) resolved to
342
344
  device space, ear-clipped and intersected with shape geometry in
@@ -502,8 +504,10 @@ engine were Rust.
502
504
  pseudo styles into a per-restyle arena, resolves `content`, and layout inserts
503
505
  their boxes before/after the DOM children. `gradient_values.cpp` models a
504
506
  background layer list (linear/radial, shape/size/position/stops),
505
- `paint_background.cpp` paints it, `paint_effects.cpp` applies filter colour
506
- functions plus nine-tap blur/drop-shadow and emits `text-shadow` runs, and
507
+ `paint_background.cpp` resolves and paints it (radial layers become one
508
+ `PaintGradient`, which `renderer_gradient.cpp` rasterises into a texture),
509
+ `paint_effects.cpp` applies filter colour functions plus nine-tap
510
+ blur/drop-shadow and emits `text-shadow` runs, and
507
511
  `paint_clip.cpp`/`renderer_clip.cpp` clip geometry against triangulated
508
512
  `clip-path` polygons. E2E: `tests/e2e/gui-effects.test.ts`.
509
513
  - **M20** ✅ rendering performance. `@media` is evaluated once per rule per
@@ -175,7 +175,7 @@ win.queryCount(selector) // 匹配元素的数量
175
175
  win.getBoundingClientRect(selector) // { x, y, width, height }(边框盒,含变换)
176
176
  win.documentTree() // 序列化的 DOM(调试用)
177
177
  win.layoutTree() // 序列化的布局盒(调试用)
178
- win.paintCount() // 显示列表中图形的数量
178
+ win.paintCount() // 显示列表中填充矩形的数量
179
179
  win.paintList() // 序列化的显示列表(调试用)
180
180
  win.inlineSvg(selector) // 序列化的内联 <svg>:{ markup, width, height, shapes }
181
181
  win.measureText(text, fontSize?, family?) // 整形后的推进宽度(像素)
@@ -166,8 +166,10 @@ canvas 由脚本重绘而不是引擎重绘,因此开销按绘制调用计。
166
166
  9 参数(本引擎为**目标优先**:`dx, dy, dw, dh, sx, sy, sw, sh`)。完整的仿射变换
167
167
  (旋转/斜切/缩放)会被应用:目标矩形变为平行四边形,每个设备像素反查到源矩形。
168
168
  `source` 可以是 `<img>` 元素(经图像模块解码)、另一个 `<canvas>`、
169
- `OffscreenCanvas` 或 `ImageBitmap`。`imageSmoothingEnabled` 为 `true`(默认)时使用
170
- 双线性采样,为 `false` 时使用最近邻。
169
+ `OffscreenCanvas`、`ImageBitmap`,或——本引擎的扩展——直接传字符串路径/`data:` URL,
170
+ 由图像模块解码,因此 SVG 或位图的 data URL 无需 `<img>` 就能绘制。矢量源按目标尺寸
171
+ (`dw`/`dh`)光栅化,放大时 SVG 依然清晰。`imageSmoothingEnabled` 为 `true`(默认)
172
+ 时使用双线性采样,为 `false` 时使用最近邻。
171
173
 
172
174
  测试钩子:`getImageData(x, y, w, h)` 把区域复制为 `{ width, height, data }`,其中
173
175
  `data` 是按行主序的 RGBA8 字节数组。越界像素为透明。
@@ -186,8 +188,8 @@ dirtyWidth, dirtyHeight])` 直接把字节写入位图(不受变换、混合
186
188
  `OffscreenCanvas.convertToBlob()` 解析为同一形状,`transferToImageBitmap()` 对位图
187
189
  做快照。
188
190
 
189
- CSS 背景:`background-image: url(...)` 接受文件路径或 `data:` URL,因此可以先把
190
- canvas 导出再作为 CSS 背景使用
191
+ CSS 背景:`background-image: url(...)` 接受文件路径或 `data:` URL(位图载荷或 SVG
192
+ 文档,base64 或百分号编码均可),因此可以先把 canvas 导出再作为 CSS 背景使用
191
193
  (`element.style.backgroundImage = "url(" + canvas.toDataURL() + ")"`)。图像层会拉伸
192
194
  到元素的边框盒;`background-size`、`background-position` 与平铺尚未建模。
193
195
 
@@ -43,22 +43,26 @@
43
43
  `var()` 自定义属性、`inset` 简写、`box-sizing`、`visibility`/`pointer-events`、
44
44
  `@media` 查询、生成内容(`::before`/`::after`)、线性/径向多层背景、
45
45
  `text-shadow`、`clip-path`(`inset`/`circle`/`ellipse`/`polygon`)以及 `filter`
46
- 的颜色函数。仍有限制:`mix-blend-mode`、`background-clip`、
47
- `background-position`/`size`/`repeat`、重复渐变与 `backdrop-filter` 合成都未实现
48
- (`backdrop-filter` 会解析并参与层叠,但采样背景需要离屏图层,因此不绘制);
46
+ 的颜色函数。仍有限制:`background-clip`、`background-position`/`size`/`repeat`
47
+ 与 `backdrop-filter` 合成都未实现(`backdrop-filter` 会解析并参与层叠,但采样
48
+ 背景需要离屏图层,因此不绘制);`mix-blend-mode` 与重复渐变已实现(见
49
+ [渲染](./rendering.md)),但组 `opacity` 是按图形 alpha 近似而非真正的组合成,
50
+ 且 D3D12/DXIL 后端没有混合合成器,那里会退回普通 source-over 绘制;
49
51
  `filter: blur()` 是按图形做九次偏移的近似,而不是真正的组模糊,重叠的子元素会加深;
50
52
  `drop-shadow` 用盒子轮廓近似;最多只交叉两层嵌套 `clip-path`(更深时保留外侧两层);
51
53
  图像/画布除图像着色器能携带的亮度与透明度染色外不做颜色滤镜。Canvas 纹理使用
52
- 最近邻采样。背景渐变仍是纯色带(线性 18 条、径向 32 个同心椭圆),柔和渐变可能
53
- 出现轻微条带。
54
+ 最近邻采样。背景线性渐变仍是纯色带(18 条,重复渐变 64 条),较大的柔和渐变
55
+ 可能出现轻微条带;径向渐变则逐像素光栅化成纹理(每边最大 1024 px,超出后拉伸,
56
+ 因此超大盒子上的硬色标会略微变软)。
54
57
  - **SVG 是 NanoSVG 的子集。** SVG *文件*(`<img src="logo.svg">`)与内联
55
58
  `<svg>` 子树共用 vendored 的 NanoSVG 解析/光栅化器(`runtime/ext_gui/svg.cpp`、
56
59
  `inline_svg.cpp`),它会渲染 `path`/`rect`/`circle`/`ellipse`/`line`/
57
60
  `polyline`/`polygon`、组、`defs` 渐变以及基于 `class` 的 `<style>` 规则。
58
61
  `<text>`、`<use>`、滤镜、遮罩与裁剪路径不会渲染,文档层叠的 CSS 也不会进入
59
- 图像内部(这里的 `fill`/`stroke` 是 SVG 表现属性,不是 CSS 属性)。需要这些
60
- 能力的矢量美术必须预先光栅化、用盒子绘制,或限制在受支持的语法内。参见
61
- [媒体](./media.md#图像m6a)。
62
+ 图像内部(这里的 `fill`/`stroke` 是 SVG 表现属性,不是 CSS 属性)。因此只靠滤镜
63
+ 出图的 SVG(`feTurbulence` 噪声、模糊等)会显示未经过滤的形状——常见情况是一个
64
+ 纯色矩形——而不是滤镜结果。需要这些能力的矢量美术必须预先光栅化、用盒子绘制,
65
+ 或限制在受支持的语法内。参见[媒体](./media.md#图像m6a)。
62
66
  - **每次变更仍是整文档重算。** 一次 DOM 变更会重新计算层叠并重新布局整个文档
63
67
  (没有增量子树失效),只影响绘制的变更(例如 hover 颜色)同样会重排。带生成
64
68
  内容(`::before`/`::after`)的文档没有特殊处理。各阶段的实测开销由
@@ -35,6 +35,13 @@ PNG/JPEG/BMP/GIF/TGA 解码为 RGBA8,并带有解码缓存与仅头文件的
35
35
  (最近最少使用),动画文档不会让它无限增长。
36
36
  - 解析结果按路径缓存;解析与光栅化器状态在 `xt_svg_shutdown`
37
37
  (`xt_image_shutdown`)中释放。
38
+ - `data:` URL 在内存中解码,凡接受路径的地方都可使用:`<img src>`、CSS
39
+ `background-image` 与 `canvas.drawImage(url, …)`。既支持 `;base64` 形式,也支持
40
+ 百分号编码(或原样)的载荷,`base64` 标志大小写不敏感。MIME 类型只用于分类:
41
+ `image/svg+xml`(或 MIME 为通用/缺失时按内容嗅探出 SVG)走 NanoSVG——data URL
42
+ 里的 SVG 与 SVG 文件一样按绘制尺寸光栅化;PNG/JPEG/BMP/GIF/TGA 走
43
+ `stb_image`。NanoSVG 的子集限制仍然生效:只靠 `<filter>`(`feTurbulence` 等)
44
+ 出图的 SVG 能解码、能布局,但滤镜本身会被忽略。
38
45
 
39
46
  文档 CSS 只作用于 `<svg>` **盒子**——尺寸、外边距、背景、边框、`transform`、
40
47
  opacity、`display`——与任何其他元素一样。盒子内部的艺术内容保持 SVG 语义:
@@ -45,8 +52,8 @@ opacity、`display`——与任何其他元素一样。盒子内部的艺术内
45
52
  重写艺术内容的程序会为每个变体保留一张纹理;更好的做法是用 `transform`、
46
53
  `opacity` 给盒子做动画,或在一小组文档之间切换。
47
54
 
48
- 图像仍需完成:CSS `background-image: url(...)`、`data:` URI、`object-fit`、
49
- 九宫格边框。
55
+ 图像仍需完成:`object-fit`、九宫格边框。(CSS `background-image: url(...)` 与
56
+ `data:` URL 已支持。)
50
57
 
51
58
  两种 SVG 都受限于 NanoSVG 的子集:`path`、`rect`、`circle`、`ellipse`、
52
59
  `line`、`polyline`、`polygon`、组(`g`)、带线性/径向渐变的 `defs` 以及基于
@@ -15,9 +15,10 @@
15
15
  顺序都能保留到渲染器。`DisplayList::dump()` 为 `paintList()`/`paintCount()`
16
16
  提供数据。
17
17
 
18
- `opacity` 只在两端生效:计算值为 `0` 的盒子(及其整棵子树)不绘制任何内容,
19
- 大于 `0` 的一律按完全不透明绘制。图形只携带其颜色自身的 alpha,因此没有部分
20
- 透明盒子的合成——CSS 解析器接受的 `opacity` 过渡因此也看不到效果。把隐藏态写成
18
+ `opacity` 会乘到该盒子及其后代绘制的每个颜色 alpha 上(沿绘制链逐层相乘),
19
+ 因此小数 opacity 以及 CSS 解析器接受的 `opacity` 过渡都能看到效果。这是按图形
20
+ 逐个近似的,而不是离屏分组:半透明盒子的重叠子元素会各自合成,浏览器则先把整组
21
+ 合成一次再应用 alpha。`opacity: 0` 仍会跳过整棵子树。把隐藏态写成
21
22
  `opacity: 0` 来隐藏/显示一层,正是 `examples/gui/pelican-bike` 游戏结束遮罩的做法。
22
23
 
23
24
  `runtime/ext_gui/renderer.{h,cpp}` 把该列表转换为每个窗口的批处理顶点缓冲
@@ -67,16 +68,25 @@
67
68
  `background-color` 与每一条 `background-image` 层都会绘制。逗号分隔的层列表按
68
69
  从底到上的顺序绘制(声明中的第一层在最上面),两种渐变均受支持:
69
70
 
70
- - **线性**渐变在 18 个位置采样并输出为纯色矩形;支持 `to <side>`、
71
- `deg`/`grad`/`rad`/`turn`、百分比与多色标(色带沿最近轴行走,因此斜角会退化为
71
+ - **线性**渐变在 18 个位置采样(重复渐变 64 个)并输出为纯色矩形;支持
72
+ `to <side>`、`deg`/`grad`/`rad`/`turn`、百分比或长度(`px`/`em`/…)偏移、
73
+ `color <start> <end>` 简写与多色标(色带沿最近轴行走,因此斜角会退化为
72
74
  最近的轴向);
73
- - **径向**渐变是 32 个同心椭圆。每个都是单位半径的圆角矩形,经图形管线的
74
- transform 槽映射,因此圆/椭圆仍保留距离场抗锯齿。支持形状/尺寸关键字
75
- (`circle`/`ellipse`、`closest-side`/`farthest-side`/`closest-corner`/
76
- `farthest-corner`、显式半径)与 `at <position>`。
77
-
78
- 背景色位于所有层下方。若渐变色标带透明度,渐变也会半透明;色带近似可能产生
79
- 轻微条带,非常透明的多色标渐变会出现 alpha 叠加。
75
+ - **径向**渐变由 `renderer_gradient.cpp` 逐像素解算成一张 RGBA 纹理,作为单个
76
+ 图像四边形绘制(`PaintGradient`)。若沿用嵌套色带,由于每条色带都会覆盖整个
77
+ 内部区域,alpha 会叠加多次——淡出到 `transparent` 的暗角会把整个场景涂成实心
78
+ 黑盘。支持形状/尺寸关键字(`circle`/`ellipse`、`closest-side`/`farthest-side`/
79
+ `closest-corner`/`farthest-corner`、显式半径)与 `at <position>`;关键字尺寸
80
+ 的椭圆保持盒子的宽高比并缩放到刚好接触对应边或角,与 CSS 一致。色标在预乘
81
+ alpha 空间中插值,最后一个色标会延伸覆盖结束形状之外的剩余盒子。
82
+
83
+ `repeating-linear-gradient()` 与 `repeating-radial-gradient()` 会在整个盒子上
84
+ 从 0 平铺到最后一个色标的偏移量;色标列表不会被强制结束在 100%,因此
85
+ `red 0%, blue 10%` 每 10% 重复一次。普通渐变的最后一个色标若小于 100%,则剩余
86
+ 区域保持该颜色,与 CSS 一致。
87
+
88
+ 背景色位于所有层下方。若渐变色标带透明度,渐变也会半透明;径向渐变逐像素精确,
89
+ 线性渐变仍采样 18 条色带,较大的柔和渐变可能出现条带。
80
90
 
81
91
  **生成内容**不需要新的显示列表类型:`::before`/`::after` 是 `style.cpp` 中
82
92
  每次重算样式时重建的普通布局盒,因此其背景、边框、文本与 transform 都走同一条
@@ -96,9 +106,21 @@ shader 改动,但边缘是硬边(没有解析抗锯齿)。支持两层嵌
96
106
  **`filter`** 的颜色函数(`brightness`、`contrast`、`grayscale`、`sepia`、
97
107
  `invert`、`saturate`、`hue-rotate`、`opacity`)会折进每个绘制颜色,因此盒子与
98
108
  其后代都会看到滤镜。`blur()` 与 `drop-shadow()` 是显示列表层面的近似:模糊把每个
99
- 图形展开成九个半透明副本,投影用盒子/文字轮廓。这样保持单渲染通道(没有离屏
100
- 渲染目标),代价是组语义:被模糊/滤镜的组内重叠后代可能加深;`backdrop-filter`
101
- 无法采样元素背后的内容,因此会解析并参与层叠,但不绘制。
109
+ 图形展开成九个半透明副本,投影用盒子/文字轮廓。这样普通绘制保持单渲染通道,代价
110
+ 是组语义:被模糊/滤镜的组内重叠后代可能加深;`backdrop-filter` 无法采样元素背后
111
+ 的内容,因此会解析并参与层叠,但不绘制。
112
+
113
+ **`mix-blend-mode`** 是唯一真正需要离屏图层的例外。非 `normal` 的盒子会被
114
+ `LayerBegin`/`LayerEnd` 标记包住(在 `paintList()` 中显示为 `layer blend=…` /
115
+ `end-layer`),渲染器把该组绘制到全视口离屏纹理,再用一个小型 GLSL 混合
116
+ shader 合成到场景上。全部十六种 CSS 模式(`multiply`、`screen`、`overlay`、
117
+ `darken`、`lighten`、`color-dodge`、`color-burn`、`hard-light`、`soft-light`、
118
+ `difference`、`exclusion`、`hue`、`saturation`、`color`、`luminosity`)都按
119
+ 合成规范公式实现。shader 在运行时由内置 glslang 编译(Vulkan 用 SPIR-V),
120
+ 再由 SPIRV-Cross 交叉编译为 MSL(Metal);每个嵌套层级的两张场景纹理跨帧复用,
121
+ 并通过 ping-pong 让背景在写入时仍可采样。既不吃 SPIR-V 也不吃 MSL 的后端
122
+ (这里的 D3D12/DXIL 路径没有 shader 编译器)仍用普通 source-over 混合绘制该组。
123
+ `backdrop-filter` 依旧不绘制。
102
124
 
103
125
  ## 文本技术栈(M4b)
104
126
 
package/doc/zh-CN/gui.md CHANGED
@@ -12,7 +12,7 @@
12
12
  渲染器,因此盒子(以及其中的文本、图像与子元素)可以被平移、缩放与旋转而
13
13
  不触发任何重排——再加上 `transition: transform`,这些姿态可以动画(自绘的示例
14
14
  `examples/gui/pelican-svg` 正是用它让矢量画动起来)。参见
15
- [布局](./gui/layout.md#变换m15)与[动画](./gui/animation.md#css-transformm15)。**M17** 增加了真正的 **WebGL 1.0**,**M18** 增加了 **WebGL 2.0**:`<canvas>.getContext("webgl2")` 接受 `#version 300 es` 着色器、顶点数组对象、实例化绘制与采样器对象,全部由内置 glslang 编译为 SPIR-V(Metal 上再由内置 SPIRV-Cross 转成 MSL)并通过 SDL_GPU 渲染,几何、光照、纹理采样与混合都在 GPU 上完成,页面直接合成颜色纹理而无须读回——见 [WebGL](./gui/webgl.md)。**M19** 补齐 CSS 装饰层:`@media` 查询、生成内容(`::before`/`::after`)、线性/径向多层背景、`text-shadow`、`clip-path` 与 `filter` 颜色/阴影效果。`backdrop-filter` 与真正的组模糊需要离屏合成器,仍为有文档说明的近似或未支持——见[绘制](./gui/rendering.md#背景生成内容与特效m19)。**M20** 让大文档保持可交互:`@media` 每条规则只求值一次,布局与绘制共享一个有界的整形缓存,一帧只做(也只提交)变更所要求的工作——见[性能](./gui/rendering.md#性能m20)。本总览记录了锁定决策、架构、里程碑计划与当前进度;引擎本身按功能拆分在以上各页中。
15
+ [布局](./gui/layout.md#变换m15)与[动画](./gui/animation.md#css-transformm15)。**M17** 增加了真正的 **WebGL 1.0**,**M18** 增加了 **WebGL 2.0**:`<canvas>.getContext("webgl2")` 接受 `#version 300 es` 着色器、顶点数组对象、实例化绘制与采样器对象,全部由内置 glslang 编译为 SPIR-V(Metal 上再由内置 SPIRV-Cross 转成 MSL)并通过 SDL_GPU 渲染,几何、光照、纹理采样与混合都在 GPU 上完成,页面直接合成颜色纹理而无须读回——见 [WebGL](./gui/webgl.md)。**M19** 补齐 CSS 装饰层:`@media` 查询、生成内容(`::before`/`::after`)、线性/径向/重复多层背景、`text-shadow`、`clip-path`、`mix-blend-mode`(通过离屏合成器)与 `filter` 颜色/阴影效果。`backdrop-filter` 仍需要背景快照,真正的组模糊也仍是近似——见[绘制](./gui/rendering.md#背景生成内容与特效m19)。**M20** 让大文档保持可交互:`@media` 每条规则只求值一次,布局与绘制共享一个有界的整形缓存,一帧只做(也只提交)变更所要求的工作——见[性能](./gui/rendering.md#性能m20)。本总览记录了锁定决策、架构、里程碑计划与当前进度;引擎本身按功能拆分在以上各页中。
16
16
 
17
17
  ## 目录
18
18
 
@@ -262,8 +262,8 @@ FFmpeg `7.1`(供可选的 `media` 归档使用;可用 `SDL3_TAG` /
262
262
  特性);`matchMedia()` 使用同一个求值器。
263
263
  - 生成内容:`::before`/`::after` 选择器、继承的伪样式、`content` 字符串/
264
264
  转义/`attr()`,以及不会泄漏到 DOM 遍历的合成布局盒。
265
- - 多层 `background-image` 的线性与径向渐变,自下而上按纯色带绘制
266
- (线性 18 条、径向 32 个同心椭圆)。
265
+ - 多层 `background-image`:线性渐变自下而上按纯色带绘制(18 条),径向渐变
266
+ 逐像素光栅化成一张图像纹理(`renderer_gradient.cpp`)。
267
267
  - `text-shadow` 作为阴影文本 run,并带九次采样模糊近似。
268
268
  - `clip-path` 多边形(`inset`/`circle`/`ellipse`/`polygon`)解析到设备空间,
269
269
  用耳切法三角化,并在 `renderer_clip.cpp` 中与图形几何求交——不需要模板/
@@ -408,9 +408,10 @@ FFmpeg `7.1`(供可选的 `media` 归档使用;可用 `SDL3_TAG` /
408
408
  识别 `::before`/`::after`。`style.cpp` 把伪样式算进每次重算样式时重建的
409
409
  arena、解析 `content`,布局在真实子元素前后插入它们的盒子。
410
410
  `gradient_values.cpp` 建模背景层列表(线性/径向、形状/尺寸/位置/色标),
411
- `paint_background.cpp` 绘制它,`paint_effects.cpp` 应用滤镜颜色函数与九次
412
- 采样模糊/投影并生成 `text-shadow` run,`paint_clip.cpp`/`renderer_clip.cpp`
413
- 对三角化的 `clip-path` 多边形裁剪几何。e2e 见
411
+ `paint_background.cpp` 解算并绘制它(径向层变成一个 `PaintGradient`,由
412
+ `renderer_gradient.cpp` 光栅化成纹理),`paint_effects.cpp` 应用滤镜颜色函数
413
+ 与九次采样模糊/投影并生成 `text-shadow` run,`paint_clip.cpp`/
414
+ `renderer_clip.cpp` 对三角化的 `clip-path` 多边形裁剪几何。e2e 见
414
415
  `tests/e2e/gui-effects.test.ts`。
415
416
  - **M20** ✅ 渲染性能。`@media` 每次重算样式时每条规则只求值一次;没有
416
417
  `::before`/`::after` 的样式表完全跳过生成内容;只有样式表包含
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xbintsc",
3
- "version": "0.5.41",
3
+ "version": "0.5.43",
4
4
  "description": "xbintsc - a TypeScript binary compiler (TypeScript -> LLVM IR -> native binary)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -72,12 +72,12 @@
72
72
  "vitest": "^2.1.8"
73
73
  },
74
74
  "optionalDependencies": {
75
- "@xbintsc/linux-x64": "0.5.41",
76
- "@xbintsc/linux-arm64": "0.5.41",
77
- "@xbintsc/darwin-x64": "0.5.41",
78
- "@xbintsc/darwin-arm64": "0.5.41",
79
- "@xbintsc/win32-x64": "0.5.41",
80
- "@xbintsc/win32-arm64": "0.5.41"
75
+ "@xbintsc/linux-x64": "0.5.43",
76
+ "@xbintsc/linux-arm64": "0.5.43",
77
+ "@xbintsc/darwin-x64": "0.5.43",
78
+ "@xbintsc/darwin-arm64": "0.5.43",
79
+ "@xbintsc/win32-x64": "0.5.43",
80
+ "@xbintsc/win32-arm64": "0.5.43"
81
81
  },
82
82
  "engines": {
83
83
  "node": ">=22"
@@ -468,41 +468,59 @@ METHOD(canvas_draw_image) {
468
468
  (void)env;
469
469
  xtgui::Canvas2D *canvas = context_canvas(self);
470
470
  if (canvas == nullptr || argc < 3) return XT_UNDEFINED;
471
+
472
+ const bool has_destination = argc >= 5;
473
+ const float dx = (float)number_arg(argc, argv, 1, 0.0);
474
+ const float dy = (float)number_arg(argc, argv, 2, 0.0);
475
+ const float destination_width =
476
+ has_destination ? (float)number_arg(argc, argv, 3, 0.0) : 0.0f;
477
+ const float destination_height =
478
+ has_destination ? (float)number_arg(argc, argv, 4, 0.0) : 0.0f;
479
+ auto raster_target = [](float value) -> int {
480
+ return value > 0.0f ? (int)(value + 0.5f) : 0;
481
+ };
482
+
471
483
  XtGuiWindow *win = nullptr;
472
484
  xt_value source_value = xt_arg(argc, argv, 0);
473
485
  const xtgui::Image *image = nullptr;
474
- /* An `OffscreenCanvas` (or a transferred one) has no DOM node; its bitmap is
475
- * reachable through the process-wide canvas id. */
476
- if (XT_IS_OBJECT(source_value)) {
477
- xt_value canvas_id = xt_object_get_cstr(source_value, "__xt_gui_canvas");
478
- if (XT_IS_NUMBER(canvas_id)) {
479
- xtgui::Canvas2D *offscreen = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
480
- if (offscreen != nullptr) {
481
- image = &offscreen->image();
486
+ if (XT_IS_STRING(source_value)) {
487
+ /* Engine extension: the source may be a path or a `data:` URL itself, so a
488
+ * data-URL bitmap/SVG can be drawn without an `<img>` element. */
489
+ const std::string source_url(xt_string_data(source_value));
490
+ image = xtgui::xt_image_load(source_url, raster_target(destination_width),
491
+ raster_target(destination_height));
492
+ } else {
493
+ /* An `OffscreenCanvas` (or a transferred one) has no DOM node; its bitmap
494
+ * is reachable through the process-wide canvas id. */
495
+ if (XT_IS_OBJECT(source_value)) {
496
+ xt_value canvas_id = xt_object_get_cstr(source_value, "__xt_gui_canvas");
497
+ if (XT_IS_NUMBER(canvas_id)) {
498
+ xtgui::Canvas2D *offscreen = xtgui::xt_canvas_by_id((uint64_t)xt_to_number(canvas_id));
499
+ if (offscreen != nullptr) image = &offscreen->image();
500
+ }
501
+ }
502
+ if (image == nullptr) {
503
+ xtgui::Node *source = resolve_node(source_value, &win);
504
+ if (source != nullptr && source->isElement() && source->isTag("img")) {
505
+ const std::string *src = source->attr("src");
506
+ if (src != nullptr) {
507
+ image = xtgui::xt_image_load(*src, raster_target(destination_width),
508
+ raster_target(destination_height));
509
+ }
510
+ } else if (source != nullptr) {
511
+ xtgui::Canvas2D *other = xt_gui_canvas_find(win, source);
512
+ if (other != nullptr) image = &other->image();
482
513
  }
483
514
  }
484
- }
485
- xtgui::Node *source = resolve_node(source_value, &win);
486
- if (source != nullptr && source->isElement() && source->isTag("img")) {
487
- const std::string *src = source->attr("src");
488
- if (src != nullptr) image = xtgui::xt_image_load(*src);
489
- } else if (source != nullptr) {
490
- xtgui::Canvas2D *other = xt_gui_canvas_find(win, source);
491
- if (other != nullptr) image = &other->image();
492
515
  }
493
516
  if (image == nullptr || image->width <= 0 || image->height <= 0) return XT_UNDEFINED;
494
- float dx = (float)number_arg(argc, argv, 1, 0.0);
495
- float dy = (float)number_arg(argc, argv, 2, 0.0);
496
- float dw = (float)image->width;
497
- float dh = (float)image->height;
517
+
518
+ const float dw = has_destination ? destination_width : (float)image->width;
519
+ const float dh = has_destination ? destination_height : (float)image->height;
498
520
  float sx = 0.0f;
499
521
  float sy = 0.0f;
500
522
  float sw = (float)image->width;
501
523
  float sh = (float)image->height;
502
- if (argc >= 5) {
503
- dw = (float)number_arg(argc, argv, 3, (double)dw);
504
- dh = (float)number_arg(argc, argv, 4, (double)dh);
505
- }
506
524
  if (argc >= 9) {
507
525
  sx = (float)number_arg(argc, argv, 5, 0.0);
508
526
  sy = (float)number_arg(argc, argv, 6, 0.0);
@@ -134,67 +134,59 @@ bool parseGradientColor(const std::string &token, Color *color, std::string *res
134
134
  return true;
135
135
  }
136
136
 
137
- /** Parse one `[<percent>]` / `<number>` offset. */
138
- bool parseGradientOffset(const std::string &raw, float *out) {
137
+ /** Parse one gradient stop offset: a percentage, a length (`px`, `em`, …) or a
138
+ * bare number (0.5 = 50%, 50 = 50%). */
139
+ bool parseGradientOffset(const std::string &raw, Length *out) {
139
140
  std::string text = xt_css_trim(raw);
140
141
  if (text.empty()) return false;
141
- bool percent = text.back() == '%';
142
- if (percent) text.pop_back();
142
+ if (text.back() == '%') {
143
+ float value = 0;
144
+ if (!xt_css_parse_number(text.substr(0, text.size() - 1), &value)) return false;
145
+ *out = Length{value, Unit::Percent};
146
+ return true;
147
+ }
143
148
  float value = 0;
144
- if (!xt_css_parse_number(text, &value)) return false;
145
- if (percent) value /= 100.0f;
146
- else if (value > 1.0f) value /= 100.0f;
147
- if (value < 0) value = 0;
148
- if (value > 1) value = 1;
149
- *out = value;
150
- return true;
149
+ if (xt_css_parse_number(text, &value)) {
150
+ /* Unitless offsets are fractions of the gradient line, matching the old
151
+ * parser: `0.5` is 50% and `50` is also 50%. */
152
+ if (value > 1.0f) *out = Length{value, Unit::Percent};
153
+ else *out = Length{value * 100.0f, Unit::Percent};
154
+ return true;
155
+ }
156
+ Length length = xt_css_parse_length(text);
157
+ if (length.unit != Unit::Invalid && length.unit != Unit::Auto &&
158
+ length.unit != Unit::None && length.unit != Unit::Number) {
159
+ *out = length;
160
+ return true;
161
+ }
162
+ return false;
151
163
  }
152
164
 
153
- /** Parse a comma-separated `[color [offset [offset]]]` stop list. */
165
+ /** Parse a comma-separated `[color [offset [offset]]]` stop list. Offsets stay
166
+ * unresolved here: `px` is resolved against the gradient line in paint. */
154
167
  bool parseStops(const std::string &text, std::vector<BackgroundGradientStop> *stops) {
155
168
  std::vector<std::string> parts = splitCommas(text);
156
169
  if (parts.size() < 2) return false;
157
- float last = -1.0f;
158
170
  for (const std::string &part : parts) {
159
171
  Color color;
160
172
  std::string rest;
161
173
  if (!parseGradientColor(part, &color, &rest)) continue;
162
174
  BackgroundGradientStop stop;
163
175
  stop.color = color;
164
- stop.offset = -1.0f;
165
- if (!rest.empty()) {
166
- std::vector<std::string> offsets = splitTokens(rest);
167
- if (!offsets.empty()) {
168
- float first = 0;
169
- if (parseGradientOffset(offsets[0], &first)) {
170
- stop.offset = first;
171
- if (stop.offset < last) stop.offset = last;
172
- last = stop.offset;
173
- }
176
+ std::vector<std::string> offsets = splitTokens(rest);
177
+ if (!offsets.empty() && parseGradientOffset(offsets[0], &stop.offset)) {
178
+ /* `color <start> <end>` is shorthand for the same colour at both offsets
179
+ * (a hard band), so emit the second stop too. */
180
+ stops->push_back(stop);
181
+ if (offsets.size() > 1) {
182
+ BackgroundGradientStop end = stop;
183
+ if (parseGradientOffset(offsets[1], &end.offset)) stops->push_back(end);
174
184
  }
185
+ } else {
186
+ stops->push_back(stop);
175
187
  }
176
- stops->push_back(stop);
177
- }
178
- if (stops->size() < 2) return false;
179
- /* Fill implicit offsets by distributing them between their neighbours. */
180
- size_t i = 0;
181
- while (i < stops->size()) {
182
- if ((*stops)[i].offset >= 0) {
183
- i++;
184
- continue;
185
- }
186
- size_t j = i;
187
- while (j < stops->size() && (*stops)[j].offset < 0) j++;
188
- float before = (i == 0) ? 0.0f : (*stops)[i - 1].offset;
189
- float after = (j >= stops->size()) ? 1.0f : (*stops)[j].offset;
190
- size_t count = j - i;
191
- for (size_t k = 0; k < count; k++)
192
- (*stops)[i + k].offset = before + (after - before) * (float)(k + 1) / (float)(count + 1);
193
- i = j;
194
188
  }
195
- stops->front().offset = 0.0f;
196
- stops->back().offset = 1.0f;
197
- return true;
189
+ return stops->size() >= 2;
198
190
  }
199
191
 
200
192
  /** CSS gradient direction/angle. Returns true when `text` was a direction. */
@@ -395,14 +387,18 @@ bool parseRadialGradient(const std::string &body, BackgroundLayer *layer) {
395
387
  /** Parse one layer, appending it when it is a supported gradient or image. */
396
388
  bool parseLayer(const std::string &part, BackgroundLayer *layer) {
397
389
  std::string body;
398
- if (findCall(part, "linear-gradient", &body) ||
399
- findCall(part, "repeating-linear-gradient", &body)) {
390
+ /* The repeating prefix is checked first: a plain substring search for
391
+ * `linear-gradient(` would also match inside `repeating-linear-gradient(`. */
392
+ if (findCall(part, "repeating-linear-gradient", &body)) {
393
+ layer->repeating = true;
400
394
  return parseLinearGradient(body, layer);
401
395
  }
402
- if (findCall(part, "radial-gradient", &body) ||
403
- findCall(part, "repeating-radial-gradient", &body)) {
396
+ if (findCall(part, "linear-gradient", &body)) return parseLinearGradient(body, layer);
397
+ if (findCall(part, "repeating-radial-gradient", &body)) {
398
+ layer->repeating = true;
404
399
  return parseRadialGradient(body, layer);
405
400
  }
401
+ if (findCall(part, "radial-gradient", &body)) return parseRadialGradient(body, layer);
406
402
  if (findCall(part, "url", &body)) {
407
403
  std::string source = xt_css_trim(body);
408
404
  if (source.size() >= 2 && (source.front() == '"' || source.front() == '\'') &&