BlendModes
ui.box混合模式与透明度合成。
BEHAVIOR CONTRACT
每种 blend mode 都经过像素断言,避免 GPU 退化成普通 alpha。
RECORDED INTERACTION
录像扫过混合模式矩阵。
// 颜色特意全用 0/255 分量:sRGB 传递函数在 0.0/1.0 处不变,线性空间与
// sRGB 空间的混合结果一致 —— e2e 能按精确 RGB 值断言,不受"shader 在
// 线性空间混合"这一实现细节影响。
// multiply: yellow(255,255,0) × cyan(0,255,255) = green(0,255,0)
// screen: red(255,0,0) ∪ blue(0,0,255) = magenta(255,0,255)
// difference: |white(255,255,255) − red(255,0,0)| = cyan(0,255,255)
pub fn buildBlend(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
_ = scope;
const a = cx.allocator;
const c = try col(cx, 16);
const Case = struct {
name: []const u8,
test_id: []const u8,
bg: ui.Color,
src: ui.Color,
mode: ui.BlendMode,
};
const cases = [_]Case{
.{ .name = "multiply: yellow x cyan = green", .test_id = "story.blend.multiply", .bg = ui.Color.rgb(255, 255, 0), .src = ui.Color.rgb(0, 255, 255), .mode = .multiply },
.{ .name = "screen: red + blue = magenta", .test_id = "story.blend.screen", .bg = ui.Color.rgb(255, 0, 0), .src = ui.Color.rgb(0, 0, 255), .mode = .screen },
.{ .name = "difference: white - red = cyan", .test_id = "story.blend.difference", .bg = ui.Color.rgb(255, 255, 255), .src = ui.Color.rgb(255, 0, 0), .mode = .difference },
};
try c.appendChild(a, try label(cx, "Blend modes (exact-color e2e targets)"));
const tiles = try row(cx, 16);
for (cases) |case| {
const cell = try col(cx, 6);
try cell.appendChild(a, try label(cx, case.name));
const outer = try ui.box(cx, .{
.width = .{ .px = 120 },
.height = .{ .px = 120 },
.background = case.bg,
.padding = ui.Padding.all(20),
}, .{});
const inner = try ui.box(cx, .{
.width = .{ .px = 80 },
.height = .{ .px = 80 },
.background = case.src,
}, .{});
(try inner.style.ensureExtFallible(a)).blend_mode = case.mode;
inner.meta.ownership.meta.test_id = case.test_id;
try outer.appendChild(a, inner);
try cell.appendChild(a, outer);
try tiles.appendChild(a, cell);
}
try c.appendChild(a, tiles);
// normal 对照组:同 cyan-on-yellow 但不混合 —— e2e 断言它仍是 cyan,
// 证明 blend 路径没把普通合成一起改了。
try c.appendChild(a, try label(cx, "normal control (should stay cyan)"));
const control_outer = try ui.box(cx, .{
.width = .{ .px = 120 },
.height = .{ .px = 120 },
.background = ui.Color.rgb(255, 255, 0),
.padding = ui.Padding.all(20),
}, .{});
const control_inner = try ui.box(cx, .{
.width = .{ .px = 80 },
.height = .{ .px = 80 },
.background = ui.Color.rgb(0, 255, 255),
}, .{});
control_inner.meta.ownership.meta.test_id = "story.blend.control";
try control_outer.appendChild(a, control_inner);
try c.appendChild(a, control_outer);
return c;
}这是 Storybook 中该 story 的原样代码(zenit 5f9add5+wip 2026-09-30)。其中 W 即 ui.widgets,col / row / label 是 Storybook 内的布局小工具。