lib/gui/src/paint/cpu/coverage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const gui = @import("../../root.zig");
4 const paint_root = @import("../root.zig");
5
6 const geometry = @import("geometry.zig");
7 const gradient = paint_root.gradient;
8 const pixel = @import("pixel.zig");
9 const sample = @import("sample.zig");
10
11 const Color = gui.model.UiColor;
12 const Command = paint_root.command.Command;
13 const ImageSet = paint_root.command.ImageSet;
14
15 pub fn covers(paint: Command, x: f32, y: f32) bool {
16 if (!geometry.inside(paint.clip, x, y)) return false;
17 return switch (paint.kind) {
18 .fill, .linear_gradient => geometry.insideRounded(paint.rect, @max(paint.radius, 0), x, y),
19 .stroke => strokeCovers(paint.rect, @max(paint.radius, 0), @max(paint.width, 0), x, y),
20 .image => geometry.inside(paint.rect, x, y),
21 .shadow => shadowCovers(paint, x, y),
22 .glyph => geometry.inside(paint.rect, x, y),
23 };
24 }
25
26 pub fn coverageCount(paint: Command, x: f32, y: f32) u32 {
27 var count: u32 = 0;
28 inline for (.{ -0.25, 0.25 }) |dy| {
29 inline for (.{ -0.25, 0.25 }) |dx| {
30 if (covers(paint, x + dx, y + dy)) count += 1;
31 }
32 }
33 return count;
34 }
35
36 pub fn commandColorAt(paint: Command, images: ImageSet, x: f32, y: f32, coverage: u32) ?Color {
37 return switch (paint.kind) {
38 .fill, .stroke => pixel.colorWithCoverage(paint.color, coverage),
39 .linear_gradient => pixel.colorWithCoverage(gradient.colorAt(paint, x, y), coverage),
40 .image => imageColorAt(paint, images, x, y, coverage),
41 .shadow => shadowColorAt(paint, x, y, coverage),
42 .glyph => glyphColorAt(paint, images, x, y, coverage),
43 };
44 }
45
46 pub fn strokeCovers(rect: gui.layout.Rect, radius: f32, width: f32, x: f32, y: f32) bool {
47 if (width <= 0) return false;
48 if (!geometry.insideRounded(rect, radius, x, y)) return false;
49 const inner = geometry.inset(rect, width);
50 if (inner.width <= 0 or inner.height <= 0) return true;
51 return !geometry.insideRounded(inner, @max(radius - width, 0), x, y);
52 }
53
54 pub fn shadowCovers(paint: Command, x: f32, y: f32) bool {
55 const blur = @max(paint.width, 0);
56 if (blur <= 0) return geometry.insideRounded(paint.rect, @max(paint.radius, 0), x, y);
57 return geometry.roundedRectOutsideDistance(paint.rect, @max(paint.radius, 0), x, y) < blur;
58 }
59
60 pub fn shadowAlphaAt(paint: Command, x: f32, y: f32) u8 {
61 const blur = @max(paint.width, 0);
62 const distance = geometry.roundedRectOutsideDistance(paint.rect, @max(paint.radius, 0), x, y);
63 if (!std.math.isFinite(distance)) return 0;
64 if (blur <= 0) return if (distance <= 0) paint.color.a else 0;
65 if (distance >= blur) return 0;
66 const t = 1 - distance / blur;
67 return alphaScale(paint.color.a, t * t);
68 }
69
70 fn alphaScale(alpha_value: u8, scale: f32) u8 {
71 const value = @as(f32, @floatFromInt(alpha_value)) * std.math.clamp(scale, @as(f32, 0), @as(f32, 1));
72 return @intFromFloat(std.math.clamp(@round(value), @as(f32, 0), @as(f32, 255)));
73 }
74
75 fn shadowColorAt(paint: Command, x: f32, y: f32, coverage: u32) Color {
76 const alpha_value = shadowAlphaAt(paint, x, y);
77 return .{
78 .r = paint.color.r,
79 .g = paint.color.g,
80 .b = paint.color.b,
81 .a = pixel.coverageAlpha(alpha_value, coverage),
82 };
83 }
84
85 fn imageColorAt(paint: Command, images: ImageSet, x: f32, y: f32, coverage: u32) ?Color {
86 var color = sample.imageSampleAt(paint, images, x, y) orelse return null;
87 color.a = pixel.coverageAlpha(@intCast(pixel.div255(@as(u32, color.a) * @as(u32, paint.color.a))), coverage);
88 return color;
89 }
90
91 fn glyphColorAt(paint: Command, images: ImageSet, x: f32, y: f32, coverage: u32) ?Color {
92 const sampled = sample.imageSampleAt(paint, images, x, y) orelse return null;
93 return .{
94 .r = paint.color.r,
95 .g = paint.color.g,
96 .b = paint.color.b,
97 .a = pixel.coverageAlpha(@intCast(pixel.div255(@as(u32, sampled.a) * @as(u32, paint.color.a))), coverage),
98 };
99 }
100
101 pub fn rasterRun(comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, y: u32, x0: u32, x1: u32) void {
102 const fy = @as(f32, @floatFromInt(y)) + 0.5;
103 var x = x0;
104 while (x < x1) : (x += 1) {
105 const fx = @as(f32, @floatFromInt(x)) + 0.5;
106 const coverage = coverageCount(paint, fx, fy);
107 if (coverage == 0) continue;
108 const src = commandColorAt(paint, images, fx, fy, coverage) orelse continue;
109 const index = @as(usize, y) * width + x;
110 store.store(index, pixel.blendPacked(store.load(index), src));
111 }
112 }
113
114 pub fn rasterBlock(comptime Store: type, store: Store, width: u32, paint: Command, images: ImageSet, y0: u32, y1: u32, x0: u32, x1: u32) void {
115 if (x0 >= x1) return;
116 var y = y0;
117 while (y < y1) : (y += 1) {
118 rasterRun(Store, store, width, paint, images, y, x0, x1);
119 }
120 }
121
122 pub fn texturedRun(comptime kind: paint_root.command.Kind, comptime Store: type, store: Store, width: u32, paint: Command, image: paint_root.command.Image, source: gui.layout.Rect, y: u32, x0: u32, x1: u32) void {
123 const fy = @as(f32, @floatFromInt(y)) + 0.5;
124 var y_count: u32 = 0;
125 inline for (.{ -0.25, 0.25 }) |dy| {
126 const sy = fy + dy;
127 if (sy >= paint.rect.y and sy < paint.rect.y + paint.rect.height and
128 sy >= paint.clip.y and sy < paint.clip.y + paint.clip.height) y_count += 1;
129 }
130 if (y_count == 0) return;
131 const sample_row = sample.sampleRow(paint, source, image, fy);
132 const image_row = @as(usize, sample_row) * image.width;
133 const target_row = @as(usize, y) * width;
134 var x = x0;
135 while (x < x1) : (x += 1) {
136 const fx = @as(f32, @floatFromInt(x)) + 0.5;
137 var x_count: u32 = 0;
138 inline for (.{ -0.25, 0.25 }) |dx| {
139 const sx = fx + dx;
140 if (sx >= paint.rect.x and sx < paint.rect.x + paint.rect.width and
141 sx >= paint.clip.x and sx < paint.clip.x + paint.clip.width) x_count += 1;
142 }
143 const count = y_count * x_count;
144 if (count == 0) continue;
145 const sampled = pixel.unpackRgba(image.pixels[image_row + sample.sampleColumn(paint, source, image, fx)]);
146 const alpha = pixel.coverageAlpha(@intCast(pixel.div255(@as(u32, sampled.a) * @as(u32, paint.color.a))), count);
147 const src = switch (kind) {
148 .glyph => Color{ .r = paint.color.r, .g = paint.color.g, .b = paint.color.b, .a = alpha },
149 .image => Color{ .r = sampled.r, .g = sampled.g, .b = sampled.b, .a = alpha },
150 else => comptime unreachable,
151 };
152 const index = target_row + x;
153 store.store(index, pixel.blendPacked(store.load(index), src));
154 }
155 }
156
157 pub fn texturedBlock(comptime kind: paint_root.command.Kind, comptime Store: type, store: Store, width: u32, paint: Command, image: paint_root.command.Image, source: gui.layout.Rect, y0: u32, y1: u32, x0: u32, x1: u32) void {
158 if (x0 >= x1) return;
159 var y = y0;
160 while (y < y1) : (y += 1) {
161 texturedRun(kind, Store, store, width, paint, image, source, y, x0, x1);
162 }
163 }
164
165 test "coverageCount is four deep inside a fill and zero outside" {
166 const paint = Command{
167 .kind = .fill,
168 .rect = .{ .x = 2, .y = 2, .width = 6, .height = 6 },
169 .clip = .{ .x = 0, .y = 0, .width = 10, .height = 10 },
170 .color = .{ .r = 1, .g = 2, .b = 3, .a = 255 },
171 };
172 try std.testing.expectEqual(@as(u32, 4), coverageCount(paint, 5.5, 5.5));
173 try std.testing.expectEqual(@as(u32, 0), coverageCount(paint, 0.5, 0.5));
174 try std.testing.expectEqual(@as(u32, 2), coverageCount(paint, 2.0, 5.5));
175 }