Skip to content

Commit b782cc6

Browse files
Merge pull request #1199 from lightpanda-io/nikneym/dummy-canvas
Dummy canvas
2 parents 4538464 + d48a661 commit b782cc6

File tree

7 files changed

+402
-3
lines changed

7 files changed

+402
-3
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
21+
const color = @import("../cssom/color.zig");
22+
const Page = @import("../page.zig").Page;
23+
24+
/// This class doesn't implement a `constructor`.
25+
/// It can be obtained with a call to `HTMLCanvasElement#getContext`.
26+
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
27+
const CanvasRenderingContext2D = @This();
28+
/// Fill color.
29+
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
30+
fill_style: color.RGBA = color.RGBA.Named.black,
31+
32+
pub fn _fillRect(
33+
self: *const CanvasRenderingContext2D,
34+
x: f64,
35+
y: f64,
36+
width: f64,
37+
height: f64,
38+
) void {
39+
_ = self;
40+
_ = x;
41+
_ = y;
42+
_ = width;
43+
_ = height;
44+
}
45+
46+
pub fn get_fillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {
47+
var w = std.Io.Writer.Allocating.init(page.call_arena);
48+
try self.fill_style.format(&w.writer);
49+
return w.written();
50+
}
51+
52+
pub fn set_fillStyle(
53+
self: *CanvasRenderingContext2D,
54+
value: []const u8,
55+
) !void {
56+
// Prefer the same fill_style if fails.
57+
self.fill_style = color.RGBA.parse(value) catch self.fill_style;
58+
}

src/browser/canvas/root.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//! Canvas API.
2+
//! https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
3+
4+
pub const Interfaces = .{
5+
@import("./CanvasRenderingContext2D.zig"),
6+
};

src/browser/cssom/CSSStyleDeclaration.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ fn isNumericWithUnit(value: []const u8) bool {
190190
return CSSKeywords.isValidUnit(unit);
191191
}
192192

