Add DOMMatrix and DOMMatrixReadOnly

These are needed for apple.com, completely broken without.

Obviously Claude wrote most of this code. The test that it wrote _does_ pass
in FireFox and a reasonable number of WPT *DOMMatrix* test pass.

A relatively small subset of this was needed for Apple, but there was plenty
of low-hanging fruit in the WPT tests.

Also, some of these WPT tests uncovered a name collision in our WPT reporter:
https://github.com/lightpanda-io/wpt/pull/70
This commit is contained in:
Karl Seguin
2026-06-04 19:17:25 +08:00
parent 720e610542
commit d7eb6d697a
4 changed files with 1576 additions and 0 deletions

View File

@@ -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"),

View File

@@ -0,0 +1,401 @@
<!DOCTYPE html>
<head>
<title>DOMMatrix Test</title>
<script src="testing.js"></script>
</head>
<body>
</body>
<script id=identity_no_args>
{
const m = new DOMMatrix();
testing.expectEqual(1, m.a);
testing.expectEqual(0, m.b);
testing.expectEqual(0, m.c);
testing.expectEqual(1, m.d);
testing.expectEqual(0, m.e);
testing.expectEqual(0, m.f);
testing.expectTrue(m.is2D);
testing.expectTrue(m.isIdentity);
testing.expectEqual(1, m.m11);
testing.expectEqual(1, m.m22);
testing.expectEqual(1, m.m33);
testing.expectEqual(1, m.m44);
}
</script>
<script id=from_6_element_array>
{
const m = new DOMMatrix([1, 2, 3, 4, 5, 6]);
testing.expectEqual(1, m.a);
testing.expectEqual(2, m.b);
testing.expectEqual(3, m.c);
testing.expectEqual(4, m.d);
testing.expectEqual(5, m.e);
testing.expectEqual(6, m.f);
testing.expectTrue(m.is2D);
testing.expectFalse(m.isIdentity);
}
</script>
<script id=from_16_element_array>
{
const m = new DOMMatrix([
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
]);
testing.expectEqual(1, m.m11);
testing.expectEqual(6, m.m22);
testing.expectEqual(11, m.m33);
testing.expectEqual(16, m.m44);
testing.expectEqual(13, m.m41);
testing.expectFalse(m.is2D);
}
</script>
<script id=from_matrix_string>
{
const m = new DOMMatrix('matrix(1, 0, 0, 1, 10, 20)');
testing.expectEqual(1, m.a);
testing.expectEqual(0, m.b);
testing.expectEqual(10, m.e);
testing.expectEqual(20, m.f);
testing.expectTrue(m.is2D);
}
</script>
<script id=from_none_and_empty>
{
const none = new DOMMatrix('none');
testing.expectTrue(none.isIdentity);
const empty = new DOMMatrix('');
testing.expectTrue(empty.isIdentity);
}
</script>
<script id=from_translate_string>
{
const m = new DOMMatrix('translate(40px, 50px)');
testing.expectEqual(40, m.e);
testing.expectEqual(50, m.f);
testing.expectTrue(m.is2D);
}
</script>
<script id=from_scale_string>
{
const m = new DOMMatrix('scale(2)');
testing.expectEqual(2, m.a);
testing.expectEqual(2, m.d);
}
</script>
<script id=from_matrix3d_string>
{
const m = new DOMMatrix('matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, 5,6,7,1)');
testing.expectEqual(5, m.m41);
testing.expectEqual(6, m.m42);
testing.expectEqual(7, m.m43);
testing.expectFalse(m.is2D);
}
</script>
<script id=rotate_90>
{
const m = new DOMMatrix('rotate(90deg)');
// cos(90)=0, sin(90)=1
testing.expectTrue(Math.abs(m.a - 0) < 1e-9);
testing.expectTrue(Math.abs(m.b - 1) < 1e-9);
testing.expectTrue(Math.abs(m.c - -1) < 1e-9);
testing.expectTrue(Math.abs(m.d - 0) < 1e-9);
}
</script>
<script id=constructor_copies_matrix_via_string>
{
// Per WebIDL the init arg is (string or sequence); a matrix is not a
// sequence, so it is stringified and re-parsed — round-tripping a copy.
const src = new DOMMatrix([
2, 1, 0, 0,
1, 2, 0, 0,
0, 0, 1, 0,
10, 10, 0, 1,
]);
const m = new DOMMatrix(src);
testing.expectEqual(2, m.m11);
testing.expectEqual(1, m.m21);
testing.expectEqual(10, m.m41);
testing.expectFalse(m.is2D);
}
</script>
<script id=constructor_invalid_string_throws_syntaxerror>
{
let name = null;
try { new DOMMatrix('not a transform'); } catch (e) { name = e.name; }
testing.expectEqual('SyntaxError', name);
}
</script>
<script id=from_matrix_dict>
{
const m = DOMMatrix.fromMatrix({ a: 2, d: 3, e: 10, f: 20 });
testing.expectEqual(2, m.a);
testing.expectEqual(3, m.d);
testing.expectEqual(10, m.e);
testing.expectEqual(20, m.f);
testing.expectTrue(m.is2D);
const ro = DOMMatrixReadOnly.fromMatrix({ m11: 1, m22: 1, m33: 1, m44: 1, is2D: false });
testing.expectTrue(ro instanceof DOMMatrixReadOnly);
testing.expectFalse(ro.is2D);
}
</script>
<script id=from_matrix_alias_conflict_throws>
{
// a and m11 both present but unequal -> TypeError
let threw = false;
try { DOMMatrix.fromMatrix({ a: 1, m11: 2 }); } catch (e) { threw = (e instanceof TypeError); }
testing.expectTrue(threw);
}
</script>
<script id=from_float_arrays>
{
const m6 = DOMMatrix.fromFloat64Array(new Float64Array([1, 0, 0, 1, 7, 8]));
testing.expectEqual(7, m6.e);
testing.expectEqual(8, m6.f);
testing.expectTrue(m6.is2D);
const m16 = DOMMatrix.fromFloat32Array(new Float32Array([
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
]));
testing.expectFalse(m16.is2D);
testing.expectTrue(m16.isIdentity);
}
</script>
<script id=multiply_translate>
{
const a = new DOMMatrix().translate(10, 0);
const b = new DOMMatrix().translate(0, 20);
const m = a.multiply(b);
testing.expectEqual(10, m.e);
testing.expectEqual(20, m.f);
}
</script>
<script id=scale_self>
{
const m = new DOMMatrix();
const r = m.scaleSelf(3);
testing.expectEqual(3, m.a);
testing.expectEqual(3, m.d);
// self methods return the same object
testing.expectTrue(r === m);
}
</script>
<script id=inverse_translate>
{
const m = new DOMMatrix().translate(10, 20);
const inv = m.inverse();
testing.expectEqual(-10, inv.e);
testing.expectEqual(-20, inv.f);
}
</script>
<script id=set_a_mutates>
{
const m = new DOMMatrix();
m.a = 5;
testing.expectEqual(5, m.m11);
}
</script>
<script id=to_string_throws_on_non_finite>
{
for (const num of [NaN, Infinity, -Infinity]) {
let name = null;
const m = new DOMMatrix([1, 0, 0, 1, 0, num]);
try { String(m); } catch (e) { name = e.name; }
testing.expectEqual('InvalidStateError', name);
}
// finite matrix stringifies fine
testing.expectEqual('matrix(1, 0, 0, 1, 0, 0)', String(new DOMMatrix()));
}
</script>
<script id=to_string_2d>
{
const m = new DOMMatrix([1, 0, 0, 1, 10, 20]);
testing.expectEqual('matrix(1, 0, 0, 1, 10, 20)', m.toString());
}
</script>
<script id=to_float32_array>
{
const m = new DOMMatrix();
const arr = m.toFloat32Array();
testing.expectTrue(arr instanceof Float32Array);
testing.expectEqual(16, arr.length);
testing.expectEqual(1, arr[0]);
testing.expectEqual(1, arr[15]);
}
</script>
<script id=set_3d_element_zero_preserves_is2d>
{
// Setting a z/w element to 0 (or -0) must preserve is2D; only a non-zero
// value clears it.
for (const attr of ['m13', 'm14', 'm23', 'm24', 'm31', 'm32', 'm34', 'm43']) {
const m = new DOMMatrix();
m[attr] = 0;
testing.expectTrue(m.is2D);
m[attr] = -0;
testing.expectTrue(m.is2D);
m[attr] = 42;
testing.expectFalse(m.is2D);
}
// m33/m44 identity is 1: setting to 1 preserves, anything else clears.
const a = new DOMMatrix();
a.m33 = 1;
testing.expectTrue(a.is2D);
a.m33 = 2;
testing.expectFalse(a.is2D);
}
</script>
<script id=multiply_no_args_is_copy>
{
const m = new DOMMatrix([1, 2, 3, 4, 5, 6]);
const r = m.multiply();
testing.expectEqual(1, r.a);
testing.expectEqual(6, r.f);
testing.expectTrue(r !== m);
}
</script>
<script id=new_object_methods>
{
const m = new DOMMatrix();
for (const method of ['scale3d', 'rotateAxisAngle', 'rotateFromVector', 'skewX', 'skewY', 'flipX', 'flipY', 'multiply']) {
const r = m[method]();
testing.expectTrue(r instanceof DOMMatrix);
testing.expectTrue(r !== m);
}
}
</script>
<script id=skew_keeps_2d>
{
const m = new DOMMatrix().skewX(30);
testing.expectTrue(m.is2D);
// tan(30deg) ~= 0.5774 lands in c (m21)
testing.expectTrue(Math.abs(m.c - Math.tan(30 * Math.PI / 180)) < 1e-9);
}
</script>
<script id=readonly_exists_and_inheritance>
{
const ro = new DOMMatrixReadOnly([1, 0, 0, 1, 10, 20]);
testing.expectEqual(10, ro.e);
testing.expectEqual(20, ro.f);
testing.expectTrue(ro instanceof DOMMatrixReadOnly);
// DOMMatrix extends DOMMatrixReadOnly
const m = new DOMMatrix();
testing.expectTrue(m instanceof DOMMatrix);
testing.expectTrue(m instanceof DOMMatrixReadOnly);
testing.expectFalse(ro instanceof DOMMatrix);
}
</script>
<script id=readonly_is_immutable>
{
const ro = new DOMMatrixReadOnly([1, 0, 0, 1, 10, 20]);
// Setting a component on a read-only matrix must not change it (the
// accessor has no setter).
try { ro.a = 5; } catch (e) {}
testing.expectEqual(1, ro.a);
}
</script>
<script id=readonly_methods_return_mutable>
{
const ro = new DOMMatrixReadOnly();
const t = ro.translate(10, 20);
// DOMMatrixReadOnly methods return a (new, mutable) DOMMatrix
testing.expectTrue(t instanceof DOMMatrix);
testing.expectEqual(10, t.e);
testing.expectEqual(20, t.f);
// original is untouched
testing.expectEqual(0, ro.e);
}
</script>
<script id=mutable_self_methods_chain>
{
const m = new DOMMatrix();
const r = m.translateSelf(5, 6).scaleSelf(2);
testing.expectTrue(r === m);
testing.expectEqual(2, m.a);
testing.expectEqual(5, m.e);
testing.expectEqual(6, m.f);
}
</script>
<script id=multiply_accepts_dict>
{
const m = new DOMMatrix();
const r = m.multiply({ m11: 1, m12: 2, m21: 0, m22: 1, m41: 0, m42: 0 });
testing.expectEqual(2, r.b);
// original not mutated
testing.expectEqual(0, m.b);
}
</script>
<script id=multiply_accepts_matrix_instance>
{
const a = new DOMMatrix().translate(10, 0);
const b = new DOMMatrix().translate(0, 20);
const m = a.multiply(b);
testing.expectEqual(10, m.e);
testing.expectEqual(20, m.f);
}
</script>
<script id=invert_self_identity_and_singular>
{
// returns self
const m = new DOMMatrix().translate(10, -20.5);
const ret = m.invertSelf();
testing.expectTrue(ret === m);
testing.expectEqual(-10, m.e);
testing.expectEqual(20.5, m.f);
// singular matrix inverts to all-NaN, is2D false
const s = new DOMMatrix([0, 0, 0, 0, 0, 0]);
s.invertSelf();
testing.expectTrue(Number.isNaN(s.m11));
testing.expectTrue(Number.isNaN(s.m44));
testing.expectFalse(s.is2D);
}
</script>
<script id=read_all_m_components>
{
// framer-motion reads m11..m44 off a matrix built from a transform string
const m = new DOMMatrix('matrix(1, 0, 0, 1, 0, 0)');
let sum = 0;
for (let r = 1; r < 5; r++) {
for (let c = 1; c < 5; c++) {
sum += m[`m${r}${c}`];
}
}
testing.expectEqual(4, sum); // identity has four 1s on the diagonal
}
</script>

View File

@@ -0,0 +1,301 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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;
}
};

View File

@@ -0,0 +1,870 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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 <transform-list> (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", .{});
}