--[[ ==README== Circular Text Define an origin, and this will put the text on a circular arc centered on that origin. An origin must be defined and it must be different from the position of the line. You can't have a circle if the radius is zero. The x coordinate of the position tag should match the x coordinate of the origin tag for best results. In other words, your original line should be at a right angle to the radius. Note that these are the x coordinates in the tags, not the x coordinates on screen, which will change if you rotate the tag. Supports varied fonts, font sizes, font spacings, and x/y scales in the same line. The resulting arc will be centered on the original rotation of your line. Only works on static lines. If you want the line to move or rotate, use another macro. ]]-- script_name = "Circular text" script_description = "Puts the text on a circular arc centered on the origin." script_version = "0.2.1" script_author = "lyger" script_namespace = "lyger.CircleText" 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() local utf8pattern = "[%z\1-\127\194-\244][\128-\191]*" --[[ Tags that can have any character after the tag declaration: \r \fn Otherwise, the first character after the tag declaration must be: a number, decimal point, open parentheses, minus sign, or ampersand ]]-- --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 --Angle in degrees, given the arc length and radius local function arc_angle(arc_length,radius) return arc_length/radius * 180/math.pi end --Main processing function function circle_text(sub,sel) 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] --Get position and origin px, py = libLyger:get_pos(line) ox, oy = libLyger:get_org(line) --Make sure pos and org are not the same if px==ox and py==oy then aegisub.log(1,"Error on line %d: Position and origin cannot be the same!",li) return end --Get radius radius=distance(px,py,ox,oy) --Remove \pos and \move --If your line was non-static, too bad line.text = LibLyger.line_exclude(line.text,{"pos","move"}) --Make sure line starts with a tag block if line.text:find("^{")==nil then line.text="{}"..line.text end --Rotation direction: positive if each character adds to the angle, --negative if each character subtracts from the angle rot_dir=sign(py-oy) --Add the \pos back with recalculated position line.text=line.text:gsub("^{",string.format("{\\pos(%d,%d)",ox,oy+rot_dir*radius)) --Get z rotation --Will only take the first one, because if you wanted the text to be on a circular arc, --why do you have more than one z rotation tag in the first place? _,_,zrot=line.text:find("\\frz([%-%.%d]+)") zrot=zrot or line.styleref.angle --Make line table line_table={} for thistag,thistext in line.text:gmatch("({[^{}]*})([^{}]*)") do table.insert(line_table,{tag=thistag,text=thistext}) end --Where data on the character widths will be stored char_data={} --Total width of line cum_width=0 --Stores current state of the line as style table current_style = util.deep_copy(line.styleref) --First pass to collect data on character widths for i,val in ipairs(line_table) do char_data[i]={} --Fix style tables to reflect override tags local _,_,font_name=val.tag:find("\\fn([^\\{}]+)") local _,_,font_size=val.tag:find("\\fs([%-%.%d]+)") local _,_,font_scx=val.tag:find("\\fscx([%-%.%d]+)") local _,_,font_scy=val.tag:find("\\fscy([%-%.%d]+)") local _,_,font_sp=val.tag:find("\\fsp([%-%.%d]+)") local _,_,_bold=val.tag:find("\\b([01])") local _,_,_italic=val.tag:find("\\i([01])") current_style.fontname=font_name or current_style.fontname current_style.fontsize=tonumber(font_size) or current_style.fontsize current_style.scale_x=tonumber(font_scx) or current_style.scale_x current_style.scale_y=tonumber(font_scy) or current_style.scale_y current_style.spacing=tonumber(font_sp) or current_style.spacing if _bold~=nil then if _bold=="1" then current_style.bold=true else current_style.bold=false end end if _italic~=nil then if _italic=="1" then current_style.italic=true else current_style.italic=false end end val.style = util.deep_copy(current_style) --Collect width data on each char for thischar in val.text:gmatch(utf8pattern) do cwidth=aegisub.text_extents(val.style,thischar) table.insert(char_data[i],{char=thischar,width=cwidth}) end --Increment cumulative width cum_width=cum_width+aegisub.text_extents(val.style,val.text) end --The angle that the rotation will begin at start_angle=zrot-(rot_dir*arc_angle(cum_width,radius))/2 rebuilt_text="" cum_rot=0 --Second pass to rebuild line with new tags for i,val in ipairs(line_table) do rebuilt_text=rebuilt_text..val.tag:gsub("\\fsp[%-%.%d]+",""):gsub("\\frz[%-%.%d]+","") for k,tchar in ipairs(char_data[i]) do --Character spacing should be the average of this character's width and the next one's --For spacing, scale width back up by the character's relevant scale_x, --because \fsp scales too. Also, subtract the existing font spacing this_spacing=0 this_width=0 if k~=#char_data[i] then this_width=(tchar.width+char_data[i][k+1].width)/2 this_spacing=-1*(this_width*100/val.style.scale_x-val.style.spacing) else this_width=i~=#line_table and (tchar.width+char_data[i+1][1].width)/2 or 0 this_spacing=i~=#line_table and -1*((tchar.width*100/val.style.scale_x + char_data[i+1][1].width*100/line_table[i+1].style.scale_x)/2 -val.style.spacing) or 0 end rebuilt_text=rebuilt_text..string.format("{\\frz%.3f\\fsp%.2f}%s", (start_angle+rot_dir*cum_rot)%360,this_spacing,tchar.char) cum_rot=cum_rot+arc_angle(this_width,radius) end end --[[ --Fuck the re library. Maybe I'll come back to this whitespaces=re.find(rebuilt_text, '(\{\\\\frz[\\d\\.\\-]+\\\\fsp[\\d\\.\\-]+\}\\S)((?:\{\\\\frz[\\d\\.\\-]+\\\\fsp[\\d\\.\\-]+\}\\s)+)') for j=1,#whitespaces-1,2 do first_tag=whitespaces[j].str other_tags=whitespaces[j+1].str aegisub.log("%s%s\n",first_tag,other_tags) first_space=first_tag:match("\\fsp([%d%.%-]+)") other_spaces=0 total_wsp=0 for _sp in other_tags:gmatch("\\fsp([%d%.%-]+)") do other_spaces=other_spaces+tonumber(_sp) total_wsp=total_wsp+1 end total_space=tonumber(first_space)+other_spaces rebuilt_text=rebuilt_text:gsub(first_tag..other_tags, first_tag:gsub("\\fsp[%d%.%-]+",string.format("\\fsp%.2f",total_space))..string.rep(" ",total_wsp)) end]]-- line.text=rebuilt_text:gsub("}{","") sub[li]=line end aegisub.set_undo_point(script_name) end rec:registerMacro(circle_text)