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.
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/tests/e2e/gui-data-url.test.d.ts +10 -0
- package/dist/tests/e2e/gui-data-url.test.js +127 -0
- package/dist/tests/e2e/gui-data-url.test.js.map +1 -0
- package/dist/tests/e2e/gui-effects.test.js +144 -4
- package/dist/tests/e2e/gui-effects.test.js.map +1 -1
- package/dist/tests/e2e/gui-paint.test.js +77 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/doc/gui/api.md +1 -1
- package/doc/gui/canvas.md +6 -2
- package/doc/gui/known-issues.md +18 -10
- package/doc/gui/media.md +11 -2
- package/doc/gui/rendering.md +48 -19
- package/doc/gui.md +12 -8
- package/doc/zh-CN/gui/api.md +1 -1
- package/doc/zh-CN/gui/canvas.md +6 -4
- package/doc/zh-CN/gui/known-issues.md +12 -8
- package/doc/zh-CN/gui/media.md +9 -2
- package/doc/zh-CN/gui/rendering.md +37 -15
- package/doc/zh-CN/gui.md +7 -6
- package/package.json +7 -7
- package/runtime/ext_gui/dom_api_canvas.cpp +42 -24
- package/runtime/ext_gui/gradient_values.cpp +44 -48
- package/runtime/ext_gui/gui_events.cpp +16 -13
- package/runtime/ext_gui/image.cpp +180 -25
- package/runtime/ext_gui/image.h +6 -4
- package/runtime/ext_gui/paint.cpp +69 -4
- package/runtime/ext_gui/paint.h +57 -1
- package/runtime/ext_gui/paint_background.cpp +197 -41
- package/runtime/ext_gui/paint_effects.cpp +2 -1
- package/runtime/ext_gui/paint_internal.h +5 -1
- package/runtime/ext_gui/renderer.cpp +81 -13
- package/runtime/ext_gui/renderer.h +19 -5
- package/runtime/ext_gui/renderer_gradient.cpp +156 -0
- package/runtime/ext_gui/renderer_image.cpp +6 -1
- package/runtime/ext_gui/renderer_internal.h +48 -0
- package/runtime/ext_gui/renderer_layer.cpp +552 -0
- package/runtime/ext_gui/style.h +29 -1
- package/runtime/ext_gui/style_print.cpp +10 -2
- package/runtime/ext_gui/style_values.cpp +10 -0
- package/src/driver/version.ts +1 -1
package/doc/gui/rendering.md
CHANGED
|
@@ -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`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
84
|
-
`to <side>`, `deg`/`grad`/`rad`/`turn`, percentage
|
|
85
|
-
(
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
94
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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,
|
|
44
|
-
`clip-path
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
339
|
-
|
|
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
|
|
506
|
-
|
|
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
|
package/doc/zh-CN/gui/api.md
CHANGED
|
@@ -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?) // 整形后的推进宽度(像素)
|
package/doc/zh-CN/gui/canvas.md
CHANGED
|
@@ -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
|
|
170
|
-
|
|
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
|
-
的颜色函数。仍有限制:`
|
|
47
|
-
`
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
图像内部(这里的 `fill`/`stroke` 是 SVG 表现属性,不是 CSS 属性)。因此只靠滤镜
|
|
63
|
+
出图的 SVG(`feTurbulence` 噪声、模糊等)会显示未经过滤的形状——常见情况是一个
|
|
64
|
+
纯色矩形——而不是滤镜结果。需要这些能力的矢量美术必须预先光栅化、用盒子绘制,
|
|
65
|
+
或限制在受支持的语法内。参见[媒体](./media.md#图像m6a)。
|
|
62
66
|
- **每次变更仍是整文档重算。** 一次 DOM 变更会重新计算层叠并重新布局整个文档
|
|
63
67
|
(没有增量子树失效),只影响绘制的变更(例如 hover 颜色)同样会重排。带生成
|
|
64
68
|
内容(`::before`/`::after`)的文档没有特殊处理。各阶段的实测开销由
|
package/doc/zh-CN/gui/media.md
CHANGED
|
@@ -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
|
-
|
|
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`
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
71
|
-
`deg`/`grad`/`rad`/`turn
|
|
71
|
+
- **线性**渐变在 18 个位置采样(重复渐变 64 个)并输出为纯色矩形;支持
|
|
72
|
+
`to <side>`、`deg`/`grad`/`rad`/`turn`、百分比或长度(`px`/`em`/…)偏移、
|
|
73
|
+
`color <start> <end>` 简写与多色标(色带沿最近轴行走,因此斜角会退化为
|
|
72
74
|
最近的轴向);
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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`
|
|
412
|
-
|
|
413
|
-
|
|
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.
|
|
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.
|
|
76
|
-
"@xbintsc/linux-arm64": "0.5.
|
|
77
|
-
"@xbintsc/darwin-x64": "0.5.
|
|
78
|
-
"@xbintsc/darwin-arm64": "0.5.
|
|
79
|
-
"@xbintsc/win32-x64": "0.5.
|
|
80
|
-
"@xbintsc/win32-arm64": "0.5.
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
495
|
-
float
|
|
496
|
-
float
|
|
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 `
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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 (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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->
|
|
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
|
-
|
|
399
|
-
|
|
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, "
|
|
403
|
-
|
|
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() == '\'') &&
|