lib/gui/src/paint/gradient.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const gui = @import("../root.zig");
  4 
  5 const Color = gui.model.UiColor;
  6 const Command = gui.paint.command.Command;
  7 const Point = gui.model.UiPoint;
  8 
  9 pub fn colorAt(paint: Command, x: f32, y: f32) Color {
 10     return Sampler.init(paint).colorAt(x, y);
 11 }
 12 
 13 pub fn parameter(paint: Command, x: f32, y: f32) f32 {
 14     return Sampler.init(paint).parameterAt(x, y);
 15 }
 16 
 17 pub const Sampler = struct {
 18     start: Point,
 19     start_color: PremultipliedColor,
 20     end_color: PremultipliedColor,
 21     dx: f32,
 22     dy: f32,
 23     length_squared: f32,
 24     degenerate: bool,
 25 
 26     pub fn init(paint: Command) Sampler {
 27         const dx = paint.gradient_end.x - paint.gradient_start.x;
 28         const dy = paint.gradient_end.y - paint.gradient_start.y;
 29         const length_squared = dx * dx + dy * dy;
 30         return .{
 31             .start = paint.gradient_start,
 32             .start_color = PremultipliedColor.init(paint.color),
 33             .end_color = PremultipliedColor.init(paint.color_end),
 34             .dx = dx,
 35             .dy = dy,
 36             .length_squared = length_squared,
 37             .degenerate = !std.math.isFinite(length_squared) or
 38                 length_squared <= std.math.floatEps(f32),
 39         };
 40     }
 41 
 42     pub fn colorAt(self: Sampler, x: f32, y: f32) Color {
 43         return self.colorAtRow(x, self.rowProjection(y));
 44     }
 45 
 46     pub fn parameterAt(self: Sampler, x: f32, y: f32) f32 {
 47         return self.parameterAtRow(x, self.rowProjection(y));
 48     }
 49 
 50     pub fn rowProjection(self: Sampler, y: f32) f32 {
 51         return (y - self.start.y) * self.dy;
 52     }
 53 
 54     pub fn colorAtRow(self: Sampler, x: f32, row_projection: f32) Color {
 55         const t = self.parameterAtRow(x, row_projection);
 56         const alpha_value = interpolateValue(self.start_color.a, self.end_color.a, t);
 57         if (alpha_value <= 0) return .{ .a = 0 };
 58         return .{
 59             .r = unpremultipliedValue(self.start_color.r, self.end_color.r, t, alpha_value),
 60             .g = unpremultipliedValue(self.start_color.g, self.end_color.g, t, alpha_value),
 61             .b = unpremultipliedValue(self.start_color.b, self.end_color.b, t, alpha_value),
 62             .a = roundedChannel(alpha_value),
 63         };
 64     }
 65 
 66     pub fn parameterAtRow(self: Sampler, x: f32, row_projection: f32) f32 {
 67         if (self.degenerate) return 1;
 68         const projected = ((x - self.start.x) * self.dx + row_projection) /
 69             self.length_squared;
 70         if (!std.math.isFinite(projected)) return 1;
 71         return std.math.clamp(projected, @as(f32, 0), @as(f32, 1));
 72     }
 73 };
 74 
 75 const PremultipliedColor = struct {
 76     r: f32,
 77     g: f32,
 78     b: f32,
 79     a: f32,
 80 
 81     fn init(color: Color) PremultipliedColor {
 82         const alpha: f32 = @floatFromInt(color.a);
 83         return .{
 84             .r = @as(f32, @floatFromInt(color.r)) * alpha,
 85             .g = @as(f32, @floatFromInt(color.g)) * alpha,
 86             .b = @as(f32, @floatFromInt(color.b)) * alpha,
 87             .a = alpha,
 88         };
 89     }
 90 };
 91 
 92 pub fn colorAtParameter(start: Color, end: Color, parameter_value: f32) Color {
 93     const t = std.math.clamp(parameter_value, @as(f32, 0), @as(f32, 1));
 94     const alpha_value = interpolate(start.a, end.a, t);
 95     if (alpha_value <= 0) return .{ .a = 0 };
 96     return .{
 97         .r = unpremultiplied(start.r, start.a, end.r, end.a, t, alpha_value),
 98         .g = unpremultiplied(start.g, start.a, end.g, end.a, t, alpha_value),
 99         .b = unpremultiplied(start.b, start.a, end.b, end.a, t, alpha_value),
100         .a = roundedChannel(alpha_value),
101     };
102 }
103 
104 fn unpremultiplied(start: u8, start_alpha: u8, end: u8, end_alpha: u8, t: f32, alpha_value: f32) u8 {
105     const start_value = @as(f32, @floatFromInt(start)) * @as(f32, @floatFromInt(start_alpha));
106     const end_value = @as(f32, @floatFromInt(end)) * @as(f32, @floatFromInt(end_alpha));
107     return unpremultipliedValue(start_value, end_value, t, alpha_value);
108 }
109 
110 fn unpremultipliedValue(start: f32, end: f32, t: f32, alpha_value: f32) u8 {
111     return roundedChannel(interpolateValue(start, end, t) / alpha_value);
112 }
113 
114 fn interpolate(start: u8, end: u8, t: f32) f32 {
115     return interpolateValue(@floatFromInt(start), @floatFromInt(end), t);
116 }
117 
118 fn interpolateValue(start: f32, end: f32, t: f32) f32 {
119     return start + (end - start) * t;
120 }
121 
122 fn roundedChannel(value: f32) u8 {
123     return @intFromFloat(std.math.clamp(@round(value), @as(f32, 0), @as(f32, 255)));
124 }
125 
126 test "linear gradient clamps and reverses direction" {
127     const paint = Command{
128         .kind = .linear_gradient,
129         .rect = .{ .width = 12, .height = 4 },
130         .clip = .{ .width = 12, .height = 4 },
131         .color = .{ .r = 10, .g = 20, .b = 30, .a = 255 },
132         .color_end = .{ .r = 210, .g = 120, .b = 80, .a = 255 },
133         .gradient_start = .{ .x = 10 },
134         .gradient_end = .{ .x = 2 },
135     };
136     try std.testing.expectEqual(paint.color, colorAt(paint, 12, 2));
137     try std.testing.expectEqual(paint.color_end, colorAt(paint, 0, 2));
138     try std.testing.expectEqual(@as(u8, 110), colorAt(paint, 6, 2).r);
139 }
140 
141 test "linear gradient degenerate vector resolves to end color" {
142     const paint = Command{
143         .kind = .linear_gradient,
144         .rect = .{ .width = 4, .height = 4 },
145         .clip = .{ .width = 4, .height = 4 },
146         .color = .{ .r = 255, .a = 255 },
147         .color_end = .{ .b = 255, .a = 120 },
148         .gradient_start = .{ .x = 2, .y = 2 },
149         .gradient_end = .{ .x = 2, .y = 2 },
150     };
151     try std.testing.expectEqual(paint.color_end, colorAt(paint, 1, 3));
152 }
153 
154 test "linear gradient premultiplies translucent interpolation" {
155     const color = colorAtParameter(
156         .{ .r = 255, .g = 40, .b = 20, .a = 255 },
157         .{ .r = 0, .g = 0, .b = 0, .a = 0 },
158         0.5,
159     );
160     try std.testing.expectEqual(@as(u8, 255), color.r);
161     try std.testing.expectEqual(@as(u8, 40), color.g);
162     try std.testing.expectEqual(@as(u8, 20), color.b);
163     try std.testing.expectEqual(@as(u8, 128), color.a);
164 }