193-
fn isHexColor(value: []const u8) bool {
193+
pub fn isHexColor(value: []const u8) bool {
194194
if (value.len == 0) {
195195
return false;
196196
}
@@ -199,7 +199,7 @@ fn isHexColor(value: []const u8) bool {
199199
}
200200

201201
const hex_part = value[1..];
202-
if (hex_part.len != 3 and hex_part.len != 6 and hex_part.len != 8) {
202+
if (hex_part.len != 3 and hex_part.len != 4 and hex_part.len != 6 and hex_part.len != 8) {
203203
return false;
204204
}
205205

@@ -551,6 +551,7 @@ test "Browser: CSS.StyleDeclaration: isNumericWithUnit - edge cases and invalid
551551

552552
test "Browser: CSS.StyleDeclaration: isHexColor - valid hex colors" {
553553
try testing.expect(isHexColor("#000"));
554+
try testing.expect(isHexColor("#0000"));
554555
try testing.expect(isHexColor("#fff"));
555556
try testing.expect(isHexColor("#123456"));
556557
try testing.expect(isHexColor("#abcdef"));
@@ -563,7 +564,6 @@ test "Browser: CSS.StyleDeclaration: isHexColor - invalid hex colors" {
563564
try testing.expect(!isHexColor("#"));
564565
try testing.expect(!isHexColor("000"));
565566
try testing.expect(!isHexColor("#00"));
566-
try testing.expect(!isHexColor("#0000"));
567567
try testing.expect(!isHexColor("#00000"));
568568
try testing.expect(!isHexColor("#0000000"));
569569
try testing.expect(!isHexColor("#000000000"));

src/browser/cssom/color.zig

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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+
const CSSParser = @import("CSSParser.zig");
23+
const isHexColor = @import("CSSStyleDeclaration.zig").isHexColor;
24+
25+
pub const RGBA = packed struct(u32) {
26+
r: u8,
27+
g: u8,
28+
b: u8,
29+
/// Opaque by default.
30+
a: u8 = std.math.maxInt(u8),
31+
32+
pub const Named = struct {
33+
// Basic colors (CSS Level 1)
34+
pub const black: RGBA = .init(0, 0, 0, 1);
35+
pub const silver: RGBA = .init(192, 192, 192, 1);
36+
pub const gray: RGBA = .init(128, 128, 128, 1);
37+
pub const white: RGBA = .init(255, 255, 255, 1);
38+
pub const maroon: RGBA = .init(128, 0, 0, 1);
39+
pub const red: RGBA = .init(255, 0, 0, 1);
40+
pub const purple: RGBA = .init(128, 0, 128, 1);
41+
pub const fuchsia: RGBA = .init(255, 0, 255, 1);
42+
pub const green: RGBA = .init(0, 128, 0, 1);
43+
pub const lime: RGBA = .init(0, 255, 0, 1);
44+
pub const olive: RGBA = .init(128, 128, 0, 1);
45+
pub const yellow: RGBA = .init(255, 255, 0, 1);
46+
pub const navy: RGBA = .init(0, 0, 128, 1);
47+
pub const blue: RGBA = .init(0, 0, 255, 1);
48+
pub const teal: RGBA = .init(0, 128, 128, 1);
49+
pub const aqua: RGBA = .init(0, 255, 255, 1);
50+
51+
// Extended colors (CSS Level 2+)
52+
pub const aliceblue: RGBA = .init(240, 248, 255, 1);
53+
pub const antiquewhite: RGBA = .init(250, 235, 215, 1);
54+
pub const aquamarine: RGBA = .init(127, 255, 212, 1);
55+
pub const azure: RGBA = .init(240, 255, 255, 1);
56+
pub const beige: RGBA = .init(245, 245, 220, 1);
57+
pub const bisque: RGBA = .init(255, 228, 196, 1);
58+
pub const blanchedalmond: RGBA = .init(255, 235, 205, 1);
59+
pub const blueviolet: RGBA = .init(138, 43, 226, 1);
60+
pub const brown: RGBA = .init(165, 42, 42, 1);
61+
pub const burlywood: RGBA = .init(222, 184, 135, 1);
62+
pub const cadetblue: RGBA = .init(95, 158, 160, 1);
63+
pub const chartreuse: RGBA = .init(127, 255, 0, 1);
64+
pub const chocolate: RGBA = .init(210, 105, 30, 1);
65+
pub const coral: RGBA = .init(255, 127, 80, 1);
66+
pub const cornflowerblue: RGBA = .init(100, 149, 237, 1);
67+
pub const cornsilk: RGBA = .init(255, 248, 220, 1);
68+
pub const crimson: RGBA = .init(220, 20, 60, 1);
69+
pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua
70+
pub const darkblue: RGBA = .init(0, 0, 139, 1);
71+
pub const darkcyan: RGBA = .init(0, 139, 139, 1);
72+
pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1);
73+
pub const darkgray: RGBA = .init(169, 169, 169, 1);
74+
pub const darkgreen: RGBA = .init(0, 100, 0, 1);
75+
pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray
76+
pub const darkkhaki: RGBA = .init(189, 183, 107, 1);
77+
pub const darkmagenta: RGBA = .init(139, 0, 139, 1);
78+
pub const darkolivegreen: RGBA = .init(85, 107, 47, 1);
79+
pub const darkorange: RGBA = .init(255, 140, 0, 1);
80+
pub const darkorchid: RGBA = .init(153, 50, 204, 1);
81+
pub const darkred: RGBA = .init(139, 0, 0, 1);
82+
pub const darksalmon: RGBA = .init(233, 150, 122, 1);
83+
pub const darkseagreen: RGBA = .init(143, 188, 143, 1);
84+
pub const darkslateblue: RGBA = .init(72, 61, 139, 1);
85+
pub const darkslategray: RGBA = .init(47, 79, 79, 1);
86+
pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray
87+
pub const darkturquoise: RGBA = .init(0, 206, 209, 1);
88+
pub const darkviolet: RGBA = .init(148, 0, 211, 1);
89+
pub const deeppink: RGBA = .init(255, 20, 147, 1);
90+
pub const deepskyblue: RGBA = .init(0, 191, 255, 1);
91+
pub const dimgray: RGBA = .init(105, 105, 105, 1);
92+
pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray
93+
pub const dodgerblue: RGBA = .init(30, 144, 255, 1);
94+
pub const firebrick: RGBA = .init(178, 34, 34, 1);
95+
pub const floralwhite: RGBA = .init(255, 250, 240, 1);
96+
pub const forestgreen: RGBA = .init(34, 139, 34, 1);
97+
pub const gainsboro: RGBA = .init(220, 220, 220, 1);
98+
pub const ghostwhite: RGBA = .init(248, 248, 255, 1);
99+
pub const gold: RGBA = .init(255, 215, 0, 1);
100+
pub const goldenrod: RGBA = .init(218, 165, 32, 1);
101+
pub const greenyellow: RGBA = .init(173, 255, 47, 1);
102+
pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray
103+
pub const honeydew: RGBA = .init(240, 255, 240, 1);
104+
pub const hotpink: RGBA = .init(255, 105, 180, 1);
105+
pub const indianred: RGBA = .init(205, 92, 92, 1);
106+
pub const indigo: RGBA = .init(75, 0, 130, 1);
107+
pub const ivory: RGBA = .init(255, 255, 240, 1);
108+
pub const khaki: RGBA = .init(240, 230, 140, 1);
109+
pub const lavender: RGBA = .init(230, 230, 250, 1);
110+
pub const lavenderblush: RGBA = .init(255, 240, 245, 1);
111+
pub const lawngreen: RGBA = .init(124, 252, 0, 1);
112+
pub const lemonchiffon: RGBA = .init(255, 250, 205, 1);
113+
pub const lightblue: RGBA = .init(173, 216, 230, 1);
114+
pub const lightcoral: RGBA = .init(240, 128, 128, 1);
115+
pub const lightcyan: RGBA = .init(224, 255, 255, 1);
116+
pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1);
117+
pub const lightgray: RGBA = .init(211, 211, 211, 1);
118+
pub const lightgreen: RGBA = .init(144, 238, 144, 1);
119+
pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray
120+
pub const lightpink: RGBA = .init(255, 182, 193, 1);
121+
pub const lightsalmon: RGBA = .init(255, 160, 122, 1);
122+
pub const lightseagreen: RGBA = .init(32, 178, 170, 1);
123+
pub const lightskyblue: RGBA = .init(135, 206, 250, 1);
124+
pub const lightslategray: RGBA = .init(119, 136, 153, 1);
125+
pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray
126+
pub const lightsteelblue: RGBA = .init(176, 196, 222, 1);
127+
pub const lightyellow: RGBA = .init(255, 255, 224, 1);
128+
pub const limegreen: RGBA = .init(50, 205, 50, 1);
129+
pub const linen: RGBA = .init(250, 240, 230, 1);
130+
pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia
131+
pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1);
132+
pub const mediumblue: RGBA = .init(0, 0, 205, 1);
133+
pub const mediumorchid: RGBA = .init(186, 85, 211, 1);
134+
pub const mediumpurple: RGBA = .init(147, 112, 219, 1);
135+
pub const mediumseagreen: RGBA = .init(60, 179, 113, 1);
136+
pub const mediumslateblue: RGBA = .init(123, 104, 238, 1);
137+
pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1);
138+
pub const mediumturquoise: RGBA = .init(72, 209, 204, 1);
139+
pub const mediumvioletred: RGBA = .init(199, 21, 133, 1);
140+
pub const midnightblue: RGBA = .init(25, 25, 112, 1);
141+
pub const mintcream: RGBA = .init(245, 255, 250, 1);
142+
pub const mistyrose: RGBA = .init(255, 228, 225, 1);
143+
pub const moccasin: RGBA = .init(255, 228, 181, 1);
144+
pub const navajowhite: RGBA = .init(255, 222, 173, 1);
145+
pub const oldlace: RGBA = .init(253, 245, 230, 1);
146+
pub const olivedrab: RGBA = .init(107, 142, 35, 1);
147+
pub const orange: RGBA = .init(255, 165, 0, 1);
148+
pub const orangered: RGBA = .init(255, 69, 0, 1);
149+
pub const orchid: RGBA = .init(218, 112, 214, 1);
150+
pub const palegoldenrod: RGBA = .init(238, 232, 170, 1);
151+
pub const palegreen: RGBA = .init(152, 251, 152, 1);
152+
pub const paleturquoise: RGBA = .init(175, 238, 238, 1);
153+
pub const palevioletred: RGBA = .init(219, 112, 147, 1);
154+
pub const papayawhip: RGBA = .init(255, 239, 213, 1);
155+
pub const peachpuff: RGBA = .init(255, 218, 185, 1);
156+
pub const peru: RGBA = .init(205, 133, 63, 1);
157+
pub const pink: RGBA = .init(255, 192, 203, 1);
158+
pub const plum: RGBA = .init(221, 160, 221, 1);
159+
pub const powderblue: RGBA = .init(176, 224, 230, 1);
160+
pub const rebeccapurple: RGBA = .init(102, 51, 153, 1);
161+
pub const rosybrown: RGBA = .init(188, 143, 143, 1);
162+
pub const royalblue: RGBA = .init(65, 105, 225, 1);
163+
pub const saddlebrown: RGBA = .init(139, 69, 19, 1);
164+
pub const salmon: RGBA = .init(250, 128, 114, 1);
165+
pub const sandybrown: RGBA = .init(244, 164, 96, 1);
166+
pub const seagreen: RGBA = .init(46, 139, 87, 1);
167+
pub const seashell: RGBA = .init(255, 245, 238, 1);
168+
pub const sienna: RGBA = .init(160, 82, 45, 1);
169+
pub const skyblue: RGBA = .init(135, 206, 235, 1);
170+
pub const slateblue: RGBA = .init(106, 90, 205, 1);
171+
pub const slategray: RGBA = .init(112, 128, 144, 1);
172+
pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray
173+
pub const snow: RGBA = .init(255, 250, 250, 1);
174+
pub const springgreen: RGBA = .init(0, 255, 127, 1);
175+
pub const steelblue: RGBA = .init(70, 130, 180, 1);
176+
pub const tan: RGBA = .init(210, 180, 140, 1);
177+
pub const thistle: RGBA = .init(216, 191, 216, 1);
178+
pub const tomato: RGBA = .init(255, 99, 71, 1);
179+
pub const transparent: RGBA = .init(0, 0, 0, 0);
180+
pub const turquoise: RGBA = .init(64, 224, 208, 1);
181+
pub const violet: RGBA = .init(238, 130, 238, 1);
182+
pub const wheat: RGBA = .init(245, 222, 179, 1);
183+
pub const whitesmoke: RGBA = .init(245, 245, 245, 1);
184+
pub const yellowgreen: RGBA = .init(154, 205, 50, 1);
185+
};
186+
187+
pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA {
188+
const clamped = std.math.clamp(a, 0, 1);
189+
return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) };
190+
}
191+
192+
/// Finds a color by its name.
193+
pub fn find(name: []const u8) ?RGBA {
194+
const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null;
195+
196+
return switch (match) {
197+
inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)),
198+
};
199+
}
200+
201+
/// Parses the given color.
202+
/// Currently we only parse hex colors and named colors; other variants
203+
/// require CSS evaluation.
204+
pub fn parse(input: []const u8) !RGBA {
205+
if (!isHexColor(input)) {
206+
// Try named colors.
207+
return find(input) orelse return error.Invalid;
208+
}
209+
210+
const slice = input[1..];
211+
switch (slice.len) {
212+
// This means the digit for a color is repeated.
213+
// Given HEX is #f0c, its interpreted the same as #FF00CC.
214+
3 => {
215+
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
216+
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
217+
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
218+
return .{ .r = r, .g = g, .b = b, .a = 255 };
219+
},
220+
4 => {
221+
const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16);
222+
const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16);
223+
const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16);
224+
const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16);
225+
return .{ .r = r, .g = g, .b = b, .a = a };
226+
},
227+
// Regular HEX format.
228+
6 => {
229+
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
230+
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
231+
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
232+
return .{ .r = r, .g = g, .b = b, .a = 255 };
233+
},
234+
8 => {
235+
const r = try std.fmt.parseInt(u8, slice[0..2], 16);
236+
const g = try std.fmt.parseInt(u8, slice[2..4], 16);
237+
const b = try std.fmt.parseInt(u8, slice[4..6], 16);
238+
const a = try std.fmt.parseInt(u8, slice[6..8], 16);
239+
return .{ .r = r, .g = g, .b = b, .a = a };
240+
},
241+
else => return error.Invalid,
242+
}
243+
}
244+
245+
/// By default, browsers prefer lowercase formatting.
246+
const format_upper = false;
247+
248+
/// Formats the `Color` according to web expectations.
249+
/// If color is opaque, HEX is preferred; RGBA otherwise.
250+
pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void {
251+
if (self.isOpaque()) {
252+
// Convert RGB to HEX.
253+
// https://gristle.tripod.com/hexconv.html
254+
// Hexadecimal characters up to 15.
255+
const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef";
256+
// This variant always prefers 6 digit format, +1 is for hash char.
257+
const buffer = [7]u8{
258+
'#',
259+
char[self.r >> 4],
260+
char[self.r & 15],
261+
char[self.g >> 4],
262+
char[self.g & 15],
263+
char[self.b >> 4],
264+
char[self.b & 15],
265+
};
266+
267+
return writer.writeAll(&buffer);
268+
}
269+
270+
// Prefer RGBA format for everything else.
271+
return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() });
272+
}
273+
274+
/// Returns true if `Color` is opaque.
275+
pub inline fn isOpaque(self: *const RGBA) bool {
276+
return self.a == std.math.maxInt(u8);
277+
}
278+
279+
/// Returns the normalized alpha value.
280+
pub inline fn normalizedAlpha(self: *const RGBA) f32 {
281+
return @as(f32, @floatFromInt(self.a)) / 255;
282+
}
283+
};

0 commit comments

Comments
 (0)