haveDepCtrl, DependencyControl, depctrl = pcall require, 'l0.DependencyControl' local amath if haveDepCtrl depctrl = DependencyControl { name: "Perspective", version: "0.2.4", description: [[Math functions for dealing with perspective transformations.]], author: "arch1t3cht", url: "https://github.com/TypesettingTools/arch1t3cht-Aegisub-Scripts", moduleName: 'arch.Perspective', { {"arch.Math", version: "0.1.8", url: "https://github.com/TypesettingTools/arch1t3cht-Aegisub-Scripts", feed: "https://raw.githubusercontent.com/TypesettingTools/arch1t3cht-Aegisub-Scripts/main/DependencyControl.json"}, } } amath = depctrl\requireModules! else amath = require"arch.Math" {:Point, :Matrix} = amath -- compatibility with Lua >= 5.2 unpack = unpack or table.unpack local Quad -- Quadrilateral (usually in 2D space) described by its four corners, in clockwise or counter-clockwise direction. -- Internally, we always use numbering that's counter-clockwise in the cartesian plane, which is clockwise on a 2D screen. class Quad extends Matrix new: (...) => super(...) assert(@height == 4) -- Computes the intersection point of the diagonals. -- Doubles as a generic function to intersect to lines in 2D space. midpoint: => la = Matrix(@[3] - @[1], @[4] - @[2])\transpose!\preim(@[4] - @[1]) return @[1] + la[1] * (@[3] - @[1]) -------------------- -- Collection of functions describing the perspective transformation between this quad and a 1x1 square. -- These were originally computed from cross-ratios and run through Mathematica to combine all the fractions, -- which makes it work in such "edge" cases as two sides of the quad being parallel. -- They were then dumped from Mathematica in InputForm and inserted here without much postprocessing, -- except for sometimes putting common denominators in an extra variable -------------------- -- Helper functions to wrap code dumped from Mathematica -- returns x1, x2, x3, x4, y1, y2, y3, y4 unwrap: => @[1][1], @[2][1], @[3][1], @[4][1], @[1][2], @[2][2], @[3][2], @[4][2] -- translates x1, y1 to 0, 0 and returns x2, x3, x4, y2, y3, y4 unwrap_rel: => @ = @ - @[1] return @[2][1], @[3][1], @[4][1], @[2][2], @[3][2], @[4][2] -- Perspective transform mapping the quad to a unit square xy_to_uv: (xy) => assert(@width == 2) x2, x3, x4, y2, y3, y4 = @unwrap_rel! x, y = unpack(xy - @[1]) u = -(((x3*y2 - x2*y3)*(x4*y - x*y4)*(x4*(-y2 + y3) + x3*(y2 - y4) + x2*(-y3 + y4)))/(x3^2*(x4*y2^2*(-y + y4) + y4*(x*y2*(y2 - y4) + x2*(y - y2)*y4)) + x3*(x4^2*y2^2*(y - y3) + 2*x4*(x2*y*y3*(y2 - y4) + x*y2*(-y2 + y3)*y4) + x2*y4*(x2*(-y + y3)*y4 + 2*x*y2*(-y3 + y4))) + y3*(x*x4^2*y2*(y2 - y3) + x2*x4^2*(y2*y3 + y*(-2*y2 + y3)) - x2^2*(x4*y*(y3 - 2*y4) + x4*y3*y4 + x*y4*(-y3 + y4))))) v = ((x2*y - x*y2)*(x4*y3 - x3*y4)*(x4*(y2 - y3) + x2*(y3 - y4) + x3*(-y2 + y4)))/(x3*(x4^2*y2^2*(-y + y3) + x2*y4*(2*x*y2*(y3 - y4) + x2*(y - y3)*y4) - 2*x4*(x2*y*y3*(y2 - y4) + x*y2*(-y2 + y3)*y4)) + x3^2*(x4*y2^2*(y - y4) + y4*(x2*(-y + y2)*y4 + x*y2*(-y2 + y4))) + y3*(x*x4^2*y2*(-y2 + y3) + x2*x4^2*(2*y*y2 - y*y3 - y2*y3) + x2^2*(x4*y*(y3 - 2*y4) + x4*y3*y4 + x*y4*(-y3 + y4)))) return Point(u, v) -- Perspective transform mapping a unit square to the quad uv_to_xy: (uv) => assert(@width == 2) x2, x3, x4, y2, y3, y4 = @unwrap_rel! u, v = unpack(uv) d = (x4*((-1 + u + v)*y2 + y3 - v*y3) + x3*(y2 - u*y2 + (-1 + v)*y4) + x2*((-1 + u)*y3 - (-1 + u + v)*y4)) x = (v*x4*(x3*y2 - x2*y3) + u*x2*(x4*y3 - x3*y4)) / d y = (v*y4*(x3*y2 - x2*y3) + u*y2*(x4*y3 - x3*y4)) / d return Point(x, y) + @[1] -- Derivative (i.e. Jacobian) of uv_to_xy at the given point d_uv_to_xy: (uv) => assert(@width == 2) x2, x3, x4, y2, y3, y4 = @unwrap_rel! u, v = unpack(uv) d = (x4*((-1 + u + v)*y2 + y3 - v*y3) + x3*(y2 - u*y2 + (-1 + v)*y4) + x2*((-1 + u)*y3 - (-1 + u + v)*y4))^2 dxdu = (x2*(x4*y3 - x3*y4)*(x4*((-1 + u + v)*y2 + y3 - v*y3) + x3*(y2 - u*y2 + (-1 + v)*y4) + x2*((-1 + u)*y3 - (-1 + u + v)*y4)) + (x3*y2 - x4*y2 + x2*(-y3 + y4))*(v*x4*(x3*y2 - x2*y3) + u*x2*(x4*y3 - x3*y4))) / d dxdv = (x4*(x3*y2 - x2*y3)*(x4*((-1 + u + v)*y2 + y3 - v*y3) + x3*(y2 - u*y2 + (-1 + v)*y4) + x2*((-1 + u)*y3 - (-1 + u + v)*y4)) - (x4*(y2 - y3) + (-x2 + x3)*y4)*(v*x4*(x3*y2 - x2*y3) + u*x2*(x4*y3 - x3*y4))) / d dydu = ((-1 + v)*x3^2*y2*(y2 - y4)*y4 + y3*((-1 + v)*x4^2*y2*(y2 - y3) + v*x2^2*(y3 - y4)*y4 + x2*x4*y2*(-y3 + y4)) + x3*y2*(2*(-1 + v)*x4*y3*y4 - (-1 + 2*v)*x2*(y3 - y4)*y4 + x4*y2*(y3 + y4 - 2*v*y4))) / d dydv = ((x3*y2 - x2*y3)*y4*(-(x4*y2) - x2*y3 + x4*y3 + x3*(y2 - y4) + x2*y4) + u*(x4^2*y2*y3*(-y2 + y3) + 2*x3*x4*y2*(y2 - y3)*y4 + y4*(2*x2*x3*y2*(y3 - y4) + x3^2*y2*(-y2 + y4) + x2^2*y3*(-y3 + y4)))) / d return Matrix({{dxdu, dxdv}, {dydu, dydv}}) -- Derivative (i.e. Jacobian) of xy_to_uv at the given point d_xy_to_uv: (xy) => assert(@width == 2) x2, x3, x4, y2, y3, y4 = @unwrap_rel! x, y = unpack(xy) d = (x3*(x4^2*y2^2*(-y + y3) + x2*y4*(2*x*y2*(y3 - y4) + x2*(y - y3)*y4) - 2*x4*(x2*y*y3*(y2 - y4) + x*y2*(-y2 + y3)*y4)) + x3^2*(x4*y2^2*(y - y4) + y4*(x2*(-y + y2)*y4 + x*y2*(-y2 + y4))) + y3*(x*x4^2*y2*(-y2 + y3) + x2*x4^2*(2*y*y2 - y*y3 - y2*y3) + x2^2*(x4*y*(y3 - 2*y4) + x4*y3*y4 + x*y4*(-y3 + y4))))^2 dudx = ((x3*y2 - x2*y3)*(x4*y2 - x2*y4)*(x4*y3 - x3*y4)*(x4*y*(y2 - y3) + x3*(y - y2)*y4 + x2*(-y + y3)*y4)*(x4*(-y2 + y3) + x3*(y2 - y4) + x2*(-y3 + y4))) / d dvdx = -((x3*y2 - x2*y3)*(x4*y2 - x2*y4)*(x4*y3 - x3*y4)*(-(x3*x4*y2) + x*x4*(y2 - y3) + x2*x4*y3 + x*(-x2 + x3)*y4)*(x4*(-y2 + y3) + x3*(y2 - y4) + x2*(-y3 + y4))) / d dudy = ((x3*y2 - x2*y3)*(x4*y2 - x2*y4)*(x4*y3 - x3*y4)*(x4*y2*(y - y3) + x2*y*(y3 - y4) + x3*y2*(-y + y4))*(x4*(y2 - y3) + x2*(y3 - y4) + x3*(-y2 + y4))) / d dvdy = ((x3*y2 - x2*y3)*(x4*y2 - x2*y4)*(-(x4*y3) + x3*y4)*(x4*(-y2 + y3) + x3*(y2 - y4) + x2*(-y3 + y4))*(x*(x3*y2 - x4*y2 - x2*y3 + x2*y4) + x2*(x4*y3 - x3*y4))) / d return Matrix({{dudx, dudy}, {dvdx, dvdy}}) screen_z = 312.5 an_xshift = { 0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1 } an_yshift = { 1, 1, 1, 0.5, 0.5, 0.5, 0, 0, 0 } -- Transforms the given list of points in a relative coordinate system according to the given .ass tags. -- If no list of points is given, a rectangle with the given dimensions is used. -- The width and height parameters should contain the raw dimensions of the line to be transformed. These are used for alignment. -- Thus, when transforming a shape with \an7, width and height can be zero. When transforming text, they should be whatever aegisub.text_extents returned. -- The table t is supposed to be a table of tags as returned by ASSFoundation, but any table with the same keys and .value or .x/.y -- fields for the respective tags works. transformPoints = (t, width, height, points=nil) -> if points == nil points = Quad { {0, 0}, {width, 0}, {width, height}, {0, height}, } else points = Matrix(points) pos = Point(t.position.x, t.position.y) org = Point(t.origin.x, t.origin.y) -- Shearing points *= Matrix({ {1, t.shear_x.value}, {t.shear_y.value, 1}, })\t! -- Translate to alignment point an = t.align.value points -= Point(width * an_xshift[an], height * an_yshift[an]) -- Apply scaling points *= (Matrix.diag(t.scale_x.value, t.scale_y.value) / 100) -- Translate relative to origin points += pos - org -- Rotate ZXY points ..= 0 points *= Matrix.rot2d(math.rad(-t.angle.value))\onSubspace(3)\t! points *= Matrix.rot2d(math.rad(-t.angle_x.value))\onSubspace(1)\t! points *= Matrix.rot2d(math.rad(t.angle_y.value))\onSubspace(2)\t! -- Project points = Matrix [ (screen_z / (p\z! + screen_z)) * p\project(2) for p in *points ] -- Move to origin points += org return points -- Given a quad on screen and the width and height of the text, returns in t (again an ASSFoundation tags table) -- the tag values that will transform this text to the given quad. -- If center = true, the center of the quad will be used as \org. If not, the \org set in t will be used. tagsFromQuad = (t, quad, width, height, center=false) -> quad = Quad(quad) if quad.__class != Quad if center center = quad\midpoint! t.origin.x = center\x! t.origin.y = center\y! -- Normalize to center org = Point(t.origin.x, t.origin.y) quad -= org -- Find a parallelogram projecting to the quad z24 = Matrix({ quad[2] - quad[3], quad[4] - quad[3] })\t!\preim(quad[1] - quad[3]) zs = Point(1, z24[1], z24\sum! - 1, z24[2]) quad ..= screen_z quad = Matrix.diag(zs) * quad -- Normalize so the origin has z=screen_z orgla = Matrix({Point(0, 0, screen_z), quad[1] - quad[2], quad[1] - quad[4]})\t!\preim(quad[1]) quad /= orgla[1] quad -= Matrix[{0, 0, screen_z} for i=1,4] -- Find the rotations n = (quad[2] - quad[1])\cross(quad[4] - quad[1]) roty = math.atan(n\x! / n\z!) roty += math.pi if n\z! < 0 ry = Matrix.rot2d(roty)\onSubspace(2) n = Point(ry * n) rotx = math.atan(n\y! / n\z!) rx = Matrix.rot2d(rotx)\onSubspace(1) quad *= ry\t! quad *= rx\t! ab = quad[2] - quad[1] rotz = math.atan(ab\y! / ab\x!) rotz += math.pi if ab\x! < 0 rz = Matrix.rot2d(-rotz)\onSubspace(3) quad *= rz\t! -- We now have a horizontal parallelogram in the 2D plane, so find the shear and the dimensions ab = quad[2] - quad[1] ad = quad[4] - quad[1] rawfax = ad\x! / ad\y! quadwidth = ab\length! quadheight = math.abs(ad\y!) scalex = quadwidth / width scaley = quadheight / height -- Find \pos an = t.align.value pos = org + (quad[1]\project(2) + Point(quadwidth * an_xshift[an], quadheight * an_yshift[an])) -- Set all the new tags t.position.x = pos\x! t.position.y = pos\y! t.angle.value = math.deg(-rotz) t.angle_x.value = math.deg(rotx) t.angle_y.value = math.deg(-roty) t.scale_x.value = 100 * scalex t.scale_y.value = 100 * scaley t.shear_x.value = rawfax * scaley / scalex t.shear_y.value = 0 lib = { :Quad, :transformPoints, :tagsFromQuad, } if haveDepCtrl lib.version = depctrl return depctrl\register lib else return lib