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 }