Skip to content

Commit 0a705b1

Browse files
committed
add color representation by RGBA
It seems we can represent most things with RGBA (at least this is what other browsers do) so a universal color API based on RGBA is nice to have, especially for CSS and Canvas.
1 parent 4cf61d1 commit 0a705b1

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed

src/browser/cssom/color.zig

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <francis@lightpanda.io>
4+
// Pierre Tachoire <pierre@lightpanda.io>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const Io = std.Io;
21+
22+
pub const RGBA = packed struct(u32) {
23+
r: u8,
24+
g: u8,
25+
b: u8,
26+
a: u8 = std.math.maxInt(u8),
27+
28+
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
29+
const clamped = std.math.clamp(a, 0, 1);
30+
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
31+
}
32+
33+
/// Initializes a `Color` by parsing the given HEX.
34+
/// HEX is either represented as RGB or RGBA by `Color`.
35+
pub fn initFromHex(hex: []const u8) !RGBA {
36+
// HEX is bit weird; its length (hash omitted) can be 3, 4, 6 or 8.
37+
// The parsing gets a bit different depending on it.
38+
const slice = hex[1..];
39+
switch (slice.len) {
40+
// This means the digit for a color is repeated.
41+
// Given HEX is #f0c, its interpreted the same as #FF00CC.
42+
3 => {
43+
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
44+
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
45+
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
46+
return .{ .r = r, .g = g, .b = b, .a = 255 };
47+
},
48+
4 => {
49+
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
50+
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
51+
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
52+
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
53+
return .{ .r = r, .g = g, .b = b, .a = a };
54+
},
55+
// Regular HEX format.
56+
6 => {
57+
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
58+
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
59+
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
60+
return .{ .r = r, .g = g, .b = b, .a = 255 };
61+
},
62+
8 => {
63+
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
64+
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
65+
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
66+
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
67+
return .{ .r = r, .g = g, .b = b, .a = a };
68+
},
69+
else => unreachable,
70+
}
71+
}
72+
73+
/// By default, browsers prefer lowercase formatting.
74+
const format_upper = false;
75+
76+
/// Formats the `Color` according to web expectations.
77+
/// If color is opaque, HEX is preferred; RGBA otherwise.
78+
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
79+
if (self.isOpaque()) {
80+
// Convert RGB to HEX.
81+
// https://gristle.tripod.com/hexconv.html
82+
// Hexadecimal characters up to 15.
83+
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
84+
// This variant always prefers 6 digit format, +1 is for hash char.
85+
const buffer = [7]u8{
86+
'#',
87+
char[self.r >> 4],
88+
char[self.r & 15],
89+
char[self.g >> 4],
90+
char[self.g & 15],
91+
char[self.b >> 4],
92+
char[self.b & 15],
93+
};
94+
95+
return writer.writeAll(&buffer);
96+
}
97+
98+
// Prefer RGBA format for everything else.
99+
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
100+
}
101+
102+
/// Returns true if `Color` is opaque.
103+
pub inline fn isOpaque(self: *const RGBA) bool {
104+
return self.a == std.math.maxInt(u8);
105+
}
106+
107+
/// Returns the normalized alpha value.
108+
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
109+
return @as(f32, @floatFromInt(self.a)) / 255;
110+
}
111+
};

0 commit comments

Comments
 (0)