BlendModes
ui.boxModes de fusion et composition avec opacité.
BEHAVIOR CONTRACT
Chaque mode de fusion est vérifié au pixel près pour que le GPU ne dégrade jamais en simple alpha.
RECORDED INTERACTION
Parcourt la matrice des modes de fusion.
// 颜色特意全用 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;
}Le code source tel quel de la story dans le Storybook (zenit 5f9add5+wip 2026-09-30). W désigne ui.widgets ; col / row / label sont de petits helpers de mise en page du Storybook.