webapi: add live SVG resource elements

This commit is contained in:
Madison Steiner authored and Karl Seguin committed 2026-07-27 19:06:05 +08:00
1 parent 302fc79a59
commit bb063c77bd
23 files changed
+1052 -35

No files matched your search

+2
View File
@@ -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,
+13
View File
@@ -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 => {},
}
+9
View File
@@ -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"),
@@ -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);
}
</script>
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<svg id=root width="200" height="100">
<defs>
<linearGradient id=linear x1="10%" gradientUnits="userSpaceOnUse"
spreadMethod="reflect" gradientTransform="translate(4 5)" transform="translate(90)" href="#base">
<stop id=stop offset="25%"></stop>
</linearGradient>
<radialGradient id=radial cx="20%" cy="30%"></radialGradient>
<clipPath id=clip clipPathUnits="objectBoundingBox" transform="scale(2)"></clipPath>
<mask id=mask></mask>
<marker id=marker></marker>
<pattern id=pattern patternTransform="translate(7 8)" href="#tile"></pattern>
</defs>
</svg>
<script id=resourceIdentities>
{
testing.expectEqual(true, $('#linear') instanceof SVGLinearGradientElement);
testing.expectEqual(true, $('#linear') instanceof SVGGradientElement);
testing.expectEqual(true, $('#linear') instanceof SVGElement);
testing.expectEqual(true, $('#radial') instanceof SVGRadialGradientElement);
testing.expectEqual(true, $('#radial') instanceof SVGGradientElement);
testing.expectEqual(true, $('#clip') instanceof SVGClipPathElement);
testing.expectEqual(true, $('#mask') instanceof SVGMaskElement);
testing.expectEqual(true, $('#marker') instanceof SVGMarkerElement);
testing.expectEqual(true, $('#pattern') instanceof SVGPatternElement);
testing.expectEqual(true, $('#stop') instanceof SVGStopElement);
const ns = 'http://www.w3.org/2000/svg';
testing.expectEqual(true, document.createElementNS(ns, 'linearGradient') instanceof SVGLinearGradientElement);
testing.expectEqual(false, document.createElementNS(ns, 'lineargradient') instanceof SVGLinearGradientElement);
testing.expectEqual(false, document.createElementNS(ns, 'clippath') instanceof SVGClipPathElement);
}
</script>
<script id=gradientReflection>
{
const linear = $('#linear');
testing.expectEqual(true, linear.gradientUnits instanceof SVGAnimatedEnumeration);
testing.expectEqual(true, linear.gradientUnits === linear.gradientUnits);
testing.expectEqual(1, linear.gradientUnits.baseVal);
testing.expectEqual(1, linear.gradientUnits.animVal);
linear.gradientUnits.baseVal = 2;
testing.expectEqual('objectBoundingBox', linear.getAttribute('gradientUnits'));
testing.expectEqual(2, linear.gradientUnits.animVal);
testing.expectError('TypeError', () => { linear.gradientUnits.baseVal = 99; });
testing.expectEqual(2, linear.spreadMethod.baseVal);
linear.setAttribute('spreadMethod', ' \treflect\n');
testing.expectEqual(2, linear.spreadMethod.baseVal);
testing.expectEqual('#base', linear.href.baseVal);
linear.href.baseVal = '#other';
testing.expectEqual('#other', linear.getAttribute('href'));
testing.expectEqual(1, linear.gradientTransform.baseVal.numberOfItems);
testing.expectEqual(4, linear.gradientTransform.baseVal.getItem(0).matrix.e);
testing.expectEqual(5, linear.gradientTransform.baseVal.getItem(0).matrix.f);
testing.expectEqual(true, linear.gradientTransform === linear.gradientTransform);
const gradientMatrix = linear.gradientTransform.baseVal.getItem(0).matrix;
gradientMatrix.e = 12;
testing.expectEqual(true, linear.getAttribute('gradientTransform').startsWith('matrix('));
testing.expectEqual('translate(90)', linear.getAttribute('transform'));
testing.expectEqual(12, linear.gradientTransform.animVal.getItem(0).matrix.e);
testing.expectError('NoModificationAllowedError', () => {
linear.gradientTransform.animVal.getItem(0).matrix.e = 99;
});
testing.expectEqual(2, linear.x1.baseVal.unitType);
testing.expectEqual(true, linear.x2 === linear.x2);
testing.expectEqual(10, linear.x1.baseVal.valueInSpecifiedUnits);
testing.expectEqual('100%', linear.x2.baseVal.valueAsString);
linear.x2.baseVal.valueInSpecifiedUnits = 80;
testing.expectEqual('80%', linear.getAttribute('x2'));
testing.expectEqual(80, linear.x2.animVal.valueInSpecifiedUnits);
testing.expectError('NoModificationAllowedError', () => { linear.x2.animVal.value = 1; });
linear.setAttribute('x2', 'invalid');
testing.expectEqual('100%', linear.x2.baseVal.valueAsString);
}
</script>
<script id=radialFallbacks>
{
const radial = $('#radial');
testing.expectEqual(true, radial.fx === radial.fx);
testing.expectEqual('20%', radial.fx.baseVal.valueAsString);
testing.expectEqual('30%', radial.fy.baseVal.valueAsString);
radial.setAttribute('cx', '40%');
testing.expectEqual('40%', radial.fx.baseVal.valueAsString);
radial.fx.baseVal.valueAsString = '15%';
radial.setAttribute('cx', '60%');
testing.expectEqual('15%', radial.fx.baseVal.valueAsString);
radial.removeAttribute('fx');
testing.expectEqual('60%', radial.fx.baseVal.valueAsString);
testing.expectEqual('50%', radial.r.baseVal.valueAsString);
testing.expectEqual('0%', radial.fr.baseVal.valueAsString);
radial.setAttribute('r', 'invalid');
testing.expectEqual('50%', radial.r.baseVal.valueAsString);
}
</script>
<script id=resourceDefaultsAndAttributes>
{
const clip = $('#clip');
testing.expectEqual(2, clip.clipPathUnits.baseVal);
testing.expectEqual(2, clip.transform.baseVal.getItem(0).matrix.a);
const mask = $('#mask');
testing.expectEqual(true, mask.x === mask.x);
testing.expectEqual('-10%', mask.x.baseVal.valueAsString);
testing.expectEqual('-10%', mask.y.baseVal.valueAsString);
testing.expectEqual('120%', mask.width.baseVal.valueAsString);
testing.expectEqual('120%', mask.height.baseVal.valueAsString);
mask.setAttribute('width', 'invalid');
testing.expectEqual('120%', mask.width.baseVal.valueAsString);
testing.expectEqual(2, mask.maskUnits.baseVal);
testing.expectEqual(1, mask.maskContentUnits.baseVal);
const marker = $('#marker');
testing.expectEqual(true, marker.markerUnits === marker.markerUnits);
testing.expectEqual(3, marker.markerWidth.baseVal.value);
testing.expectEqual(3, marker.markerHeight.baseVal.value);
marker.setAttribute('markerWidth', 'invalid');
testing.expectEqual(3, marker.markerWidth.baseVal.value);
testing.expectEqual(2, marker.markerUnits.baseVal);
const pattern = $('#pattern');
testing.expectEqual(true, pattern.patternTransform === pattern.patternTransform);
testing.expectEqual(2, pattern.patternUnits.baseVal);
testing.expectEqual(1, pattern.patternContentUnits.baseVal);
testing.expectEqual(7, pattern.patternTransform.baseVal.getItem(0).matrix.e);
testing.expectEqual(8, pattern.patternTransform.baseVal.getItem(0).matrix.f);
const patternMatrix = pattern.patternTransform.baseVal.getItem(0).matrix;
patternMatrix.f = 11;
testing.expectEqual(true, pattern.getAttribute('patternTransform').startsWith('matrix('));
testing.expectEqual(11, pattern.patternTransform.animVal.getItem(0).matrix.f);
testing.expectEqual('#tile', pattern.href.baseVal);
const stop = $('#stop');
testing.expectEqual(true, stop.offset === stop.offset);
testing.expectEqual('number', typeof stop.offset.baseVal);
testing.expectEqual(0.25, stop.offset.baseVal);
stop.offset.baseVal = 0.5;
testing.expectEqual('0.5', stop.getAttribute('offset'));
testing.expectEqual(0.5, stop.offset.animVal);
testing.expectEqual(false, Reflect.set(stop.offset, 'animVal', 1));
testing.expectEqual(0.5, stop.offset.animVal);
stop.setAttribute('offset', 'invalid');
testing.expectEqual(0, stop.offset.baseVal);
}
</script>
@@ -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'));
+16 -1
View File
@@ -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,
};
}
@@ -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, .{});
};
@@ -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, .{});
};
+1 -1
View File
@@ -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 {
@@ -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, .{});
};
+53
View File
@@ -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, .{});
};
+57
View File
@@ -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, .{});
};
@@ -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, .{});
};
@@ -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, .{});
};
+33
View File
@@ -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, .{});
};
+2 -2
View File
@@ -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 {
@@ -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, .{});
};
+171 -15
View File
@@ -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;
}
+15 -1
View File
@@ -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;
}
@@ -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,
+34 -6
View File
@@ -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;
+9 -2
View File
@@ -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;