From bb063c77bda6dd583563e7167dcff261c1c51bc5 Mon Sep 17 00:00:00 2001
From: Madison Steiner <8176115+mh0pe@users.noreply.github.com>
Date: Tue, 21 Jul 2026 22:59:12 -0700
Subject: [PATCH] webapi: add live SVG resource elements
---
src/browser/Frame.zig | 2 +
src/browser/frame/node_factory.zig | 13 ++
src/browser/js/bridge.zig | 9 +
src/browser/tests/element/svg/hierarchy.html | 9 +
src/browser/tests/element/svg/resources.html | 152 ++++++++++++++
src/browser/tests/element/svg/svgsvg.html | 4 +
src/browser/webapi/element/Svg.zig | 17 +-
src/browser/webapi/element/svg/ClipPath.zig | 42 ++++
.../webapi/element/svg/GradientElement.zig | 79 ++++++++
src/browser/webapi/element/svg/Graphics.zig | 2 +-
.../webapi/element/svg/LinearGradient.zig | 45 +++++
src/browser/webapi/element/svg/Marker.zig | 53 +++++
src/browser/webapi/element/svg/Mask.zig | 57 ++++++
src/browser/webapi/element/svg/Pattern.zig | 67 +++++++
.../webapi/element/svg/RadialGradient.zig | 53 +++++
src/browser/webapi/element/svg/Stop.zig | 33 ++++
src/browser/webapi/element/svg/Svg.zig | 4 +-
.../webapi/svg/AnimatedEnumeration.zig | 153 ++++++++++++++
src/browser/webapi/svg/AnimatedLength.zig | 186 ++++++++++++++++--
src/browser/webapi/svg/AnimatedNumber.zig | 16 +-
.../webapi/svg/AnimatedTransformList.zig | 40 +++-
src/browser/webapi/svg/Length.zig | 40 +++-
src/browser/webapi/svg/TransformList.zig | 11 +-
23 files changed, 1052 insertions(+), 35 deletions(-)
create mode 100644 src/browser/tests/element/svg/resources.html
create mode 100644 src/browser/webapi/element/svg/ClipPath.zig
create mode 100644 src/browser/webapi/element/svg/GradientElement.zig
create mode 100644 src/browser/webapi/element/svg/LinearGradient.zig
create mode 100644 src/browser/webapi/element/svg/Marker.zig
create mode 100644 src/browser/webapi/element/svg/Mask.zig
create mode 100644 src/browser/webapi/element/svg/Pattern.zig
create mode 100644 src/browser/webapi/element/svg/RadialGradient.zig
create mode 100644 src/browser/webapi/element/svg/Stop.zig
create mode 100644 src/browser/webapi/svg/AnimatedEnumeration.zig
diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig
index a1072e995..f0dd75881 100644
--- a/src/browser/Frame.zig
+++ b/src/browser/Frame.zig
@@ -64,6 +64,7 @@ const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
const PointList = @import("webapi/svg/PointList.zig");
const StringList = @import("webapi/svg/StringList.zig");
+const AnimatedEnumeration = @import("webapi/svg/AnimatedEnumeration.zig");
const AnimatedLength = @import("webapi/svg/AnimatedLength.zig");
const AnimatedNumber = @import("webapi/svg/AnimatedNumber.zig");
const AnimatedString = @import("webapi/svg/AnimatedString.zig");
@@ -150,6 +151,7 @@ _element_shadow_roots: Element.ShadowRootLookup = .empty,
_node_owner_documents: Node.OwnerDocumentLookup = .empty,
_element_scroll_positions: Element.ScrollPositionLookup = .empty,
_element_namespace_uris: Element.NamespaceUriLookup = .empty,
+_svg_animated_enumerations: AnimatedEnumeration.Lookup = .empty,
_svg_animated_lengths: AnimatedLength.Lookup = .empty,
_svg_animated_numbers: AnimatedNumber.Lookup = .empty,
_svg_animated_preserve_aspect_ratios: AnimatedPreserveAspectRatio.Lookup = .empty,
diff --git a/src/browser/frame/node_factory.zig b/src/browser/frame/node_factory.zig
index 024b6614e..6fd085403 100644
--- a/src/browser/frame/node_factory.zig
+++ b/src/browser/frame/node_factory.zig
@@ -902,7 +902,9 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
4 => switch (@as(u32, @bitCast(name[0..4].*))) {
asUint("defs") => return createSvgElementT(frame, Graphics.Defs, name, attribute_iterator, .{ ._proto = undefined }),
asUint("desc") => return createSvgElementT(frame, Element.Svg.Desc, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("mask") => return createSvgElementT(frame, Element.Svg.Mask, name, attribute_iterator, .{ ._proto = undefined }),
asUint("rect") => return createSvgElementT(frame, Geometry.Rect, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("stop") => return createSvgElementT(frame, Element.Svg.Stop, name, attribute_iterator, .{ ._proto = undefined }),
asUint("line") => return createSvgElementT(frame, Geometry.Line, name, attribute_iterator, .{ ._proto = undefined }),
asUint("path") => return createSvgElementT(frame, Geometry.Path, name, attribute_iterator, .{ ._proto = undefined }),
asUint("view") => return createSvgElementT(frame, Element.Svg.View, name, attribute_iterator, .{ ._proto = undefined }),
@@ -915,16 +917,19 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
},
6 => switch (@as(u48, @bitCast(name[0..6].*))) {
asUint("circle") => return createSvgElementT(frame, Geometry.Circle, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("marker") => return createSvgElementT(frame, Element.Svg.Marker, name, attribute_iterator, .{ ._proto = undefined }),
asUint("switch") => return createSvgElementT(frame, Graphics.Switch, name, attribute_iterator, .{ ._proto = undefined }),
asUint("symbol") => return createSvgElementT(frame, Graphics.Symbol, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
7 => switch (@as(u56, @bitCast(name[0..7].*))) {
asUint("ellipse") => return createSvgElementT(frame, Geometry.Ellipse, name, attribute_iterator, .{ ._proto = undefined }),
+ asUint("pattern") => return createSvgElementT(frame, Element.Svg.Pattern, name, attribute_iterator, .{ ._proto = undefined }),
asUint("polygon") => return createSvgElementT(frame, Geometry.Polygon, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
8 => switch (@as(u64, @bitCast(name[0..8].*))) {
+ asUint("clipPath") => return createSvgElementT(frame, Element.Svg.ClipPath, name, attribute_iterator, .{ ._proto = undefined }),
asUint("metadata") => return createSvgElementT(frame, Element.Svg.Metadata, name, attribute_iterator, .{ ._proto = undefined }),
asUint("polyline") => return createSvgElementT(frame, Geometry.Polyline, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
@@ -933,6 +938,14 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
asUint("foreignObject") => return createSvgElementT(frame, Graphics.ForeignObject, name, attribute_iterator, .{ ._proto = undefined }),
else => {},
},
+ 14 => {
+ if (std.mem.eql(u8, name, "linearGradient")) {
+ return createSvgElementT(frame, Element.Svg.GradientElement.LinearGradient, name, attribute_iterator, .{ ._proto = undefined });
+ }
+ if (std.mem.eql(u8, name, "radialGradient")) {
+ return createSvgElementT(frame, Element.Svg.GradientElement.RadialGradient, name, attribute_iterator, .{ ._proto = undefined });
+ }
+ },
else => {},
}
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 953d0bf97..53af562df 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -1068,6 +1068,14 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/element/svg/Title.zig"),
@import("../webapi/element/svg/Desc.zig"),
@import("../webapi/element/svg/Metadata.zig"),
+ @import("../webapi/element/svg/GradientElement.zig"),
+ @import("../webapi/element/svg/LinearGradient.zig"),
+ @import("../webapi/element/svg/RadialGradient.zig"),
+ @import("../webapi/element/svg/ClipPath.zig"),
+ @import("../webapi/element/svg/Marker.zig"),
+ @import("../webapi/element/svg/Mask.zig"),
+ @import("../webapi/element/svg/Pattern.zig"),
+ @import("../webapi/element/svg/Stop.zig"),
@import("../webapi/element/svg/Rect.zig"),
@import("../webapi/element/svg/Circle.zig"),
@import("../webapi/element/svg/Ellipse.zig"),
@@ -1082,6 +1090,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/svg/Transform.zig"),
@import("../webapi/svg/AnimatedLength.zig"),
@import("../webapi/svg/AnimatedNumber.zig"),
+ @import("../webapi/svg/AnimatedEnumeration.zig"),
@import("../webapi/svg/PreserveAspectRatio.zig"),
@import("../webapi/svg/AnimatedPreserveAspectRatio.zig"),
@import("../webapi/svg/PointList.zig"),
diff --git a/src/browser/tests/element/svg/hierarchy.html b/src/browser/tests/element/svg/hierarchy.html
index 3a1e8a795..32ab775ca 100644
--- a/src/browser/tests/element/svg/hierarchy.html
+++ b/src/browser/tests/element/svg/hierarchy.html
@@ -39,6 +39,7 @@
testing.expectEqual('function', typeof SVGAnimatedString);
testing.expectEqual('function', typeof SVGNumber);
testing.expectEqual('function', typeof SVGAnimatedNumber);
+ testing.expectEqual('function', typeof SVGAnimatedEnumeration);
testing.expectEqual('function', typeof SVGSymbolElement);
testing.expectEqual('function', typeof SVGSwitchElement);
testing.expectEqual('function', typeof SVGForeignObjectElement);
@@ -46,6 +47,14 @@
testing.expectEqual('function', typeof SVGTitleElement);
testing.expectEqual('function', typeof SVGDescElement);
testing.expectEqual('function', typeof SVGMetadataElement);
+ testing.expectEqual('function', typeof SVGGradientElement);
+ testing.expectEqual('function', typeof SVGLinearGradientElement);
+ testing.expectEqual('function', typeof SVGRadialGradientElement);
+ testing.expectEqual('function', typeof SVGClipPathElement);
+ testing.expectEqual('function', typeof SVGMarkerElement);
+ testing.expectEqual('function', typeof SVGMaskElement);
+ testing.expectEqual('function', typeof SVGPatternElement);
+ testing.expectEqual('function', typeof SVGStopElement);
}
diff --git a/src/browser/tests/element/svg/resources.html b/src/browser/tests/element/svg/resources.html
new file mode 100644
index 000000000..aadb8b233
--- /dev/null
+++ b/src/browser/tests/element/svg/resources.html
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/element/svg/svgsvg.html b/src/browser/tests/element/svg/svgsvg.html
index 8fa1379f8..c81845a9b 100644
--- a/src/browser/tests/element/svg/svgsvg.html
+++ b/src/browser/tests/element/svg/svgsvg.html
@@ -178,6 +178,10 @@
testing.expectError('NoModificationAllowedError', () => { aspect.animVal.meetOrSlice = 2; });
const empty = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ testing.expectEqual('100%', empty.width.baseVal.valueAsString);
+ testing.expectEqual('100%', empty.height.baseVal.valueAsString);
+ empty.setAttribute('width', 'invalid');
+ testing.expectEqual('100%', empty.width.baseVal.valueAsString);
testing.expectEqual(SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID, empty.preserveAspectRatio.baseVal.align);
testing.expectEqual(SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET, empty.preserveAspectRatio.baseVal.meetOrSlice);
testing.expectEqual(null, empty.getAttribute('preserveAspectRatio'));
diff --git a/src/browser/webapi/element/Svg.zig b/src/browser/webapi/element/Svg.zig
index 442d60402..f53a16160 100644
--- a/src/browser/webapi/element/Svg.zig
+++ b/src/browser/webapi/element/Svg.zig
@@ -31,6 +31,12 @@ pub const View = @import("svg/View.zig");
pub const Title = @import("svg/Title.zig");
pub const Desc = @import("svg/Desc.zig");
pub const Metadata = @import("svg/Metadata.zig");
+pub const GradientElement = @import("svg/GradientElement.zig");
+pub const ClipPath = @import("svg/ClipPath.zig");
+pub const Marker = @import("svg/Marker.zig");
+pub const Mask = @import("svg/Mask.zig");
+pub const Pattern = @import("svg/Pattern.zig");
+pub const Stop = @import("svg/Stop.zig");
const String = lp.String;
@@ -45,6 +51,12 @@ pub const Type = union(enum) {
title: *Title,
desc: *Desc,
metadata: *Metadata,
+ gradient: *GradientElement,
+ clip_path: *ClipPath,
+ marker: *Marker,
+ mask: *Mask,
+ pattern: *Pattern,
+ stop: *Stop,
generic: *Generic,
};
@@ -59,6 +71,9 @@ pub fn is(self: *Svg, comptime T: type) ?*T {
if (self._type == .graphics) {
return self._type.graphics.is(T);
}
+ if (self._type == .gradient) {
+ return self._type.gradient.is(T);
+ }
return null;
}
@@ -82,7 +97,7 @@ pub fn getTag(self: *const Svg) Element.Tag {
},
.generic => |g| g._tag,
.title => .title,
- .view, .desc, .metadata => .unknown,
+ .view, .desc, .metadata, .gradient, .clip_path, .marker, .mask, .pattern, .stop => .unknown,
};
}
diff --git a/src/browser/webapi/element/svg/ClipPath.zig b/src/browser/webapi/element/svg/ClipPath.zig
new file mode 100644
index 000000000..d80155d1e
--- /dev/null
+++ b/src/browser/webapi/element/svg/ClipPath.zig
@@ -0,0 +1,42 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
+const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig");
+
+const ClipPath = @This();
+_proto: *Svg,
+
+pub fn asElement(self: *ClipPath) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *ClipPath) *Node {
+ return self.asElement().asNode();
+}
+
+fn getClipPathUnits(self: *ClipPath, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .clip_path_units, frame);
+}
+
+fn getTransform(self: *ClipPath, frame: *Frame) !*AnimatedTransformList {
+ return AnimatedTransformList.getOrCreate(self.asElement(), .transform, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ClipPath);
+ pub const Meta = struct {
+ pub const name = "SVGClipPathElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true });
+ pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true });
+ pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true });
+ pub const clipPathUnits = bridge.accessor(ClipPath.getClipPathUnits, null, .{});
+ pub const transform = bridge.accessor(ClipPath.getTransform, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/GradientElement.zig b/src/browser/webapi/element/svg/GradientElement.zig
new file mode 100644
index 000000000..a36f350d4
--- /dev/null
+++ b/src/browser/webapi/element/svg/GradientElement.zig
@@ -0,0 +1,79 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
+const AnimatedString = @import("../../svg/AnimatedString.zig");
+const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig");
+
+pub const LinearGradient = @import("LinearGradient.zig");
+pub const RadialGradient = @import("RadialGradient.zig");
+
+const GradientElement = @This();
+_proto: *Svg,
+_type: Type,
+
+pub const Type = union(enum) {
+ linear: *LinearGradient,
+ radial: *RadialGradient,
+};
+
+pub fn is(self: *GradientElement, comptime T: type) ?*T {
+ inline for (@typeInfo(Type).@"union".fields) |field| {
+ if (@field(Type, field.name) == self._type) {
+ if (field.type == *T) {
+ return @field(self._type, field.name);
+ }
+ }
+ }
+ return null;
+}
+
+pub fn asElement(self: *GradientElement) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *GradientElement) *Node {
+ return self.asElement().asNode();
+}
+
+fn getGradientUnits(self: *GradientElement, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .gradient_units, frame);
+}
+
+fn getSpreadMethod(self: *GradientElement, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .spread_method, frame);
+}
+
+fn getGradientTransform(self: *GradientElement, frame: *Frame) !*AnimatedTransformList {
+ return AnimatedTransformList.getOrCreate(self.asElement(), .gradient_transform, frame);
+}
+
+fn getHref(self: *GradientElement, frame: *Frame) !*AnimatedString {
+ return AnimatedString.getOrCreate(self.asElement(), .href, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(GradientElement);
+ pub const Meta = struct {
+ pub const name = "SVGGradientElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true });
+ pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true });
+ pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true });
+ pub const SVG_SPREADMETHOD_UNKNOWN = bridge.property(0, .{ .template = true });
+ pub const SVG_SPREADMETHOD_PAD = bridge.property(1, .{ .template = true });
+ pub const SVG_SPREADMETHOD_REFLECT = bridge.property(2, .{ .template = true });
+ pub const SVG_SPREADMETHOD_REPEAT = bridge.property(3, .{ .template = true });
+
+ pub const gradientUnits = bridge.accessor(GradientElement.getGradientUnits, null, .{});
+ pub const spreadMethod = bridge.accessor(GradientElement.getSpreadMethod, null, .{});
+ pub const gradientTransform = bridge.accessor(GradientElement.getGradientTransform, null, .{});
+ pub const href = bridge.accessor(GradientElement.getHref, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Graphics.zig b/src/browser/webapi/element/svg/Graphics.zig
index d9b4dc269..cbdf90715 100644
--- a/src/browser/webapi/element/svg/Graphics.zig
+++ b/src/browser/webapi/element/svg/Graphics.zig
@@ -204,7 +204,7 @@ fn transformMatrix(element: *Element) PathData.Matrix {
}
pub fn getTransform(self: *Graphics, frame: *Frame) !*AnimatedTransformList {
- return AnimatedTransformList.getOrCreate(self.asElement(), frame);
+ return AnimatedTransformList.getOrCreate(self.asElement(), .transform, frame);
}
pub fn getRequiredExtensions(self: *Graphics, frame: *Frame) !*StringList {
diff --git a/src/browser/webapi/element/svg/LinearGradient.zig b/src/browser/webapi/element/svg/LinearGradient.zig
new file mode 100644
index 000000000..8f79d3e2c
--- /dev/null
+++ b/src/browser/webapi/element/svg/LinearGradient.zig
@@ -0,0 +1,45 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const AnimatedLength = @import("../../svg/AnimatedLength.zig");
+const GradientElement = @import("GradientElement.zig");
+
+const LinearGradient = @This();
+_proto: *GradientElement,
+
+pub fn asElement(self: *LinearGradient) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *LinearGradient) *Node {
+ return self.asElement().asNode();
+}
+
+fn getX1(self: *LinearGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_x1, frame);
+}
+fn getY1(self: *LinearGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_y1, frame);
+}
+fn getX2(self: *LinearGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_x2, frame);
+}
+fn getY2(self: *LinearGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .linear_gradient_y2, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(LinearGradient);
+ pub const Meta = struct {
+ pub const name = "SVGLinearGradientElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const x1 = bridge.accessor(LinearGradient.getX1, null, .{});
+ pub const y1 = bridge.accessor(LinearGradient.getY1, null, .{});
+ pub const x2 = bridge.accessor(LinearGradient.getX2, null, .{});
+ pub const y2 = bridge.accessor(LinearGradient.getY2, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Marker.zig b/src/browser/webapi/element/svg/Marker.zig
new file mode 100644
index 000000000..6e9c580a8
--- /dev/null
+++ b/src/browser/webapi/element/svg/Marker.zig
@@ -0,0 +1,53 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
+const AnimatedLength = @import("../../svg/AnimatedLength.zig");
+
+const Marker = @This();
+_proto: *Svg,
+
+pub fn asElement(self: *Marker) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Marker) *Node {
+ return self.asElement().asNode();
+}
+
+fn getRefX(self: *Marker, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .marker_ref_x, frame);
+}
+fn getRefY(self: *Marker, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .marker_ref_y, frame);
+}
+fn getMarkerWidth(self: *Marker, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .marker_width, frame);
+}
+fn getMarkerHeight(self: *Marker, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .marker_height, frame);
+}
+fn getMarkerUnits(self: *Marker, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .marker_units, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Marker);
+ pub const Meta = struct {
+ pub const name = "SVGMarkerElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const SVG_MARKERUNITS_UNKNOWN = bridge.property(0, .{ .template = true });
+ pub const SVG_MARKERUNITS_USERSPACEONUSE = bridge.property(1, .{ .template = true });
+ pub const SVG_MARKERUNITS_STROKEWIDTH = bridge.property(2, .{ .template = true });
+ pub const refX = bridge.accessor(Marker.getRefX, null, .{});
+ pub const refY = bridge.accessor(Marker.getRefY, null, .{});
+ pub const markerWidth = bridge.accessor(Marker.getMarkerWidth, null, .{});
+ pub const markerHeight = bridge.accessor(Marker.getMarkerHeight, null, .{});
+ pub const markerUnits = bridge.accessor(Marker.getMarkerUnits, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Mask.zig b/src/browser/webapi/element/svg/Mask.zig
new file mode 100644
index 000000000..4b397505f
--- /dev/null
+++ b/src/browser/webapi/element/svg/Mask.zig
@@ -0,0 +1,57 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
+const AnimatedLength = @import("../../svg/AnimatedLength.zig");
+
+const Mask = @This();
+_proto: *Svg,
+
+pub fn asElement(self: *Mask) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Mask) *Node {
+ return self.asElement().asNode();
+}
+
+fn getX(self: *Mask, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .mask_x, frame);
+}
+fn getY(self: *Mask, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .mask_y, frame);
+}
+fn getWidth(self: *Mask, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .mask_width, frame);
+}
+fn getHeight(self: *Mask, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .mask_height, frame);
+}
+fn getMaskUnits(self: *Mask, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .mask_units, frame);
+}
+fn getMaskContentUnits(self: *Mask, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .mask_content_units, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Mask);
+ pub const Meta = struct {
+ pub const name = "SVGMaskElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true });
+ pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true });
+ pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true });
+ pub const x = bridge.accessor(Mask.getX, null, .{});
+ pub const y = bridge.accessor(Mask.getY, null, .{});
+ pub const width = bridge.accessor(Mask.getWidth, null, .{});
+ pub const height = bridge.accessor(Mask.getHeight, null, .{});
+ pub const maskUnits = bridge.accessor(Mask.getMaskUnits, null, .{});
+ pub const maskContentUnits = bridge.accessor(Mask.getMaskContentUnits, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Pattern.zig b/src/browser/webapi/element/svg/Pattern.zig
new file mode 100644
index 000000000..0f2a614bf
--- /dev/null
+++ b/src/browser/webapi/element/svg/Pattern.zig
@@ -0,0 +1,67 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+const AnimatedEnumeration = @import("../../svg/AnimatedEnumeration.zig");
+const AnimatedLength = @import("../../svg/AnimatedLength.zig");
+const AnimatedString = @import("../../svg/AnimatedString.zig");
+const AnimatedTransformList = @import("../../svg/AnimatedTransformList.zig");
+
+const Pattern = @This();
+_proto: *Svg,
+
+pub fn asElement(self: *Pattern) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Pattern) *Node {
+ return self.asElement().asNode();
+}
+
+fn getX(self: *Pattern, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .pattern_x, frame);
+}
+fn getY(self: *Pattern, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .pattern_y, frame);
+}
+fn getWidth(self: *Pattern, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .pattern_width, frame);
+}
+fn getHeight(self: *Pattern, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .pattern_height, frame);
+}
+fn getPatternUnits(self: *Pattern, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .pattern_units, frame);
+}
+fn getPatternContentUnits(self: *Pattern, frame: *Frame) !*AnimatedEnumeration {
+ return AnimatedEnumeration.getOrCreate(self.asElement(), .pattern_content_units, frame);
+}
+fn getPatternTransform(self: *Pattern, frame: *Frame) !*AnimatedTransformList {
+ return AnimatedTransformList.getOrCreate(self.asElement(), .pattern_transform, frame);
+}
+fn getHref(self: *Pattern, frame: *Frame) !*AnimatedString {
+ return AnimatedString.getOrCreate(self.asElement(), .href, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Pattern);
+ pub const Meta = struct {
+ pub const name = "SVGPatternElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const SVG_UNIT_TYPE_UNKNOWN = bridge.property(0, .{ .template = true });
+ pub const SVG_UNIT_TYPE_USERSPACEONUSE = bridge.property(1, .{ .template = true });
+ pub const SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = bridge.property(2, .{ .template = true });
+ pub const x = bridge.accessor(Pattern.getX, null, .{});
+ pub const y = bridge.accessor(Pattern.getY, null, .{});
+ pub const width = bridge.accessor(Pattern.getWidth, null, .{});
+ pub const height = bridge.accessor(Pattern.getHeight, null, .{});
+ pub const patternUnits = bridge.accessor(Pattern.getPatternUnits, null, .{});
+ pub const patternContentUnits = bridge.accessor(Pattern.getPatternContentUnits, null, .{});
+ pub const patternTransform = bridge.accessor(Pattern.getPatternTransform, null, .{});
+ pub const href = bridge.accessor(Pattern.getHref, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/RadialGradient.zig b/src/browser/webapi/element/svg/RadialGradient.zig
new file mode 100644
index 000000000..ef8522545
--- /dev/null
+++ b/src/browser/webapi/element/svg/RadialGradient.zig
@@ -0,0 +1,53 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const AnimatedLength = @import("../../svg/AnimatedLength.zig");
+const GradientElement = @import("GradientElement.zig");
+
+const RadialGradient = @This();
+_proto: *GradientElement,
+
+pub fn asElement(self: *RadialGradient) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *RadialGradient) *Node {
+ return self.asElement().asNode();
+}
+
+fn getCx(self: *RadialGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_cx, frame);
+}
+fn getCy(self: *RadialGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_cy, frame);
+}
+fn getR(self: *RadialGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_r, frame);
+}
+fn getFx(self: *RadialGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_fx, frame);
+}
+fn getFy(self: *RadialGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_fy, frame);
+}
+fn getFr(self: *RadialGradient, frame: *Frame) !*AnimatedLength {
+ return AnimatedLength.getOrCreate(self.asElement(), .radial_gradient_fr, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(RadialGradient);
+ pub const Meta = struct {
+ pub const name = "SVGRadialGradientElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const cx = bridge.accessor(RadialGradient.getCx, null, .{});
+ pub const cy = bridge.accessor(RadialGradient.getCy, null, .{});
+ pub const r = bridge.accessor(RadialGradient.getR, null, .{});
+ pub const fx = bridge.accessor(RadialGradient.getFx, null, .{});
+ pub const fy = bridge.accessor(RadialGradient.getFy, null, .{});
+ pub const fr = bridge.accessor(RadialGradient.getFr, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Stop.zig b/src/browser/webapi/element/svg/Stop.zig
new file mode 100644
index 000000000..1ca7a1059
--- /dev/null
+++ b/src/browser/webapi/element/svg/Stop.zig
@@ -0,0 +1,33 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const js = @import("../../../js/js.zig");
+const Frame = @import("../../../Frame.zig");
+const Node = @import("../../Node.zig");
+const Element = @import("../../Element.zig");
+const Svg = @import("../Svg.zig");
+const AnimatedNumber = @import("../../svg/AnimatedNumber.zig");
+
+const Stop = @This();
+_proto: *Svg,
+
+pub fn asElement(self: *Stop) *Element {
+ return self._proto.asElement();
+}
+pub fn asNode(self: *Stop) *Node {
+ return self.asElement().asNode();
+}
+
+fn getOffset(self: *Stop, frame: *Frame) !*AnimatedNumber {
+ return AnimatedNumber.getOrCreate(self.asElement(), .offset, frame);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(Stop);
+ pub const Meta = struct {
+ pub const name = "SVGStopElement";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+ pub const offset = bridge.accessor(Stop.getOffset, null, .{});
+};
diff --git a/src/browser/webapi/element/svg/Svg.zig b/src/browser/webapi/element/svg/Svg.zig
index ab60357f9..17d3db27e 100644
--- a/src/browser/webapi/element/svg/Svg.zig
+++ b/src/browser/webapi/element/svg/Svg.zig
@@ -70,11 +70,11 @@ pub fn getY(self: *Svg, frame: *Frame) !*AnimatedLength {
}
pub fn getWidth(self: *Svg, frame: *Frame) !*AnimatedLength {
- return AnimatedLength.getOrCreate(self.asElement(), .width, frame);
+ return AnimatedLength.getOrCreate(self.asElement(), .svg_width, frame);
}
pub fn getHeight(self: *Svg, frame: *Frame) !*AnimatedLength {
- return AnimatedLength.getOrCreate(self.asElement(), .height, frame);
+ return AnimatedLength.getOrCreate(self.asElement(), .svg_height, frame);
}
pub fn getPreserveAspectRatio(self: *Svg, frame: *Frame) !*AnimatedPreserveAspectRatio {
diff --git a/src/browser/webapi/svg/AnimatedEnumeration.zig b/src/browser/webapi/svg/AnimatedEnumeration.zig
new file mode 100644
index 000000000..0a042e77c
--- /dev/null
+++ b/src/browser/webapi/svg/AnimatedEnumeration.zig
@@ -0,0 +1,153 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+const std = @import("std");
+const lp = @import("lightpanda");
+
+const js = @import("../../js/js.zig");
+const Frame = @import("../../Frame.zig");
+const Element = @import("../Element.zig");
+
+const AnimatedEnumeration = @This();
+
+_element: *Element,
+_attr_name: lp.String,
+_entries: []const Entry,
+_default_value: u16,
+
+pub const Entry = struct {
+ keyword: []const u8,
+ value: u16,
+};
+
+const unit_entries = [_]Entry{
+ .{ .keyword = "userSpaceOnUse", .value = 1 },
+ .{ .keyword = "objectBoundingBox", .value = 2 },
+};
+const marker_unit_entries = [_]Entry{
+ .{ .keyword = "userSpaceOnUse", .value = 1 },
+ .{ .keyword = "strokeWidth", .value = 2 },
+};
+const spread_entries = [_]Entry{
+ .{ .keyword = "pad", .value = 1 },
+ .{ .keyword = "reflect", .value = 2 },
+ .{ .keyword = "repeat", .value = 3 },
+};
+
+pub const Kind = enum {
+ clip_path_units,
+ gradient_units,
+ spread_method,
+ marker_units,
+ mask_units,
+ mask_content_units,
+ pattern_units,
+ pattern_content_units,
+
+ fn attributeName(self: Kind, frame: *Frame) !lp.String {
+ const name = switch (self) {
+ .clip_path_units => "clipPathUnits",
+ .gradient_units => "gradientUnits",
+ .spread_method => "spreadMethod",
+ .marker_units => "markerUnits",
+ .mask_units => "maskUnits",
+ .mask_content_units => "maskContentUnits",
+ .pattern_units => "patternUnits",
+ .pattern_content_units => "patternContentUnits",
+ };
+ return lp.String.init(frame.arena, name, .{ .dupe = false });
+ }
+
+ fn entries(self: Kind) []const Entry {
+ return switch (self) {
+ .marker_units => &marker_unit_entries,
+ .spread_method => &spread_entries,
+ else => &unit_entries,
+ };
+ }
+
+ fn defaultValue(self: Kind) u16 {
+ return switch (self) {
+ .clip_path_units,
+ .mask_content_units,
+ .pattern_content_units,
+ .spread_method,
+ => 1,
+ else => 2,
+ };
+ }
+};
+
+pub const Key = struct {
+ element: *Element,
+ kind: Kind,
+};
+
+pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedEnumeration);
+
+pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedEnumeration {
+ const key: Key = .{ .element = element, .kind = kind };
+ const gop = try frame._svg_animated_enumerations.getOrPut(frame.arena, key);
+ if (!gop.found_existing) {
+ errdefer _ = frame._svg_animated_enumerations.remove(key);
+ gop.value_ptr.* = try create(
+ element,
+ try kind.attributeName(frame),
+ kind.entries(),
+ kind.defaultValue(),
+ frame,
+ );
+ }
+ return gop.value_ptr.*;
+}
+
+pub fn create(
+ element: *Element,
+ attr_name: lp.String,
+ entries: []const Entry,
+ default_value: u16,
+ frame: *Frame,
+) !*AnimatedEnumeration {
+ return frame._factory.create(AnimatedEnumeration{
+ ._element = element,
+ ._attr_name = attr_name,
+ ._entries = entries,
+ ._default_value = default_value,
+ });
+}
+
+pub fn getBaseVal(self: *const AnimatedEnumeration) u16 {
+ const raw = self._element.getAttributeSafe(self._attr_name) orelse return self._default_value;
+ const trimmed = std.mem.trim(u8, raw, " \t\r\n\x0c");
+ for (self._entries) |entry| {
+ if (std.mem.eql(u8, trimmed, entry.keyword)) return entry.value;
+ }
+ return 0;
+}
+
+pub fn setBaseVal(self: *AnimatedEnumeration, value: u16, frame: *Frame) !void {
+ for (self._entries) |entry| {
+ if (entry.value == value) {
+ try self._element.setAttributeSafe(self._attr_name, lp.String.wrap(entry.keyword), frame);
+ return;
+ }
+ }
+ return error.TypeError;
+}
+
+pub fn getAnimVal(self: *const AnimatedEnumeration) u16 {
+ return self.getBaseVal();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(AnimatedEnumeration);
+
+ pub const Meta = struct {
+ pub const name = "SVGAnimatedEnumeration";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const baseVal = bridge.accessor(AnimatedEnumeration.getBaseVal, AnimatedEnumeration.setBaseVal, .{});
+ pub const animVal = bridge.accessor(AnimatedEnumeration.getAnimVal, null, .{});
+};
diff --git a/src/browser/webapi/svg/AnimatedLength.zig b/src/browser/webapi/svg/AnimatedLength.zig
index 47fb4927a..bab50c803 100644
--- a/src/browser/webapi/svg/AnimatedLength.zig
+++ b/src/browser/webapi/svg/AnimatedLength.zig
@@ -34,6 +34,8 @@ pub const Kind = enum {
y,
width,
height,
+ svg_width,
+ svg_height,
cx,
cy,
r,
@@ -43,30 +45,141 @@ pub const Kind = enum {
y1,
x2,
y2,
+ linear_gradient_x1,
+ linear_gradient_y1,
+ linear_gradient_x2,
+ linear_gradient_y2,
+ radial_gradient_cx,
+ radial_gradient_cy,
+ radial_gradient_r,
+ radial_gradient_fx,
+ radial_gradient_fy,
+ radial_gradient_fr,
+ marker_ref_x,
+ marker_ref_y,
+ marker_width,
+ marker_height,
+ mask_x,
+ mask_y,
+ mask_width,
+ mask_height,
+ pattern_x,
+ pattern_y,
+ pattern_width,
+ pattern_height,
fn attributeName(self: Kind) lp.String {
return switch (self) {
- .x => comptime .wrap("x"),
- .y => comptime .wrap("y"),
- .width => comptime .wrap("width"),
- .height => comptime .wrap("height"),
- .cx => comptime .wrap("cx"),
- .cy => comptime .wrap("cy"),
- .r => comptime .wrap("r"),
+ .x, .mask_x, .pattern_x => comptime .wrap("x"),
+ .y, .mask_y, .pattern_y => comptime .wrap("y"),
+ .width, .svg_width, .mask_width, .pattern_width => comptime .wrap("width"),
+ .height, .svg_height, .mask_height, .pattern_height => comptime .wrap("height"),
+ .cx, .radial_gradient_cx => comptime .wrap("cx"),
+ .cy, .radial_gradient_cy => comptime .wrap("cy"),
+ .r, .radial_gradient_r => comptime .wrap("r"),
.rx => comptime .wrap("rx"),
.ry => comptime .wrap("ry"),
- .x1 => comptime .wrap("x1"),
- .y1 => comptime .wrap("y1"),
- .x2 => comptime .wrap("x2"),
- .y2 => comptime .wrap("y2"),
+ .x1, .linear_gradient_x1 => comptime .wrap("x1"),
+ .y1, .linear_gradient_y1 => comptime .wrap("y1"),
+ .x2, .linear_gradient_x2 => comptime .wrap("x2"),
+ .y2, .linear_gradient_y2 => comptime .wrap("y2"),
+ .radial_gradient_fx => comptime .wrap("fx"),
+ .radial_gradient_fy => comptime .wrap("fy"),
+ .radial_gradient_fr => comptime .wrap("fr"),
+ .marker_ref_x => comptime .wrap("refX"),
+ .marker_ref_y => comptime .wrap("refY"),
+ .marker_width => comptime .wrap("markerWidth"),
+ .marker_height => comptime .wrap("markerHeight"),
};
}
fn direction(self: Kind) Length.Direction {
return switch (self) {
- .x, .width, .cx, .rx, .x1, .x2 => .horizontal,
- .y, .height, .cy, .ry, .y1, .y2 => .vertical,
- .r => .unspecified,
+ .x,
+ .width,
+ .svg_width,
+ .cx,
+ .rx,
+ .x1,
+ .x2,
+ .linear_gradient_x1,
+ .linear_gradient_x2,
+ .radial_gradient_cx,
+ .radial_gradient_fx,
+ .marker_ref_x,
+ .marker_width,
+ .mask_x,
+ .mask_width,
+ .pattern_x,
+ .pattern_width,
+ => .horizontal,
+ .y,
+ .height,
+ .svg_height,
+ .cy,
+ .ry,
+ .y1,
+ .y2,
+ .linear_gradient_y1,
+ .linear_gradient_y2,
+ .radial_gradient_cy,
+ .radial_gradient_fy,
+ .marker_ref_y,
+ .marker_height,
+ .mask_y,
+ .mask_height,
+ .pattern_y,
+ .pattern_height,
+ => .vertical,
+ .r, .radial_gradient_r, .radial_gradient_fr => .unspecified,
+ };
+ }
+
+ fn defaultValue(self: Kind) f64 {
+ return switch (self) {
+ .svg_width, .svg_height => 100,
+ .linear_gradient_x2 => 100,
+ .radial_gradient_cx,
+ .radial_gradient_cy,
+ .radial_gradient_r,
+ .radial_gradient_fx,
+ .radial_gradient_fy,
+ => 50,
+ .marker_width, .marker_height => 3,
+ .mask_x, .mask_y => -10,
+ .mask_width, .mask_height => 120,
+ else => 0,
+ };
+ }
+
+ fn defaultUnit(self: Kind) Length.Unit {
+ return switch (self) {
+ .svg_width,
+ .svg_height,
+ .linear_gradient_x1,
+ .linear_gradient_y1,
+ .linear_gradient_x2,
+ .linear_gradient_y2,
+ .radial_gradient_cx,
+ .radial_gradient_cy,
+ .radial_gradient_r,
+ .radial_gradient_fx,
+ .radial_gradient_fy,
+ .radial_gradient_fr,
+ .mask_x,
+ .mask_y,
+ .mask_width,
+ .mask_height,
+ => .percentage,
+ else => .number,
+ };
+ }
+
+ fn fallbackAttributeName(self: Kind) ?lp.String {
+ return switch (self) {
+ .radial_gradient_fx => comptime .wrap("cx"),
+ .radial_gradient_fy => comptime .wrap("cy"),
+ else => null,
};
}
};
@@ -86,7 +199,15 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedLengt
const gop = try frame._svg_animated_lengths.getOrPut(frame.arena, key);
if (!gop.found_existing) {
errdefer _ = frame._svg_animated_lengths.remove(key);
- gop.value_ptr.* = try create(element, kind.attributeName(), kind.direction(), frame);
+ gop.value_ptr.* = try createConfigured(
+ element,
+ kind.attributeName(),
+ kind.direction(),
+ kind.defaultValue(),
+ kind.defaultUnit(),
+ kind.fallbackAttributeName(),
+ frame,
+ );
}
return gop.value_ptr.*;
}
@@ -100,6 +221,41 @@ pub fn create(element: *Element, attr_name: lp.String, direction: Length.Directi
});
}
+pub fn createConfigured(
+ element: *Element,
+ attr_name: lp.String,
+ direction: Length.Direction,
+ default_value: f64,
+ default_unit: Length.Unit,
+ fallback_attr_name: ?lp.String,
+ frame: *Frame,
+) !*AnimatedLength {
+ const base_val = try Length.reflectedConfigured(
+ element,
+ attr_name,
+ direction,
+ default_value,
+ default_unit,
+ fallback_attr_name,
+ false,
+ frame,
+ );
+ const anim_val = try Length.reflectedConfigured(
+ element,
+ attr_name,
+ direction,
+ default_value,
+ default_unit,
+ fallback_attr_name,
+ true,
+ frame,
+ );
+ return frame._factory.create(AnimatedLength{
+ ._base_val = base_val,
+ ._anim_val = anim_val,
+ });
+}
+
pub fn getBaseVal(self: *AnimatedLength) *Length {
return self._base_val;
}
diff --git a/src/browser/webapi/svg/AnimatedNumber.zig b/src/browser/webapi/svg/AnimatedNumber.zig
index e96684772..7d0e97906 100644
--- a/src/browser/webapi/svg/AnimatedNumber.zig
+++ b/src/browser/webapi/svg/AnimatedNumber.zig
@@ -27,13 +27,23 @@ const AnimatedNumber = @This();
_element: *Element,
_attr_name: lp.String,
+_allow_percentage: bool,
pub const Kind = enum {
path_length,
+ offset,
fn attributeName(self: Kind) lp.String {
return switch (self) {
.path_length => comptime .wrap("pathLength"),
+ .offset => comptime .wrap("offset"),
+ };
+ }
+
+ fn allowsPercentage(self: Kind) bool {
+ return switch (self) {
+ .path_length => false,
+ .offset => true,
};
}
};
@@ -53,6 +63,7 @@ pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedNumbe
gop.value_ptr.* = try frame._factory.create(AnimatedNumber{
._element = element,
._attr_name = kind.attributeName(),
+ ._allow_percentage = kind.allowsPercentage(),
});
}
return gop.value_ptr.*;
@@ -77,7 +88,10 @@ pub fn getAnimVal(self: *const AnimatedNumber) f32 {
fn currentValue(self: *const AnimatedNumber) f32 {
const raw = self._element.getAttributeSafe(self._attr_name) orelse return 0;
const trimmed = std.mem.trim(u8, raw, " \t\r\n\x0c");
- const value = std.fmt.parseFloat(f32, trimmed) catch return 0;
+ const value = if (self._allow_percentage and std.mem.endsWith(u8, trimmed, "%"))
+ (std.fmt.parseFloat(f32, trimmed[0 .. trimmed.len - 1]) catch return 0) / 100
+ else
+ std.fmt.parseFloat(f32, trimmed) catch return 0;
return if (std.math.isFinite(value)) value else 0;
}
diff --git a/src/browser/webapi/svg/AnimatedTransformList.zig b/src/browser/webapi/svg/AnimatedTransformList.zig
index 0ec5670f6..04a26992d 100644
--- a/src/browser/webapi/svg/AnimatedTransformList.zig
+++ b/src/browser/webapi/svg/AnimatedTransformList.zig
@@ -6,6 +6,7 @@
// (at your option) any later version.
const std = @import("std");
+const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
@@ -18,20 +19,45 @@ const AnimatedTransformList = @This();
_base_val: *TransformList,
_anim_val: *TransformList,
-pub const Lookup = std.AutoHashMapUnmanaged(*Element, *AnimatedTransformList);
+pub const Kind = enum {
+ transform,
+ gradient_transform,
+ pattern_transform,
-pub fn getOrCreate(element: *Element, frame: *Frame) !*AnimatedTransformList {
- const gop = try frame._svg_animated_transform_lists.getOrPut(frame.arena, element);
+ fn attributeName(self: Kind, frame: *Frame) !lp.String {
+ const name = switch (self) {
+ .transform => "transform",
+ .gradient_transform => "gradientTransform",
+ .pattern_transform => "patternTransform",
+ };
+ return lp.String.init(frame.arena, name, .{ .dupe = false });
+ }
+};
+
+pub const Key = struct {
+ element: *Element,
+ kind: Kind,
+};
+
+pub const Lookup = std.AutoHashMapUnmanaged(Key, *AnimatedTransformList);
+
+pub fn getOrCreate(element: *Element, kind: Kind, frame: *Frame) !*AnimatedTransformList {
+ const key: Key = .{ .element = element, .kind = kind };
+ const gop = try frame._svg_animated_transform_lists.getOrPut(frame.arena, key);
if (!gop.found_existing) {
- errdefer _ = frame._svg_animated_transform_lists.remove(element);
- gop.value_ptr.* = try create(element, frame);
+ errdefer _ = frame._svg_animated_transform_lists.remove(key);
+ gop.value_ptr.* = try createForAttribute(element, try kind.attributeName(frame), frame);
}
return gop.value_ptr.*;
}
pub fn create(element: *Element, frame: *Frame) !*AnimatedTransformList {
- const base_val = try TransformList.create(element, false, frame);
- const anim_val = try TransformList.create(element, true, frame);
+ return createForAttribute(element, comptime .wrap("transform"), frame);
+}
+
+pub fn createForAttribute(element: *Element, attr_name: lp.String, frame: *Frame) !*AnimatedTransformList {
+ const base_val = try TransformList.createForAttribute(element, attr_name, false, frame);
+ const anim_val = try TransformList.createForAttribute(element, attr_name, true, frame);
return frame._factory.create(AnimatedTransformList{
._base_val = base_val,
._anim_val = anim_val,
diff --git a/src/browser/webapi/svg/Length.zig b/src/browser/webapi/svg/Length.zig
index 86274696d..84f94fc02 100644
--- a/src/browser/webapi/svg/Length.zig
+++ b/src/browser/webapi/svg/Length.zig
@@ -32,6 +32,9 @@ _element: ?*Element = null,
_attr_name: String = .empty,
_direction: Direction = .unspecified,
_read_only: bool = false,
+_default_value: f64 = 0,
+_default_unit: Unit = .number,
+_fallback_attr_name: ?String = null,
pub const Direction = enum {
horizontal,
@@ -39,7 +42,7 @@ pub const Direction = enum {
unspecified,
};
-const Unit = enum(u16) {
+pub const Unit = enum(u16) {
unknown = 0,
number = 1,
percentage = 2,
@@ -68,6 +71,28 @@ pub fn reflected(element: *Element, attr_name: String, direction: Direction, rea
});
}
+pub fn reflectedConfigured(
+ element: *Element,
+ attr_name: String,
+ direction: Direction,
+ default_value: f64,
+ default_unit: Unit,
+ fallback_attr_name: ?String,
+ read_only: bool,
+ frame: *Frame,
+) !*Length {
+ try ensureFinite(default_value);
+ return frame._factory.create(Length{
+ ._element = element,
+ ._attr_name = attr_name,
+ ._direction = direction,
+ ._read_only = read_only,
+ ._default_value = default_value,
+ ._default_unit = default_unit,
+ ._fallback_attr_name = fallback_attr_name,
+ });
+}
+
pub fn getUnitType(self: *Length) u16 {
self.syncFromAttribute();
return @intFromEnum(self._unit);
@@ -155,14 +180,17 @@ fn ensureFinite(value: f64) !void {
fn syncFromAttribute(self: *Length) void {
const element = self._element orelse return;
- const raw = element.getAttributeSafe(self._attr_name) orelse {
- self._value = 0;
- self._unit = .number;
+ const raw = element.getAttributeSafe(self._attr_name) orelse raw: {
+ if (self._fallback_attr_name) |fallback_attr_name| {
+ if (element.getAttributeSafe(fallback_attr_name)) |fallback| break :raw fallback;
+ }
+ self._value = self._default_value;
+ self._unit = self._default_unit;
return;
};
const parsed = parse(raw) catch {
- self._value = 0;
- self._unit = .unknown;
+ self._value = self._default_value;
+ self._unit = self._default_unit;
return;
};
self._value = parsed.value;
diff --git a/src/browser/webapi/svg/TransformList.zig b/src/browser/webapi/svg/TransformList.zig
index 8baf90052..c882f0690 100644
--- a/src/browser/webapi/svg/TransformList.zig
+++ b/src/browser/webapi/svg/TransformList.zig
@@ -6,6 +6,7 @@
// (at your option) any later version.
const std = @import("std");
+const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
@@ -17,6 +18,7 @@ const Transform = @import("Transform.zig");
const TransformList = @This();
_frame: *Frame,
+_attr_name: lp.String,
_read_only: bool,
_element: *Element,
_synced: bool = false,
@@ -25,9 +27,14 @@ _items: std.ArrayList(*Transform) = .empty,
_retired: std.ArrayList(*Transform) = .empty,
pub fn create(element: *Element, read_only: bool, frame: *Frame) !*TransformList {
+ return createForAttribute(element, comptime .wrap("transform"), read_only, frame);
+}
+
+pub fn createForAttribute(element: *Element, attr_name: lp.String, read_only: bool, frame: *Frame) !*TransformList {
return frame._factory.create(TransformList{
._frame = frame,
._element = element,
+ ._attr_name = attr_name,
._read_only = read_only,
});
}
@@ -202,7 +209,7 @@ fn mutateTransform(context: *anyopaque, transform: *Transform, state: Transform.
fn sync(self: *TransformList, frame: *Frame) !void {
self.releaseRetired(frame._page);
- const raw = self._element.getAttributeSafe(comptime .wrap("transform")) orelse "";
+ const raw = self._element.getAttributeSafe(self._attr_name) orelse "";
if (self._synced and std.mem.eql(u8, self._snapshot.items, raw)) return;
self._synced = false;
@@ -285,7 +292,7 @@ fn setAttributeWithOverride(self: *TransformList, index: usize, state: Transform
fn commitAttribute(self: *TransformList, serialized: []const u8, frame: *Frame) !void {
self._synced = false;
- try self._element.setAttributeSafe(comptime .wrap("transform"), .wrap(serialized), frame);
+ try self._element.setAttributeSafe(self._attr_name, .wrap(serialized), frame);
self._snapshot.clearRetainingCapacity();
try self._snapshot.appendSlice(frame.arena, serialized);
self._synced = true;