dotfiles/.aegisub/automation/autoload/lyger.ClipBlur.lua
2020-10-05 05:46:57 +02:00

472 lines
12 KiB
Lua

--[[
==README==
Blur clip
There's really not much to explain here. \clip statements produce a sharp edge. This script
draws new \clip statements with decreasing alphas in order to imitate the effect of a blur.
The appearance won't always be perfect because of the limitations of precision with vector
clip coordinates. The "precision" parameter ameliorates this somewhat, but the odd jagged
line here and there is inevitable.
A note on the "precision" parameter: it scales exponentionally. If you want a 5-pixel blur,
then a precision of 1 produces 6 lines (5 for the blur, 1 for the center). Precision 2 will
generate 11 lines (10 for the blur, 1 for the center) and precision 3 will generate 21 lines
(20 for the blur, 1 for the center). As you've probably figured out, a precision of 4 will
create a whopping 41 lines. Use with caution.
]]--
script_name = "Blur clip"
script_description = "Blurs a vector clip."
script_version = "1.2.0"
script_author = "lyger"
script_namespace = "lyger.ClipBlur"
local DependencyControl = require("l0.DependencyControl")
local rec = DependencyControl{
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
{
{"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"},
"aegisub.util"
}
}
local LibLyger, util = rec:requireModules()
local libLyger = LibLyger()
--Distance between two points
local function distance(x1,y1,x2,y2)
return math.sqrt((x2-x1)^2+(y2-y1)^2)
end
--Sign of a value
local function sign(n)
return n/math.abs(n)
end
--Haha I didn't know these functions existed. May as well just alias them
local todegree=math.deg
local torad=math.rad
--Parses vector shape and makes it into a table
function make_vector_table(vstring)
local vtable={}
local vexp=vstring:match("^([1-4]),")
vexp=tonumber(vexp) or 1
for vtype,vcoords in vstring:gmatch("([mlb])([%d%s%-]+)") do
for vx,vy in vcoords:gmatch("([%d%-]+)%s+([%d%-]+)") do
table.insert(vtable,{["class"]=vtype,["x"]=tonumber(vx),["y"]=tonumber(vy)})
end
end
return vtable,vexp
end
--Reverses a vector table object
function reverse_vector_table(vtable)
local nvtable={}
if #vtable<1 then return nvtable end
--Make sure vtable does not end in an m. I don't know why this would happen but still
maxi=#vtable
while vtable[maxi].class=="m" do
maxi=maxi-1
end
--All vector shapes start with m
nstart = util.copy(vtable[maxi])
tclass=nstart.class
nstart.class="m"
table.insert(nvtable,nstart)
--Reinsert coords in backwards order, but shift the class over by 1
--because that's how vector shapes behave in aegi
for i=maxi-1,1,-1 do
tcoord = util.copy(vtable[i])
_temp=tcoord.class
tcoord.class=tclass
tclass=_temp
table.insert(nvtable,tcoord)
end
return nvtable
end
--Turns vector table into string
function vtable_to_string(vt)
cclass=nil
result=""
for i=1,#vt,1 do
if vt[i].class~=cclass then
result=result..string.format("%s %d %d ",vt[i].class,vt[i].x,vt[i].y)
cclass=vt[i].class
else
result=result..string.format("%d %d ",vt[i].x,vt[i].y)
end
end
return result
end
--Rounds to the given number of decimal places
function round(n,dec)
dec=dec or 0
return math.floor(n*10^dec+0.5)/(10^dec)
end
--Grows vt outward by the radius r scaled by sc
function grow(vt,r,sc)
ch=get_chirality(vt)
local wvt=wrap(vt)
local nvt={}
sc=sc or 1
--Grow
for i=2,#wvt-1,1 do
cpt=wvt[i]
ppt=wvt[i].prev
npt=wvt[i].next
while distance(cpt.x,cpt.y,ppt.x,ppt.y)==0 do
ppt=ppt.prev
end
while distance(cpt.x,cpt.y,npt.x,npt.y)==0 do
npt=npt.prev
end
rot1=todegree(math.atan2(cpt.y-ppt.y,cpt.x-ppt.x))
rot2=todegree(math.atan2(npt.y-cpt.y,npt.x-cpt.x))
drot=(rot2-rot1)%360
--Angle to expand at
nrot=(0.5*drot+90)%180
if ch<0 then nrot=nrot+180 end
--Adjusted radius
__ar=math.cos(torad(ch*90-nrot)) --<3
ar=(__ar<0.00001 and r) or r/math.abs(__ar)
newx=cpt.x*sc
newy=cpt.y*sc
if r~=0 then
newx=newx+sc*round(ar*math.cos(torad(nrot+rot1)))
newy=newy+sc*round(ar*math.sin(torad(nrot+rot1)))
end
table.insert(nvt,{["class"]=cpt.class,
["x"]=newx,
["y"]=newy})
end
--Check for "crossovers"
--New data type to store points with same coordinates
local mvt={}
local wnvt=wrap(nvt)
for i,p in ipairs(wnvt) do
table.insert(mvt,{["class"]={p.class},["x"]=p.x,["y"]=p.y})
end
--Number of merges so far
merges=0
for i=2,#wnvt,1 do
mi=i-merges
dx=wvt[i].x-wvt[i-1].x
dy=wvt[i].y-wvt[i-1].y
ndx=wnvt[i].x-wnvt[i-1].x
ndy=wnvt[i].y-wnvt[i-1].y
if (dy*ndy<0 or dx*ndx<0) then
--Multiplicities
c1=#mvt[mi-1].class
c2=#mvt[mi].class
--Weighted average
mvt[mi-1].x=(c1*mvt[mi-1].x+c2*mvt[mi].x)/(c1+c2)
mvt[mi-1].y=(c1*mvt[mi-1].y+c2*mvt[mi].y)/(c1+c2)
--Merge classes
mvt[mi-1].class={unpack(mvt[mi-1].class),unpack(mvt[mi].class)}
--Delete point
table.remove(mvt,mi)
merges=merges+1
end
end
--Rebuild wrapped new vector table
wnvt={}
for i,p in ipairs(mvt) do
for k,pclass in ipairs(p.class) do
table.insert(wnvt,{["class"]=pclass,["x"]=p.x,["y"]=p.y})
end
end
return unwrap(wnvt)
end
function merge_identical(vt)
local mvt = util.copy(vt)
i=2
lx=mvt[1].x
ly=mvt[1].y
while i<#mvt do
if mvt[i].x==lx and mvt[i].y==ly then
table.remove(mvt,i)
else
lx=mvt[i].x
ly=mvt[i].y
i=i+1
end
end
return mvt
end
--Returns chirality of vector shape. +1 if counterclockwise, -1 if clockwise
function get_chirality(vt)
local wvt=wrap(vt)
wvt=merge_identical(wvt)
trot=0
for i=2,#wvt-1,1 do
rot1=math.atan2(wvt[i].y-wvt[i-1].y,wvt[i].x-wvt[i-1].x)
rot2=math.atan2(wvt[i+1].y-wvt[i].y,wvt[i+1].x-wvt[i].x)
drot=todegree(rot2-rot1)%360
if drot>180 then drot=360-drot elseif drot==180 then drot=0 else drot=-1*drot end
trot=trot+drot
end
return sign(trot)
end
--Duplicates first and last coordinates at the end and beginning of shape,
--to allow for wraparound calculations
function wrap(vt)
local wvt={}
table.insert(wvt,util.copy(vt[#vt]))
for i=1,#vt,1 do
table.insert(wvt,util.copy(vt[i]))
end
table.insert(wvt,util.copy(vt[1]))
--Add linked list capability. Because. Hacky fix gogogogo
for i=2,#wvt-1 do
wvt[i].prev=wvt[i-1]
wvt[i].next=wvt[i+1]
end
--And link the start and end
wvt[2].prev=wvt[#wvt-1]
wvt[#wvt-1].next=wvt[2]
return wvt
end
--Cuts off the first and last coordinates, to undo the effects of "wrap"
function unwrap(wvt)
local vt={}
for i=2,#wvt-1,1 do
table.insert(vt,util.copy(wvt[i]))
end
return vt
end
--Main execution function
function blur_clip(sub,sel)
--GUI config
config=
{
{
class="label",
label="Blur size:",
x=0,y=0,width=1,height=1
},
{
class="floatedit",
name="bsize",
min=0,step=0.5,value=1,
x=1,y=0,width=1,height=1
},
{
class="label",
label="Blur position:",
x=0,y=1,width=1,height=1
},
{
class="dropdown",
name="bpos",
items={"outside","middle","inside"},
value="outside",
x=1,y=1,width=1,height=1
},
{
class="label",
label="Precision:",
x=0,y=2,width=1,height=1
},
{
class="intedit",
name="bprec",
min=1,max=4,value=2,
x=1,y=2,width=1,height=1
}
}
--Show dialog
pressed,results=aegisub.dialog.display(config,{"Go","Cancel"})
if pressed=="Cancel" then aegisub.cancel() end
--Size of the blur
bsize=results["bsize"]
--Scale exponent for all the numbers
sexp=results["bprec"]
--How far to offset the blur by
boffset=0
if results["bpos"]=="inside" then boffset=bsize
elseif results["bpos"]=="middle" then boffset=bsize/2 end
--How far to offset the next line read
lines_added=0
libLyger:set_sub(sub, sel)
for si,li in ipairs(sel) do
--Progress report
aegisub.progress.task("Processing line "..si.."/"..#sel)
aegisub.progress.set(100*si/#sel)
--Read in the line
line = libLyger.lines[li]
--Comment it out
line.comment=true
sub[li+lines_added]=line
line.comment=false
--Find the clipping shape
ctype,tvector=line.text:match("\\(i?clip)%(([^%(%)]+)%)")
--Cancel if it doesn't exist
if tvector==nil then
aegisub.log("Make sure all lines have a clip statement.")
aegisub.cancel()
end
--Get position and add
px,py = libLyger:get_pos(line)
if line.text:match("\\pos")==nil and line.text:match("\\move")==nil then
line.text=string.format("{\\pos(%d,%d)}",px,py)..line.text
end
--Round
local function rnd(num)
num=tonumber(num) or 0
if num<0 then
num=num-0.5
return math.ceil(num)
end
num=num+0.5
return math.floor(num)
end
--If it's a rectangular clip, convert to vector clip
if tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)")~=nil then
_x1,_y1,_x2,_y2=tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)")
tvector=string.format("m %d %d l %d %d %d %d %d %d",
rnd(_x1),rnd(_y1),rnd(_x2),rnd(_y1),rnd(_x2),rnd(_y2),rnd(_x1),rnd(_y2))
end
--The original table and original scale exponent
otable,oexp=make_vector_table(tvector)
--Effective scale and scale exponent
eexp=sexp-oexp+1
escale=2^(eexp-1)
--aegisub.log("Escale: %.2f",escale)
--The innermost line
iline = util.copy(line)
itable={}
if ctype=="iclip" then
itable=grow(otable,bsize*2^(oexp-1)-boffset,escale)
else
itable=grow(otable,-1*boffset,escale)
end
iline.text=iline.text:gsub("\\i?clip%([^%(%)]+%)","\\"..ctype.."("..sexp..","..vtable_to_string(itable)..")")
--Add it to the subs
sub.insert(li+lines_added+1,iline)
lines_added=lines_added+1
--Set default alpha values
dalpha={}
dalpha[1]=alpha_from_style(line.styleref.color1)
dalpha[2]=alpha_from_style(line.styleref.color2)
dalpha[3]=alpha_from_style(line.styleref.color3)
dalpha[4]=alpha_from_style(line.styleref.color4)
--First tag block
ftag=line.text:match("^{[^{}]*}")
if ftag==nil then
ftag="{}"
line.text="{}"..line.text
end
--List of alphas not yet accounted for in the first tag
unacc={}
if ftag:match("\\alpha")==nil then
if ftag:match("\\1a")==nil then table.insert(unacc,1) end
if ftag:match("\\2a")==nil then table.insert(unacc,2) end
if ftag:match("\\3a")==nil then table.insert(unacc,3) end
if ftag:match("\\4a")==nil then table.insert(unacc,4) end
end
--Add tags if any are unaccounted for
if #unacc>0 then
--If all the unaccounted-for alphas are equal, only add an "alpha" tag
_tempa=dalpha[unacc[1]]
_equal=true
for _k,_a in ipairs(unacc) do
if dalpha[_a]~=_tempa then _equal=false end
end
if _equal then line.text=line.text:gsub("^{","{\\alpha"..dalpha[unacc[1]])
else
for _k,ui in ipairs(unacc) do
line.text=line.text:gsub("^{","{\\"..ui.."a"..dalpha[ui])
end
end
end
prevclip=itable
for j=1,math.ceil(bsize*escale*2^(oexp-1)),1 do
--Interpolation factor
factor=j/(bsize*escale+1)
--Flip if it's an iclip
if ctype=="iclip" then factor=1-factor end
--Copy the line
tline = util.copy(line)
--Sub in the interpolated alphas
tline.text=tline.text:gsub("\\alpha([^\\{}]+)",
function(a) return "\\alpha"..interpolate_alpha(factor,a,"&HFF&") end)
tline.text=tline.text:gsub("\\([1-4]a)([^\\{}]+)",
function(a,b) return "\\"..a..interpolate_alpha(factor,b,"&HFF&") end)
--Write the correct clip
thisclip=grow(otable,j/escale-boffset,escale)
clipstring=vtable_to_string(thisclip)..vtable_to_string(reverse_vector_table(prevclip))
prevclip=thisclip
tline.text=tline.text:gsub("\\i?clip%([^%(%)]+%)","\\clip("..sexp..","..clipstring..")")
--Insert the line
sub.insert(li+lines_added+1,tline)
lines_added=lines_added+1
end
end
aegisub.set_undo_point(script_name)
end
rec:registerMacro(blur_clip)