From 4cf61d101c4f09a83b21baf59651ff4fc889ca53 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 5 Nov 2025 11:50:57 +0300 Subject: [PATCH 1/2] initial dummy canvas --- .../canvas/CanvasRenderingContext2D.zig | 35 +++++++++++++++++++ src/browser/canvas/root.zig | 6 ++++ src/browser/html/elements.zig | 22 ++++++++++++ src/browser/js/types.zig | 1 + src/tests/html/canvas.html | 11 ++++++ 5 files changed, 75 insertions(+) create mode 100644 src/browser/canvas/CanvasRenderingContext2D.zig create mode 100644 src/browser/canvas/root.zig create mode 100644 src/tests/html/canvas.html diff --git a/src/browser/canvas/CanvasRenderingContext2D.zig b/src/browser/canvas/CanvasRenderingContext2D.zig new file mode 100644 index 000000000..30d126173 --- /dev/null +++ b/src/browser/canvas/CanvasRenderingContext2D.zig @@ -0,0 +1,35 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/// This class doesn't implement a `constructor`. +/// It can be obtained with a call to `HTMLCanvasElement#getContext`. +/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D +const CanvasRenderingContext2D = @This(); + +pub fn _fillRect(x: f64, y: f64, width: f64, height: f64) void { + _ = x; + _ = y; + _ = width; + _ = height; +} + +pub fn get_fillStyle(_: *const CanvasRenderingContext2D) []const u8 { + return ""; +} + +pub fn set_fillStyle(_: *const CanvasRenderingContext2D, _: []const u8) void {} diff --git a/src/browser/canvas/root.zig b/src/browser/canvas/root.zig new file mode 100644 index 000000000..ad4157457 --- /dev/null +++ b/src/browser/canvas/root.zig @@ -0,0 +1,6 @@ +//! Canvas API. +//! https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API + +pub const Interfaces = .{ + @import("./CanvasRenderingContext2D.zig"), +}; diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index e8dfdfbdf..61f85cdda 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -32,6 +32,7 @@ const DataSet = @import("DataSet.zig"); const StyleSheet = @import("../cssom/StyleSheet.zig"); const CSSStyleDeclaration = @import("../cssom/CSSStyleDeclaration.zig"); +const CanvasRenderingContext2D = @import("../canvas/CanvasRenderingContext2D.zig"); // HTMLElement interfaces pub const Interfaces = .{ @@ -487,6 +488,23 @@ pub const HTMLCanvasElement = struct { pub const Self = parser.Canvas; pub const prototype = *HTMLElement; pub const subtype = .node; + + /// This should be a union once we support other context types. + const ContextAttributes = struct { + alpha: bool, + color_space: []const u8 = "srgb", + }; + + pub fn _getContext( + ctx_type: []const u8, + _: ?ContextAttributes, + ) !CanvasRenderingContext2D { + if (!std.mem.eql(u8, ctx_type, "2d")) { + return error.NotSupported; + } + + return .{}; + } }; pub const HTMLDListElement = struct { @@ -1356,3 +1374,7 @@ test "Browser: HTML.HtmlScriptElement" { test "Browser: HTML.HtmlSlotElement" { try testing.htmlRunner("html/slot.html"); } + +test "Browser: HTML.HTMLCanvasElement" { + try testing.htmlRunner("html/canvas.html"); +} diff --git a/src/browser/js/types.zig b/src/browser/js/types.zig index 1849b72e3..bd4b595ec 100644 --- a/src/browser/js/types.zig +++ b/src/browser/js/types.zig @@ -18,6 +18,7 @@ const Interfaces = generate.Tuple(.{ @import("../xhr/xhr.zig").Interfaces, @import("../navigation/root.zig").Interfaces, @import("../file/root.zig").Interfaces, + @import("../canvas/root.zig").Interfaces, @import("../xhr/form_data.zig").Interfaces, @import("../xmlserializer/xmlserializer.zig").Interfaces, @import("../fetch/fetch.zig").Interfaces, diff --git a/src/tests/html/canvas.html b/src/tests/html/canvas.html new file mode 100644 index 000000000..36c058ceb --- /dev/null +++ b/src/tests/html/canvas.html @@ -0,0 +1,11 @@ + + + + From 0a705b15ce24ee1996d82a77fc44975a81a688ea Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 10 Nov 2025 16:57:35 +0300 Subject: [PATCH 2/2] 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. --- src/browser/cssom/color.zig | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/browser/cssom/color.zig diff --git a/src/browser/cssom/color.zig b/src/browser/cssom/color.zig new file mode 100644 index 000000000..7980eefa1 --- /dev/null +++ b/src/browser/cssom/color.zig @@ -0,0 +1,111 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const Io = std.Io; + +pub const RGBA = packed struct(u32) { + r: u8, + g: u8, + b: u8, + a: u8 = std.math.maxInt(u8), + + pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA { + const clamped = std.math.clamp(a, 0, 1); + return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) }; + } + + /// Initializes a `Color` by parsing the given HEX. + /// HEX is either represented as RGB or RGBA by `Color`. + pub fn initFromHex(hex: []const u8) !RGBA { + // HEX is bit weird; its length (hash omitted) can be 3, 4, 6 or 8. + // The parsing gets a bit different depending on it. + const slice = hex[1..]; + switch (slice.len) { + // This means the digit for a color is repeated. + // Given HEX is #f0c, its interpreted the same as #FF00CC. + 3 => { + const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16); + const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16); + const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16); + return .{ .r = r, .g = g, .b = b, .a = 255 }; + }, + 4 => { + const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16); + const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16); + const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16); + const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16); + return .{ .r = r, .g = g, .b = b, .a = a }; + }, + // Regular HEX format. + 6 => { + const r = try std.fmt.parseInt(u8, slice[0..2], 16); + const g = try std.fmt.parseInt(u8, slice[2..4], 16); + const b = try std.fmt.parseInt(u8, slice[4..6], 16); + return .{ .r = r, .g = g, .b = b, .a = 255 }; + }, + 8 => { + const r = try std.fmt.parseInt(u8, slice[0..2], 16); + const g = try std.fmt.parseInt(u8, slice[2..4], 16); + const b = try std.fmt.parseInt(u8, slice[4..6], 16); + const a = try std.fmt.parseInt(u8, slice[6..8], 16); + return .{ .r = r, .g = g, .b = b, .a = a }; + }, + else => unreachable, + } + } + + /// By default, browsers prefer lowercase formatting. + const format_upper = false; + + /// Formats the `Color` according to web expectations. + /// If color is opaque, HEX is preferred; RGBA otherwise. + pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void { + if (self.isOpaque()) { + // Convert RGB to HEX. + // https://gristle.tripod.com/hexconv.html + // Hexadecimal characters up to 15. + const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef"; + // This variant always prefers 6 digit format, +1 is for hash char. + const buffer = [7]u8{ + '#', + char[self.r >> 4], + char[self.r & 15], + char[self.g >> 4], + char[self.g & 15], + char[self.b >> 4], + char[self.b & 15], + }; + + return writer.writeAll(&buffer); + } + + // Prefer RGBA format for everything else. + return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() }); + } + + /// Returns true if `Color` is opaque. + pub inline fn isOpaque(self: *const RGBA) bool { + return self.a == std.math.maxInt(u8); + } + + /// Returns the normalized alpha value. + pub inline fn normalizedAlpha(self: *const RGBA) f32 { + return @as(f32, @floatFromInt(self.a)) / 255; + } +};