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 }