diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index ef215554..675935ea 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -822,6 +822,8 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/DOMTreeWalker.zig"),
@import("../webapi/DOMNodeIterator.zig"),
@import("../webapi/DOMRect.zig"),
+ @import("../webapi/DOMMatrixReadOnly.zig"),
+ @import("../webapi/DOMMatrix.zig"),
@import("../webapi/DOMParser.zig"),
@import("../webapi/XMLSerializer.zig"),
@import("../webapi/AbstractRange.zig"),
@@ -1008,6 +1010,8 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/event/PromiseRejectionEvent.zig"),
@import("../webapi/event/CloseEvent.zig"),
@import("../webapi/DOMException.zig"),
+ @import("../webapi/DOMMatrixReadOnly.zig"),
+ @import("../webapi/DOMMatrix.zig"),
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/encoding/TextDecoder.zig"),
diff --git a/src/browser/tests/dommatrix.html b/src/browser/tests/dommatrix.html
new file mode 100644
index 00000000..9726316e
--- /dev/null
+++ b/src/browser/tests/dommatrix.html
@@ -0,0 +1,401 @@
+
+
+ DOMMatrix Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/DOMMatrix.zig b/src/browser/webapi/DOMMatrix.zig
new file mode 100644
index 00000000..7e7c1384
--- /dev/null
+++ b/src/browser/webapi/DOMMatrix.zig
@@ -0,0 +1,301 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+
+const js = @import("../js/js.zig");
+const Page = @import("../Page.zig");
+const RO = @import("DOMMatrixReadOnly.zig");
+
+const DOMMatrix = @This();
+
+_proto: *RO,
+
+pub fn init(init_: ?js.Value, exec: *const js.Execution) !*DOMMatrix {
+ const parsed = try RO.Parsed.init(init_, exec);
+ return create(parsed.m, parsed.is_2d, exec.page);
+}
+
+// Builds the [DOMMatrixReadOnly, DOMMatrix] prototype chain on a single arena
+// (owned by the base) and cross-links them, the same way File wraps Blob.
+pub fn create(m: [16]f64, is_2d: bool, page: *Page) !*DOMMatrix {
+ const proto = try RO.createBare(m, is_2d, page);
+ errdefer proto.deinit(page);
+
+ const self = try proto._arena.create(DOMMatrix);
+ self.* = .{ ._proto = proto };
+ proto._type = .{ .mutable = self };
+ return self;
+}
+
+pub fn fromMatrix(other_: ?RO.DOMMatrixInit, page: *Page) !*DOMMatrix {
+ const parsed = try RO.fixupDict(other_ orelse .{});
+ return create(parsed.m, parsed.is_2d, page);
+}
+
+pub fn fromFloat32Array(array: js.TypedArray(f32), page: *Page) !*DOMMatrix {
+ const parsed = try RO.floatsToParsed(f32, array.values);
+ return create(parsed.m, parsed.is_2d, page);
+}
+
+pub fn fromFloat64Array(array: js.TypedArray(f64), page: *Page) !*DOMMatrix {
+ const parsed = try RO.floatsToParsed(f64, array.values);
+ return create(parsed.m, parsed.is_2d, page);
+}
+
+// The base already exposes read-only getters, but a redeclared accessor's
+// getter must be typed to this (owner) struct, so we provide DOMMatrix-typed
+// getters that read through `_proto`.
+
+pub fn getA(self: *const DOMMatrix) f64 {
+ return self._proto._m[0];
+}
+pub fn getB(self: *const DOMMatrix) f64 {
+ return self._proto._m[1];
+}
+pub fn getC(self: *const DOMMatrix) f64 {
+ return self._proto._m[4];
+}
+pub fn getD(self: *const DOMMatrix) f64 {
+ return self._proto._m[5];
+}
+pub fn getE(self: *const DOMMatrix) f64 {
+ return self._proto._m[12];
+}
+pub fn getF(self: *const DOMMatrix) f64 {
+ return self._proto._m[13];
+}
+
+pub fn setA(self: *DOMMatrix, v: f64) void {
+ self._proto._m[0] = v;
+}
+pub fn setB(self: *DOMMatrix, v: f64) void {
+ self._proto._m[1] = v;
+}
+pub fn setC(self: *DOMMatrix, v: f64) void {
+ self._proto._m[4] = v;
+}
+pub fn setD(self: *DOMMatrix, v: f64) void {
+ self._proto._m[5] = v;
+}
+pub fn setE(self: *DOMMatrix, v: f64) void {
+ self._proto._m[12] = v;
+}
+pub fn setF(self: *DOMMatrix, v: f64) void {
+ self._proto._m[13] = v;
+}
+
+pub fn translateSelf(self: *DOMMatrix, tx_: ?f64, ty_: ?f64, tz_: ?f64) *DOMMatrix {
+ const tz = tz_ orelse 0;
+ const p = self._proto;
+ p._m = RO.multiplyMatrix(p._m, RO.translationMatrix(tx_ orelse 0, ty_ orelse 0, tz));
+ if (tz != 0) p._is_2d = false;
+ return self;
+}
+
+pub fn scaleSelf(self: *DOMMatrix, sx_: ?f64, sy_: ?f64, sz_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64) *DOMMatrix {
+ const sx = sx_ orelse 1;
+ const sy = sy_ orelse sx;
+ const sz = sz_ orelse 1;
+ const ox = ox_ orelse 0;
+ const oy = oy_ orelse 0;
+ const oz = oz_ orelse 0;
+ const p = self._proto;
+ var m = RO.multiplyMatrix(p._m, RO.translationMatrix(ox, oy, oz));
+ m = RO.multiplyMatrix(m, RO.scaleMatrix(sx, sy, sz));
+ m = RO.multiplyMatrix(m, RO.translationMatrix(-ox, -oy, -oz));
+ p._m = m;
+ if (sz != 1 or oz != 0) p._is_2d = false;
+ return self;
+}
+
+pub fn scale3dSelf(self: *DOMMatrix, scale_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64) *DOMMatrix {
+ const s = scale_ orelse 1;
+ const ox = ox_ orelse 0;
+ const oy = oy_ orelse 0;
+ const oz = oz_ orelse 0;
+ const p = self._proto;
+ var m = RO.multiplyMatrix(p._m, RO.translationMatrix(ox, oy, oz));
+ m = RO.multiplyMatrix(m, RO.scaleMatrix(s, s, s));
+ m = RO.multiplyMatrix(m, RO.translationMatrix(-ox, -oy, -oz));
+ p._m = m;
+ if (s != 1) p._is_2d = false;
+ return self;
+}
+
+pub fn rotateSelf(self: *DOMMatrix, rx_: ?f64, ry_: ?f64, rz_: ?f64) *DOMMatrix {
+ const p = self._proto;
+ if (ry_ == null and rz_ == null) {
+ p._m = RO.multiplyMatrix(p._m, RO.rotateZMatrix(RO.toRadians(rx_ orelse 0, .deg)));
+ } else {
+ p._m = RO.multiplyMatrix(p._m, RO.rotateXMatrix(RO.toRadians(rx_ orelse 0, .deg)));
+ p._m = RO.multiplyMatrix(p._m, RO.rotateYMatrix(RO.toRadians(ry_ orelse 0, .deg)));
+ p._m = RO.multiplyMatrix(p._m, RO.rotateZMatrix(RO.toRadians(rz_ orelse 0, .deg)));
+ p._is_2d = false;
+ }
+ return self;
+}
+
+pub fn rotateFromVectorSelf(self: *DOMMatrix, x_: ?f64, y_: ?f64) *DOMMatrix {
+ const x = x_ orelse 0;
+ const y = y_ orelse 0;
+ const rad = if (x == 0 and y == 0) 0 else std.math.atan2(y, x);
+ const p = self._proto;
+ p._m = RO.multiplyMatrix(p._m, RO.rotateZMatrix(rad));
+ return self;
+}
+
+pub fn rotateAxisAngleSelf(self: *DOMMatrix, x_: ?f64, y_: ?f64, z_: ?f64, angle_: ?f64) *DOMMatrix {
+ const p = self._proto;
+ p._m = RO.multiplyMatrix(p._m, RO.axisAngleMatrix(x_ orelse 0, y_ orelse 0, z_ orelse 0, RO.toRadians(angle_ orelse 0, .deg)));
+ if ((x_ orelse 0) != 0 or (y_ orelse 0) != 0) p._is_2d = false;
+ return self;
+}
+
+pub fn skewXSelf(self: *DOMMatrix, sx_: ?f64) *DOMMatrix {
+ const p = self._proto;
+ p._m = RO.multiplyMatrix(p._m, RO.skewMatrix(RO.toRadians(sx_ orelse 0, .deg), 0));
+ return self;
+}
+
+pub fn skewYSelf(self: *DOMMatrix, sy_: ?f64) *DOMMatrix {
+ const p = self._proto;
+ p._m = RO.multiplyMatrix(p._m, RO.skewMatrix(0, RO.toRadians(sy_ orelse 0, .deg)));
+ return self;
+}
+
+pub fn multiplySelf(self: *DOMMatrix, other_: ?RO.DOMMatrixInit) !*DOMMatrix {
+ const p = self._proto;
+ const other = try RO.fixupDict(other_ orelse .{});
+ p._m = RO.multiplyMatrix(p._m, other.m);
+ p._is_2d = p._is_2d and other.is_2d;
+ return self;
+}
+
+pub fn preMultiplySelf(self: *DOMMatrix, other_: ?RO.DOMMatrixInit) !*DOMMatrix {
+ const p = self._proto;
+ const other = try RO.fixupDict(other_ orelse .{});
+ p._m = RO.multiplyMatrix(other.m, p._m);
+ p._is_2d = p._is_2d and other.is_2d;
+ return self;
+}
+
+pub fn invertSelf(self: *DOMMatrix) *DOMMatrix {
+ const p = self._proto;
+ if (RO.invertMatrix(p._m)) |v| {
+ p._m = v;
+ } else {
+ p._m = .{std.math.nan(f64)} ** 16;
+ p._is_2d = false;
+ }
+ return self;
+}
+
+pub fn setMatrixValue(self: *DOMMatrix, transform: []const u8) !*DOMMatrix {
+ var m = RO.identity();
+ var is_2d = true;
+ try RO.parseTransformList(transform, &m, &is_2d);
+ self._proto._m = m;
+ self._proto._is_2d = is_2d;
+ return self;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(DOMMatrix);
+
+ pub const Meta = struct {
+ pub const name = "DOMMatrix";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(DOMMatrix.init, .{ .dom_exception = true });
+
+ pub const fromMatrix = bridge.function(DOMMatrix.fromMatrix, .{ .static = true });
+ pub const fromFloat32Array = bridge.function(DOMMatrix.fromFloat32Array, .{ .static = true });
+ pub const fromFloat64Array = bridge.function(DOMMatrix.fromFloat64Array, .{ .static = true });
+
+ // Make the components writable (the read-only getters are reused from the
+ // base; the setters are ours).
+ pub const a = bridge.accessor(DOMMatrix.getA, DOMMatrix.setA, .{});
+ pub const b = bridge.accessor(DOMMatrix.getB, DOMMatrix.setB, .{});
+ pub const c = bridge.accessor(DOMMatrix.getC, DOMMatrix.setC, .{});
+ pub const d = bridge.accessor(DOMMatrix.getD, DOMMatrix.setD, .{});
+ pub const e = bridge.accessor(DOMMatrix.getE, DOMMatrix.setE, .{});
+ pub const f = bridge.accessor(DOMMatrix.getF, DOMMatrix.setF, .{});
+
+ pub const m11 = bridge.accessor(getM(0), setM(0), .{});
+ pub const m12 = bridge.accessor(getM(1), setM(1), .{});
+ pub const m13 = bridge.accessor(getM(2), setM(2), .{});
+ pub const m14 = bridge.accessor(getM(3), setM(3), .{});
+ pub const m21 = bridge.accessor(getM(4), setM(4), .{});
+ pub const m22 = bridge.accessor(getM(5), setM(5), .{});
+ pub const m23 = bridge.accessor(getM(6), setM(6), .{});
+ pub const m24 = bridge.accessor(getM(7), setM(7), .{});
+ pub const m31 = bridge.accessor(getM(8), setM(8), .{});
+ pub const m32 = bridge.accessor(getM(9), setM(9), .{});
+ pub const m33 = bridge.accessor(getM(10), setM(10), .{});
+ pub const m34 = bridge.accessor(getM(11), setM(11), .{});
+ pub const m41 = bridge.accessor(getM(12), setM(12), .{});
+ pub const m42 = bridge.accessor(getM(13), setM(13), .{});
+ pub const m43 = bridge.accessor(getM(14), setM(14), .{});
+ pub const m44 = bridge.accessor(getM(15), setM(15), .{});
+
+ pub const translateSelf = bridge.function(DOMMatrix.translateSelf, .{});
+ pub const scaleSelf = bridge.function(DOMMatrix.scaleSelf, .{});
+ pub const scale3dSelf = bridge.function(DOMMatrix.scale3dSelf, .{});
+ pub const rotateSelf = bridge.function(DOMMatrix.rotateSelf, .{});
+ pub const rotateFromVectorSelf = bridge.function(DOMMatrix.rotateFromVectorSelf, .{});
+ pub const rotateAxisAngleSelf = bridge.function(DOMMatrix.rotateAxisAngleSelf, .{});
+ pub const skewXSelf = bridge.function(DOMMatrix.skewXSelf, .{});
+ pub const skewYSelf = bridge.function(DOMMatrix.skewYSelf, .{});
+ pub const multiplySelf = bridge.function(DOMMatrix.multiplySelf, .{});
+ pub const preMultiplySelf = bridge.function(DOMMatrix.preMultiplySelf, .{});
+ pub const invertSelf = bridge.function(DOMMatrix.invertSelf, .{});
+ // setMatrixValue parses a CSS transform string; Window-only.
+ pub const setMatrixValue = bridge.function(DOMMatrix.setMatrixValue, .{ .dom_exception = true, .exposed = .window });
+
+ fn getM(comptime idx: usize) fn (*const DOMMatrix) f64 {
+ return struct {
+ fn get(self: *const DOMMatrix) f64 {
+ return self._proto._m[idx];
+ }
+ }.get;
+ }
+
+ fn setM(comptime idx: usize) fn (*DOMMatrix, f64) void {
+ return struct {
+ fn set(self: *DOMMatrix, v: f64) void {
+ self._proto._m[idx] = v;
+ // Assigning a z/w element a value other than its identity drops the
+ // 2D flag. Setting it back to the identity value (0 for the
+ // off-diagonal elements, 1 for m33/m44) preserves is2D. Note `-0`
+ // compares equal to `0`, so it preserves it too, per spec.
+ switch (idx) {
+ 2, 3, 6, 7, 8, 9, 11, 14 => if (v != 0) {
+ self._proto._is_2d = false;
+ },
+ 10, 15 => if (v != 1) {
+ self._proto._is_2d = false;
+ },
+ else => {},
+ }
+ }
+ }.set;
+ }
+};
diff --git a/src/browser/webapi/DOMMatrixReadOnly.zig b/src/browser/webapi/DOMMatrixReadOnly.zig
new file mode 100644
index 00000000..ca39901a
--- /dev/null
+++ b/src/browser/webapi/DOMMatrixReadOnly.zig
@@ -0,0 +1,870 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const lp = @import("lightpanda");
+
+const js = @import("../js/js.zig");
+const Page = @import("../Page.zig");
+const DOMMatrix = @import("DOMMatrix.zig");
+
+const Allocator = std.mem.Allocator;
+
+const DOMMatrixReadOnly = @This();
+
+pub const _prototype_root = true;
+
+_type: Type,
+_rc: lp.RC(u8),
+_arena: Allocator,
+
+// Stored column-major, matching the spec's mAB naming where A is the column
+// and B is the row:
+// _m[0.._4] = m11, m12, m13, m14 (first column)
+// _m[4.._8] = m21, m22, m23, m24
+// _m[8..12] = m31, m32, m33, m34
+// _m[12..16] = m41, m42, m43, m44
+//
+// A point (x, y, z, w) is transformed as:
+// out[row] = sum_col _m[col*4 + row] * in[col]
+_m: [16]f64,
+_is_2d: bool,
+
+pub const Type = union(enum) {
+ generic,
+ mutable: *DOMMatrix,
+};
+
+pub fn init(init_: ?js.Value, exec: *const js.Execution) !*DOMMatrixReadOnly {
+ const parsed = try Parsed.init(init_, exec);
+ return createBare(parsed.m, parsed.is_2d, exec.page);
+}
+
+pub fn deinit(self: *DOMMatrixReadOnly, page: *Page) void {
+ page.releaseArena(self._arena);
+}
+
+pub fn acquireRef(self: *DOMMatrixReadOnly) void {
+ self._rc.acquire();
+}
+
+pub fn releaseRef(self: *DOMMatrixReadOnly, page: *Page) void {
+ self._rc.release(self, page);
+}
+
+pub fn createBare(m: [16]f64, is_2d: bool, page: *Page) !*DOMMatrixReadOnly {
+ const arena = try page.getArena(.tiny, "DOMMatrix");
+ errdefer page.releaseArena(arena);
+
+ const self = try arena.create(DOMMatrixReadOnly);
+ self.* = .{
+ ._rc = .{},
+ ._arena = arena,
+ ._type = .generic,
+ ._m = m,
+ ._is_2d = is_2d,
+ };
+ return self;
+}
+
+pub const DOMMatrixInit = struct {
+ a: ?f64 = null,
+ b: ?f64 = null,
+ c: ?f64 = null,
+ d: ?f64 = null,
+ e: ?f64 = null,
+ f: ?f64 = null,
+ m11: ?f64 = null,
+ m12: ?f64 = null,
+ m13: ?f64 = null,
+ m14: ?f64 = null,
+ m21: ?f64 = null,
+ m22: ?f64 = null,
+ m23: ?f64 = null,
+ m24: ?f64 = null,
+ m31: ?f64 = null,
+ m32: ?f64 = null,
+ m33: ?f64 = null,
+ m34: ?f64 = null,
+ m41: ?f64 = null,
+ m42: ?f64 = null,
+ m43: ?f64 = null,
+ m44: ?f64 = null,
+ is2D: ?bool = null,
+};
+
+// Implements "validate and fixup a DOMMatrixInit dictionary".
+pub fn fixupDict(d: DOMMatrixInit) !Parsed {
+ if (aliasConflict(d.m11, d.a) or aliasConflict(d.m12, d.b) or
+ aliasConflict(d.m21, d.c) or aliasConflict(d.m22, d.d) or
+ aliasConflict(d.m41, d.e) or aliasConflict(d.m42, d.f))
+ {
+ return error.TypeError;
+ }
+
+ // An explicit is2D:true is incompatible with any 3D member being set.
+ if (d.is2D) |is_2d| {
+ if (is_2d and has3dMembers(d)) {
+ return error.TypeError;
+ }
+ }
+
+ const m: [16]f64 = .{
+ d.m11 orelse d.a orelse 1, d.m12 orelse d.b orelse 0, d.m13 orelse 0, d.m14 orelse 0,
+ d.m21 orelse d.c orelse 0, d.m22 orelse d.d orelse 1, d.m23 orelse 0, d.m24 orelse 0,
+ d.m31 orelse 0, d.m32 orelse 0, d.m33 orelse 1, d.m34 orelse 0,
+ d.m41 orelse d.e orelse 0, d.m42 orelse d.f orelse 0, d.m43 orelse 0, d.m44 orelse 1,
+ };
+
+ const is_2d = d.is2D orelse !has3dMembers(d);
+ return .{ .m = m, .is_2d = is_2d };
+}
+
+// Builds a matrix from a 6- or 16-element float sequence (toFloat*Array order).
+pub fn floatsToParsed(comptime T: type, values: []const T) !Parsed {
+ var m = identity();
+ if (values.len == 6) {
+ m = .{
+ values[0], values[1], 0, 0,
+ values[2], values[3], 0, 0,
+ 0, 0, 1, 0,
+ values[4], values[5], 0, 1,
+ };
+ return .{ .m = m, .is_2d = true };
+ }
+
+ if (values.len == 16) {
+ for (0..16) |i| m[i] = values[i];
+ return .{ .m = m, .is_2d = false };
+ }
+
+ return error.TypeError;
+}
+
+pub fn fromMatrix(other_: ?DOMMatrixInit, page: *Page) !*DOMMatrixReadOnly {
+ const parsed = try fixupDict(other_ orelse .{});
+ return createBare(parsed.m, parsed.is_2d, page);
+}
+
+pub fn fromFloat32Array(array: js.TypedArray(f32), page: *Page) !*DOMMatrixReadOnly {
+ const parsed = try floatsToParsed(f32, array.values);
+ return createBare(parsed.m, parsed.is_2d, page);
+}
+
+pub fn fromFloat64Array(array: js.TypedArray(f64), page: *Page) !*DOMMatrixReadOnly {
+ const parsed = try floatsToParsed(f64, array.values);
+ return createBare(parsed.m, parsed.is_2d, page);
+}
+
+pub fn identity() [16]f64 {
+ return .{
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+// Returns lhs * rhs (composition: applying the result is lhs(rhs(point))).
+pub fn multiplyMatrix(lhs: [16]f64, rhs: [16]f64) [16]f64 {
+ var out: [16]f64 = undefined;
+ for (0..4) |col| {
+ for (0..4) |row| {
+ var sum: f64 = 0;
+ for (0..4) |k| {
+ sum += lhs[k * 4 + row] * rhs[col * 4 + k];
+ }
+ out[col * 4 + row] = sum;
+ }
+ }
+ return out;
+}
+
+pub fn translationMatrix(tx: f64, ty: f64, tz: f64) [16]f64 {
+ return .{
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ tx, ty, tz, 1,
+ };
+}
+
+pub fn scaleMatrix(sx: f64, sy: f64, sz: f64) [16]f64 {
+ return .{
+ sx, 0, 0, 0,
+ 0, sy, 0, 0,
+ 0, 0, sz, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+pub fn rotateZMatrix(rad: f64) [16]f64 {
+ const c = @cos(rad);
+ const s = @sin(rad);
+ return .{
+ c, s, 0, 0,
+ -s, c, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+pub fn rotateXMatrix(rad: f64) [16]f64 {
+ const c = @cos(rad);
+ const s = @sin(rad);
+ return .{
+ 1, 0, 0, 0,
+ 0, c, s, 0,
+ 0, -s, c, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+pub fn rotateYMatrix(rad: f64) [16]f64 {
+ const c = @cos(rad);
+ const s = @sin(rad);
+ return .{
+ c, 0, -s, 0,
+ 0, 1, 0, 0,
+ s, 0, c, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+// Rotation by `rad` about the (possibly unnormalised) axis (x, y, z).
+pub fn axisAngleMatrix(x_in: f64, y_in: f64, z_in: f64, rad: f64) [16]f64 {
+ var x = x_in;
+ var y = y_in;
+ var z = z_in;
+ const len = @sqrt(x * x + y * y + z * z);
+ if (len == 0) {
+ return identity();
+ }
+
+ x /= len;
+ y /= len;
+ z /= len;
+ const c = @cos(rad);
+ const s = @sin(rad);
+ const t = 1 - c;
+ return .{
+ t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0,
+ t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0,
+ t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+pub fn skewMatrix(ax_rad: f64, ay_rad: f64) [16]f64 {
+ return .{
+ 1, @tan(ay_rad), 0, 0,
+ @tan(ax_rad), 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
+ };
+}
+
+// Inverse of a 4x4 matrix; returns null if non-invertible.
+pub fn invertMatrix(m: [16]f64) ?[16]f64 {
+ var inv: [16]f64 = undefined;
+ inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
+ inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
+ inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
+ inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
+ inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
+ inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
+ inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
+ inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
+ inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
+ inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
+ inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
+ inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
+ inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
+ inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
+ inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
+ inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
+
+ var det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
+ if (det == 0) {
+ return null;
+ }
+ det = 1.0 / det;
+
+ var out: [16]f64 = undefined;
+ for (0..16) |i| {
+ out[i] = inv[i] * det;
+ }
+ return out;
+}
+
+// Parses a CSS (e.g. "matrix(1,0,0,1,10,20) scale(2)") and
+// accumulates it into `m`. "none"/empty leave the matrix as identity.
+pub fn parseTransformList(input: []const u8, m: *[16]f64, is_2d: *bool) !void {
+ const trimmed = std.mem.trim(u8, input, " \t\r\n");
+ if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "none")) {
+ return;
+ }
+
+ var i: usize = 0;
+ while (i < trimmed.len) {
+ // skip whitespace and separating commas
+ while (i < trimmed.len and (std.ascii.isWhitespace(trimmed[i]) or trimmed[i] == ',')) : (i += 1) {}
+ if (i >= trimmed.len) {
+ break;
+ }
+
+ const name_start = i;
+ while (i < trimmed.len and trimmed[i] != '(') : (i += 1) {}
+ if (i >= trimmed.len) {
+ return error.SyntaxError;
+ }
+ const name = std.mem.trim(u8, trimmed[name_start..i], " \t\r\n");
+
+ i += 1; // consume '('
+ const args_start = i;
+ while (i < trimmed.len and trimmed[i] != ')') : (i += 1) {}
+ if (i >= trimmed.len) {
+ return error.SyntaxError;
+ }
+ const args = trimmed[args_start..i];
+ i += 1; // consume ')'
+
+ const func = try parseFunction(name, args, is_2d);
+ m.* = multiplyMatrix(m.*, func);
+ }
+}
+
+fn parseFunction(name: []const u8, args: []const u8, is_2d: *bool) ![16]f64 {
+ var nums: [16]f64 = undefined;
+ var units: [16]ParsedValue.Unit = undefined;
+ var count: usize = 0;
+
+ var it = std.mem.splitScalar(u8, args, ',');
+ while (it.next()) |raw| {
+ const tok = std.mem.trim(u8, raw, " \t\r\n");
+ if (tok.len == 0) {
+ continue;
+ }
+ if (count >= 16) {
+ return error.SyntaxError;
+ }
+ const parsed = try ParsedValue.parse(tok);
+ nums[count] = parsed.value;
+ units[count] = parsed.unit;
+ count += 1;
+ }
+
+ const Eql = std.mem.eql;
+ if (Eql(u8, name, "matrix")) {
+ if (count != 6) {
+ return error.SyntaxError;
+ }
+ return .{
+ nums[0], nums[1], 0, 0,
+ nums[2], nums[3], 0, 0,
+ 0, 0, 1, 0,
+ nums[4], nums[5], 0, 1,
+ };
+ }
+
+ if (Eql(u8, name, "matrix3d")) {
+ if (count != 16) {
+ return error.SyntaxError;
+ }
+ is_2d.* = false;
+ return nums;
+ }
+
+ if (Eql(u8, name, "translate")) {
+ const tx = nums[0];
+ const ty = if (count > 1) nums[1] else 0;
+ return translationMatrix(tx, ty, 0);
+ }
+
+ if (Eql(u8, name, "translateX")) {
+ return translationMatrix(nums[0], 0, 0);
+ }
+
+ if (Eql(u8, name, "translateY")) {
+ return translationMatrix(0, nums[0], 0);
+ }
+
+ if (Eql(u8, name, "translateZ")) {
+ is_2d.* = false;
+ return translationMatrix(0, 0, nums[0]);
+ }
+
+ if (Eql(u8, name, "translate3d")) {
+ is_2d.* = false;
+ return translationMatrix(nums[0], nums[1], nums[2]);
+ }
+
+ if (Eql(u8, name, "scale")) {
+ const sx = nums[0];
+ const sy = if (count > 1) nums[1] else sx;
+ return scaleMatrix(sx, sy, 1);
+ }
+
+ if (Eql(u8, name, "scaleX")) {
+ return scaleMatrix(nums[0], 1, 1);
+ }
+
+ if (Eql(u8, name, "scaleY")) {
+ return scaleMatrix(1, nums[0], 1);
+ }
+
+ if (Eql(u8, name, "scaleZ")) {
+ is_2d.* = false;
+ return scaleMatrix(1, 1, nums[0]);
+ }
+
+ if (Eql(u8, name, "scale3d")) {
+ is_2d.* = false;
+ return scaleMatrix(nums[0], nums[1], nums[2]);
+ }
+ if (Eql(u8, name, "rotate") or Eql(u8, name, "rotateZ")) {
+ if (Eql(u8, name, "rotateZ")) is_2d.* = false;
+ return rotateZMatrix(toRadians(nums[0], units[0]));
+ }
+
+ if (Eql(u8, name, "rotateX")) {
+ is_2d.* = false;
+ return rotateXMatrix(toRadians(nums[0], units[0]));
+ }
+
+ if (Eql(u8, name, "rotateY")) {
+ is_2d.* = false;
+ return rotateYMatrix(toRadians(nums[0], units[0]));
+ }
+
+ if (Eql(u8, name, "rotate3d")) {
+ is_2d.* = false;
+ if (count != 4) {
+ return error.SyntaxError;
+ }
+ return axisAngleMatrix(nums[0], nums[1], nums[2], toRadians(nums[3], units[3]));
+ }
+
+ if (Eql(u8, name, "skew")) {
+ const ax = toRadians(nums[0], units[0]);
+ const ay = if (count > 1) toRadians(nums[1], units[1]) else 0;
+ return skewMatrix(ax, ay);
+ }
+
+ if (Eql(u8, name, "skewX")) {
+ return skewMatrix(toRadians(nums[0], units[0]), 0);
+ }
+
+ if (Eql(u8, name, "skewY")) {
+ return skewMatrix(0, toRadians(nums[0], units[0]));
+ }
+
+ if (Eql(u8, name, "perspective")) {
+ is_2d.* = false;
+ var out = identity();
+ if (nums[0] != 0) out[11] = -1.0 / nums[0];
+ return out;
+ }
+
+ return error.SyntaxError;
+}
+
+pub fn toRadians(value: f64, unit: ParsedValue.Unit) f64 {
+ return switch (unit) {
+ .rad => value,
+ .grad => value * std.math.pi / 200.0,
+ .turn => value * std.math.tau,
+ // bare numbers in rotate()/skew() are interpreted as degrees
+ .deg, .none => value * std.math.pi / 180.0,
+ };
+}
+
+pub fn getA(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[0];
+}
+pub fn getB(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[1];
+}
+pub fn getC(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[4];
+}
+pub fn getD(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[5];
+}
+pub fn getE(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[12];
+}
+pub fn getF(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[13];
+}
+
+pub fn getIs2D(self: *const DOMMatrixReadOnly) bool {
+ return self._is_2d;
+}
+
+pub fn getIsIdentity(self: *const DOMMatrixReadOnly) bool {
+ const id = identity();
+ for (0..16) |i| {
+ if (self._m[i] != id[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+pub fn translate(self: *const DOMMatrixReadOnly, tx_: ?f64, ty_: ?f64, tz_: ?f64, page: *Page) !*DOMMatrix {
+ const tz = tz_ orelse 0;
+ return DOMMatrix.create(
+ multiplyMatrix(self._m, translationMatrix(tx_ orelse 0, ty_ orelse 0, tz)),
+ self._is_2d and tz == 0,
+ page,
+ );
+}
+
+pub fn scale(self: *const DOMMatrixReadOnly, sx_: ?f64, sy_: ?f64, sz_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64, page: *Page) !*DOMMatrix {
+ const sx = sx_ orelse 1;
+ const sy = sy_ orelse sx;
+ const sz = sz_ orelse 1;
+ const ox = ox_ orelse 0;
+ const oy = oy_ orelse 0;
+ const oz = oz_ orelse 0;
+ var m = multiplyMatrix(self._m, translationMatrix(ox, oy, oz));
+ m = multiplyMatrix(m, scaleMatrix(sx, sy, sz));
+ m = multiplyMatrix(m, translationMatrix(-ox, -oy, -oz));
+ return DOMMatrix.create(m, self._is_2d and sz == 1 and oz == 0, page);
+}
+
+pub fn scaleNonUniform(self: *const DOMMatrixReadOnly, sx_: ?f64, sy_: ?f64, page: *Page) !*DOMMatrix {
+ const sx = sx_ orelse 1;
+ const sy = sy_ orelse 1;
+ return DOMMatrix.create(multiplyMatrix(self._m, scaleMatrix(sx, sy, 1)), self._is_2d, page);
+}
+
+pub fn scale3d(self: *const DOMMatrixReadOnly, scale_: ?f64, ox_: ?f64, oy_: ?f64, oz_: ?f64, page: *Page) !*DOMMatrix {
+ const s = scale_ orelse 1;
+ const ox = ox_ orelse 0;
+ const oy = oy_ orelse 0;
+ const oz = oz_ orelse 0;
+ var m = multiplyMatrix(self._m, translationMatrix(ox, oy, oz));
+ m = multiplyMatrix(m, scaleMatrix(s, s, s));
+ m = multiplyMatrix(m, translationMatrix(-ox, -oy, -oz));
+ return DOMMatrix.create(m, self._is_2d and s == 1, page);
+}
+
+pub fn rotate(self: *const DOMMatrixReadOnly, rx_: ?f64, ry_: ?f64, rz_: ?f64, page: *Page) !*DOMMatrix {
+ var out = self._m;
+ var is_2d = self._is_2d;
+ // With a single argument, it is the Z rotation.
+ if (ry_ == null and rz_ == null) {
+ out = multiplyMatrix(out, rotateZMatrix(toRadians(rx_ orelse 0, .deg)));
+ } else {
+ out = multiplyMatrix(out, rotateXMatrix(toRadians(rx_ orelse 0, .deg)));
+ out = multiplyMatrix(out, rotateYMatrix(toRadians(ry_ orelse 0, .deg)));
+ out = multiplyMatrix(out, rotateZMatrix(toRadians(rz_ orelse 0, .deg)));
+ is_2d = false;
+ }
+ return DOMMatrix.create(out, is_2d, page);
+}
+
+pub fn rotateFromVector(self: *const DOMMatrixReadOnly, x_: ?f64, y_: ?f64, page: *Page) !*DOMMatrix {
+ const x = x_ orelse 0;
+ const y = y_ orelse 0;
+ const rad = if (x == 0 and y == 0) 0 else std.math.atan2(y, x);
+ return DOMMatrix.create(multiplyMatrix(self._m, rotateZMatrix(rad)), self._is_2d, page);
+}
+
+pub fn rotateAxisAngle(self: *const DOMMatrixReadOnly, x_: ?f64, y_: ?f64, z_: ?f64, angle_: ?f64, page: *Page) !*DOMMatrix {
+ return DOMMatrix.create(
+ multiplyMatrix(self._m, axisAngleMatrix(x_ orelse 0, y_ orelse 0, z_ orelse 0, toRadians(angle_ orelse 0, .deg))),
+ // Only a rotation purely about the z axis stays 2D.
+ self._is_2d and (x_ orelse 0) == 0 and (y_ orelse 0) == 0,
+ page,
+ );
+}
+
+pub fn skewX(self: *const DOMMatrixReadOnly, sx_: ?f64, page: *Page) !*DOMMatrix {
+ return DOMMatrix.create(multiplyMatrix(self._m, skewMatrix(toRadians(sx_ orelse 0, .deg), 0)), self._is_2d, page);
+}
+
+pub fn skewY(self: *const DOMMatrixReadOnly, sy_: ?f64, page: *Page) !*DOMMatrix {
+ return DOMMatrix.create(multiplyMatrix(self._m, skewMatrix(0, toRadians(sy_ orelse 0, .deg))), self._is_2d, page);
+}
+
+pub fn multiply(self: *const DOMMatrixReadOnly, other_: ?DOMMatrixInit, page: *Page) !*DOMMatrix {
+ const other = try fixupDict(other_ orelse .{});
+ return DOMMatrix.create(multiplyMatrix(self._m, other.m), self._is_2d and other.is_2d, page);
+}
+
+pub fn flipX(self: *const DOMMatrixReadOnly, page: *Page) !*DOMMatrix {
+ return DOMMatrix.create(multiplyMatrix(self._m, scaleMatrix(-1, 1, 1)), self._is_2d, page);
+}
+
+pub fn flipY(self: *const DOMMatrixReadOnly, page: *Page) !*DOMMatrix {
+ return DOMMatrix.create(multiplyMatrix(self._m, scaleMatrix(1, -1, 1)), self._is_2d, page);
+}
+
+pub fn inverse(self: *const DOMMatrixReadOnly, page: *Page) !*DOMMatrix {
+ if (invertMatrix(self._m)) |v| {
+ return DOMMatrix.create(v, self._is_2d, page);
+ }
+ // Non-invertible matrices become all-NaN with is2D = false.
+ return DOMMatrix.create(.{std.math.nan(f64)} ** 16, false, page);
+}
+
+pub fn toFloat32Array(self: *const DOMMatrixReadOnly, exec: *const js.Execution) !js.TypedArray(f32) {
+ const out = try exec.call_arena.alloc(f32, 16);
+ for (0..16) |i| {
+ out[i] = @floatCast(self._m[i]);
+ }
+ return .{ .values = out };
+}
+
+pub fn toFloat64Array(self: *const DOMMatrixReadOnly, exec: *const js.Execution) !js.TypedArray(f64) {
+ const out = try exec.call_arena.dupe(f64, &self._m);
+ return .{ .values = out };
+}
+
+pub fn toString(self: *const DOMMatrixReadOnly, exec: *const js.Execution) ![]const u8 {
+ const m = self._m;
+ if (self._is_2d) {
+ // Per the stringifier: throw if any serialized component is non-finite.
+ for ([_]f64{ m[0], m[1], m[4], m[5], m[12], m[13] }) |v| {
+ if (!std.math.isFinite(v)) {
+ return error.InvalidStateError;
+ }
+ }
+ return std.fmt.allocPrint(exec.call_arena, "matrix({d}, {d}, {d}, {d}, {d}, {d})", .{
+ m[0], m[1], m[4], m[5], m[12], m[13],
+ });
+ }
+ for (m) |v| {
+ if (!std.math.isFinite(v)) {
+ return error.InvalidStateError;
+ }
+ }
+ return std.fmt.allocPrint(exec.call_arena, "matrix3d({d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d}, {d})", .{
+ m[0], m[1], m[2], m[3],
+ m[4], m[5], m[6], m[7],
+ m[8], m[9], m[10], m[11],
+ m[12], m[13], m[14], m[15],
+ });
+}
+
+fn aliasConflict(x: ?f64, y: ?f64) bool {
+ const a = x orelse return false;
+ const b = y orelse return false;
+ if (std.math.isNan(a) and std.math.isNan(b)) {
+ return false;
+ }
+ return a != b;
+}
+
+// True when the dict specifies any 3D-only member away from its identity value.
+fn has3dMembers(d: DOMMatrixInit) bool {
+ return (d.m13 orelse 0) != 0 or (d.m14 orelse 0) != 0 or
+ (d.m23 orelse 0) != 0 or (d.m24 orelse 0) != 0 or
+ (d.m31 orelse 0) != 0 or (d.m32 orelse 0) != 0 or
+ (d.m34 orelse 0) != 0 or (d.m43 orelse 0) != 0 or
+ (d.m33 orelse 1) != 1 or (d.m44 orelse 1) != 1;
+}
+
+pub const Parsed = struct {
+ m: [16]f64,
+ is_2d: bool,
+
+ pub fn init(init_: ?js.Value, exec: *const js.Execution) !Parsed {
+ var m: [16]f64 = identity();
+ var is_2d = true;
+
+ if (init_) |in| {
+ if (!in.isUndefined()) {
+ if (in.isArray()) {
+ try sequenceToMatrix(in.toArray(), &m, &is_2d);
+ } else {
+ // Per WebIDL the union is `(DOMString or sequence)`: a value
+ // that isn't a sequence is converted to a DOMString. So a
+ // string parses directly, and any other value (a number, null,
+ // or another matrix) is stringified first — which is how
+ // `new DOMMatrix(otherMatrix)` round-trips via its
+ // matrix()/matrix3d() serialization.
+ if (exec.js.global == .worker) {
+ return error.TypeError;
+ }
+ const str = try in.toStringSmart();
+ try parseTransformList(str, &m, &is_2d);
+ }
+ }
+ }
+
+ return .{ .m = m, .is_2d = is_2d };
+ }
+};
+
+const ParsedValue = struct {
+ value: f64,
+ unit: Unit,
+
+ const Unit = enum {
+ none,
+ deg,
+ rad,
+ grad,
+ turn,
+ };
+
+ // Parses a single CSS dimension token: a number with an optional unit suffix.
+ // Length units are ignored (we don't resolve layout), so the numeric part is
+ // taken verbatim; angle units are recorded so they can be normalised.
+ fn parse(tok: []const u8) !ParsedValue {
+ var end: usize = 0;
+ while (end < tok.len) : (end += 1) {
+ const c = tok[end];
+ if ((c >= '0' and c <= '9') or c == '.' or c == '+' or c == '-' or c == 'e' or c == 'E') {
+ // 'e'/'E' is ambiguous with exponents; only treat as exponent when
+ // followed by a digit/sign.
+ if ((c == 'e' or c == 'E') and end > 0) {
+ if (end + 1 >= tok.len) break;
+ const nx = tok[end + 1];
+ if (!((nx >= '0' and nx <= '9') or nx == '+' or nx == '-')) break;
+ }
+ continue;
+ }
+ break;
+ }
+ if (end == 0) {
+ return error.SyntaxError;
+ }
+ const value = try std.fmt.parseFloat(f64, tok[0..end]);
+ const suffix = tok[end..];
+
+ var unit: Unit = .none;
+ if (std.ascii.eqlIgnoreCase(suffix, "deg")) {
+ unit = .deg;
+ } else if (std.ascii.eqlIgnoreCase(suffix, "rad")) {
+ unit = .rad;
+ } else if (std.ascii.eqlIgnoreCase(suffix, "grad")) {
+ unit = .grad;
+ } else if (std.ascii.eqlIgnoreCase(suffix, "turn")) {
+ unit = .turn;
+ }
+ return .{ .value = value, .unit = unit };
+ }
+};
+
+fn sequenceToMatrix(arr: js.Array, m: *[16]f64, is_2d: *bool) !void {
+ const n = arr.len();
+ if (n == 6) {
+ // matrix(a, b, c, d, e, f)
+ var v: [6]f64 = undefined;
+ for (0..6) |i| {
+ v[i] = try (try arr.get(@intCast(i))).toF64();
+ }
+ m.* = .{
+ v[0], v[1], 0, 0,
+ v[2], v[3], 0, 0,
+ 0, 0, 1, 0,
+ v[4], v[5], 0, 1,
+ };
+ is_2d.* = true;
+ return;
+ }
+
+ if (n == 16) {
+ for (0..16) |i| {
+ m[i] = try (try arr.get(@intCast(i))).toF64();
+ }
+ is_2d.* = false;
+ return;
+ }
+ return error.TypeError;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(DOMMatrixReadOnly);
+
+ pub const Meta = struct {
+ pub const name = "DOMMatrixReadOnly";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(DOMMatrixReadOnly.init, .{ .dom_exception = true });
+
+ pub const fromMatrix = bridge.function(DOMMatrixReadOnly.fromMatrix, .{ .static = true });
+ pub const fromFloat32Array = bridge.function(DOMMatrixReadOnly.fromFloat32Array, .{ .static = true });
+ pub const fromFloat64Array = bridge.function(DOMMatrixReadOnly.fromFloat64Array, .{ .static = true });
+
+ pub const a = bridge.accessor(DOMMatrixReadOnly.getA, null, .{});
+ pub const b = bridge.accessor(DOMMatrixReadOnly.getB, null, .{});
+ pub const c = bridge.accessor(DOMMatrixReadOnly.getC, null, .{});
+ pub const d = bridge.accessor(DOMMatrixReadOnly.getD, null, .{});
+ pub const e = bridge.accessor(DOMMatrixReadOnly.getE, null, .{});
+ pub const f = bridge.accessor(DOMMatrixReadOnly.getF, null, .{});
+
+ pub const m11 = bridge.accessor(getM(0), null, .{});
+ pub const m12 = bridge.accessor(getM(1), null, .{});
+ pub const m13 = bridge.accessor(getM(2), null, .{});
+ pub const m14 = bridge.accessor(getM(3), null, .{});
+ pub const m21 = bridge.accessor(getM(4), null, .{});
+ pub const m22 = bridge.accessor(getM(5), null, .{});
+ pub const m23 = bridge.accessor(getM(6), null, .{});
+ pub const m24 = bridge.accessor(getM(7), null, .{});
+ pub const m31 = bridge.accessor(getM(8), null, .{});
+ pub const m32 = bridge.accessor(getM(9), null, .{});
+ pub const m33 = bridge.accessor(getM(10), null, .{});
+ pub const m34 = bridge.accessor(getM(11), null, .{});
+ pub const m41 = bridge.accessor(getM(12), null, .{});
+ pub const m42 = bridge.accessor(getM(13), null, .{});
+ pub const m43 = bridge.accessor(getM(14), null, .{});
+ pub const m44 = bridge.accessor(getM(15), null, .{});
+
+ pub const is2D = bridge.accessor(DOMMatrixReadOnly.getIs2D, null, .{});
+ pub const isIdentity = bridge.accessor(DOMMatrixReadOnly.getIsIdentity, null, .{});
+
+ pub const translate = bridge.function(DOMMatrixReadOnly.translate, .{});
+ pub const scale = bridge.function(DOMMatrixReadOnly.scale, .{});
+ pub const scaleNonUniform = bridge.function(DOMMatrixReadOnly.scaleNonUniform, .{});
+ pub const scale3d = bridge.function(DOMMatrixReadOnly.scale3d, .{});
+ pub const rotate = bridge.function(DOMMatrixReadOnly.rotate, .{});
+ pub const rotateFromVector = bridge.function(DOMMatrixReadOnly.rotateFromVector, .{});
+ pub const rotateAxisAngle = bridge.function(DOMMatrixReadOnly.rotateAxisAngle, .{});
+ pub const skewX = bridge.function(DOMMatrixReadOnly.skewX, .{});
+ pub const skewY = bridge.function(DOMMatrixReadOnly.skewY, .{});
+ pub const multiply = bridge.function(DOMMatrixReadOnly.multiply, .{});
+ pub const flipX = bridge.function(DOMMatrixReadOnly.flipX, .{});
+ pub const flipY = bridge.function(DOMMatrixReadOnly.flipY, .{});
+ pub const inverse = bridge.function(DOMMatrixReadOnly.inverse, .{});
+ pub const toFloat32Array = bridge.function(DOMMatrixReadOnly.toFloat32Array, .{});
+ pub const toFloat64Array = bridge.function(DOMMatrixReadOnly.toFloat64Array, .{});
+ // The stringifier depends on CSS serialization and is Window-only.
+ pub const toString = bridge.function(DOMMatrixReadOnly.toString, .{ .dom_exception = true, .exposed = .window });
+
+ // m11..m44 getters are generated from the storage index.
+ fn getM(comptime idx: usize) fn (*const DOMMatrixReadOnly) f64 {
+ return struct {
+ fn get(self: *const DOMMatrixReadOnly) f64 {
+ return self._m[idx];
+ }
+ }.get;
+ }
+};
+
+const testing = @import("../../testing.zig");
+test "WebApi: DOMMatrixReadOnly" {
+ try testing.htmlRunner("dommatrix.html", .{});
+}