gears.matrix: Implement matrices in Lua

Instead of going through LGI to call cairo, this now implements the various
matrix operations directly in Lua. The plan is to avoid the overhead that we hit
due to LGI.

Signed-off-by: Uli Schlachter <psychon@znc.in>
This commit is contained in:
Uli Schlachter 2015-09-16 13:48:33 +02:00
parent 2330040eb5
commit 34927b187d
2 changed files with 267 additions and 44 deletions

View File

@ -1,8 +1,10 @@
---------------------------------------------------------------------------
-- An implementation of matrices for describing and working with affine
-- transformations.
-- @author Uli Schlachter
-- @copyright 2015 Uli Schlachter
-- @release @AWESOME_VERSION@
-- @module gears.matrix
-- @classmod gears.matrix
---------------------------------------------------------------------------
local cairo = require("lgi").cairo
@ -19,32 +21,156 @@ function matrix.copy(matrix)
return ret
end
--- Check if two cairo matrices are equal
-- @param m1 The first matrix to compare with.
-- @param m2 The second matrix to compare with.
-- @return True if they are equal.
function matrix.equals(m1, m2)
-- Metatable for matrix instances. This is set up near the end of the file.
local matrix_mt = {}
--- Create a new matrix instance
-- @tparam number xx The xx transformation part.
-- @tparam number yx The yx transformation part.
-- @tparam number xy The xy transformation part.
-- @tparam number yy The yy transformation part.
-- @tparam number x0 The x0 transformation part.
-- @tparam number y0 The y0 transformation part.
-- @return A new matrix describing the given transformation.
function matrix.create(xx, yx, xy, yy, x0, y0)
return setmetatable({
xx = xx, xy = xy, x0 = x0,
yx = yx, yy = yy, y0 = y0
}, matrix_mt)
end
--- Create a new translation matrix
-- @tparam number x The translation in x direction.
-- @tparam number y The translation in y direction.
-- @return A new matrix describing the given transformation.
function matrix.create_translate(x, y)
return matrix.create(1, 0, 0, 1, x, y)
end
--- Create a new scaling matrix
-- @tparam number sx The scaling in x direction.
-- @tparam number sy The scaling in y direction.
-- @return A new matrix describing the given transformation.
function matrix.create_scale(sx, sy)
return matrix.create(sx, 0, 0, sy, 0, 0)
end
--- Create a new rotation matrix
-- @tparam number angle The angle of the rotation in radians.
-- @return A new matrix describing the given transformation.
function matrix.create_rotate(angle)
local c, s = math.cos(angle), math.sin(angle)
return matrix.create(c, s, -s, c, 0, 0)
end
--- Translate this matrix
-- @tparam number x The translation in x direction.
-- @tparam number y The translation in y direction.
-- @return A new matrix describing the new transformation.
function matrix:translate(x, y)
return matrix.create_translate(x, y):multiply(self)
end
--- Scale this matrix
-- @tparam number sx The scaling in x direction.
-- @tparam number sy The scaling in y direction.
-- @return A new matrix describing the new transformation.
function matrix:scale(sx, sy)
return matrix.create_scale(sx, sy):multiply(self)
end
--- Rotate this matrix
-- @tparam number angle The angle of the rotation in radians.
-- @return A new matrix describing the new transformation.
function matrix:rotate(angle)
return matrix.create_rotate(angle):multiply(self)
end
--- Invert this matrix
-- @return A new matrix describing the inverse transformation.
function matrix:invert()
-- Beware of math! (I just copied the algorithm from cairo's source code)
local a, b, c, d, x0, y0 = self.xx, self.yx, self.xy, self.yy, self.x0, self.y0
local inv_det = 1/(a*d - b*c)
return matrix.create(inv_det * d, inv_det * -b,
inv_det * -c, inv_det * a,
inv_det * (c * y0 - d * x0), inv_det * (b * x0 - a * y0))
end
--- Multiply this matrix with another matrix.
-- The resulting matrix describes a transformation that is equivalent to first
-- applying this transformation and then the transformation from `other`.
-- Note that this function can also be called by directly multiplicating two
-- matrix instances: `a * b == a:multiply(b)`.
-- @tparam gears.matrix|cairo.Matrix other The other matrix to multiply with.
-- @return The multiplication result.
function matrix:multiply(other)
return matrix.create(self.xx * other.xx + self.yx * other.xy,
self.xx * other.yx + self.yx * other.yy,
self.xy * other.xx + self.yy * other.xy,
self.xy * other.yx + self.yy * other.yy,
self.x0 * other.xx + self.y0 * other.xy + other.x0,
self.x0 * other.yx + self.y0 * other.yy + other.y0)
end
--- Check if two matrices are equal.
-- Note that this function cal also be called by directly comparing two matrix
-- instances: `a == b`.
-- @tparam gears.matrix|cairo.Matrix other The matrix to compare with.
-- @return True if this and the other matrix are equal.
function matrix:equals(other)
for _, k in pairs{ "xx", "xy", "yx", "yy", "x0", "y0" } do
if m1[k] ~= m2[k] then
if self[k] ~= other[k] then
return false
end
end
return true
end
--- Get a string representation of this matrix
-- @return A string showing this matrix in column form.
function matrix:tostring()
return string.format("[[%g, %g], [%g, %g], [%g, %g]]",
self.xx, self.yx, self.xy,
self.yy, self.x0, self.y0)
end
--- Transform a distance by this matrix.
-- The difference to @{matrix:transform_point} is that the translation part of
-- this matrix is ignored.
-- @tparam number x The x coordinate of the point.
-- @tparam number y The y coordinate of the point.
-- @treturn number The x coordinate of the transformed point.
-- @treturn number The x coordinate of the transformed point.
function matrix:transform_distance(x, y)
return self.xx * x + self.xy * y, self.yx * x + self.yy * y
end
--- Transform a point by this matrix.
-- @tparam number x The x coordinate of the point.
-- @tparam number y The y coordinate of the point.
-- @treturn number The x coordinate of the transformed point.
-- @treturn number The x coordinate of the transformed point.
function matrix:transform_point(x, y)
local x, y = self:transform_distance(x, y)
return self.x0 + x, self.y0 + y
end
--- Calculate a bounding rectangle for transforming a rectangle by a matrix.
-- @param matrix The cairo matrix that describes the transformation.
-- @param x The x coordinate of the rectangle.
-- @param y The y coordinate of the rectangle.
-- @param width The width of the rectangle.
-- @param height The height of the rectangle.
-- @return The x, y, width and height of the bounding rectangle.
function matrix.transform_rectangle(matrix, x, y, width, height)
-- @tparam number x The x coordinate of the rectangle.
-- @tparam number y The y coordinate of the rectangle.
-- @tparam number width The width of the rectangle.
-- @tparam number height The height of the rectangle.
-- @treturn number X coordinate of the bounding rectangle.
-- @treturn number Y coordinate of the bounding rectangle.
-- @treturn number Width of the bounding rectangle.
-- @treturn number Height of the bounding rectangle.
function matrix:transform_rectangle(x, y, width, height)
-- Transform all four corners of the rectangle
local x1, y1 = matrix:transform_point(x, y)
local x2, y2 = matrix:transform_point(x, y + height)
local x3, y3 = matrix:transform_point(x + width, y + height)
local x4, y4 = matrix:transform_point(x + width, y)
local x1, y1 = self:transform_point(x, y)
local x2, y2 = self:transform_point(x, y + height)
local x3, y3 = self:transform_point(x + width, y + height)
local x4, y4 = self:transform_point(x + width, y)
-- Find the extremal points of the result
local x = math.min(x1, x2, x3, x4)
local y = math.min(y1, y2, y3, y4)
@ -54,6 +180,30 @@ function matrix.transform_rectangle(matrix, x, y, width, height)
return x, y, width, height
end
--- Convert to a cairo matrix
-- @treturn cairo.Matrix A cairo matrix describing the same transformation.
function matrix:to_cairo_matrix()
local ret = cairo.Matrix()
ret:init(self.xx, self.yx, self.xy, self.yy, self.x0, self.y0)
return ret
end
--- Convert to a cairo matrix
-- @tparam cairo.Matrix mat A cairo matrix describing the sought transformation
-- @treturn gears.matrix A matrix instance describing the same transformation.
function matrix.from_cairo_matrix(mat)
return matrix.create(mat.xx, mat.yx, mat.xy, mat.yy, mat.x0, mat.y0)
end
matrix_mt.__index = matrix
matrix_mt.__newindex = error
matrix_mt.__eq = matrix.equals
matrix_mt.__mul = matrix.multiply
matrix_mt.__tostring = matrix.tostring
--- A constant for the identity matrix.
matrix.identity = matrix.create(1, 0, 0, 1, 0, 0)
return matrix
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80

View File

@ -7,48 +7,98 @@ local matrix = require("gears.matrix")
local cairo = require("lgi").cairo
describe("gears.matrix", function()
describe("copy", function()
it("Test copy", function()
local m1 = cairo.Matrix()
m1:init(1, 2, 3, 4, 5, 6)
local m2 = matrix.copy(m1)
assert.is.equal(m1.xx, m2.xx)
assert.is.equal(m1.xy, m2.xy)
assert.is.equal(m1.yx, m2.yx)
assert.is.equal(m1.yy, m2.yy)
assert.is.equal(m1.x0, m2.x0)
assert.is.equal(m1.y0, m2.y0)
local function round(n)
return math.floor(n + 0.5)
end
m1.x0 = 42
assert.is_not.equal(m1.x0, m2.x0)
describe("cannot modify", function()
assert.has.errors(function()
matrix.identity.something = 42
end)
end)
describe("equals", function()
it("Same matrix", function()
local m = cairo.Matrix.create_rotate(1)
local m = matrix.create_rotate(1)
assert.is_true(matrix.equals(m, m))
assert.is.equal(m, m)
end)
it("Different matrix equals", function()
local m1 = cairo.Matrix.create_rotate(1)
local m2 = cairo.Matrix.create_rotate(1)
local m1 = matrix.create_rotate(1)
local m2 = matrix.create_rotate(1)
assert.is_true(matrix.equals(m1, m2))
assert.is.equal(m1, m2)
end)
it("Different matrix unequal", function()
local m1 = cairo.Matrix()
local m2 = cairo.Matrix()
m1:init(1, 2, 3, 4, 5, 6)
m2:init(1, 2, 3, 4, 5, 0)
local m1 = matrix.create(1, 2, 3, 4, 5, 6)
local m2 = matrix.create(1, 2, 3, 4, 5, 0)
assert.is_false(matrix.equals(m1, m2))
assert.is_not.equal(m1, m2)
end)
it("Identity matrix", function()
local m1 = matrix.identity
local m2 = matrix.create(1, 0, 0, 1, 0, 0)
assert.is_true(matrix.equals(m1, m2))
assert.is.equal(m1, m2)
end)
end)
describe("create", function()
it("translate", function()
assert.is.equal(matrix.create(1, 0, 0, 1, 2, 3), matrix.create_translate(2, 3))
end)
it("scale", function()
assert.is.equal(matrix.create(2, 0, 0, 3, 0, 0), matrix.create_scale(2, 3))
end)
it("rotate", function()
local m = matrix.create_rotate(math.pi / 2)
assert.is_true(math.abs(round(m.xx) - m.xx) < 0.00000001)
assert.is.equal(-1, m.xy)
assert.is.equal(1, m.yx)
assert.is_true(math.abs(round(m.yy) - m.yy) < 0.00000001)
assert.is.equal(0, m.x0)
assert.is.equal(0, m.y0)
end)
end)
it("multiply", function()
-- Just some random matrices which I multiplied by hand
local a = matrix.create(1, 2, 3, 4, 5, 6)
local b = matrix.create(7, 8, 9, 1, 1, 1)
local m = matrix.create(25, 10, 57, 28, 90, 47)
assert.is.equal(m, a:multiply(b))
assert.is.equal(m, a * b)
end)
describe("invert", function()
it("translate", function()
local m1, m2 = matrix.create_translate(2, 3), matrix.create_translate(-2, -3)
assert.is.equal(m2, m1:invert())
assert.is.equal(matrix.identity, m1 * m2)
assert.is.equal(matrix.identity, m2 * m1)
end)
it("scale", function()
local m1, m2 = matrix.create_scale(2, 3), matrix.create_scale(1/2, 1/3)
assert.is.equal(m2, m1:invert())
assert.is.equal(matrix.identity, m1 * m2)
assert.is.equal(matrix.identity, m2 * m1)
end)
it("rotate", function()
local m1, m2 = matrix.create_rotate(2), matrix.create_rotate(-2)
assert.is.equal(m2, m1:invert())
assert.is.equal(matrix.identity, m1 * m2)
assert.is.equal(matrix.identity, m2 * m1)
end)
end)
describe("transform_rectangle", function()
local function round(n)
return math.floor(n + 0.5)
end
local function test(m, x, y, width, height,
expected_x, expected_y, expected_width, expected_height)
local actual_x, actual_y, actual_width, actual_height =
@ -64,19 +114,42 @@ describe("gears.matrix", function()
assert.is_true(math.abs(round(actual_height) - actual_height) < 0.00000001)
end
it("Identity matrix", function()
test(cairo.Matrix.create_identity(), 1, 2, 3, 4, 1, 2, 3, 4)
test(matrix.identity, 1, 2, 3, 4, 1, 2, 3, 4)
end)
it("Rotate 180", function()
test(cairo.Matrix.create_rotate(math.pi),
test(matrix.create_rotate(math.pi),
1, 2, 3, 4, -4, -6, 3, 4)
end)
it("Rotate 90", function()
test(cairo.Matrix.create_rotate(math.pi / 2),
test(matrix.create_rotate(math.pi / 2),
1, 2, 3, 4, -6, 1, 4, 3)
end)
end)
it("tostring", function()
local m = matrix.create(1, 2, 3, 4, 5, 6)
local expected = "[[1, 2], [3, 4], [5, 6]]"
assert.is.equal(expected, m:tostring())
assert.is.equal(expected, tostring(m))
end)
it("from_cairo_matrix", function()
local m1 = matrix.create_translate(2, 3)
local m2 = matrix.from_cairo_matrix(cairo.Matrix.create_translate(2, 3))
assert.is.equal(m1, m2)
end)
it("to_cairo_matrix", function()
local m = matrix.create_scale(3, 4):to_cairo_matrix()
assert.is.equal(3, m.xx)
assert.is.equal(0, m.xy)
assert.is.equal(0, m.yx)
assert.is.equal(4, m.yy)
assert.is.equal(0, m.x0)
assert.is.equal(0, m.y0)
end)
end)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80