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:
parent
2330040eb5
commit
34927b187d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue