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 -- @author Uli Schlachter
-- @copyright 2015 Uli Schlachter -- @copyright 2015 Uli Schlachter
-- @release @AWESOME_VERSION@ -- @release @AWESOME_VERSION@
-- @module gears.matrix -- @classmod gears.matrix
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
local cairo = require("lgi").cairo local cairo = require("lgi").cairo
@ -19,32 +21,156 @@ function matrix.copy(matrix)
return ret return ret
end end
--- Check if two cairo matrices are equal -- Metatable for matrix instances. This is set up near the end of the file.
-- @param m1 The first matrix to compare with. local matrix_mt = {}
-- @param m2 The second matrix to compare with.
-- @return True if they are equal. --- Create a new matrix instance
function matrix.equals(m1, m2) -- @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 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 return false
end end
end end
return true return true
end 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. --- Calculate a bounding rectangle for transforming a rectangle by a matrix.
-- @param matrix The cairo matrix that describes the transformation. -- @tparam number x The x coordinate of the rectangle.
-- @param x The x coordinate of the rectangle. -- @tparam number y The y coordinate of the rectangle.
-- @param y The y coordinate of the rectangle. -- @tparam number width The width of the rectangle.
-- @param width The width of the rectangle. -- @tparam number height The height of the rectangle.
-- @param height The height of the rectangle. -- @treturn number X coordinate of the bounding rectangle.
-- @return The x, y, width and height of the bounding rectangle. -- @treturn number Y coordinate of the bounding rectangle.
function matrix.transform_rectangle(matrix, x, y, width, height) -- @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 -- Transform all four corners of the rectangle
local x1, y1 = matrix:transform_point(x, y) local x1, y1 = self:transform_point(x, y)
local x2, y2 = matrix:transform_point(x, y + height) local x2, y2 = self:transform_point(x, y + height)
local x3, y3 = matrix:transform_point(x + width, y + height) local x3, y3 = self:transform_point(x + width, y + height)
local x4, y4 = matrix:transform_point(x + width, y) local x4, y4 = self:transform_point(x + width, y)
-- Find the extremal points of the result -- Find the extremal points of the result
local x = math.min(x1, x2, x3, x4) local x = math.min(x1, x2, x3, x4)
local y = math.min(y1, y2, y3, y4) 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 return x, y, width, height
end 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 return matrix
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 -- 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 local cairo = require("lgi").cairo
describe("gears.matrix", function() describe("gears.matrix", function()
describe("copy", function() local function round(n)
it("Test copy", function() return math.floor(n + 0.5)
local m1 = cairo.Matrix() end
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)
m1.x0 = 42 describe("cannot modify", function()
assert.is_not.equal(m1.x0, m2.x0) assert.has.errors(function()
matrix.identity.something = 42
end) end)
end) end)
describe("equals", function() describe("equals", function()
it("Same matrix", 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_true(matrix.equals(m, m))
assert.is.equal(m, m)
end) end)
it("Different matrix equals", function() it("Different matrix equals", function()
local m1 = cairo.Matrix.create_rotate(1) local m1 = matrix.create_rotate(1)
local m2 = cairo.Matrix.create_rotate(1) local m2 = matrix.create_rotate(1)
assert.is_true(matrix.equals(m1, m2)) assert.is_true(matrix.equals(m1, m2))
assert.is.equal(m1, m2)
end) end)
it("Different matrix unequal", function() it("Different matrix unequal", function()
local m1 = cairo.Matrix() local m1 = matrix.create(1, 2, 3, 4, 5, 6)
local m2 = cairo.Matrix() local m2 = matrix.create(1, 2, 3, 4, 5, 0)
m1:init(1, 2, 3, 4, 5, 6)
m2:init(1, 2, 3, 4, 5, 0)
assert.is_false(matrix.equals(m1, m2)) 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)
end) end)
describe("transform_rectangle", function() describe("transform_rectangle", function()
local function round(n)
return math.floor(n + 0.5)
end
local function test(m, x, y, width, height, local function test(m, x, y, width, height,
expected_x, expected_y, expected_width, expected_height) expected_x, expected_y, expected_width, expected_height)
local actual_x, actual_y, actual_width, actual_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) assert.is_true(math.abs(round(actual_height) - actual_height) < 0.00000001)
end end
it("Identity matrix", function() 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) end)
it("Rotate 180", function() 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) 1, 2, 3, 4, -4, -6, 3, 4)
end) end)
it("Rotate 90", function() 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) 1, 2, 3, 4, -6, 1, 4, 3)
end) end)
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) end)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80