├── .gitattributes ├── .gitignore ├── DependencyControl.json ├── LICENSE ├── README.mediawiki ├── macros ├── lyger.BorderSplit.lua ├── lyger.CircleText.lua ├── lyger.ClipBlur.lua ├── lyger.ClipGrad.lua ├── lyger.ClipShifter.lua ├── lyger.FbfTransform.moon ├── lyger.GradientByChar.lua ├── lyger.GradientEverything.moon ├── lyger.Image2ASS.lua ├── lyger.KaraHelper.lua ├── lyger.KaraReplacer.lua ├── lyger.LayerIncrement.lua ├── lyger.LuaInterpret.lua ├── lyger.ModifyMocha.lua ├── lyger.MoveClip.lua ├── lyger.SemiColorCalc.lua ├── lyger.TemplateManager.lua ├── lyger.VecClipGradient.lua └── split-tags.lua └── modules └── LibLyger.moon /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 lyger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.mediawiki: -------------------------------------------------------------------------------- 1 | == Introduction == 2 | 3 | This is the page where I'll be uploading and updating the various automation scripts I write for Aegisub. These are primarily typesetting-oriented, but there are a few scripts related to karaoke timing, and I might write editing- or QC-related scripts in the future. 4 | 5 | I take requests (if you can convince me you have a task that would be useful automated). Also, if you find a bug in any of these scripts, by all means tell me so I can fix it. You can find me as lyger on irc.rizon.net. 6 | 7 | == Required Modules == 8 | 9 | === DependencyControl === 10 | My scripts require DependencyControl (https://github.com/TypesettingCartel/DependencyControl) for versioning and dependency management. Update feeds are provided, so all scripts can be installed using the DependencyControl Toolbox and will automatically update whenever new versions are released. 11 | 12 | === LibLyger === 13 | This module is required for most of my advanced scripts. I noticed I was reusing a lot of code in multiple automations, and it was getting to be a pain to update the same function in every script whenever somebody found a bug. From now on, functions that I frequently use will be collected in this library, so that I only need to update one file when somebody finds a bug in one of the functions. 14 | 15 | == Typesetting Scripts == 16 | 17 | === BorderSplit === 18 | 19 | The first and probably the most useful automation script I wrote. Now that I have a lot more experience writing these automations, I realize how messy the code is, but it works and is remarkably bug-free. 20 | 21 | This takes a single bordered line and splits it into two lines. For a single bordered line, applying a \blur tag will only blur the outer edge. This automatically splits the line in two so that the \blur will apply to both edges. There are a handful of other options that let you customize the behavior of the automation. Read the readme at the top of the script for full instructions. 22 | 23 | Compared to my other scripts, the behavior is a bit odd. Instead of applying to the selected lines, the script applies to lines with "bord" in the actor field. This is mostly an artifact of my days working with karaoke templates, but I honestly rather like the workflow. 24 | 25 | === TemplateManager === 26 | 27 | This one gets a spot up here because of its usefulness. This automation is built around typesetting efficiency. The idea is you do all the hard work once, and everything afterwards becomes super easy. 28 | 29 | This is actually a macro that creates other macros which will be added to the automation menu. The main "Template manager" automation doesn't change anything about your subtitles. Instead, it lets you create and modify templates for repeated typesets. The templates are very customizable and allow variables and simple arithmetic. 30 | 31 | Once you've finished creating and modifying your template group, reload your automations and the newly created macro will appear on the automations menu, allowing you to hotkey it. Most aspects of the script remember your last action. 32 | 33 | === FbfTransform === 34 | 35 | The first "advanced" automation I wrote since writing border-split. This script is very powerful and versatile, but it expects a certain amount out of the typesetter. Read the instructions at the top of the script file for full details. 36 | 37 | This allows you to animate nearly all of the parameters of a typeset frame-by-frame. You start with an existing frame-by-frame typeset (possibly generated using Mocha data, or just by pressing ctrl-d a lot). Then you visually typeset "keyframes" until they look the way you want them to. This script will fill in all the frames in between the keyframes so that they transform smoothly. 38 | 39 | Note that as a typesetter, you will need to typeset the keyframes by hand. In essence, instead of punching in all your options into a popup window, you can work on the appearance of the line the way you normally typeset. I personally find this to be more intuitive, but again, it requires more work out of the typesetter than a simple click-on-buttons interface. 40 | 41 | Also note that there is no requirement that the lines you select actually represent a frame-by-frame typeset that is sorted by time. This script transforms parameters across the lines you select; that's all. How you use it is up to you. 42 | 43 | === GradientEverything === 44 | 45 | This was written based on the code of FbfTransform. Instead of creating frame-by-frame animations, GradientEveryting will "gradient" the parameters. Put another way, FbfTransform will transform parameters in time, while GradientEveryting can transform parameters in space. 46 | 47 | Again, you define "key" lines, but unlike FbfTransform you don't need to have all the lines in between already; the script will generate them for you. These "key" lines will be smoothly merged together by the script, based on the options you enter from the popup menu. 48 | 49 | The Bounding box for the gradient will be determined using SubInspector when the module is available on your system. If it isn't, you must provide it as a rectangular clip (on any of the input lines). 50 | 51 | ==== ClipShifter ==== 52 | 53 | This is primarily a helper script to go with GradientEveryting. If you want to apply GradientEveryting to many lines without having to redraw the bounding box each time, ClipShifter will move and add the bounding box for you. 54 | 55 | === GradientByChar === 56 | 57 | A by-character version of GradientEveryting. It has the most of the features of GradientEveryting, but its resolution is only by-character, and for obvious reasons vertical gradients are not possible. 58 | 59 | That being said, I will recommend that typesetters use this script instead of GradientEveryting in the vast majority of cases, simply because GradientEveryting will generate dozens of lines and result in lots of lag if you're not careful. In most cases, gradient-by-char will be sufficient. Not to mention, working with a single-line typeset is usually easier. 60 | 61 | This automation has no popup interface (unlike GradientEveryting). It's locked to transform any different parameters that it detects. It's also locked to "rotations less than 180 degrees" mode (you can read more about this in the readme at the top of the script). 62 | 63 | === LuaInterpret === 64 | 65 | It seems I never wrote a description for this, so here goes. 66 | 67 | This was one of my most ambitious projects, and despite a plethora of bugs and user-unfriendliness, today it's one of my most-used scripts. I noticed I was repeating a lot of code in my automations, and many tasks kept popping up over and over again, so I thought to myself, "Why don't I create a script that does all the grunt work for you, so you only have to code the important part?" 68 | 69 | Well, it sort of worked. In most cases nowadays, when a typesetter asks me for a specific task automated, I'm able to accomplish it in under fifty lines using LuaInterpret. If you ever come to me with a request for automation, I'll either direct you to existing scripts that can do what you want, or give you some code to use with this script. 70 | 71 | It's also a super handy tool for adjusting massive multi-line typesets or doing arithmetic on tag parameters. Need to double the font size? mod("fs",mul(2)). Add 5 degrees to all the rotations in a line? mod("frz",add(5)). One line of code will apply to all the tags in all the selected lines. 72 | 73 | === ClipBlur === 74 | 75 | The \clip tag naturally generates a very sharp edge. For a typesetter who wants his signs to blend in, that sharp edge can stick out like a sore thumb. 76 | 77 | This script serves the much-needed function of blurring the edge of a \clip by duplicating the line, drawing more \clips, and decreasing the alpha. Due to the nature of \clip, the appearance will not always be perfect, even if all the numbers match. Nonetheless, it's better than nothing. 78 | 79 | The "precision" option can improve the appearance, but note that it scales exponentially. A precision of 4 generates 8 times as many lines as a precision of 1. 80 | 81 | === ClipGrad === 82 | 83 | This script is not as versatile as I'd have liked, due to limitations caused by the way anti-aliasing works. Nonetheless, it should be useful in plenty of situations. 84 | 85 | This works the same as ClipBlur, but instead of blurring the edge of the \clip, it gradients from one color to another (unlike GradientEveryting, it does not gradient any other parameters. Sorry). To correct for anti-aliasing artifacts, there is a forced one-pixel overlap for each stripe of the gradient. As a result, this script does not work on semitransparent typesets at all. 86 | 87 | Nonetheless, it provides the ability to create a gradient of more or less any arbitrary shape. 88 | 89 | === VecClipGradient === 90 | 91 | Intersects a lines-only vector clip with a rectangular clip. Less awesome than it sounds, since I found out after writing this script that you can have two \clip tags in the same line, so long as one is vector and one is rectangular. Welp. 92 | 93 | === Image2ASS === 94 | 95 | This script started as a standalone Lua script that I eventually wrapped in an Aegisub macro to make it easier to use. It's probably the worst lag machine in this entire repo, though it can also be an incredibly powerful tool. 96 | 97 | It basically does what the title says. Input a 24-bit or 32-bit Windows-format bitmap image, and this macro will convert it pixel-by-pixel into .ass drawings. Also supports alpha masks, which are black-and-white bitmaps loaded separately. 98 | 99 | To mitigate the obvious lag implications of this, a basic color similarity compression is run to merge adjacent pixels of similar color, and the user is notified after running the script of the approximate amount of data added, so they can adjust the compression factor accordingly. To get an idea of how different compression levels look, see [http://i.imgur.com/ol6tfZu.png this comparison]. 100 | 101 | It's worth noting that no compression is performed on the alpha mask. It's the user's responsibility to avoid needless alpha variations in their mask. 102 | 103 | The latest version supports non-bitmap images, which are converted to bitmap automatically with the convert tool from ImageMagick. Download the executable [http://www.mediafire.com/download/zdxn75nte1n6cq6/convert.exe here] and save it to your automation\autoload directory. 104 | 105 | === ModifyMocha === 106 | 107 | I wrote this script when I had to deal with a Mocha-tracked typeset that appeared in a flashback in a later episode. Obviously, I didn't want to re-typeset the sign, and instead copy-pasted the sign from the earlier episode. Unfortunately, the colors were all grayed out in the flashback, and the sign itself was a couple layers, with borders and shadows and whatnot. Fixing all those colors would have been a find-and-replace nightmare. 108 | 109 | This automation allows you to duplicate the first frame of the typeset, modify it until its appearance matches the new appearance that you want, and automatically apply these changes to the rest of the lines. Detailed instructions are at the top of the script. 110 | 111 | === split-tags.lua === 112 | 113 | Some effects only work if each section of a typeset is split onto a different line. For example, if you want the letters to zoom in one-by-one. But once you've split all the letters onto different lines, positioning them correctly is a huge hassle. 114 | 115 | This will split a single line into multiple lines while preserving the appearance of the original single-line typeset. In theory, after running this, your typeset will look exactly the same as it did before, but on multiple lines. The script splits every time it sees an {override block}. You can put blank {} blocks to force a split, and it should still work. 116 | 117 | === CircleText === 118 | 119 | Puts text onto a circle, on a single line. This is accomplished using \fsp and \frz trickery. The curve of the circle is defined by placing the origin. If you select the z rotation tool in Aegisub, it will display a circle that is roughly the same as the circle your text will end up on after applying this automation. Detailed instructions are at the top of the script, but it's pretty straightforward. 120 | 121 | === LayerIncrement === 122 | 123 | Makes selected lines have layers that count up or down (e.g. 1,2,3,4,5...). The minimum layer number will be the highest layer within the selection. This is a predecessor to a possible GradientEveryting clone based around creating outline gradients. 124 | 125 | === MoveClip === 126 | 127 | Turns lines with \pos and a rectangular \clip into lines with \move and a \t transformed \clip, based on the user-input delta-x and delta-y values. This is primarily intended to allow rectangular-clipped gradients to move using \move statements. 128 | 129 | == Karaoke Scripts == 130 | 131 | === KaraHelper === 132 | 133 | Certain karaoke effects require blank syllables at the beginning and end of the k-timed line, in order to leave room for transition-in and transition-out effects. This automation primarily takes care of making those sorts of timing adjustments automatically. 134 | 135 | If any k-timers have tasks they'd like to see added to this script, just contact me. 136 | 137 | === KaraReplacer === 138 | 139 | Ever had to k-time a song and encountered a verse with the same tune and rhythm as a previous verse, but different lyrics? This automation allows you to reuse that timing, and quickly and efficiently replace the lyrics. Highlight the lines you wish to apply this to, run the automation, and click "help" for full instructions. 140 | -------------------------------------------------------------------------------- /macros/lyger.CircleText.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Circular Text 5 | 6 | Define an origin, and this will put the text on a circular arc centered on that origin. 7 | 8 | An origin must be defined and it must be different from the position of the line. You can't have 9 | a circle if the radius is zero. 10 | 11 | The x coordinate of the position tag should match the x coordinate of the origin tag for best 12 | results. In other words, your original line should be at a right angle to the radius. Note that 13 | these are the x coordinates in the tags, not the x coordinates on screen, which will change if 14 | you rotate the tag. 15 | 16 | Supports varied fonts, font sizes, font spacings, and x/y scales in the same line. 17 | 18 | The resulting arc will be centered on the original rotation of your line. 19 | 20 | Only works on static lines. If you want the line to move or rotate, use another macro. 21 | 22 | 23 | ]]-- 24 | 25 | script_name = "Circular text" 26 | script_description = "Puts the text on a circular arc centered on the origin." 27 | script_version = "0.2.1" 28 | script_author = "lyger" 29 | script_namespace = "lyger.CircleText" 30 | 31 | local DependencyControl = require("l0.DependencyControl") 32 | local rec = DependencyControl{ 33 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 34 | { 35 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 36 | "aegisub.util" 37 | } 38 | } 39 | local LibLyger, util = rec:requireModules() 40 | local libLyger = LibLyger() 41 | 42 | local utf8pattern = "[%z\1-\127\194-\244][\128-\191]*" 43 | 44 | --[[ 45 | Tags that can have any character after the tag declaration: 46 | \r 47 | \fn 48 | Otherwise, the first character after the tag declaration must be: 49 | a number, decimal point, open parentheses, minus sign, or ampersand 50 | ]]-- 51 | 52 | 53 | --Distance between two points 54 | local function distance(x1,y1,x2,y2) 55 | return math.sqrt((x2-x1)^2+(y2-y1)^2) 56 | end 57 | 58 | --Sign of a value 59 | local function sign(n) 60 | return n/math.abs(n) 61 | end 62 | 63 | --Angle in degrees, given the arc length and radius 64 | local function arc_angle(arc_length,radius) 65 | return arc_length/radius * 180/math.pi 66 | end 67 | 68 | --Main processing function 69 | function circle_text(sub,sel) 70 | libLyger:set_sub(sub, sel) 71 | for si,li in ipairs(sel) do 72 | --Progress report 73 | aegisub.progress.task("Processing line "..si.."/"..#sel) 74 | aegisub.progress.set(100*si/#sel) 75 | 76 | --Read in the line 77 | line = libLyger.lines[li] 78 | 79 | --Get position and origin 80 | px, py = libLyger:get_pos(line) 81 | ox, oy = libLyger:get_org(line) 82 | 83 | --Make sure pos and org are not the same 84 | if px==ox and py==oy then 85 | aegisub.log(1,"Error on line %d: Position and origin cannot be the same!",li) 86 | return 87 | end 88 | 89 | --Get radius 90 | radius=distance(px,py,ox,oy) 91 | 92 | --Remove \pos and \move 93 | --If your line was non-static, too bad 94 | line.text = LibLyger.line_exclude(line.text,{"pos","move"}) 95 | 96 | --Make sure line starts with a tag block 97 | if line.text:find("^{")==nil then 98 | line.text="{}"..line.text 99 | end 100 | 101 | --Rotation direction: positive if each character adds to the angle, 102 | --negative if each character subtracts from the angle 103 | rot_dir=sign(py-oy) 104 | 105 | --Add the \pos back with recalculated position 106 | line.text=line.text:gsub("^{",string.format("{\\pos(%d,%d)",ox,oy+rot_dir*radius)) 107 | 108 | --Get z rotation 109 | --Will only take the first one, because if you wanted the text to be on a circular arc, 110 | --why do you have more than one z rotation tag in the first place? 111 | _,_,zrot=line.text:find("\\frz([%-%.%d]+)") 112 | zrot=zrot or line.styleref.angle 113 | 114 | --Make line table 115 | line_table={} 116 | for thistag,thistext in line.text:gmatch("({[^{}]*})([^{}]*)") do 117 | table.insert(line_table,{tag=thistag,text=thistext}) 118 | end 119 | 120 | --Where data on the character widths will be stored 121 | char_data={} 122 | 123 | --Total width of line 124 | cum_width=0 125 | 126 | --Stores current state of the line as style table 127 | current_style = util.deep_copy(line.styleref) 128 | 129 | --First pass to collect data on character widths 130 | for i,val in ipairs(line_table) do 131 | 132 | char_data[i]={} 133 | 134 | --Fix style tables to reflect override tags 135 | local _,_,font_name=val.tag:find("\\fn([^\\{}]+)") 136 | local _,_,font_size=val.tag:find("\\fs([%-%.%d]+)") 137 | local _,_,font_scx=val.tag:find("\\fscx([%-%.%d]+)") 138 | local _,_,font_scy=val.tag:find("\\fscy([%-%.%d]+)") 139 | local _,_,font_sp=val.tag:find("\\fsp([%-%.%d]+)") 140 | local _,_,_bold=val.tag:find("\\b([01])") 141 | local _,_,_italic=val.tag:find("\\i([01])") 142 | 143 | current_style.fontname=font_name or current_style.fontname 144 | current_style.fontsize=tonumber(font_size) or current_style.fontsize 145 | current_style.scale_x=tonumber(font_scx) or current_style.scale_x 146 | current_style.scale_y=tonumber(font_scy) or current_style.scale_y 147 | current_style.spacing=tonumber(font_sp) or current_style.spacing 148 | if _bold~=nil then 149 | if _bold=="1" then current_style.bold=true 150 | else current_style.bold=false end 151 | end 152 | if _italic~=nil then 153 | if _italic=="1" then current_style.italic=true 154 | else current_style.italic=false end 155 | end 156 | 157 | val.style = util.deep_copy(current_style) 158 | 159 | --Collect width data on each char 160 | for thischar in val.text:gmatch(utf8pattern) do 161 | cwidth=aegisub.text_extents(val.style,thischar) 162 | table.insert(char_data[i],{char=thischar,width=cwidth}) 163 | end 164 | 165 | --Increment cumulative width 166 | cum_width=cum_width+aegisub.text_extents(val.style,val.text) 167 | 168 | end 169 | 170 | --The angle that the rotation will begin at 171 | start_angle=zrot-(rot_dir*arc_angle(cum_width,radius))/2 172 | 173 | rebuilt_text="" 174 | cum_rot=0 175 | 176 | --Second pass to rebuild line with new tags 177 | for i,val in ipairs(line_table) do 178 | 179 | rebuilt_text=rebuilt_text..val.tag:gsub("\\fsp[%-%.%d]+",""):gsub("\\frz[%-%.%d]+","") 180 | 181 | for k,tchar in ipairs(char_data[i]) do 182 | --Character spacing should be the average of this character's width and the next one's 183 | --For spacing, scale width back up by the character's relevant scale_x, 184 | --because \fsp scales too. Also, subtract the existing font spacing 185 | this_spacing=0 186 | this_width=0 187 | if k~=#char_data[i] then 188 | this_width=(tchar.width+char_data[i][k+1].width)/2 189 | this_spacing=-1*(this_width*100/val.style.scale_x-val.style.spacing) 190 | else 191 | this_width=i~=#line_table and (tchar.width+char_data[i+1][1].width)/2 or 0 192 | this_spacing=i~=#line_table 193 | and -1*((tchar.width*100/val.style.scale_x 194 | + char_data[i+1][1].width*100/line_table[i+1].style.scale_x)/2 195 | -val.style.spacing) 196 | or 0 197 | end 198 | 199 | rebuilt_text=rebuilt_text..string.format("{\\frz%.3f\\fsp%.2f}%s", 200 | (start_angle+rot_dir*cum_rot)%360,this_spacing,tchar.char) 201 | 202 | cum_rot=cum_rot+arc_angle(this_width,radius) 203 | end 204 | end 205 | 206 | --[[ 207 | --Fuck the re library. Maybe I'll come back to this 208 | whitespaces=re.find(rebuilt_text, 209 | '(\{\\\\frz[\\d\\.\\-]+\\\\fsp[\\d\\.\\-]+\}\\S)((?:\{\\\\frz[\\d\\.\\-]+\\\\fsp[\\d\\.\\-]+\}\\s)+)') 210 | 211 | for j=1,#whitespaces-1,2 do 212 | first_tag=whitespaces[j].str 213 | other_tags=whitespaces[j+1].str 214 | aegisub.log("%s%s\n",first_tag,other_tags) 215 | first_space=first_tag:match("\\fsp([%d%.%-]+)") 216 | other_spaces=0 217 | total_wsp=0 218 | for _sp in other_tags:gmatch("\\fsp([%d%.%-]+)") do 219 | other_spaces=other_spaces+tonumber(_sp) 220 | total_wsp=total_wsp+1 221 | end 222 | total_space=tonumber(first_space)+other_spaces 223 | rebuilt_text=rebuilt_text:gsub(first_tag..other_tags, 224 | first_tag:gsub("\\fsp[%d%.%-]+",string.format("\\fsp%.2f",total_space))..string.rep(" ",total_wsp)) 225 | end]]-- 226 | 227 | line.text=rebuilt_text:gsub("}{","") 228 | 229 | sub[li]=line 230 | 231 | end 232 | 233 | aegisub.set_undo_point(script_name) 234 | 235 | end 236 | 237 | rec:registerMacro(circle_text) 238 | -------------------------------------------------------------------------------- /macros/lyger.ClipBlur.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Blur clip 5 | 6 | There's really not much to explain here. \clip statements produce a sharp edge. This script 7 | draws new \clip statements with decreasing alphas in order to imitate the effect of a blur. 8 | 9 | The appearance won't always be perfect because of the limitations of precision with vector 10 | clip coordinates. The "precision" parameter ameliorates this somewhat, but the odd jagged 11 | line here and there is inevitable. 12 | 13 | A note on the "precision" parameter: it scales exponentionally. If you want a 5-pixel blur, 14 | then a precision of 1 produces 6 lines (5 for the blur, 1 for the center). Precision 2 will 15 | generate 11 lines (10 for the blur, 1 for the center) and precision 3 will generate 21 lines 16 | (20 for the blur, 1 for the center). As you've probably figured out, a precision of 4 will 17 | create a whopping 41 lines. Use with caution. 18 | 19 | 20 | ]]-- 21 | script_name = "Blur clip" 22 | script_description = "Blurs a vector clip." 23 | script_version = "1.2.0" 24 | script_author = "lyger" 25 | script_namespace = "lyger.ClipBlur" 26 | 27 | local DependencyControl = require("l0.DependencyControl") 28 | local rec = DependencyControl{ 29 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 30 | { 31 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 32 | "aegisub.util" 33 | } 34 | } 35 | local LibLyger, util = rec:requireModules() 36 | local libLyger = LibLyger() 37 | 38 | --Distance between two points 39 | local function distance(x1,y1,x2,y2) 40 | return math.sqrt((x2-x1)^2+(y2-y1)^2) 41 | end 42 | 43 | --Sign of a value 44 | local function sign(n) 45 | return n/math.abs(n) 46 | end 47 | 48 | --Haha I didn't know these functions existed. May as well just alias them 49 | local todegree=math.deg 50 | local torad=math.rad 51 | 52 | --Parses vector shape and makes it into a table 53 | function make_vector_table(vstring) 54 | local vtable={} 55 | local vexp=vstring:match("^([1-4]),") 56 | vexp=tonumber(vexp) or 1 57 | for vtype,vcoords in vstring:gmatch("([mlb])([%d%s%-]+)") do 58 | for vx,vy in vcoords:gmatch("([%d%-]+)%s+([%d%-]+)") do 59 | table.insert(vtable,{["class"]=vtype,["x"]=tonumber(vx),["y"]=tonumber(vy)}) 60 | end 61 | end 62 | return vtable,vexp 63 | end 64 | 65 | --Reverses a vector table object 66 | function reverse_vector_table(vtable) 67 | local nvtable={} 68 | if #vtable<1 then return nvtable end 69 | --Make sure vtable does not end in an m. I don't know why this would happen but still 70 | maxi=#vtable 71 | while vtable[maxi].class=="m" do 72 | maxi=maxi-1 73 | end 74 | 75 | --All vector shapes start with m 76 | nstart = util.copy(vtable[maxi]) 77 | tclass=nstart.class 78 | nstart.class="m" 79 | table.insert(nvtable,nstart) 80 | 81 | --Reinsert coords in backwards order, but shift the class over by 1 82 | --because that's how vector shapes behave in aegi 83 | for i=maxi-1,1,-1 do 84 | tcoord = util.copy(vtable[i]) 85 | _temp=tcoord.class 86 | tcoord.class=tclass 87 | tclass=_temp 88 | table.insert(nvtable,tcoord) 89 | end 90 | 91 | return nvtable 92 | end 93 | 94 | --Turns vector table into string 95 | function vtable_to_string(vt) 96 | cclass=nil 97 | result="" 98 | 99 | for i=1,#vt,1 do 100 | if vt[i].class~=cclass then 101 | result=result..string.format("%s %d %d ",vt[i].class,vt[i].x,vt[i].y) 102 | cclass=vt[i].class 103 | else 104 | result=result..string.format("%d %d ",vt[i].x,vt[i].y) 105 | end 106 | end 107 | 108 | return result 109 | end 110 | 111 | --Rounds to the given number of decimal places 112 | function round(n,dec) 113 | dec=dec or 0 114 | return math.floor(n*10^dec+0.5)/(10^dec) 115 | end 116 | 117 | --Grows vt outward by the radius r scaled by sc 118 | function grow(vt,r,sc) 119 | ch=get_chirality(vt) 120 | local wvt=wrap(vt) 121 | local nvt={} 122 | sc=sc or 1 123 | 124 | --Grow 125 | for i=2,#wvt-1,1 do 126 | cpt=wvt[i] 127 | ppt=wvt[i].prev 128 | npt=wvt[i].next 129 | while distance(cpt.x,cpt.y,ppt.x,ppt.y)==0 do 130 | ppt=ppt.prev 131 | end 132 | while distance(cpt.x,cpt.y,npt.x,npt.y)==0 do 133 | npt=npt.prev 134 | end 135 | rot1=todegree(math.atan2(cpt.y-ppt.y,cpt.x-ppt.x)) 136 | rot2=todegree(math.atan2(npt.y-cpt.y,npt.x-cpt.x)) 137 | drot=(rot2-rot1)%360 138 | 139 | --Angle to expand at 140 | nrot=(0.5*drot+90)%180 141 | if ch<0 then nrot=nrot+180 end 142 | 143 | --Adjusted radius 144 | __ar=math.cos(torad(ch*90-nrot)) --<3 145 | ar=(__ar<0.00001 and r) or r/math.abs(__ar) 146 | 147 | newx=cpt.x*sc 148 | newy=cpt.y*sc 149 | 150 | if r~=0 then 151 | newx=newx+sc*round(ar*math.cos(torad(nrot+rot1))) 152 | newy=newy+sc*round(ar*math.sin(torad(nrot+rot1))) 153 | end 154 | 155 | table.insert(nvt,{["class"]=cpt.class, 156 | ["x"]=newx, 157 | ["y"]=newy}) 158 | end 159 | 160 | --Check for "crossovers" 161 | --New data type to store points with same coordinates 162 | local mvt={} 163 | local wnvt=wrap(nvt) 164 | for i,p in ipairs(wnvt) do 165 | table.insert(mvt,{["class"]={p.class},["x"]=p.x,["y"]=p.y}) 166 | end 167 | 168 | --Number of merges so far 169 | merges=0 170 | 171 | for i=2,#wnvt,1 do 172 | mi=i-merges 173 | dx=wvt[i].x-wvt[i-1].x 174 | dy=wvt[i].y-wvt[i-1].y 175 | ndx=wnvt[i].x-wnvt[i-1].x 176 | ndy=wnvt[i].y-wnvt[i-1].y 177 | 178 | if (dy*ndy<0 or dx*ndx<0) then 179 | --Multiplicities 180 | c1=#mvt[mi-1].class 181 | c2=#mvt[mi].class 182 | 183 | --Weighted average 184 | mvt[mi-1].x=(c1*mvt[mi-1].x+c2*mvt[mi].x)/(c1+c2) 185 | mvt[mi-1].y=(c1*mvt[mi-1].y+c2*mvt[mi].y)/(c1+c2) 186 | 187 | --Merge classes 188 | mvt[mi-1].class={unpack(mvt[mi-1].class),unpack(mvt[mi].class)} 189 | 190 | --Delete point 191 | table.remove(mvt,mi) 192 | merges=merges+1 193 | end 194 | end 195 | 196 | --Rebuild wrapped new vector table 197 | wnvt={} 198 | for i,p in ipairs(mvt) do 199 | for k,pclass in ipairs(p.class) do 200 | table.insert(wnvt,{["class"]=pclass,["x"]=p.x,["y"]=p.y}) 201 | end 202 | end 203 | 204 | return unwrap(wnvt) 205 | end 206 | 207 | function merge_identical(vt) 208 | local mvt = util.copy(vt) 209 | i=2 210 | lx=mvt[1].x 211 | ly=mvt[1].y 212 | while i<#mvt do 213 | if mvt[i].x==lx and mvt[i].y==ly then 214 | table.remove(mvt,i) 215 | else 216 | lx=mvt[i].x 217 | ly=mvt[i].y 218 | i=i+1 219 | end 220 | end 221 | return mvt 222 | end 223 | 224 | --Returns chirality of vector shape. +1 if counterclockwise, -1 if clockwise 225 | function get_chirality(vt) 226 | local wvt=wrap(vt) 227 | wvt=merge_identical(wvt) 228 | trot=0 229 | for i=2,#wvt-1,1 do 230 | rot1=math.atan2(wvt[i].y-wvt[i-1].y,wvt[i].x-wvt[i-1].x) 231 | rot2=math.atan2(wvt[i+1].y-wvt[i].y,wvt[i+1].x-wvt[i].x) 232 | drot=todegree(rot2-rot1)%360 233 | if drot>180 then drot=360-drot elseif drot==180 then drot=0 else drot=-1*drot end 234 | trot=trot+drot 235 | end 236 | return sign(trot) 237 | end 238 | 239 | --Duplicates first and last coordinates at the end and beginning of shape, 240 | --to allow for wraparound calculations 241 | function wrap(vt) 242 | local wvt={} 243 | table.insert(wvt,util.copy(vt[#vt])) 244 | for i=1,#vt,1 do 245 | table.insert(wvt,util.copy(vt[i])) 246 | end 247 | table.insert(wvt,util.copy(vt[1])) 248 | 249 | --Add linked list capability. Because. Hacky fix gogogogo 250 | for i=2,#wvt-1 do 251 | wvt[i].prev=wvt[i-1] 252 | wvt[i].next=wvt[i+1] 253 | end 254 | --And link the start and end 255 | wvt[2].prev=wvt[#wvt-1] 256 | wvt[#wvt-1].next=wvt[2] 257 | 258 | return wvt 259 | end 260 | 261 | --Cuts off the first and last coordinates, to undo the effects of "wrap" 262 | function unwrap(wvt) 263 | local vt={} 264 | for i=2,#wvt-1,1 do 265 | table.insert(vt,util.copy(wvt[i])) 266 | end 267 | return vt 268 | end 269 | 270 | --Main execution function 271 | function blur_clip(sub,sel) 272 | --GUI config 273 | config= 274 | { 275 | { 276 | class="label", 277 | label="Blur size:", 278 | x=0,y=0,width=1,height=1 279 | }, 280 | { 281 | class="floatedit", 282 | name="bsize", 283 | min=0,step=0.5,value=1, 284 | x=1,y=0,width=1,height=1 285 | }, 286 | { 287 | class="label", 288 | label="Blur position:", 289 | x=0,y=1,width=1,height=1 290 | }, 291 | { 292 | class="dropdown", 293 | name="bpos", 294 | items={"outside","middle","inside"}, 295 | value="outside", 296 | x=1,y=1,width=1,height=1 297 | }, 298 | { 299 | class="label", 300 | label="Precision:", 301 | x=0,y=2,width=1,height=1 302 | }, 303 | { 304 | class="intedit", 305 | name="bprec", 306 | min=1,max=4,value=2, 307 | x=1,y=2,width=1,height=1 308 | } 309 | } 310 | 311 | --Show dialog 312 | pressed,results=aegisub.dialog.display(config,{"Go","Cancel"}) 313 | if pressed=="Cancel" then aegisub.cancel() end 314 | 315 | --Size of the blur 316 | bsize=results["bsize"] 317 | 318 | --Scale exponent for all the numbers 319 | sexp=results["bprec"] 320 | 321 | --How far to offset the blur by 322 | boffset=0 323 | if results["bpos"]=="inside" then boffset=bsize 324 | elseif results["bpos"]=="middle" then boffset=bsize/2 end 325 | 326 | --How far to offset the next line read 327 | lines_added=0 328 | 329 | libLyger:set_sub(sub, sel) 330 | for si,li in ipairs(sel) do 331 | --Progress report 332 | aegisub.progress.task("Processing line "..si.."/"..#sel) 333 | aegisub.progress.set(100*si/#sel) 334 | 335 | --Read in the line 336 | line = libLyger.lines[li] 337 | 338 | --Comment it out 339 | line.comment=true 340 | sub[li+lines_added]=line 341 | line.comment=false 342 | 343 | --Find the clipping shape 344 | ctype,tvector=line.text:match("\\(i?clip)%(([^%(%)]+)%)") 345 | 346 | --Cancel if it doesn't exist 347 | if tvector==nil then 348 | aegisub.log("Make sure all lines have a clip statement.") 349 | aegisub.cancel() 350 | end 351 | 352 | --Get position and add 353 | px,py = libLyger:get_pos(line) 354 | if line.text:match("\\pos")==nil and line.text:match("\\move")==nil then 355 | line.text=string.format("{\\pos(%d,%d)}",px,py)..line.text 356 | end 357 | 358 | --Round 359 | local function rnd(num) 360 | num=tonumber(num) or 0 361 | if num<0 then 362 | num=num-0.5 363 | return math.ceil(num) 364 | end 365 | num=num+0.5 366 | return math.floor(num) 367 | end 368 | --If it's a rectangular clip, convert to vector clip 369 | if tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)")~=nil then 370 | _x1,_y1,_x2,_y2=tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)") 371 | tvector=string.format("m %d %d l %d %d %d %d %d %d", 372 | rnd(_x1),rnd(_y1),rnd(_x2),rnd(_y1),rnd(_x2),rnd(_y2),rnd(_x1),rnd(_y2)) 373 | end 374 | 375 | --The original table and original scale exponent 376 | otable,oexp=make_vector_table(tvector) 377 | 378 | --Effective scale and scale exponent 379 | eexp=sexp-oexp+1 380 | escale=2^(eexp-1) 381 | --aegisub.log("Escale: %.2f",escale) 382 | 383 | --The innermost line 384 | iline = util.copy(line) 385 | itable={} 386 | if ctype=="iclip" then 387 | itable=grow(otable,bsize*2^(oexp-1)-boffset,escale) 388 | else 389 | itable=grow(otable,-1*boffset,escale) 390 | end 391 | iline.text=iline.text:gsub("\\i?clip%([^%(%)]+%)","\\"..ctype.."("..sexp..","..vtable_to_string(itable)..")") 392 | 393 | --Add it to the subs 394 | sub.insert(li+lines_added+1,iline) 395 | lines_added=lines_added+1 396 | 397 | --Set default alpha values 398 | dalpha={} 399 | dalpha[1]=alpha_from_style(line.styleref.color1) 400 | dalpha[2]=alpha_from_style(line.styleref.color2) 401 | dalpha[3]=alpha_from_style(line.styleref.color3) 402 | dalpha[4]=alpha_from_style(line.styleref.color4) 403 | 404 | --First tag block 405 | ftag=line.text:match("^{[^{}]*}") 406 | if ftag==nil then 407 | ftag="{}" 408 | line.text="{}"..line.text 409 | end 410 | 411 | --List of alphas not yet accounted for in the first tag 412 | unacc={} 413 | 414 | if ftag:match("\\alpha")==nil then 415 | if ftag:match("\\1a")==nil then table.insert(unacc,1) end 416 | if ftag:match("\\2a")==nil then table.insert(unacc,2) end 417 | if ftag:match("\\3a")==nil then table.insert(unacc,3) end 418 | if ftag:match("\\4a")==nil then table.insert(unacc,4) end 419 | end 420 | 421 | --Add tags if any are unaccounted for 422 | if #unacc>0 then 423 | --If all the unaccounted-for alphas are equal, only add an "alpha" tag 424 | _tempa=dalpha[unacc[1]] 425 | _equal=true 426 | for _k,_a in ipairs(unacc) do 427 | if dalpha[_a]~=_tempa then _equal=false end 428 | end 429 | 430 | if _equal then line.text=line.text:gsub("^{","{\\alpha"..dalpha[unacc[1]]) 431 | else 432 | for _k,ui in ipairs(unacc) do 433 | line.text=line.text:gsub("^{","{\\"..ui.."a"..dalpha[ui]) 434 | end 435 | end 436 | end 437 | 438 | prevclip=itable 439 | 440 | for j=1,math.ceil(bsize*escale*2^(oexp-1)),1 do 441 | 442 | --Interpolation factor 443 | factor=j/(bsize*escale+1) 444 | 445 | --Flip if it's an iclip 446 | if ctype=="iclip" then factor=1-factor end 447 | 448 | --Copy the line 449 | tline = util.copy(line) 450 | 451 | --Sub in the interpolated alphas 452 | tline.text=tline.text:gsub("\\alpha([^\\{}]+)", 453 | function(a) return "\\alpha"..interpolate_alpha(factor,a,"&HFF&") end) 454 | tline.text=tline.text:gsub("\\([1-4]a)([^\\{}]+)", 455 | function(a,b) return "\\"..a..interpolate_alpha(factor,b,"&HFF&") end) 456 | 457 | --Write the correct clip 458 | thisclip=grow(otable,j/escale-boffset,escale) 459 | clipstring=vtable_to_string(thisclip)..vtable_to_string(reverse_vector_table(prevclip)) 460 | prevclip=thisclip 461 | 462 | tline.text=tline.text:gsub("\\i?clip%([^%(%)]+%)","\\clip("..sexp..","..clipstring..")") 463 | 464 | --Insert the line 465 | sub.insert(li+lines_added+1,tline) 466 | lines_added=lines_added+1 467 | end 468 | end 469 | aegisub.set_undo_point(script_name) 470 | end 471 | 472 | rec:registerMacro(blur_clip) -------------------------------------------------------------------------------- /macros/lyger.ClipGrad.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Gradient along clip edge 5 | 6 | Expands a vector clip shape in order to create a freeform color gradient. You can use this to 7 | create diagonal gradients, zigzag gradients, or gradients in the shape of a curve. 8 | 9 | Use the vector clip tool to draw the shape of the gradient you want. If you only want one of 10 | the edges to have the gradient, make sure the other edges are placed with a wide margin around 11 | and enclosing your typeset. 12 | 13 | THIS SCRIPT ONLY WORKS ON SOLID ALPHA TYPESETS. That is, it will NOT work for any typesets that 14 | have any form of transparency. This is a consequence of the way anti-aliasing is rendered. 15 | 16 | Furthermore, although the interface provides options for all four colors, it's advised that 17 | you only gradient one color per layer of the typeset. If you want to gradient a bordered 18 | typeset, put the border on another, lower layer, and set the top layer border to zero. There's 19 | some odd quirk with the way vector clips are rendered that causes tiny stripes of the border to 20 | interfere with the gradient. The same goes for shadow. 21 | 22 | ]]-- 23 | 24 | script_name = "Gradient along clip edge" 25 | script_description = "Color gradient along clip edge. Solid alpha only." 26 | script_version = "0.2.0" 27 | script_author = "lyger" 28 | script_namespace = "lyger.ClipGrad" 29 | 30 | local DependencyControl = require("l0.DependencyControl") 31 | local rec = DependencyControl{ 32 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 33 | { 34 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 35 | "aegisub.util" 36 | } 37 | } 38 | local LibLyger, util = rec:requireModules() 39 | local libLyger = LibLyger() 40 | 41 | -- allow storing of data across multiple runs 42 | local gconfig 43 | 44 | --Distance between two points 45 | local function distance(x1,y1,x2,y2) 46 | return math.sqrt((x2-x1)^2+(y2-y1)^2) 47 | end 48 | 49 | --Sign of a value 50 | local function sign(n) 51 | return n/math.abs(n) 52 | end 53 | 54 | --Parses vector shape and makes it into a table 55 | local function make_vector_table(vstring) 56 | local vtable={} 57 | local vexp=vstring:match("^([1-4]),") 58 | vexp=tonumber(vexp) or 1 59 | for vtype,vcoords in vstring:gmatch("([mlb])([%d%s%-]+)") do 60 | for vx,vy in vcoords:gmatch("([%d%-]+)%s+([%d%-]+)") do 61 | table.insert(vtable,{["class"]=vtype,["x"]=tonumber(vx),["y"]=tonumber(vy)}) 62 | end 63 | end 64 | return vtable,vexp 65 | end 66 | 67 | --Reverses a vector table object 68 | local function reverse_vector_table(vtable) 69 | local nvtable={} 70 | if #vtable<1 then return nvtable end 71 | --Make sure vtable does not end in an m. I don't know why this would happen but still 72 | local maxi = #vtable 73 | while vtable[maxi].class=="m" do 74 | maxi=maxi-1 75 | end 76 | 77 | --All vector shapes start with m 78 | local nstart = util.copy(vtable[maxi]) 79 | local tclass = nstart.class 80 | nstart.class="m" 81 | table.insert(nvtable,nstart) 82 | 83 | --Reinsert coords in backwards order, but shift the class over by 1 84 | --because that's how vector shapes behave in aegi 85 | for i=maxi-1,1,-1 do 86 | local tcoord = util.copy(vtable[i]) 87 | tclass, tcoord.class = tcoord.class, tclass 88 | table.insert(nvtable,tcoord) 89 | end 90 | 91 | return nvtable 92 | end 93 | 94 | --Turns vector table into string 95 | function vtable_to_string(vt) 96 | local result, cclass = {} 97 | 98 | for i=1,#vt,1 do 99 | if vt[i].class~=cclass then 100 | result[i] = string.format("%s %d %d ",vt[i].class,vt[i].x,vt[i].y) 101 | cclass=vt[i].class 102 | else 103 | result[i] = string.format("%d %d ",vt[i].x,vt[i].y) 104 | end 105 | end 106 | 107 | return table.concat(result) 108 | end 109 | 110 | --Rounds to the given number of decimal places 111 | function round(n,dec) 112 | dec=dec or 0 113 | return math.floor(n*10^dec+0.5)/(10^dec) 114 | end 115 | 116 | --Grows vt outward by the radius r scaled by sc 117 | function grow(vt,r,sc) 118 | local ch = get_chirality(vt) 119 | local wvt, nvt = wrap(vt), {} 120 | sc=sc or 1 121 | 122 | --Grow 123 | for i=2,#wvt-1,1 do 124 | local cpt, ppt, npt = wvt[i], wvt[i].prev, wvt[i].next 125 | while distance(cpt.x,cpt.y,ppt.x,ppt.y)==0 do 126 | ppt=ppt.prev 127 | end 128 | while distance(cpt.x,cpt.y,npt.x,npt.y)==0 do 129 | npt=npt.prev 130 | end 131 | local rot1 = math.deg(math.atan2(cpt.y-ppt.y,cpt.x-ppt.x)) 132 | local rot2 = math.deg(math.atan2(npt.y-cpt.y,npt.x-cpt.x)) 133 | local drot = (rot2-rot1)%360 134 | 135 | --Angle to expand at 136 | local nrot = (0.5*drot + 90) % 180 137 | if ch<0 then nrot=nrot+180 end 138 | 139 | --Adjusted radius 140 | local __ar = math.cos(math.rad(ch*90-nrot)) --<3 141 | local ar = (__ar<0.00001 and r) or r/math.abs(__ar) 142 | 143 | local newx, newy = cpt.x*sc, cpt.y*sc 144 | 145 | if r~=0 then 146 | newx=newx+sc*round(ar*math.cos(math.rad(nrot+rot1))) 147 | newy=newy+sc*round(ar*math.sin(math.rad(nrot+rot1))) 148 | end 149 | 150 | table.insert(nvt,{["class"]=cpt.class, 151 | ["x"]=newx, 152 | ["y"]=newy}) 153 | end 154 | 155 | --Check for "crossovers" 156 | --New data type to store points with same coordinates 157 | local mvt={} 158 | local wnvt=wrap(nvt) 159 | for i,p in ipairs(wnvt) do 160 | table.insert(mvt,{["class"]={p.class},["x"]=p.x,["y"]=p.y}) 161 | end 162 | 163 | --Number of merges so far 164 | local merges = 0 165 | 166 | for i=2,#wnvt,1 do 167 | local mi=i-merges 168 | local dx=wvt[i].x-wvt[i-1].x 169 | local dy=wvt[i].y-wvt[i-1].y 170 | local ndx=wnvt[i].x-wnvt[i-1].x 171 | local ndy=wnvt[i].y-wnvt[i-1].y 172 | 173 | if (dy*ndy<0 or dx*ndx<0) then 174 | --Multiplicities 175 | local c1, c2 = #mvt[mi-1].class, #mvt[mi].class 176 | 177 | --Weighted average 178 | mvt[mi-1].x=(c1*mvt[mi-1].x+c2*mvt[mi].x)/(c1+c2) 179 | mvt[mi-1].y=(c1*mvt[mi-1].y+c2*mvt[mi].y)/(c1+c2) 180 | 181 | --Merge classes 182 | mvt[mi-1].class={unpack(mvt[mi-1].class),unpack(mvt[mi].class)} 183 | 184 | --Delete point 185 | table.remove(mvt,mi) 186 | merges=merges+1 187 | end 188 | end 189 | 190 | --Rebuild wrapped new vector table 191 | wnvt = {} 192 | for i,p in ipairs(mvt) do 193 | for k,pclass in ipairs(p.class) do 194 | table.insert(wnvt,{["class"]=pclass,["x"]=p.x,["y"]=p.y}) 195 | end 196 | end 197 | 198 | return unwrap(wnvt) 199 | end 200 | 201 | function merge_identical(vt) 202 | local mvt=util.copy(vt) 203 | local i, lx, ly = 2, mvt[1].x, mvt[1].y 204 | 205 | while i<#mvt do 206 | if mvt[i].x==lx and mvt[i].y==ly then 207 | table.remove(mvt,i) 208 | else 209 | lx=mvt[i].x 210 | ly=mvt[i].y 211 | i=i+1 212 | end 213 | end 214 | return mvt 215 | end 216 | 217 | --Returns chirality of vector shape. +1 if counterclockwise, -1 if clockwise 218 | function get_chirality(vt) 219 | local wvt = merge_identical(wrap(vt)) 220 | local trot = 0 221 | for i=2,#wvt-1,1 do 222 | local rot1 = math.atan2(wvt[i].y-wvt[i-1].y,wvt[i].x-wvt[i-1].x) 223 | local rot2 = math.atan2(wvt[i+1].y-wvt[i].y,wvt[i+1].x-wvt[i].x) 224 | local drot = math.deg(rot2-rot1)%360 225 | if drot>180 then drot=360-drot elseif drot==180 then drot=0 else drot=-1*drot end 226 | trot = trot + drot 227 | end 228 | return sign(trot) 229 | end 230 | 231 | --Duplicates first and last coordinates at the end and beginning of shape, 232 | --to allow for wraparound calculations 233 | function wrap(vt) 234 | local wvt={} 235 | table.insert(wvt,util.copy(vt[#vt])) 236 | for i=1,#vt,1 do 237 | table.insert(wvt,util.copy(vt[i])) 238 | end 239 | table.insert(wvt,util.copy(vt[1])) 240 | 241 | --Add linked list capability. Because. Hacky fix gogogogo 242 | for i=2,#wvt-1 do 243 | wvt[i].prev=wvt[i-1] 244 | wvt[i].next=wvt[i+1] 245 | end 246 | --And link the start and end 247 | wvt[2].prev=wvt[#wvt-1] 248 | wvt[#wvt-1].next=wvt[2] 249 | 250 | return wvt 251 | end 252 | 253 | --Cuts off the first and last coordinates, to undo the effects of "wrap" 254 | function unwrap(wvt) 255 | local vt={} 256 | for i=2,#wvt-1,1 do 257 | table.insert(vt,util.copy(wvt[i])) 258 | end 259 | return vt 260 | end 261 | 262 | function get_color_dlg(line, which) 263 | local pattern = table.concat{"\\", tostring(which), which == 1 and "?" or "", "c(&H%x+&)"} 264 | local r, g, b = util.extract_color(line.text:match(pattern) or line.styleref["color"..tostring(which)]) 265 | return ("#%02X%02X%02X"):format(r, g, b) 266 | end 267 | 268 | --Main execution function 269 | function grad_clip(sub,sel) 270 | libLyger:set_sub(sub, sel) 271 | if not gconfig then 272 | --Reference line to grab default gradient colors from 273 | local refline = libLyger.lines[sel[1]] 274 | local refc1, refc2, refc3, refc4 = get_color_dlg(refline, 1), get_color_dlg(refline, 2), 275 | get_color_dlg(refline, 3), get_color_dlg(refline, 4) 276 | 277 | --GUI config 278 | gconfig = 279 | { 280 | { 281 | class="label", 282 | label="Gradient size:", 283 | x=0,y=0,width=2,height=1 284 | }, 285 | gsize= 286 | { 287 | class="floatedit", 288 | name="gsize", 289 | min=0,step=0.5,value=20, 290 | x=2,y=0,width=2,height=1 291 | }, 292 | { 293 | class="label", 294 | label="Gradient position:", 295 | x=0,y=1,width=2,height=1 296 | }, 297 | gpos= 298 | { 299 | class="dropdown", 300 | name="gpos", 301 | items={"outside","middle","inside"}, 302 | value="outside", 303 | x=2,y=1,width=2,height=1 304 | }, 305 | { 306 | class="label", 307 | label="Step size:", 308 | x=0,y=2,width=2,height=1 309 | }, 310 | gstep= 311 | { 312 | class="intedit", 313 | name="gstep", 314 | min=1,max=20,value=1, 315 | x=2,y=2,width=2,height=1 316 | }, 317 | { 318 | class="label", 319 | label="Color1", 320 | x=0,y=3,width=1,height=1 321 | }, 322 | { 323 | class="label", 324 | label="Color2", 325 | x=1,y=3,width=1,height=1 326 | }, 327 | { 328 | class="label", 329 | label="Color3", 330 | x=2,y=3,width=1,height=1 331 | }, 332 | { 333 | class="label", 334 | label="Color4", 335 | x=3,y=3,width=1,height=1 336 | }, 337 | c1_1= 338 | { 339 | class="color", 340 | name="c1_1", 341 | x=0,y=4,width=1,height=1, 342 | value=refc1 343 | }, 344 | c2_1= 345 | { 346 | class="color", 347 | name="c2_1", 348 | x=1,y=4,width=1,height=1, 349 | value=refc2 350 | }, 351 | c3_1= 352 | { 353 | class="color", 354 | name="c3_1", 355 | x=2,y=4,width=1,height=1, 356 | value=refc3 357 | }, 358 | c4_1= 359 | { 360 | class="color", 361 | name="c4_1", 362 | x=3,y=4,width=1,height=1, 363 | value=refc4 364 | }, 365 | c1_2= 366 | { 367 | class="color", 368 | name="c1_2", 369 | x=0,y=5,width=1,height=1, 370 | value=refc1 371 | }, 372 | c2_2= 373 | { 374 | class="color", 375 | name="c2_2", 376 | x=1,y=5,width=1,height=1, 377 | value=refc2 378 | }, 379 | c3_2= 380 | { 381 | class="color", 382 | name="c3_2", 383 | x=2,y=5,width=1,height=1, 384 | value=refc3 385 | }, 386 | c4_2= 387 | { 388 | class="color", 389 | name="c4_2", 390 | x=3,y=5,width=1,height=1, 391 | value=refc4 392 | } 393 | } 394 | 395 | end 396 | 397 | --Show dialog 398 | local pressed, results=aegisub.dialog.display(gconfig,{"Go","Cancel"}) 399 | if pressed~="Go" then aegisub.cancel() end 400 | 401 | --Size of the blur and step size 402 | local gsize, gstep = results["gsize"], results["gstep"] 403 | 404 | --Colors table 405 | local tcolors = {} 406 | if results["c1_1"]~=results["c1_2"] then 407 | table.insert(tcolors,{ 408 | ["idx"]=1, 409 | ["start"]=util.ass_color(util.extract_color(results["c1_1"])), 410 | ["end"]=util.ass_color(util.extract_color(results["c1_2"])) 411 | }) end 412 | if results["c2_1"]~=results["c2_2"] then 413 | table.insert(tcolors,{ 414 | ["idx"]=2, 415 | ["start"]=util.ass_color(util.extract_color(results["c2_1"])), 416 | ["end"]=util.ass_color(util.extract_color(results["c2_2"])) 417 | }) end 418 | if results["c3_1"]~=results["c3_2"] then 419 | table.insert(tcolors,{ 420 | ["idx"]=3, 421 | ["start"]=util.ass_color(util.extract_color(results["c3_1"])), 422 | ["end"]=util.ass_color(util.extract_color(results["c3_2"])) 423 | }) end 424 | if results["c4_1"]~=results["c4_2"] then 425 | table.insert(tcolors,{ 426 | ["idx"]=4, 427 | ["start"]=util.ass_color(util.extract_color(results["c4_1"])), 428 | ["end"]=util.ass_color(util.extract_color(results["c4_2"])) 429 | }) end 430 | 431 | --How far to offset the blur by 432 | local goffset = 0 433 | if results["gpos"]=="inside" then goffset=gsize 434 | elseif results["gpos"]=="middle" then goffset=gsize/2 end 435 | 436 | --How far to offset the next line read 437 | local lines_added = 0 438 | 439 | --Update config 440 | for gk,gv in pairs(results) do 441 | gconfig[gk].value=gv 442 | end 443 | 444 | for si,li in ipairs(sel) do 445 | 446 | --Progress report 447 | aegisub.progress.task("Processing line "..si.."/"..#sel) 448 | aegisub.progress.set(100*si/#sel) 449 | 450 | --Read in the line 451 | local line = libLyger.sub[li+lines_added] 452 | 453 | --Comment it out 454 | line.comment=true 455 | sub[li+lines_added]=line 456 | line.comment=false 457 | 458 | --Find the clipping shape 459 | local ctype, tvector=line.text:match("\\(i?clip)%(([^%(%)]+)%)") 460 | 461 | --Cancel if it doesn't exist 462 | if not tvector then 463 | aegisub.log("Make sure all lines have a clip statement.") 464 | aegisub.cancel() 465 | end 466 | 467 | --If it's a rectangular clip, convert to vector clip 468 | if tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)") then 469 | local x1, y1, x2, y2 = tvector:match("([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)") 470 | tvector = ("m %d %d l %d %d %d %d %d %d").format(x1, y1, x2, y1, x2, y2, x1, y2) 471 | end 472 | 473 | --The original table and original scale exponent 474 | local otable, oexp = make_vector_table(tvector) 475 | local oscale = 2^(oexp-1) 476 | 477 | --Add tag block if none exists 478 | if line.text:match("^{")==nil then line.text="{}"..line.text end 479 | 480 | --Get position and add 481 | local px, py = libLyger:get_pos(line) 482 | if line.text:match("\\pos")==nil and line.text:match("\\move")==nil then 483 | line.text=line.text:gsub("^{",string.format("{\\pos(%d,%d)",px,py)) 484 | end 485 | 486 | --The innermost line 487 | local iline, itable = util.copy(line) 488 | 489 | if ctype=="iclip" then 490 | itable = grow(otable, gsize-goffset-1, oscale) 491 | else 492 | itable = grow(otable, -1*goffset, oscale) 493 | end 494 | iline.text=iline.text:gsub("\\i?clip%([^%(%)]+%)","\\"..ctype.."("..oexp..","..vtable_to_string(itable)..")") 495 | 496 | --Add colors 497 | for _,val in pairs(tcolors) do 498 | if val.idx==1 then iline.text=iline.text:gsub("\\c&H%x+&","") end 499 | iline.text=iline.text:gsub("\\"..val.idx.."c&H%x+&","") 500 | iline.text=iline.text:gsub("^{","{\\"..val.idx.."c"..val.start) 501 | end 502 | 503 | --Add it to the subs 504 | sub.insert(li+lines_added+1,iline) 505 | lines_added = lines_added+1 506 | 507 | local prevclip = itable 508 | 509 | for j=1,math.ceil(gsize/gstep),1 do 510 | 511 | --Interpolation factor 512 | local factor = j/math.ceil(gsize/gstep+1) 513 | 514 | --Flip if it's an iclip 515 | if ctype=="iclip" then factor=1-factor end 516 | 517 | --Copy the line 518 | local tline = util.copy(line) 519 | 520 | --Add colors 521 | for _,val in pairs(tcolors) do 522 | if val.idx==1 then tline.text=tline.text:gsub("\\c&H%x+&","") end 523 | tline.text=tline.text:gsub("\\"..val.idx.."c&H%x+&","") 524 | tline.text=tline.text:gsub("^{", 525 | "{\\"..val.idx.."c"..util.interpolate_color(factor,val["start"],val["end"])) 526 | end 527 | 528 | --Write the correct clip 529 | local thisclip = grow(otable,(j*gstep1 and 74 | sub[sel[1]].text:find("\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)")~=nil 75 | end 76 | 77 | rec:registerMacro(clip_shift, validate_clip_shift) -------------------------------------------------------------------------------- /macros/lyger.FbfTransform.moon: -------------------------------------------------------------------------------- 1 | [[ 2 | ==README== 3 | 4 | Frame-by-Frame Transform Automation Script 5 | 6 | Smoothly transforms various parameters across multi-line, frame-by-frame typesets. 7 | 8 | Useful for adding smooth transitions to frame-by-frame typesets that cannot be tracked with mocha, 9 | or as a substitute for the stepped \t transforms generated by the Aegisub-Motion.lua script, which 10 | may cause more lag than hard-coded values. 11 | 12 | First generate the frame-by-frame typeset that you will be adding the effect to. Find the lines where 13 | you want the effect to begin and the effect to end, and visually typeset them until they look the way 14 | you want them to. 15 | 16 | These lines will act as "keyframes", and the automation will modify all the lines in between so that 17 | the appearance of the first line smoothly transitions into the appearance of the last line. Simply 18 | highlight the first line, the last line, and all the lines in between, and run the automation. 19 | 20 | It will only affect the tags that are checked in the popup menu when you run the automation. If you wish 21 | to save specific sets of parameters that you would like to run together, you can use the presets manager. 22 | For example, you can go to the presets manager, check all the color tags, and save a preset named "Colors". 23 | The next time you want to transform all the colors, just select "Colors" from the preset dropdown menu. 24 | The "All" preset is included by default and cannot be deleted. If you want a specific preset to be loaded 25 | when you start the script, name it "Default" when you define the preset. 26 | 27 | This may be obvious, but this automation only works on one layer or one component of a frame-by-frame 28 | typeset at a time. If you have a frame-by-frame typeset that has two lines per frame, which looks like: 29 | 30 | A1 31 | B1 32 | A2 33 | B2 34 | A3 35 | B3 36 | etc. 37 | 38 | Then this automation will not work. The lines must be organized as: 39 | 40 | A1 41 | A2 42 | A3 43 | etc. 44 | B1 45 | B2 46 | B3 47 | etc. 48 | 49 | And you would have to run the automation twice, once on A and once on B. Furthermore, the text of each 50 | line must be exactly the same once all tags are removed. You can have as many tag blocks as you want 51 | in whatever positions you want for the "keyframe" lines (the first and the last). But once the tags are 52 | taken out, the text of the lines must be identical, down to the last space. If you are using ctrl-D or 53 | copy-pasting, this should be a given, but it's worth a warning. 54 | 55 | The lines in between can have any tags you want in them. So long as the automation is not transforming 56 | those particular tags, they will be left untouched. If you need the typeset to suddenly turn huge for one 57 | frame, simply uncheck "fscx" and "fscy" when you run the automation, and the size of the line won't be 58 | touched. 59 | 60 | If you are transforming rotations, there is something to watch out for. If you want a line to start 61 | with \frz10 and rotate to \frz350, then with default options, the line will rotate 340 degrees around the 62 | circle until it gets to 350. You probably wanted it to rotate only 20 degrees, passing through 0. The 63 | solution is to check the "Rotate in shortest direction" checkbox from the popup window. This will cause 64 | the line to always pick the rotation direction that has a total rotation of less than 180 degrees. 65 | 66 | New feature: ignore text. Requires you to only have one tag block in each line, at the beginning. 67 | 68 | 69 | Comes with an extra automation "Remove tags" that utilizes functions that were written for the main 70 | automation. You can comment out (two dashes) the line at the bottom that adds this automation if you don't 71 | want it. 72 | 73 | 74 | TODO: 75 | Check that all lines text match 76 | iclip support 77 | 78 | ]] 79 | 80 | export script_name = "Frame-by-frame transform" 81 | export script_description = "Smoothly transforms between the first and last selected lines." 82 | export script_version = "2.0.1" 83 | export script_namespace = "lyger.FbfTransform" 84 | 85 | DependencyControl = require "l0.DependencyControl" 86 | rec = DependencyControl{ 87 | feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 88 | { 89 | {"lyger.LibLyger", version: "2.0.0", url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 90 | {"l0.Functional", version: "0.3.0", url: "https://github.com/TypesettingTools/ASSFoundation", 91 | feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"}, 92 | } 93 | } 94 | LibLyger, Functional = rec\requireModules! 95 | import list, math, string, table, unicode, util, re from Functional 96 | logger, libLyger = rec\getLogger!, LibLyger! 97 | 98 | -- tag list, grouped by dialog layout 99 | tags_grouped = { 100 | {"c", "2c", "3c", "4c"}, 101 | {"alpha", "1a", "2a", "3a", "4a"}, 102 | {"fscx", "fscy", "fax", "fay"}, 103 | {"frx", "fry", "frz"}, 104 | {"bord", "shad", "fs", "fsp"}, 105 | {"xbord", "ybord", "xshad", "yshad"}, 106 | {"blur", "be"}, 107 | {"pos", "org", "clip"} 108 | } 109 | tags_flat = list.join unpack tags_grouped 110 | 111 | -- default settings for every preset 112 | preset_defaults = { skiptext: false, flip_rot: false, accel: 1.0, 113 | tags: {tag, false for tag in *tags_flat } 114 | } 115 | 116 | -- the default preset must always be available and cannot be deleted 117 | config = rec\getConfigHandler { 118 | presets: { 119 | Default: {} 120 | "[Last Settings]": {description: "Repeats the last #{script_name} operation"} 121 | } 122 | startupPreset: "Default" 123 | } 124 | unless config\load! 125 | -- write example preset on first time load 126 | config.c.presets["All"] = tags: {tag, true for tag in *tags_flat} 127 | config\write! 128 | 129 | create_dialog = (preset) -> 130 | config\load! 131 | preset_names = [preset for preset, _ in pairs config.c.presets] 132 | table.sort preset_names 133 | dlg = { 134 | -- Flip rotation 135 | { name: "flip_rot", class: "checkbox", x: 0, y: 9, width: 3, height: 1, 136 | label: "Rotate in shortest direction", value: preset.c.flip_rot }, 137 | { name: "skiptext", class: "checkbox", x: 3, y: 9, width: 2, height: 1, 138 | label: "Ignore text", value: preset.c.skiptext }, 139 | -- Acceleration 140 | { class: "label", x: 0, y: 10, width: 2, height: 1, 141 | label: "Acceleration: ", }, 142 | { name: "accel", class:"floatedit", x: 2, y: 10, width: 3, height: 1, 143 | value: preset.c.accel, hint: "1 means no acceleration, >1 starts slow and ends fast, <1 starts fast and ends slow" }, 144 | { class: "label", x: 0, y: 11, width: 2, height: 1, 145 | label: "Preset: " }, 146 | { name: "preset_select", class: "dropdown", x: 2, y: 11, width: 2, height: 1, 147 | items: preset_names, value: preset.section[#preset.section] }, 148 | { name: "preset_modify", class: "dropdown", x: 4, y: 11, width: 2, height: 1, 149 | items: {"Load", "Save", "Delete", "Rename"}, value: "Load" } 150 | } 151 | 152 | -- generate tag checkboxes 153 | for y, group in ipairs tags_grouped 154 | dlg[#dlg+1] = { name: tag, class: "checkbox", x: x-1, y: y, width: 1, height: 1, 155 | label: "\\#{tag}", value: preset.c.tags[tag] } for x, tag in ipairs group 156 | 157 | btn, res = aegisub.dialog.display dlg, {"OK", "Cancel", "Mod Preset", "Create Preset"} 158 | return btn, res, preset 159 | 160 | save_preset = (preset, res) -> 161 | preset\import res, nil, true 162 | if res.__class != DependencyControl.ConfigHandler 163 | preset.c.tags[k] = res[k] for k in *tags_flat 164 | preset\write! 165 | 166 | create_preset = (settings, name) -> 167 | msg = if not name 168 | "Onii-chan, what name would you like your preset to listen to?" 169 | elseif name == "" 170 | "Onii-chan, did you forget to name the preset?" 171 | elseif config.c.presets[name] 172 | "Onii-chan, it's not good to name a preset the same thing as another one~" 173 | 174 | if msg 175 | btn, res = aegisub.dialog.display { 176 | { class: "label", x: 0, y: 0, width: 2, height: 1, label: msg } 177 | { class: "label", x: 0, y: 1, width: 1, height: 1, label: "Preset Name: " }, 178 | { class: "edit", x: 1, y: 1, width: 1, height: 1, name: "name", text: name } 179 | } 180 | return btn and create_preset settings, res.name 181 | 182 | preset = config\getSectionHandler {"presets", name}, preset_defaults 183 | save_preset preset, settings 184 | return name 185 | 186 | prepare_line = (i, preset) -> 187 | line = libLyger.lines[i] 188 | 189 | -- Figure out the correct position and origin values 190 | posx, posy = libLyger\get_pos line 191 | orgx, orgy = libLyger\get_org line 192 | 193 | -- Look for clips 194 | clip = {line.text\match "\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)"} 195 | 196 | -- Make sure each line starts with tags 197 | line.text = "{}#{line.text}" unless line.text\find "^{" 198 | -- Turn all \1c tags into \c tags, just for convenience 199 | line.text = line.text\gsub "\\1c", "\\c" 200 | 201 | --Separate line into a table of tags and text 202 | line_table = if preset.c.skiptext 203 | while not line.text\match "^{[^}]+}[^{]" 204 | line.text = line.text\gsub "}{", "", 1 205 | tag, text = line.text\match "^({[^}]+})(.+)$" 206 | {{:tag, :text}} 207 | else [{:tag, :text} for tag, text in line.text\gmatch "({[^}]*})([^{]*)"] 208 | 209 | return line, line_table, posx, posy, orgx, orgy, #clip > 0 and clip 210 | 211 | --The main body of code that runs the frame transform 212 | frame_transform = (sub, sel, res) -> 213 | -- save last settings 214 | preset = config\getSectionHandler {"presets", "[Last Settings]"}, preset_defaults 215 | save_preset preset, res 216 | 217 | libLyger\set_sub sub 218 | -- Set the first and last lines in the selection 219 | first_line, start_table, sposx, sposy, sorgx, sorgy, sclip = prepare_line sel[1], preset 220 | last_line, end_table, eposx, eposy, eorgx, eorgy, eclip = prepare_line sel[#sel], preset 221 | 222 | -- If either the first or last line do not contain a rectangular clip, 223 | -- you will not be clipping today 224 | preset.c.tags.clip = false unless sclip and eclip 225 | -- These are the tags to transform 226 | transform_tags = [tag for tag in *tags_flat when preset.c.tags[tag]] 227 | 228 | -- Make sure both lines have the same splits 229 | LibLyger.match_splits start_table, end_table 230 | 231 | -- Tables that store tables for each tag block, consisting of the state of all relevant tags 232 | -- that are in the transform_tags table 233 | start_state_table = LibLyger.make_state_table start_table, transform_tags 234 | end_state_table = LibLyger.make_state_table end_table, transform_tags 235 | 236 | -- Insert default values when not included for the state of each tag block, 237 | -- or inherit values from previous tag block 238 | start_style = libLyger\style_lookup first_line 239 | end_style = libLyger\style_lookup last_line 240 | 241 | current_end_state, current_start_state = {}, {} 242 | 243 | for k, sval in ipairs start_state_table 244 | -- build current state tables 245 | for skey, sparam in pairs sval 246 | current_start_state[skey] = sparam 247 | 248 | for ekey, eparam in pairs end_state_table[k] 249 | current_end_state[ekey] = eparam 250 | 251 | -- check if end is missing any tags that start has 252 | for skey, sparam in pairs sval 253 | end_state_table[k][skey] or= current_end_state[skey] or end_style[skey] 254 | 255 | -- check if start is missing any tags that end has 256 | for ekey, eparam in pairs end_state_table[ k] 257 | start_state_table[k][ekey] or= current_start_state[ekey] or start_style[ekey] 258 | 259 | -- Insert proper state into each intervening line 260 | for i = 2, #sel-1 261 | aegisub.progress.set 100 * (i-1) / (#sel-1) 262 | this_line = libLyger.lines[sel[i]] 263 | 264 | -- Turn all \1c tags into \c tags, just for convenience 265 | this_line.text = this_line.text\gsub "\\1c","\\c" 266 | 267 | -- Remove all the relevant tags so they can be replaced with their proper interpolated values 268 | this_line.text = LibLyger.time_exclude this_line.text, transform_tags 269 | this_line.text = LibLyger.line_exclude this_line.text, transform_tags 270 | this_line.text = this_line.text\gsub "{}","" 271 | 272 | -- Make sure this line starts with tags 273 | this_line.text = "{}#{this_line.text}" unless this_line.text\find "^{" 274 | 275 | -- The interpolation factor for this particular line 276 | factor = (i-1)^preset.c.accel / (#sel-1)^preset.c.accel 277 | 278 | -- Handle pos transform 279 | if preset.c.tags.pos then 280 | x = LibLyger.float2str util.interpolate factor, sposx, eposx 281 | y = LibLyger.float2str util.interpolate factor, sposy, eposy 282 | this_line.text = this_line.text\gsub "^{", "{\\pos(#{x},#{y})" 283 | 284 | -- Handle org transform 285 | if preset.c.tags.org then 286 | x = LibLyger.float2str util.interpolate factor, sorgx, eorgx 287 | y = LibLyger.float2str util.interpolate factor, sorgy, eorgy 288 | this_line.text = this_line.text\gsub "^{", "{\\org(#{x},#{y})" 289 | 290 | -- Handle clip transform 291 | if preset.c.tags.clip then 292 | clip = [util.interpolate factor, ord, eclip[i] for i, ord in ipairs sclip] 293 | logger\dump{clip, sclip, eclip} 294 | this_line.text = this_line.text\gsub "^{", "{\\clip(%d,%d,%d,%d)"\format unpack clip 295 | 296 | -- Break the line into a table 297 | local this_table 298 | if preset.c.skiptext 299 | while not this_line.text\match "^{[^}]+}[^{]" 300 | this_line.text = this_line.text\gsub "}{", "", 1 301 | tag, text = this_line.text\match "^({[^}]+})(.+)$" 302 | this_table = {{:tag, :text}} 303 | else 304 | this_table = [{:tag, :text} for tag, text in this_line.text\gmatch "({[^}]*})([^{]*)"] 305 | -- Make sure it has the same splits 306 | j = 1 307 | while j <= #start_table 308 | stext = start_table[j].text 309 | ttext, ttag = this_table[j].text, this_table[j].tag 310 | 311 | -- ttext might contain miscellaneous tags that are not being checked for, 312 | -- so remove them temporarily 313 | ttext_temp = ttext\gsub "{[^{}]*}", "" 314 | 315 | -- If this table item has longer text, break it in two based on 316 | -- the text of the start table 317 | if #ttext_temp > #stext 318 | newtext = ttext_temp\match "#{LibLyger.esc stext}(.*)" 319 | for i = #this_table, j+1,-1 320 | this_table[i+1] = this_table[i] 321 | 322 | this_table[j] = tag: ttag, text: ttext\gsub "#{LibLyger.esc newtext}$","" 323 | this_table[j+1] = tag: "{}", text: newtext 324 | 325 | -- If the start table has longer text, then perhaps ttext was split 326 | -- at a tag that's not being transformed 327 | if #ttext < #stext 328 | -- It should be impossible for this to happen at the end, but check anyway 329 | assert this_table[j+1], "You fucked up big time somewhere. Sorry." 330 | 331 | this_table[j].text = table.concat {ttext, this_table[j+1].tag, this_table[j+1].text} 332 | if this_table[j+2] 333 | this_table[i] = this_table[i+1] for i = j+1, #this_table-1 334 | 335 | this_table[#this_table] = nil 336 | j -= 1 337 | 338 | j += 1 339 | 340 | --Interpolate all the relevant parameters and insert 341 | this_line.text = LibLyger.interpolate this_table, start_state_table, end_state_table, 342 | factor, preset 343 | sub[sel[i]] = this_line 344 | 345 | 346 | validate_fbf = (sub, sel) -> #sel >= 3 347 | 348 | load_tags_remove = (sub, sel) -> 349 | pressed, res = aegisub.dialog.display { 350 | { class: "label", label: "Enter the tags you would like to remove: ", 351 | x: 0, y: 0, width: 1,height: 1 }, 352 | { class: "textbox", name: "tag_list", text: "", 353 | x: 0, y: 1,width: 1, height: 1 }, 354 | { class: "checkbox", label: "Remove all EXCEPT", name: "do_except", value: false, 355 | x: 0,y: 2, width: 1, height: 1 } 356 | }, {"Remove","Cancel"}, {ok: "Remove", cancel: "Cancel"} 357 | 358 | return if pressed == "Cancel" 359 | 360 | tag_list = [tag for tag in res.tag_list\gmatch "\\?(%w+)[%s\\n,;]*"] 361 | 362 | --Remove or remove except the tags in the table 363 | for li in *sel 364 | line = sub[li] 365 | f = res.do_except and LibLyger.line_exclude_except or LibLyger.line_exclude 366 | line.text = f(line.text, tag_list)\gsub "{}", "" 367 | sub[li] = line 368 | 369 | fbf_gui = (sub, sel, _, preset_name = config.c.startupPreset) -> 370 | preset = config\getSectionHandler {"presets", preset_name}, preset_defaults 371 | btn, res = create_dialog preset 372 | 373 | switch btn 374 | when "OK" do frame_transform sub, sel, res 375 | when "Create Preset" do fbf_gui sub, sel, nil, create_preset res 376 | when "Mod Preset" 377 | if preset_name != res.preset_select 378 | preset = config\getSectionHandler {"presets", res.preset_select}, preset_defaults 379 | preset_name = res.preset_select 380 | 381 | switch res.preset_modify 382 | when "Delete" 383 | preset\delete! 384 | preset_name = nil 385 | when "Save" do save_preset preset, res 386 | when "Rename" 387 | preset_name = create_preset preset.userConfig, preset_name 388 | preset\delete! 389 | fbf_gui sub, sel, nil, preset_name 390 | 391 | -- register macros 392 | rec\registerMacros { 393 | {script_name, nil, fbf_gui, validate_fbf}, 394 | {"Remove tags", "Remove or remove all except the input tags.", load_tags_remove} 395 | } 396 | for name, preset in pairs config.c.presets 397 | f = (sub, sel) -> frame_transform sub, sel, config\getSectionHandler {"presets", name} 398 | rec\registerMacro "Presets/#{name}", preset.description, f, validate_fbf, nil, true -------------------------------------------------------------------------------- /macros/lyger.GradientByChar.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | No GUI, pretty straightforward. 5 | 6 | For example, to make a line bend in an arc and transition from blue to red, do this: 7 | 8 | {\frz10\c&HFF0000&}This is a line of tex{\frz350\c&H0000FF&}t 9 | 10 | Run the automation and it'll add a tag before each character to make the rotation and color 11 | transition change smoothly across the line. 12 | 13 | Rotations are locked to less than 180 degree rotations. If you want a bend of more than 180, 14 | then split it up into multiple rotations of less than 180 each. This script is meant for 15 | convenience above all, so it runs with a single button press and no time-consuming options menu. 16 | 17 | ]]-- 18 | 19 | script_name = "Gradient by character" 20 | script_description = "Smoothly transforms tags across your line, by character." 21 | script_version = "1.3.1" 22 | script_author = "lyger" 23 | script_namespace = "lyger.GradientByChar" 24 | 25 | local DependencyControl = require("l0.DependencyControl") 26 | local rec = DependencyControl{ 27 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 28 | { 29 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 30 | "aegisub.util", "aegisub.re" 31 | } 32 | } 33 | local LibLyger, util, re = rec:requireModules() 34 | local libLyger = LibLyger() 35 | 36 | function grad_char(sub,sel) 37 | libLyger:set_sub(sub, sel) 38 | 39 | for si,li in ipairs(sel) do 40 | --Read in the line 41 | this_line=libLyger.lines[li] 42 | 43 | --Make sure line starts with tags 44 | if this_line.text:find("^{")==nil then this_line.text="{}"..this_line.text end 45 | 46 | --Turn all \1c tags into \c tags, just for convenience 47 | this_line.text=this_line.text:gsub("\\1c","\\c") 48 | 49 | --Make line table 50 | this_table={} 51 | x=1 52 | for thistag,thistext in this_line.text:gsub("}","}\t"):gmatch("({[^{}]*})([^{}]*)") do 53 | this_table[x]={tag=thistag,text=thistext:gsub("\t","")} 54 | x=x+1 55 | end 56 | 57 | if #this_table<2 then 58 | aegisub.log("There must be more than one tag block in the line!") 59 | return 60 | end 61 | 62 | --Transform these tags 63 | transform_tags={ 64 | "c","2c","3c","4c", 65 | "alpha","1a","2a","3a", 66 | "fscx","fscy","fax","fay", 67 | "frx","fry","frz", 68 | "fs","fsp", 69 | "bord","shad", 70 | "xbord","ybord","xshad","yshad", 71 | "blur","be" 72 | } 73 | 74 | --Make state table 75 | this_state = LibLyger.make_state_table(this_table,transform_tags) 76 | 77 | --Style lookup 78 | this_style = libLyger:style_lookup(this_line) 79 | 80 | --Running record of the state of the line 81 | current_state={} 82 | 83 | --Outer control loop 84 | for i=2,#this_table,1 do 85 | --Update current state 86 | for ctag,cval in pairs(this_state[i-1]) do 87 | current_state[ctag]=cval 88 | end 89 | 90 | --Stores state of each character, to prevent redundant tags 91 | char_state=util.deep_copy(current_state) 92 | 93 | --Local function for interpolation 94 | local function handle_interpolation(factor,tag,sval,eval) 95 | local param_type, ivalue = libLyger.param_type, "" 96 | --Handle differently depending on the type of tag 97 | if param_type[tag]=="alpha" then 98 | ivalue=interpolate_alpha(factor,sval,eval) 99 | elseif param_type[tag]=="color" then 100 | ivalue=interpolate_color(factor,sval,eval) 101 | elseif param_type[tag]=="angle" then 102 | nstart=tonumber(sval) 103 | nend=tonumber(eval) 104 | 105 | --Use "Rotate in shortest direction" by default 106 | nstart=nstart%360 107 | nend=nend%360 108 | ndelta=nend-nstart 109 | if math.abs(ndelta)>180 then nstart=nstart+(ndelta*360)/math.abs(ndelta) end 110 | 111 | --Interpolate 112 | nvalue=interpolate(factor,nstart,nend) 113 | if nvalue<0 then nvalue=nvalue+360 end 114 | 115 | --Convert to string 116 | ivalue=libLyger.float2str(nvalue) 117 | 118 | elseif param_type[tag]=="number" then 119 | nstart=tonumber(sval) 120 | nend=tonumber(eval) 121 | 122 | --Interpolate and convert to string 123 | ivalue=libLyger.float2str(interpolate(factor,nstart,nend)) 124 | end 125 | return ivalue 126 | end 127 | 128 | local ttext=this_table[i-1].text 129 | 130 | if ttext:len()>0 then 131 | 132 | --Rebuilt text 133 | local rtext="" 134 | 135 | --Skip the first character 136 | local first=true 137 | 138 | --Starting values 139 | idx=1 140 | 141 | matches=re.find(ttext,'\\\\[Nh]|\\X') 142 | 143 | total=#matches 144 | 145 | for _,match in ipairs(matches) do 146 | 147 | ch=match.str 148 | 149 | if not first then 150 | --Interpolation factor 151 | factor=idx/total 152 | 153 | idx=idx+1 154 | 155 | --Do nothing if the character is a space 156 | if ch:find("%s")~=nil then 157 | rtext=rtext..ch 158 | else 159 | 160 | --The tags in and out of the time statement 161 | local non_time_tags="" 162 | 163 | --Go through all the state tags in this tag block 164 | for ttag,tparam in pairs(this_state[i]) do 165 | --Figure out the starting state of the param 166 | local sparam=current_state[ttag] 167 | if sparam==nil then sparam=this_style[ttag] end 168 | if type(sparam)~="number" then sparam=sparam:gsub("%)","") end--Just in case a \t tag snuck in 169 | 170 | --Prevent redundancy 171 | if sparam~=tparam then 172 | --The string version of the interpolated parameter 173 | local iparam=handle_interpolation(factor,ttag,sparam,tparam) 174 | 175 | if iparam~=tostring(char_state[ttag]) then 176 | non_time_tags=non_time_tags.."\\"..ttag..iparam 177 | char_state[ttag]=iparam 178 | end 179 | end 180 | end 181 | 182 | if non_time_tags:len() < 1 then 183 | --If no tags were added, do nothing 184 | rtext=rtext..ch 185 | else 186 | --The final tag, with a star to indicate it was added through interpolation 187 | rtext=rtext.."{*"..non_time_tags.."}"..ch 188 | end 189 | 190 | end 191 | else 192 | rtext=rtext..ch 193 | end 194 | first=false 195 | end 196 | 197 | this_table[i-1].text=rtext 198 | 199 | end 200 | 201 | end 202 | 203 | rebuilt_text="" 204 | 205 | for i,val in pairs(this_table) do 206 | rebuilt_text=rebuilt_text..val.tag..val.text 207 | end 208 | this_line.text=rebuilt_text 209 | sub[li]=this_line 210 | end 211 | 212 | aegisub.set_undo_point(script_name) 213 | end 214 | 215 | function remove_grad_char(sub,sel) 216 | for si,li in ipairs(sel) do 217 | this_line=sub[li] 218 | this_line.text=this_line.text:gsub("{%*[^{}]*}","") 219 | sub[li]=this_line 220 | end 221 | end 222 | 223 | -- Register the macro 224 | rec:registerMacros{ 225 | {"Apply Gradient", script_description, grad_char}, 226 | {"Remove Gradient", "Removes gradient generated by #{script_name}", remove_grad_char} 227 | } 228 | -------------------------------------------------------------------------------- /macros/lyger.GradientEverything.moon: -------------------------------------------------------------------------------- 1 | [[ 2 | ==README== 3 | 4 | Gradient Everything 5 | 6 | Define "key" lines, and this will gradient almost anything. 7 | 8 | If you've used the "frame-by-frame transform" script, this behaves very similarly. The typesetter 9 | creates lines that he wants to morph into each other, then highlights them and runs the automation. 10 | 11 | The automation cannot calculate how to draw the \clip statements unless you give it a bounding box. 12 | This is essentially the smallest box that will enclose your entire typeset without cutting any part 13 | of it off. Use the rectangular clip tool in aegisub to define a bounding box on any of the lines you 14 | want to gradient, and the automation will detect it. 15 | 16 | As a simple example, say you want to create a line with a gradient from red to blue. First typeset 17 | the line and make it red. Then duplicate that line and make it blue. Use the rectancular clip tool 18 | to draw a bounding box that encloses the typeset (it doesn't have to be super tight, but keep the 19 | margins small or the gradient might not look right). You can do this on either of the lines, it 20 | doesn't matter. 21 | 22 | Now highlight both lines, go to the automation menu, and select "gradient everything". Check all the 23 | tags you wish to be affected by the gradient. In this case, you want to be sure to check the color 24 | tags. Select whether you want the gradient to be vertical or horizontal, and pick how many pixels 25 | per strip you prefer (the fewer pixels per strip, the smoother the gradient, the more lines, and 26 | the more lag). Press "Gradient" and you're done. 27 | 28 | This script uses the same preset system as frame-by-frame transform. You can save, delete, and load 29 | preset sets of options so you don't have to check the tags you want each time. If you name a preset 30 | "Default", it will be the preset that's loaded when you open the automation. 31 | 32 | If you are gradienting rotations, there is something to watch out for. If you want a line to start 33 | with \frz10 and bend into \frz350, then with default options, the "gradient everything" automation will 34 | make the line bend 340 degrees around the circle until it gets to 350. You probably wanted it to bend 35 | only 20 degrees, passing through 0. The solution is to check the "Rotate in shortest direction" checkbox 36 | from the popup window. This will cause the line to always pick the rotation direction that has a total 37 | rotation of less than 180 degrees. 38 | 39 | Furthermore, you don't have to gradient from only one line to one other line. You are allowed to have 40 | as many lines as you want. For example, if you define three lines, one red, one yellow, and one green, 41 | then "gradient everything" will make it red on the left, yellow in the center, and green on the right. 42 | 43 | As such, the order of your lines matters. If you select "horizontal", then "gradient everything" will 44 | gradient your lines in order from left to right. If you select "vertical", then it will gradient your 45 | lines in order from top to bottom. If you want the gradient to go the other way, then change the order 46 | of your lines. You must select all the lines that you wish to include in the gradient. 47 | 48 | Much like "frame-by-frame transform", all the lines you are gradienting must have the exact same text 49 | once tags are removed. 50 | 51 | Oh yeah, I've tested this script on about four things so far, so don't be surprised if it's buggy. 52 | 53 | 54 | TODO: Debug, debug, and keep debugging 55 | 56 | ]] 57 | 58 | export script_name = "Gradient Everything" 59 | export script_description = "This will gradient everything." 60 | export script_version = "2.0.3" 61 | export script_namespace = "lyger.GradientEverything" 62 | 63 | DependencyControl = require "l0.DependencyControl" 64 | rec = DependencyControl{ 65 | feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 66 | { 67 | {"lyger.LibLyger", version: "2.0.1", url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 68 | {"l0.Functional", version: "0.3.0", url: "https://github.com/TypesettingTools/Functional", 69 | feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"}, 70 | {"SubInspector.Inspector", version: "0.6.0", url: "https://github.com/TypesettingTools/SubInspector", 71 | feed: "https://raw.githubusercontent.com/TypesettingTools/SubInspector/master/DependencyControl.json", 72 | optional: true} 73 | } 74 | } 75 | LibLyger, Functional, SubInspector = rec\requireModules! 76 | import list, math, string, table, unicode, util, re from Functional 77 | 78 | have_SubInspector = rec\checkOptionalModules "SubInspector.Inspector" 79 | logger, libLyger = rec\getLogger!, LibLyger! 80 | 81 | -- tag list, grouped by dialog layout 82 | tags_grouped = { 83 | {"c", "2c", "3c", "4c"}, 84 | {"alpha", "1a", "2a", "3a", "4a"}, 85 | {"fscx", "fscy", "fax", "fay"}, 86 | {"frx", "fry", "frz"}, 87 | {"bord", "shad", "fs", "fsp"}, 88 | {"xbord", "ybord", "xshad", "yshad"}, 89 | {"blur", "be"}, 90 | {"pos", "org"} 91 | } 92 | tags_flat = list.join unpack tags_grouped 93 | 94 | -- default settings for every preset 95 | preset_defaults = { strip: 5, hv_select: "Horizontal", flip_rot: false, accel: 1.0, 96 | tags: {tag, false for tag in *tags_flat } 97 | } 98 | 99 | tag_section_split = re.compile "((?:\\{.*?\\})*)([^\\{]+)" 100 | -- will be moved into ASSFoundation.Common 101 | re.ggmatch = (str, pattern, ...) -> 102 | regex = type(pattern) == "table" and pattern._regex and pattern or re.compile pattern, ... 103 | chars = unicode.toCharTable str 104 | charCnt, last = #chars, 0 105 | -> 106 | return if last >= charCnt 107 | matches = regex\match table.concat chars, "", last+1, charCnt 108 | matchCnt = #matches 109 | return unless matches 110 | last += matches[1].last 111 | start = matchCnt == 1 and 1 or 2 112 | unpack [matches[i].str for i = start, matchCnt] 113 | 114 | -- the default preset must always be available and cannot be deleted 115 | config = rec\getConfigHandler { 116 | presets: { 117 | Default: {} 118 | "[Last Settings]": {description: "Repeats the last #{script_name} operation"} 119 | } 120 | startupPreset: "Default" 121 | } 122 | unless config\load! 123 | -- write example preset on first time load 124 | config.c.presets["Horizontal all"] = tags: {tag, true for tag in *tags_flat} 125 | config\write! 126 | 127 | create_dialog = (preset) -> 128 | config\load! 129 | preset_names = [preset for preset, _ in pairs config.c.presets] 130 | table.sort preset_names 131 | dlg = { 132 | -- define pixels per strip 133 | { class: "label", x: 0, y: 0, width: 2, height: 1, 134 | label:"Pixels per strip: " }, 135 | { name: "strip", class: "intedit", x: 2, y: 0, width: 2, height: 1, 136 | min: 1, value: preset.c.strip, step: 1 }, 137 | { name: "hv_select", class: "dropdown", x: 4, y: 0, width: 1, height: 1, 138 | items: {"Horizontal", "Vertical"}, value: preset.c.hv_select }, 139 | -- Flip rotation 140 | { name: "flip_rot", class: "checkbox", x: 0, y: 9, width: 4, height: 1, 141 | label: "Rotate in shortest direction", value: preset.c.flip_rot }, 142 | -- Acceleration 143 | { class: "label", x: 0, y: 10, width: 2, height: 1, 144 | label: "Acceleration: ", }, 145 | { name: "accel", class:"floatedit", x: 2, y: 10, width: 2, height: 1, 146 | value: preset.c.accel, hint: "1 means no acceleration, >1 starts slow and ends fast, <1 starts fast and ends slow" }, 147 | { class: "label", x: 0, y: 11, width: 2, height: 1, 148 | label: "Preset: " }, 149 | { name: "preset_select", class: "dropdown", x: 2, y: 11, width: 2, height: 1, 150 | items: preset_names, value: preset.section[#preset.section] }, 151 | { name: "preset_modify", class: "dropdown", x: 4, y: 11, width: 2, height: 1, 152 | items: {"Load", "Save", "Delete", "Rename"}, value: "Load" } 153 | } 154 | 155 | -- generate tag checkboxes 156 | for y, group in ipairs tags_grouped 157 | dlg[#dlg+1] = { name: tag, class: "checkbox", x: x-1, y: y, width: 1, height: 1, 158 | label: "\\#{tag}", value: preset.c.tags[tag] } for x, tag in ipairs group 159 | 160 | btn, res = aegisub.dialog.display dlg, {"OK", "Cancel", "Mod Preset", "Create Preset"} 161 | return btn, res, preset 162 | 163 | save_preset = (preset, res) -> 164 | preset\import res, nil, true 165 | if res.__class != DependencyControl.ConfigHandler 166 | preset.c.tags[k] = res[k] for k in *tags_flat 167 | preset\write! 168 | 169 | create_preset = (settings, name) -> 170 | msg = if not name 171 | "Onii-chan, what name would you like your preset to listen to?" 172 | elseif name == "" 173 | "Onii-chan, did you forget to name the preset?" 174 | elseif config.c.presets[name] 175 | "Onii-chan, it's not good to name a preset the same thing as another one~" 176 | 177 | if msg 178 | btn, res = aegisub.dialog.display { 179 | { class: "label", x: 0, y: 0, width: 2, height: 1, label: msg } 180 | { class: "label", x: 0, y: 1, width: 1, height: 1, label: "Preset Name: " }, 181 | { class: "edit", x: 1, y: 1, width: 1, height: 1, name: "name", text: name } 182 | } 183 | return btn and create_preset settings, res.name 184 | 185 | preset = config\getSectionHandler {"presets", name}, preset_defaults 186 | save_preset preset, settings 187 | return name 188 | 189 | prepare_line = (i) -> 190 | line = libLyger.lines[libLyger.sel[i]] 191 | line.comment = true 192 | libLyger.sub[line.i] = line 193 | 194 | -- Figure out the correct position and origin values 195 | posx, posy = libLyger\get_pos line 196 | orgx, orgy = libLyger\get_org line 197 | -- Make sure each line starts with tags 198 | line.text = "{}#{line.text}" unless line.text\find "^{" 199 | -- Turn all \1c tags into \c tags, just for convenience 200 | line.text = line.text\gsub "\\1c", "\\c" 201 | -- The tables that store the line as objects consisting of a tag and the text that follows it 202 | -- Separate each line into a table of tags and text 203 | line_table = [{:tag, :text} for tag, text in line.text\gmatch "({[^{}]*})([^{}]*)"] 204 | 205 | return line, line_table, posx, posy, orgx, orgy 206 | 207 | interpolate_point = (tag, text, sposx, eposx, sposy, eposy, factor) -> 208 | text = LibLyger.line_exclude text, {tag} 209 | posx = LibLyger.float2str util.interpolate factor, sposx, eposx 210 | posy = LibLyger.float2str util.interpolate factor, sposy, eposy 211 | return text\gsub "^{", "{\\#{tag}(#{posx},#{posy})" 212 | 213 | -- The main body of code that runs the frame transform 214 | gradient_everything = (sub, sel, res) -> 215 | -- save last settings 216 | preset = config\getSectionHandler {"presets", "[Last Settings]"}, preset_defaults 217 | save_preset preset, res 218 | 219 | line_cnt, lines, bounds = #sel, {}, {} 220 | libLyger\set_sub sub, sel 221 | -- nothing to if not at least 2 lines were selected 222 | return if line_cnt < 2 223 | -- These are the tags to transform 224 | transform_tags = [tag for tag in *tags_flat when preset.c.tags[tag]] 225 | 226 | -- Look for a clip statement in one of the lines 227 | for i, li in ipairs sel 228 | lines[i] = sub[li] 229 | lines[i].assi_exhaustive = true 230 | bounds = {lines[i].text\match "\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)"} 231 | if #bounds > 0 232 | bounds = [tonumber(ord) for ord in *bounds] 233 | break 234 | 235 | if #bounds == 0 236 | -- Exit if neither a clip nor the SubInspector module have been found 237 | unless have_SubInspector 238 | logger\warn "Please put a rectangular clip in one of the selected lines or install SubInspector." 239 | return 240 | 241 | -- if no rectangular clip was found, get the combined bounding box of all selected lines 242 | assi, msg = SubInspector sub 243 | assert assi, "SubInspector Error: %s."\format msg 244 | bounds, times = assi\getBounds lines 245 | assert bounds~=nil, "SubInspector Error: %s."\format times 246 | 247 | local left, top, right, bottom 248 | for i = 1, #times 249 | if b = bounds[i] 250 | left, top = math.min(b.x, left or b.x), math.min(b.y, top or b.y) 251 | right, bottom = math.max(b.x+b.w, right or 0), math.max(b.y+b.h, bottom or 0) 252 | 253 | if left 254 | bounds = {left-3, top-3, right+3, bottom+3} 255 | else 256 | logger\warn "Nothing to gradient: The selected lines didn't render to any non-transparent pixels." 257 | return 258 | 259 | 260 | -- Make sure left is the left and right is the right 261 | if bounds[1] > bounds[3] then 262 | bounds[1], bounds[3] = bounds[3], bounds[1] 263 | 264 | -- Make sure top is the top and bottom is the bottom 265 | if bounds[2] > bounds[4] then 266 | bounds[2], bounds[4] = bounds[4], bounds[2] 267 | 268 | -- The pixel dimension of the relevant direction of gradient 269 | span = preset.c.hv_select == "Vertical" and bounds[4]-bounds[2] or bounds[3]-bounds[1] 270 | 271 | --Stores how many frames between each key line 272 | --Index 1 is how many frames between keys 1 and 2, and so on 273 | 274 | frames_per, prev_end_frame = {}, 0 275 | avg_frame_cnt = span / (preset.c.strip * (line_cnt-1)) 276 | for i = 1, line_cnt-1 277 | curr_end_frame = math.ceil i*avg_frame_cnt 278 | frames_per[i] = curr_end_frame - prev_end_frame 279 | prev_end_frame = curr_end_frame 280 | 281 | -- IMPORTANT CONTROL VARIABLES 282 | -- Must be initialized here 283 | -- The cumulative pixel offset that indicates the start clip offset of the line 284 | cum_off = 0 285 | -- Store the index of insertion and the new selection 286 | new_sel, ins_index = {}, sel[line_cnt]+1 287 | 288 | -- Master control loop 289 | -- First cycle through all the selected "intervals" (pairs of two consecutive selected lines) 290 | for i = 2, line_cnt 291 | -- Read the first and last lines 292 | first_line, start_table, sposx, sposy, sorgx, sorgy = prepare_line i-1 293 | last_line, end_table, eposx, eposy, eorgx, eorgy = prepare_line i 294 | 295 | -- Make sure both lines have the same splits 296 | LibLyger.match_splits start_table, end_table 297 | 298 | -- Tables that store tables for each tag block, consisting of the state of all relevant tags 299 | -- that are in the transform_tags table 300 | start_state_table = LibLyger.make_state_table start_table, transform_tags 301 | end_state_table = LibLyger.make_state_table end_table, transform_tags 302 | 303 | -- Insert default values when not included for the state of each tag block, 304 | -- or inherit values from previous tag block 305 | start_style = libLyger\style_lookup first_line 306 | end_style = libLyger\style_lookup last_line 307 | 308 | current_start_state, current_end_state = {}, {} 309 | 310 | for k, sval in ipairs start_state_table 311 | -- build current state tables 312 | for skey, sparam in pairs sval 313 | current_start_state[skey] = sparam 314 | 315 | for ekey, eparam in pairs end_state_table[k] 316 | current_end_state[ekey] = eparam 317 | 318 | -- check if end is missing any tags that start has 319 | for skey, sparam in pairs sval 320 | end_state_table[k][skey] or= current_end_state[skey] or end_style[skey] 321 | 322 | -- check if start is missing any tags that end has 323 | for ekey, eparam in pairs end_state_table[k] 324 | start_state_table[k][ekey] or= current_start_state[ekey] or start_style[ekey] 325 | 326 | -- Create a line table based on first_line, but without relevant tags 327 | stripped = LibLyger.line_exclude first_line.text, list.join transform_tags, {"clip"} 328 | this_table = [{:tag, :text} for tag, text in re.ggmatch stripped, tag_section_split] 329 | -- Inner control loop 330 | -- For the number of lines indicated by the frames_per table, create a gradient 331 | for j = 1, frames_per[i-1] 332 | -- The interpolation factor for this particular line 333 | -- Failsafe because dividing by 0 is bad 334 | factor = frames_per[i-1] < 2 and 1 or (j-1)^preset.c.accel / (frames_per[i-1]-1)^preset.c.accel 335 | 336 | -- Create this line 337 | this_line = util.deep_copy first_line 338 | 339 | -- Create the relevant clip tag 340 | -- As of this version, the 1 pixel overlap has been removed. 341 | -- Hopefully colors still look fine 342 | 343 | clip_tag = "\\clip(%d,%d,%d,%d)" 344 | if preset.c.hv_select == "Vertical" 345 | clip_tag = clip_tag\format bounds[1], bounds[2]+cum_off+(j-1)*preset.c.strip, 346 | bounds[3], bounds[2]+cum_off+j*preset.c.strip 347 | else 348 | clip_tag=clip_tag\format bounds[1]+cum_off+(j-1)*preset.c.strip, bounds[2], 349 | bounds[1]+cum_off+j*preset.c.strip, bounds[4] 350 | 351 | -- Interpolate all the relevant parameters and insert 352 | text = LibLyger.interpolate this_table, start_state_table, end_state_table, 353 | factor, preset 354 | this_line.comment = false 355 | 356 | -- Forcibly add \pos 357 | text = interpolate_point "pos", text, sposx, eposx, sposy, eposy, factor 358 | 359 | -- Handle org transform 360 | if preset.c.tags.org then 361 | text = interpolate_point "org", text, sorgx, eorgx, sorgy, eorgy, factor 362 | 363 | -- Oh yeah, and add the clip tag 364 | this_line.text = text\gsub "^{", "{#{clip_tag}" 365 | 366 | -- Reinsert the line 367 | sub.insert ins_index, this_line 368 | new_sel[#new_sel+1] = ins_index 369 | ins_index += 1 370 | 371 | -- Increase the cumulative offset 372 | cum_off += frames_per[i-1] * preset.c.strip 373 | return new_sel 374 | 375 | validate_ge = (sub, sel) -> #sel>=2 376 | 377 | ge_gui = (sub, sel, _, preset_name = config.c.startupPreset) -> 378 | preset = config\getSectionHandler {"presets", preset_name}, preset_defaults 379 | btn, res = create_dialog preset 380 | 381 | switch btn 382 | when "OK" do gradient_everything sub, sel, res 383 | when "Create Preset" do ge_gui sub, sel, nil, create_preset res 384 | when "Mod Preset" 385 | if preset_name != res.preset_select 386 | preset = config\getSectionHandler {"presets", res.preset_select}, preset_defaults 387 | preset_name = res.preset_select 388 | 389 | switch res.preset_modify 390 | when "Delete" 391 | preset\delete! 392 | preset_name = nil 393 | when "Save" do save_preset preset, res 394 | when "Rename" 395 | preset_name = create_preset preset.userConfig, preset_name 396 | preset\delete! 397 | ge_gui sub, sel, nil, preset_name 398 | 399 | -- register macros 400 | rec\registerMacro ge_gui, validate_ge, nil, true 401 | for name, preset in pairs config.c.presets 402 | f = (sub, sel) -> gradient_everything sub, sel, config\getSectionHandler {"presets", name} 403 | rec\registerMacro "Presets/#{name}", preset.description, f, validate_ge, nil, true 404 | -------------------------------------------------------------------------------- /macros/lyger.Image2ASS.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | README: 3 | 4 | ***ALSO REQUIRES(1) CONVERT.EXE STANDALONE FROM IMAGEMAGICK*** 5 | 6 | (1) Only if you want to use non-bitmap images or the resize capability. 7 | Save the binary to your Aegisub\automation\autoload directory: 8 | http://www.mediafire.com/download/zdxn75nte1n6cq6/convert.exe 9 | 10 | 11 | Image to .ass 12 | 13 | Converts a 24-bit or 32-bit bitmap image pixel-by-pixel into an .ass drawing. 14 | Runs a basic compression algorithm on the resultant drawing. Compression 15 | level can be adjusted from the interface (the higher the number, the more 16 | compressed). 17 | 18 | Time a line to the times you want the image to appear. If positioning or 19 | alignment tags are present in the line, and you select "from line" for the 20 | position handling, then these tags will be used on the drawing. Any other 21 | text in the original line will be ignored. 22 | 23 | Also allows you to use an alpha mask, which is a grayscale bitmap loaded 24 | separately. Black represents solid and white represents transparent, just 25 | like .ass color codes. This is inverted compared to, for example, Photoshop 26 | alpha masks, so you may have to invert your mask before loading it. 27 | 28 | If you're using an alpha mask, the original image should extend beyond the 29 | mask. For example, if you have textured text on a white background as your 30 | main image, the text will already be antialised to the white background. 31 | When you apply the alpha mask, the antialiasing pixels will be antialiased 32 | again, making a weird white glow. Instead, your main image should be the 33 | solid texture, thus creating smooth antialiasing pixels when you apply the 34 | mask. 35 | 36 | Be aware that due to subpixel alignment errors in the current version of 37 | xy-vsfilter, the image may appear transparent or have subpixel gaps if you 38 | use certain alignments and positionings. Corner alignments and whole-number 39 | positions are the most reliable. 40 | 41 | Supports \move but you are strongly advised NOT to use it. 42 | 43 | ]] 44 | 45 | script_name = "Image to .ass" 46 | script_description = "Converts bitmap image to .ass lines." 47 | script_version = "2.3.0" 48 | script_author = "lyger" 49 | script_namespace = "lyger.Image2ASS" 50 | 51 | local DependencyControl = require("l0.DependencyControl") 52 | local rec = DependencyControl{ 53 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 54 | { "aegisub.util", "ffi" } 55 | } 56 | local util, ffi = rec:requireModules() 57 | 58 | --[[ Detect whether to use *nix or Windows style paths. ]]-- 59 | local winpaths = ffi.os == "Windows" 60 | 61 | function make_config() 62 | return 63 | { 64 | {x=0,y=0,height=1,width=1,class="label",label="Output drawing"}, 65 | {x=1,y=0,height=1,width=1,class="dropdown",name="otype", 66 | items={"all on one line","with each row on a new line"}, 67 | value="with each row on a new line"}, 68 | {x=0,y=1,height=1,width=1,class="label",label="Position:"}, 69 | {x=1,y=1,height=1,width=1,class="dropdown",name="postype", 70 | items={"from line","default"},value="from line"}, 71 | {x=0,y=2,height=1,width=1,class="label",label="Compression:"}, 72 | {x=1,y=2,height=1,width=1,class="intedit",name="tol", 73 | max=3000,min=1,value=40}, 74 | {x=0,y=3,height=1,width=1,class="label",label="Resize image (%):"}, 75 | {x=1,y=3,height=1,width=1,class="floatedit",name="resize", 76 | max=100,min=1,value=100}, 77 | {x=0,y=4,height=1,width=1,class="label",label="Pixel size:"}, 78 | {x=1,y=4,height=1,width=1,class="intedit",name="pxsize", 79 | max=250,min=1,value=1} 80 | } 81 | end 82 | 83 | --Parse out properties from a bitmap header 84 | function parse_header(fn) 85 | --Open 86 | _file=io.open(fn,"rb") 87 | 88 | --Read irrelevant data 89 | _file:read(18) 90 | 91 | --Read in the pixel width of the image 92 | _width=_file:read(4) 93 | swidth="" 94 | for _w in _width:gmatch(".") do 95 | swidth=string.format("%02X",string.byte(_w))..swidth 96 | end 97 | _iw=tonumber(swidth,16) 98 | 99 | --Read the pixel height of the image, including its orientation 100 | _height=_file:read(4) 101 | sheight="" 102 | for _h in _height:gmatch(".") do 103 | sheight=string.format("%02X",string.byte(_h))..sheight 104 | end 105 | _ih=tonumber(sheight,16) 106 | 107 | --Handle two's complement. Good god this is hacky 108 | if _ih>tonumber("7FFFFFFF",16) then 109 | _ih=_ih-tonumber("FFFFFFFF",16)-1 110 | end 111 | 112 | _file:read(2) 113 | 114 | --Read in whether the bitmap is 24 or 32 bit (fuck handling anything less) 115 | bitsize=string.byte(_file:read(1)) 116 | 117 | _ws=bitsize/8 118 | 119 | _file:close() 120 | 121 | --Return width, height, and wordsize 122 | return _iw, _ih, _ws 123 | end 124 | 125 | function convert_to_bmp(filename,scale) 126 | 127 | local cfname=filename:gsub("%.%a+$",".bmp") 128 | 129 | local prefix=aegisub.decode_path("?data").."\\automation\\autoload\\" 130 | 131 | --Make sure convert binary exists 132 | local cex=io.open(prefix.."convert.exe") 133 | if cex==nil then 134 | aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label", 135 | label="convert.exe not found. Make sure the\n".. 136 | "executable is in your automation\\autoload\n".. 137 | "directory."}},{"OK"}) 138 | aegisub.cancel() 139 | else 140 | cex:close() 141 | end 142 | 143 | --Write the self-deleting batch and run it 144 | opts="-type TrueColor" 145 | if scale then 146 | opts=opts.." -resize "..scale.."%%" 147 | cfname=cfname:gsub("%.bmp$","_"..scale..".bmp") 148 | end 149 | 150 | --Make sure the filenames are different 151 | if cfname==filename then 152 | cfname=cfname:gsub("%.bmp$","_copy.bmp") 153 | end 154 | 155 | local command="\""..prefix.."convert.exe\" \"" 156 | ..filename.."\" "..opts.." BMP3:\""..cfname.."\"" 157 | 158 | convertfile=io.open(prefix.."image2ass_converter.bat","wb") 159 | convertfile:write(command.."\ndel %0") 160 | convertfile:close() 161 | 162 | os.execute("\""..prefix.."image2ass_converter.bat\"") 163 | 164 | return cfname 165 | 166 | end 167 | 168 | function run_i2a(subs,sel) 169 | 170 | local ffilter = "Bitmap images (.bmp)|*.bmp" 171 | if winpaths then ffilter="All images (.bmp; .jpg; .png; .gif)|*.bmp;*.jpg;*.png;*.gif" end 172 | --Prompt for bitmap image 173 | fname=aegisub.dialog.open("Select image","","",ffilter,false,true) 174 | if not fname then aegisub.cancel() end 175 | 176 | cleanfiles={} 177 | --Convert to .bmp if not .bmp already 178 | if not fname:lower():match("%.bmp$") then 179 | fname=convert_to_bmp(fname) 180 | table.insert(cleanfiles,fname) 181 | end 182 | 183 | 184 | --Initialize some values 185 | dconfig=make_config() 186 | results=nil 187 | afname="" 188 | alpha=false 189 | buttons={"Convert","Add alpha mask","Cancel"} 190 | repeat 191 | --Show options 192 | pressed,results=aegisub.dialog.display(dconfig,buttons) 193 | 194 | if pressed=="Cancel" then aegisub.cancel() 195 | elseif pressed=="Add alpha mask" then 196 | 197 | --Prompt for bitmap image 198 | afname=aegisub.dialog.open("Select image to use as alpha mask","","",ffilter,false,true) 199 | if not afname then 200 | aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label", 201 | label="Error, invalid file."}},{"OK"}) 202 | else 203 | alpha=true 204 | if not afname:lower():match(".bmp$") then 205 | afname=convert_to_bmp(afname) 206 | table.insert(cleanfiles,afname) 207 | end 208 | table.insert(dconfig,{x=0,y=5,height=1,width=2,class="label", 209 | label="Alpha mask loaded."}) 210 | table.remove(buttons,2) 211 | end 212 | 213 | end 214 | until pressed=="Convert" 215 | 216 | if results["resize"]~=100 then 217 | fname=convert_to_bmp(fname,results["resize"]) 218 | table.insert(cleanfiles,fname) 219 | if alpha then 220 | afname=convert_to_bmp(afname,results["resize"]) 221 | table.insert(cleanfiles,afname) 222 | end 223 | end 224 | 225 | --Parse headers 226 | rowsize,imgheight,wordsize=parse_header(fname) 227 | awordsize=0 228 | if alpha then 229 | _aiw,_aih,awordsize=parse_header(afname) 230 | if _aiw~=rowsize or _aih~=imgheight then 231 | aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label", 232 | label="Error, alpha channel is not the same size\n".. 233 | "as image."}},{"OK"}) 234 | aegisub.cancel() 235 | end 236 | end 237 | 238 | --Check wordsize 239 | if (wordsize~=3 and wordsize~=4) or (alpha and awordsize~=3 and awordsize~=4) then 240 | aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label", 241 | label="Error, images must be 24-bit or 32-bit bitmap."}},{"OK"}) 242 | aegisub.cancel() 243 | end 244 | 245 | --Compile results 246 | tolerance=results["tol"] 247 | px=results["pxsize"] 248 | oneline=(results["otype"]=="all on one line") 249 | readpos=(results["postype"]=="from line") 250 | 251 | --Open the file 252 | file=io.open(fname,"rb") 253 | file:read(54) 254 | if alpha then 255 | afile=io.open(afname,"rb") 256 | afile:read(54) 257 | end 258 | 259 | --Distance in rgb space 260 | local function cdist(r1,g1,b1,r2,g2,b2) 261 | return math.sqrt((r1-r2)^2+(g1-g2)^2+(b1-b2)^2) 262 | end 263 | 264 | --Counter variables 265 | counter=0 266 | 267 | bytesread=0 268 | abytesread=0 269 | 270 | --Stores previous color used, standard deviation, last color 271 | _r,_g,_b=-1*tolerance-1,-1*tolerance-1,-1*tolerance-1 272 | sr,sg,sb=_r,_g,_b 273 | lr,lg,lb=_r,_g,_b 274 | 275 | --Stores current and previous alphas 276 | aval="00" 277 | praval="00" 278 | ppraval="00" 279 | 280 | --Previous color code used 281 | pcode="" 282 | 283 | --Width of next shape to draw 284 | width=1 285 | 286 | --String to store each line 287 | line="" 288 | 289 | --Table to store processed image 290 | imgtable={} 291 | 292 | --Force alpha tag if alpha channel is on 293 | if alpha then ppraval="GG" end 294 | 295 | while true do 296 | byte=file:read(wordsize) 297 | bytesread=bytesread+wordsize 298 | 299 | if byte==nil then break end 300 | 301 | b,g,r=byte:match("^(.)(.)(.)") 302 | 303 | if b==nil or g==nil or r==nil then break end 304 | 305 | r=string.byte(r) 306 | g=string.byte(g) 307 | b=string.byte(b) 308 | 309 | --Temporary old values of the average 310 | _tr,_tg,_tb=_r,_g,_b 311 | 312 | if _r>=0 then 313 | 314 | --Keep a running average of the rgb values 315 | _r=_r+(r-_r)/(width+1) 316 | _g=_g+(g-_g)/(width+1) 317 | _b=_b+(b-_b)/(width+1) 318 | 319 | --Keep a running standard deviation or the rbg values 320 | sr=sr+(r-_tr)*(r-_r) 321 | sg=sg+(g-_tg)*(g-_g) 322 | sb=sb+(b-_tb)*(b-_b) 323 | end 324 | 325 | --Read and average alpha channel 326 | if alpha then 327 | abyte=afile:read(awordsize) 328 | abytesread=abytesread+awordsize 329 | ab,ag,ar=abyte:match("^(.)(.)(.)") 330 | aval=string.format("%02X", 331 | math.floor((string.byte(ab)+string.byte(ag)+string.byte(ar))/3)) 332 | end 333 | 334 | if ((cdist(lr,lg,lb,r,g,b)=0 then 345 | shape=string.format("m 0 0 l 0 %d %d %d %d 0",px,width*px,px,width*px) 346 | 347 | --Add color code 348 | code=string.format("%02X%02X%02X",_tb,_tg,_tr) 349 | 350 | line=line.."{" 351 | if praval~=ppraval then 352 | line=line.."\\alpha&H"..praval.."&" 353 | end 354 | if code~=pcode and praval~="FF" then 355 | line=line.."\\c&H"..code.."&" 356 | pcode=code 357 | end 358 | line=line.."}"..shape 359 | end 360 | 361 | --Reset width and colors 362 | width=1 363 | _r,_g,_b=r,g,b 364 | sr,sg,sb=0,0,0 365 | 366 | --Set last alpha value 367 | if alpha then 368 | ppraval=praval 369 | praval=aval 370 | end 371 | end 372 | 373 | --Set last r,g,b values 374 | lr,lg,lb=r,g,b 375 | 376 | counter=counter+1 377 | if counter%rowsize==0 then 378 | 379 | --Read filler bytes 380 | file:read(math.abs((4-bytesread)%4)) 381 | if alpha then afile:read(math.abs((4-abytesread)%4)) end 382 | bytesread=0 383 | abytesread=0 384 | 385 | --Dump current shape on end of line 386 | code=string.format("%02X%02X%02X",_b,_g,_r) 387 | shape=string.format("m 0 0 l 0 %d %d %d %d 0",px,width*px,px,width*px) 388 | line=line.."{" 389 | if paval~=aval then 390 | line=line.."\\alpha&H"..aval.."&" 391 | end 392 | if pcode~=code then 393 | line=line.."\\c&H"..code.."&" 394 | end 395 | line=line.."}"..shape 396 | 397 | --Sometimes the algorithm inserts blank tags. I can't be bothered 398 | --to figure out why, so just remove them 399 | while line:match("0{}m") do 400 | line=line:gsub( 401 | "m 0 0 l 0 "..px.." (%d+) "..px.." %d+ 0{}m 0 0 l 0 "..px.." (%d+) "..px.." %d+ 0", 402 | function(w1,w2) 403 | local nw=tonumber(w1)+tonumber(w2) 404 | return string.format("m 0 0 l 0 %d %d %d %d 0",px,nw,px,nw) 405 | end) 406 | end 407 | 408 | --Add line to table 409 | if imgheight<0 then 410 | table.insert(imgtable,line) 411 | else 412 | table.insert(imgtable,1,line) 413 | end 414 | 415 | --Progress report 416 | rprog=math.floor(counter/rowsize) 417 | aegisub.progress.set(rprog*100/math.abs(imgheight)) 418 | aegisub.progress.task(string.format("Processing %d/%d rows",rprog,math.abs(imgheight))) 419 | 420 | --Reset the line 421 | line="" 422 | 423 | --Reset previous colors 424 | _r=-1*tolerance-1 425 | _g=-1*tolerance-1 426 | _b=-1*tolerance-1 427 | sr,sg,sb=_r,_g,_b 428 | lr,lg,lb=_r,_g,_b 429 | 430 | --Reset alpha if alpha channel is on 431 | if alpha then praval="GG" end 432 | 433 | --Reset previous code 434 | pcode="" 435 | 436 | --Reset width 437 | width=1 438 | end 439 | end 440 | 441 | --Close files 442 | file:close() 443 | if alpha then afile:close() end 444 | 445 | aegisub.progress.task("Writing to subtitles...")--No progress bar because this should be near instant 446 | 447 | --Read in the line 448 | line=subs[sel[1]] 449 | line.comment=false 450 | 451 | --Get style info 452 | meta,styles=karaskel.collect_head(subs,false) 453 | lstyle=styles[line.style] 454 | 455 | --Estimate filesize 456 | fsize=0 457 | 458 | --New selection 459 | newsel={} 460 | 461 | --If the drawing is to be written all on one line 462 | if oneline then 463 | oline=util.copy(line) 464 | 465 | rtext="{" 466 | if readpos then 467 | if oline.text:match("\\move") then 468 | mtag=oline.text:match("(\\move%b())") 469 | rtext=rtext..mtag 470 | end 471 | if oline.text:match("\\pos") then 472 | ptag=oline.text:match("(\\pos%b())") 473 | rtext=rtext..ptag 474 | end 475 | end 476 | 477 | if lstyle.outline~=0 then rtext=rtext.."\\bord0" end 478 | if lstyle.shadow~=0 then rtext=rtext.."\\shad0" end 479 | 480 | rtext=rtext.."}" 481 | rtext=rtext:gsub("{}","") 482 | 483 | prefix="{\\p1}" 484 | eol="{\\p0}\\N" 485 | for i,row in ipairs(imgtable) do 486 | rtext=rtext..prefix..row 487 | if i~=#imgtable then rtext=rtext..eol end 488 | end 489 | 490 | oline.text=rtext 491 | fsize=#rtext+44+#oline.style+#oline.effect+#oline.actor 492 | 493 | subs.insert(sel[1]+1,oline) 494 | 495 | newsel={sel[1]+1} 496 | 497 | --If the drawing is to be written across multiple lines 498 | else 499 | prefix="{\\p1" 500 | pfmt="\\pos(%d,%d)" 501 | align="\\an7" 502 | bx,by,bx2,by2=0,0,0,0 503 | if readpos then 504 | if line.text:match("\\move") then 505 | mx1,my1,mx2,my2,msuf=line.text:match( 506 | "\\move%(([%d%.%-]+),([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)([^%)]*%))") 507 | bx=tonumber(mx1) 508 | by=tonumber(my1) 509 | bx2=tonumber(mx2) 510 | by2=tonumber(my2) 511 | pfmt="\\move(%d,%d,%d,%d"..msuf 512 | end 513 | if line.text:match("\\pos") then 514 | p_x,p_y=line.text:match("\\pos%(([%d%.%-]+),([%d%.%-]+)%)") 515 | bx=tonumber(p_x) 516 | by=tonumber(p_y) 517 | end 518 | align=line.text:match("\\an?%d%d?") or align 519 | end 520 | prefix=prefix..align..pfmt 521 | 522 | if lstyle.outline~=0 then prefix=prefix.."\\bord0" end 523 | if lstyle.shadow~=0 then prefix=prefix.."\\shad0" end 524 | 525 | prefix=prefix.."}" 526 | 527 | inserts=1 528 | for i,row in ipairs(imgtable) do 529 | _,alphanum=row:gsub("\\alpha","\\alpha") 530 | dowrite=true 531 | if alphanum==1 then 532 | alphavalue=row:match("\\alpha&H(..)&") 533 | if alphavalue=="FF" then dowrite=false end 534 | end 535 | if dowrite then 536 | nline=util.copy(line) 537 | nline.text=prefix:format(bx,by+(i-1)*px,bx2,by2+(i-1)*px) 538 | nline.text=nline.text..row 539 | subs.insert(sel[1]+inserts,nline) 540 | table.insert(newsel,sel[1]+inserts) 541 | inserts=inserts+1 542 | fsize=fsize+#nline.text+44+#nline.style+#nline.effect+#nline.actor 543 | end 544 | end 545 | end 546 | 547 | line.text=fname 548 | line.comment=true 549 | subs[sel[1]]=line 550 | 551 | mbytes=string.format("%.2f",fsize/1048576):gsub("0+$",""):gsub("%.$","") 552 | msg="Conversion finished.\nApproximate added filesize: "..mbytes.." MB." 553 | if mbytes=="0" then 554 | kbytes=string.format("%.2f",fsize/1025):gsub("0+$",""):gsub("%.$","") 555 | msg="Conversion finished.\nApproximate added filesize: "..kbytes.." kB." 556 | else 557 | end 558 | aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label", 559 | label=msg}}, 560 | {"OK"}) 561 | 562 | --Delete generated bitmap, if applicable 563 | for _,cleanf in ipairs(cleanfiles) do os.execute("del \""..cleanf.."\"") end 564 | 565 | aegisub.set_undo_point(script_name) 566 | return newsel 567 | end 568 | 569 | rec:registerMacro(run_i2a) -------------------------------------------------------------------------------- /macros/lyger.KaraHelper.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | README 3 | 4 | Karaoke Helper 5 | 6 | Does simple karaoke tasks. Adds blank padding syllables to the beginning of lines, 7 | and also adjusts final syllable so it matches the line length. 8 | 9 | Will add more features as ktimers suggest them to me. 10 | 11 | 12 | ]]-- 13 | 14 | script_name = "Karaoke helper" 15 | script_description = "Miscellaneous tools for assisting in karaoke timing." 16 | script_version = "0.2.0" 17 | script_author = "lyger" 18 | script_namespace = "lyger.KaraHelper" 19 | 20 | local DependencyControl = require("l0.DependencyControl") 21 | local rec = DependencyControl{ 22 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 23 | { 24 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 25 | } 26 | } 27 | local LibLyger = rec:requireModules() 28 | local libLyger = LibLyger() 29 | 30 | function make_config(styles) 31 | local stopts={"selected lines"} 32 | for i=1,styles.n,1 do 33 | stopts[i+1] = ("style: %q").format(styles[i].name) 34 | end 35 | local config= 36 | { 37 | --What to apply the automation on 38 | { 39 | class="label", 40 | label="Apply to:", 41 | x=0,y=0,width=1,height=1 42 | }, 43 | { 44 | class="dropdown", 45 | name="sselect",items=stopts, 46 | x=1,y=0,width=1,height=1, 47 | value="selected lines" 48 | }, 49 | --Match syls to line length 50 | { 51 | class="checkbox", 52 | name="match",label="Match syllable lengths to line length", 53 | x=0,y=1,width=2,height=1, 54 | value=true 55 | }, 56 | --Add blank syl at the start 57 | { 58 | class="checkbox", 59 | name="leadin",label="Add start padding:", 60 | x=0,y=2,width=1,height=1, 61 | value=false 62 | }, 63 | { 64 | class="intedit", 65 | name="leadindur", 66 | x=1,y=2,width=1,height=1, 67 | min=0, 68 | value=0 69 | }, 70 | --Add blank syl at the end 71 | { 72 | class="checkbox", 73 | name="leadout",label="Add end padding:", 74 | x=0,y=3,width=1,height=1, 75 | value=false 76 | }, 77 | { 78 | class="intedit", 79 | name="leadoutdur", 80 | x=1,y=3,width=1,height=1, 81 | min=0, 82 | value=0 83 | } 84 | } 85 | return config 86 | end 87 | 88 | --Match syllable and line durations 89 | function match_durs(line) 90 | local ldur=line.end_time-line.start_time 91 | local cum_sdur=0 92 | for sdur in line.text:gmatch("\\[Kk][fo]?(%d+)") do 93 | cum_sdur=cum_sdur+tonumber(sdur) 94 | end 95 | local delta=math.floor(ldur/10)-cum_sdur 96 | line.text=line.text:gsub("({[^{}]*\\[Kk][fo]?)(%d+)([^{}]*}[^{}]*)$", 97 | function(pre,val,post) 98 | return ("%s%d%s"):format(pre, tonumber(val)+delta, post) 99 | end) 100 | return line 101 | end 102 | 103 | --Add padding at the start 104 | function add_prepad(line,pdur) 105 | line.text=line.text:gsub("^({[^{}]*\\[Kk][fo]?)(%d+)", 106 | function(pre,val) 107 | return ("{\\k%d}%s%d"):format(pdur, pre, tonumber(val)-pdur) 108 | end) 109 | line.text=line.text:gsub("^{\\k(%d+)}({[^{}]*\\[Kk][fo]?)(%-?%d+)([^{}]*}{)", 110 | function(val1,mid,val2,post) 111 | return ("%s%d%s"):format(mid, tonumber(val1)+tonumber(val2), post) 112 | end) 113 | return line 114 | end 115 | 116 | --Add padding at the end 117 | function add_postpad(line,pdur) 118 | line.text=line.text:gsub("(\\[Kk][fo]?)(%d+)([^{}]*}[^{}]*)$", 119 | function(pre,val,post) 120 | return ("%s%d%s{\\k%d}"):format(pre, tonumber(val)-pdur, post, pdur) 121 | end) 122 | line.text=line.text:gsub("(\\[Kk][fo]?)(%-?%d+)([^{}]*}){\\k(%d+)}$", 123 | function(pre,val1,mid,val2) 124 | return ("%s%d%s"):format(pre, tonumber(val1)+tonumber(val2), mid) 125 | end) 126 | return line 127 | end 128 | 129 | --Load config and display 130 | function load_kh(sub,sel) 131 | libLyger:set_sub(sub, sel) 132 | 133 | -- Basic header collection, config, dialog display 134 | local config = make_config(libLyger.styles) 135 | local pressed,results=aegisub.dialog.display(config) 136 | if pressed=="Cancel" then aegisub.cancel() end 137 | 138 | --Determine how to retrieve the next line, based on the dropdown selection 139 | local tstyle, line_cnt, get_next = results["sselect"], #sub 140 | 141 | if tstyle:match("^style: ") then 142 | tstyle=tstyle:match("^style: \"(.+)\"$") 143 | get_next = function(uindex) 144 | for i = uindex, line_cnt do 145 | local line = libLyger.dialogue[uindex] 146 | if line.style == tstyle and (not line.comment or line.effect == "karaoke") then 147 | return line, i 148 | end 149 | end 150 | end 151 | else 152 | get_next = function(uindex) 153 | if uindex <= #sel then 154 | return libLyger.lines[sel[uindex]], uindex+1 155 | end 156 | end 157 | end 158 | 159 | --Control loop 160 | local line, uindex = get_next(1) 161 | while line do 162 | if results["match"] then match_durs(line) end 163 | if results["leadin"] then add_prepad(line, results["leadindur"]) end 164 | if results["leadout"] then add_postpad(line, results["leadoutdur"]) end 165 | sub[line.i] = line 166 | line, uindex = get_next(uindex) 167 | end 168 | 169 | aegisub.set_undo_point(script_name) 170 | end 171 | 172 | rec:registerMacro(load_kh) 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /macros/lyger.KaraReplacer.lua: -------------------------------------------------------------------------------- 1 | script_name = "Karaoke replacer" 2 | script_description = "Replaces the syllables of a verse." 3 | script_version = "0.3.0" 4 | script_author = "lyger" 5 | script_namespace = "lyger.KaraReplacer" 6 | 7 | local DependencyControl = require("l0.DependencyControl") 8 | local rec = DependencyControl{ 9 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json" 10 | } 11 | 12 | --Fuck it, I should comment this code. Her goes 13 | function kara_replace(sub,sel) 14 | for si,li in ipairs(sel) do 15 | --Read in line 16 | local line = sub[li] 17 | 18 | --Split at karaoke tags and create table of tags and text 19 | local line_table = {} 20 | 21 | for tag,text in line.text:gmatch("({[^{}]*\\[kK][^{}]*})([^{}]*)") do 22 | table.insert(line_table,{["tag"]=tag,["text"]=text}) 23 | end 24 | 25 | --Put the line back together with spaces between syllables 26 | local rebuilt_parts = {} 27 | 28 | for i,val in ipairs(line_table) do 29 | rebuilt_parts[i] = val.text 30 | end 31 | 32 | --Add some padding so it displays better 33 | local rebuilt_original = table.concat(rebuilt_parts, " ") 34 | rebuilt_original = rebuilt_original .. string.rep(" ", math.floor(rebuilt_original:len()/2) -1) 35 | 36 | --Dialog display 37 | local config = { 38 | { 39 | class="label", 40 | label=rebuilt_original, 41 | x=0,y=0,width=1,height=1 42 | } 43 | , 44 | { 45 | class="edit", 46 | name="replace", 47 | x=0,y=1,width=1,height=1 48 | } 49 | } 50 | 51 | --Instructions 52 | local help_config = { 53 | { 54 | class="label", 55 | label= 56 | "The syllables of the original line will be displayed.\n".. 57 | "Type the syllables you would like to replace them with,\n".. 58 | "with spaces between each syllable.\n\n".. 59 | "If you want a space in the lyrics, type a double space.\n".. 60 | "Two join a syllable with the one after it, put a + after\n".. 61 | "the syllable.\n".. 62 | "To split a syllable (you'll have to adjust it yourself),\n".. 63 | "put a | where you want the split.\n".. 64 | "To insert a blank syllable (for padding), type _\n".. 65 | "You can ignore any blank syllables in the original line.\n\n".. 66 | "Example:\n".. 67 | "_ ko re wa+ re|i de su", 68 | x=0,y=0,width=1,height=1 69 | } 70 | } 71 | 72 | --Show dialog and get input for each line 73 | local pressed 74 | 75 | repeat 76 | pressed,result=aegisub.dialog.display(config,{"Next line","Help"}) 77 | if pressed=="Help" then 78 | aegisub.dialog.display(help_config,{"OK"}) 79 | end 80 | until pressed~="Help" 81 | 82 | --Split input at spaces and store in table 83 | local replace = {} 84 | 85 | for newsyl in result["replace"]:gsub(" ","\t "):gmatch("[^ ]+") do 86 | newsyl=newsyl:gsub("\t"," ") 87 | table.insert(replace,newsyl) 88 | end 89 | 90 | local rebuilt_text, r = {}, 1 91 | --Indices of original and replacement tables 92 | local oi, ri = 1, 1 93 | 94 | while oi<=#line_table do 95 | --Skip if it's a blank syl (used for padding) or we're out of replacements 96 | if line_table[oi].text:len()>0 and replace[ri]~=nil then 97 | --Handle splitting syls 98 | if replace[ri]:find("|")~=nil then 99 | 100 | --Split the replacement line at | characters 101 | subtab={} 102 | for subsyl in replace[ri]:gmatch("[^|]+") do 103 | table.insert(subtab,subsyl) 104 | end 105 | 106 | --Find the original time of the karaoke syllable 107 | local otime = tonumber(line_table[oi].tag:match("\\[kK][fo]?(%d+)")) 108 | --The remaining time (for last syl, to ensure they add up to the original time) 109 | local ltime = otime 110 | --Add all but the last syl 111 | for x=1,#subtab-1,1 do 112 | --To minimize truncation error, alternate between ceil and floor 113 | local ttime = 0 114 | if x%2==1 then 115 | ttime = math.floor(otime/#subtab) 116 | else 117 | ttime = math.ceil(otime/#subtab) 118 | end 119 | rebuilt_text[r] = line_table[oi].tag:gsub("(\\[kK][fo]?)%d+","\1"..tostring(ttime)) 120 | rebuilt_text[r+1], r = subtab[x], r+2 121 | ltime=ltime-ttime 122 | end 123 | --Add the last syl 124 | rebuilt_text[r] = line_table[oi].tag:gsub("(\\[kK][fo]?)%d+","\1"..tostring(ltime)) 125 | rebuilt_text[r+1], r = subtab[#subtab], r+2 126 | 127 | --Handle merging syls 128 | --Only merge if it's not the last syl 129 | elseif replace[ri]:find("+")~=nil and oi<#line_table then 130 | local temp_tag = line_table[oi].tag 131 | oi=oi+1 132 | stime=tonumber(line_table[oi].tag:match("\\[kK][fo]?(%d+)")) 133 | temp_tag=temp_tag:gsub("(\\[kK][fo]?)(%d+)",function(a,b) 134 | return a..tostring(tonumber(b)+stime) 135 | end) 136 | rebuilt_text[r], rebuilt_text[r+1] = temp_tag, replace[ri]:gsub("+","") 137 | r = r+2 138 | 139 | --The usual replacement 140 | else 141 | rebuilt_text[r], rebuilt_text[r+1] = line_table[oi].tag, replace[ri]:gsub("_","") 142 | r = r+2 143 | end 144 | 145 | --Increment indices 146 | oi=oi+1 147 | ri=ri+1 148 | else 149 | rebuilt_text[r], r = line_table[oi].tag, r+1 150 | oi=oi+1 151 | end 152 | end 153 | 154 | line.text = table.concat(rebuilt_text) 155 | sub[li]=line 156 | if finished then break end 157 | end 158 | end 159 | 160 | --Old behavior. If automations are ever modified so that hitting "enter" from a text box 161 | --will execute the "OK" button, then this behavior is probably better. 162 | --For now, this function doesn't do anything 163 | function kara_replace_old(sub,sel) 164 | for si,li in ipairs(sel) do 165 | line=sub[li] 166 | 167 | line_table={} 168 | 169 | for tag,text in line.text:gmatch("({[^{}]*\\[kK][^{}]*})([^{}]*)") do 170 | table.insert(line_table,{["tag"]=tag,["text"]=text}) 171 | end 172 | 173 | rebuilt_text="" 174 | 175 | finished=false 176 | 177 | for i,val in ipairs(line_table) do 178 | 179 | local function hl_syl(lt,idx) 180 | result="" 181 | for k,a in ipairs(lt) do 182 | if k==idx then result=result.." ["..a.text:upper().."] " 183 | else result=result..a.text end 184 | end 185 | return result 186 | end 187 | 188 | if val.text:len()<1 or finished then 189 | rebuilt_text=rebuilt_text..val.tag..val.text 190 | else 191 | config= 192 | { 193 | { 194 | class="label", 195 | label="Enter the syllable to replace with, or nothing to close.", 196 | x=0,y=0,width=1,height=1 197 | }, 198 | { 199 | class="label", 200 | label=hl_syl(line_table,i), 201 | x=0,y=2,width=1,height=1 202 | }, 203 | { 204 | class="edit", 205 | name="replace", 206 | x=0,y=3,width=1,height=1 207 | } 208 | } 209 | _,res=aegisub.dialog.display(config,{"OK"}) 210 | if res["replace"]:len()<1 then 211 | rebuilt_text=rebuilt_text..val.tag..val.text 212 | finished=true 213 | else 214 | rebuilt_text=rebuilt_text..val.tag..res["replace"] 215 | end 216 | end 217 | end 218 | line.text=rebuilt_text 219 | sub[li]=line 220 | if finished then break end 221 | end 222 | end 223 | 224 | rec:registerMacro(kara_replace) -------------------------------------------------------------------------------- /macros/lyger.LayerIncrement.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Layer Increment 5 | 6 | Basic utility that will make selected lines have increasing or decreasing layer numbers. 7 | 8 | ]] 9 | 10 | script_name = "Layer increment" 11 | script_description = "Makes increasing or decreasing layer numbers." 12 | script_version = "1.1.0" 13 | script_author = "lyger" 14 | script_namespace = "lyger.LayerIncrement" 15 | 16 | local DependencyControl = require("l0.DependencyControl") 17 | local rec = DependencyControl{ 18 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json" 19 | } 20 | 21 | local config = { 22 | { 23 | class="dropdown", name="updown", 24 | items={"Count up","Count down"}, 25 | x=0,y=0,width=2,height=1, 26 | value="Count up" 27 | }, 28 | { 29 | class="label",label="Interval", 30 | x=0,y=1,width=1,height=1 31 | }, 32 | { 33 | class="intedit",name="int", 34 | x=1,y=1,width=1,height=1, 35 | min=1,value=1 36 | } 37 | } 38 | 39 | function layer_inc(sub,sel) 40 | local pressed, results = aegisub.dialog.display(config,{"Go","Cancel"}) 41 | 42 | local min_layer = 0 43 | for _,li in ipairs(sel) do 44 | local line = sub[li] 45 | if line.layer>min_layer then 46 | min_layer = line.layer 47 | end 48 | end 49 | 50 | local start_layer = min_layer 51 | local factor=1 52 | local interval = results["int"] 53 | 54 | if results["updown"]=="Count down" then 55 | start_layer = min_layer + (#sel-1)*interval 56 | factor = -1 57 | end 58 | 59 | for j,li in ipairs(sel) do 60 | local line = sub[li] 61 | line.layer = start_layer + (j-1)*factor*interval 62 | sub[li] = line 63 | end 64 | 65 | return sel 66 | end 67 | 68 | rec:registerMacro(layer_inc) 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /macros/lyger.LuaInterpret.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Lua Interpreter 5 | 6 | This allows you to run Lua code on-the-fly on an .ass file. The code will 7 | be applied to all the selected lines. A simple API is provided to make 8 | modifying line properties more efficient. 9 | 10 | Calling it a "Lua interpreter" may be a misnomer, but I can't think of 11 | anything better at the moment. 12 | 13 | The code the user inputs is run for each "section" of text, as marked by 14 | the override blocks. A "section" of text is defined as the part of the 15 | line that has all the same properties. For example, this line: 16 | 17 | Never gonna {\fs200}give {\alpha&H55&}you up 18 | 19 | has three sections. The first section is "Never gonna " and contains all 20 | default properties. The second section is "{\fs200}give ". All text in 21 | this section has font size 200, and default properties otherwise. The 22 | third and last section is "{\alpha&H55&}you up", which has font size 200, 23 | an alpha of 55 hex, and default properties otherwise. 24 | 25 | Any code you input into the interpreter will thus run once for each of 26 | these three sections, changing the properties as appropriate. 27 | 28 | Functions are as follows: 29 | 30 | modify(tag, method) 31 | mod(tag,method) 32 | Modify tag using method. tag is a string that indicates the override 33 | tag (property) that you want to modify. method is a function that 34 | dictates how the modification is done. For example, to double the 35 | font size, do: 36 | 37 | modify("fs",multiply(2)) 38 | 39 | mod is an alias for modify, which you can use to save typing. 40 | 41 | modify_line(property, method) 42 | modln(propery,method) 43 | Works like modify(), but acts on line properties, not override tags. 44 | For example, to modify the layer of a line: 45 | 46 | modify_line("layer",add(1)) 47 | 48 | For a list of line properties that can be modified, see: 49 | http://docs.aegisub.org/3.0/Automation/Lua/Modules/karaskel.lua/#index12h3 50 | 51 | add(...) 52 | Returns a function that will add the given values. Can have multiple 53 | parameters. For example, to expand a rectangular clip by 10 pixels on 54 | all sides, assuming the first two coordinates represent top left and 55 | the last two coordinates represent bottom right, do: 56 | 57 | modify("clip",add(-10,-10,10,10)) 58 | 59 | This will add -10, -10, 10, and 10, in that order, to the four 60 | parameters of \clip. There is no subtract() function; simply add a 61 | negative number to subtract. 62 | 63 | multiply(...) 64 | mul(...) 65 | Works like add(). There is no divide() function. Simply multiply by 66 | a decimal or a fraction. Example: 67 | 68 | modify("fscx",multiply(0.5)) 69 | 70 | replace(x) 71 | rep(x) 72 | Returns a function that returns x. When used inside modify(), this 73 | will effectively replace the original parameter of the tag with x. 74 | 75 | modify("fn",replace("Comic Sans MS")) 76 | 77 | append(x) 78 | app(x) 79 | Returns a function that appends x to the parameter. For example: 80 | 81 | modify_line("actor",append(" the great")) 82 | 83 | I'm not sure why I wrote this function either. Completeness' sake, 84 | perhaps. 85 | 86 | get(tag) 87 | Returns the parameter of the tag. If the tag has multiple parameters, 88 | they are returned as a table. Example: 89 | 90 | main_color=get("c") 91 | 92 | remove(...) 93 | rem(...) 94 | Removes all the tags listed. Example: 95 | 96 | remove("bord","shad") 97 | 98 | select() 99 | Adds the current line to the final selection. If this function is 100 | never used, the original selection will be returned. 101 | 102 | duplicate() 103 | DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING. This will insert a 104 | copy of the current line after the current line. Beware of recursion! 105 | If you do not put some sort of if statement around duplicate(), then 106 | your first line will be duplicated, then the duplicate will be 107 | duplicated, then the duplicate of the duplicate will be duplicated, 108 | and you end up in an infinite loop. I suggest you use the function 109 | like this: 110 | 111 | if i%2==1 then 112 | duplicate() 113 | 114 | --Code to run on the original line 115 | 116 | else 117 | 118 | --Code to run on the duplicate line 119 | 120 | end 121 | 122 | Note that "once per line" functions such as duplicate() are run at 123 | the end of the rest of the execution, but before changes are saved. 124 | In other words, duplicate() will always create a line that looks like 125 | your current line did originally, before you modified it at all. 126 | 127 | You also have access to all functions in utils.lua and karaskel.lua, such 128 | as functions for doing math on alpha and color values. I may eventually 129 | write alpha and color handling into the already complex modify function, 130 | but for now, code such as modify("alpha",add(50)) will not work. 131 | 132 | 133 | 134 | Global variables are as follows: 135 | 136 | i 137 | This is the index within your selection. In other words, when the 138 | code is being run on the first line, i will have the value 1. When 139 | the code is being run on the third line, i will have the value 3. 140 | In the code example under duplicate() above, i will be odd for all 141 | of the original lines and even for all of the duplicates, thus 142 | the check "i%2==1" is made. 143 | 144 | li 145 | This is the line number of the current line. 146 | 147 | j 148 | This is the number (counting from 1) of the section that the code is 149 | currently looking at. 150 | 151 | state 152 | This is a table containing the current state of the line, indexed by 153 | tag name. For example, to find out what the current x scaling is, use: 154 | 155 | state["fscx"] 156 | 157 | This table automatically updates when your code modifies properties 158 | of the line. To see the state of the untouched line, use the variable 159 | dstate (for default state). 160 | 161 | pos 162 | This is a table (or object) with two fields: x and y. Use pos.x to 163 | access the x coordinate and pos.y to access the y coordinate. The 164 | coordinates are guaranteed to match the line's position on screen, 165 | even if no position is defined in-line. You can perform arithmetic 166 | on this object, but it may not behave the way you want it to. You 167 | are advised to use modify("pos",...) instead. 168 | 169 | org 170 | Like pos, but for the origin. 171 | 172 | flags 173 | A global table for values you want to store outside of the loop. Most 174 | other variables will change or be reset once the the script starts to 175 | run on the next line. It's empty by default. 176 | 177 | ]] 178 | 179 | script_name = "Lua Interpreter" 180 | script_description = "Run Lua code on the fly." 181 | script_version = "1.3.1" 182 | script_author = "lyger" 183 | script_namespace = "lyger.LuaInterpret" 184 | 185 | local DependencyControl = require("l0.DependencyControl") 186 | local rec = DependencyControl{ 187 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 188 | { 189 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 190 | "aegisub.util" 191 | } 192 | } 193 | 194 | local LibLyger, util = rec:requireModules() 195 | local libLyger = LibLyger() 196 | local f2s, esc = LibLyger.float2str, LibLyger.esc 197 | 198 | --Set the location of the config file 199 | local config_pre=aegisub.decode_path("?user") 200 | local config_name="luaint-presets.config" 201 | local psep=config_pre:match("\\") and "\\" or "/" 202 | 203 | --Old config path, to allow old data to be copied over to the proper location 204 | local old_config_path=config_pre..config_name 205 | --Proper config path 206 | local config_path=config_pre..psep..config_name 207 | 208 | 209 | local textbox, dialog_conf = {}, {} 210 | 211 | --Lookup table for once-per-line tags 212 | local opl= { 213 | ["pos"]=true, 214 | ["org"]=true, 215 | ["move"]=true, 216 | ["a"]=true, 217 | ["an"]=true, 218 | ["fad"]=true, 219 | ["clip"]=true 220 | } 221 | 222 | --Remake the configuration defaults 223 | local function make_conf() 224 | textbox={class="textbox",name="code",x=0,y=1,width=40,height=6} 225 | dialog_conf= 226 | { 227 | {class="label",label="Enter code below:",x=0,y=0,width=40,height=1}, 228 | textbox 229 | } 230 | end 231 | 232 | 233 | --Returns a function that adds by each number 234 | function add(...) -- exposed to script 235 | local x = table.pack(...) 236 | return function(...) 237 | local y, z = table.pack(...), {} 238 | for i=1, #y do 239 | y[i]=tonumber(y[i]) or 0 240 | x[i]=tonumber(x[i]) or 0 241 | z[i]=y[i]+x[i] 242 | end 243 | return unpack(z) 244 | end 245 | end 246 | 247 | --Returns a function that multiplies by each number 248 | function multiply(...) -- exposed to script 249 | local x = table.pack(...) 250 | return function(...) 251 | local y, z = table.pack(...), {} 252 | for i=1, #y do 253 | y[i]=tonumber(y[i]) or 0 254 | x[i]=tonumber(x[i]) or 0 255 | z[i]=y[i]*x[i] 256 | end 257 | return unpack(z) 258 | end 259 | end 260 | 261 | --Returns a function that replaces with x 262 | function replace(...) -- exposed to script 263 | local args = table.pack(...) 264 | return function() return unpack(args) end 265 | end 266 | 267 | --Returns a function that appends x 268 | function append(x) -- exposed to script 269 | return function(y) return y..x end 270 | end 271 | 272 | --Write presets table to file 273 | local function table_to_file(path,wtable) 274 | local wfile=io.open(path,"wb") 275 | wfile:write("return\n") 276 | LibLyger.write_table(wtable,wfile," ") 277 | wfile:close() 278 | end 279 | 280 | --Read presets table from file 281 | local function table_from_file(path) 282 | local lfile=io.open(path,"r") 283 | if not lfile then return end 284 | local return_presets,err = loadstring(lfile:read("*all")) 285 | if err then 286 | aegisub.log(err) 287 | return 288 | end 289 | lfile:close() 290 | return return_presets() 291 | end 292 | 293 | 294 | function lua_interpret(sub,sel) 295 | make_conf() 296 | libLyger:set_sub(sub, sel) 297 | 298 | --Copies old data over in the case of first run after upgrade 299 | local oldpresets=table_from_file(old_config_path) 300 | if oldpresets then 301 | table_to_file(config_path,oldpresets) 302 | end 303 | 304 | --Load presets or create if none 305 | local presets = table_from_file(config_path) 306 | if not presets then 307 | presets={["Example - Duplicate and Blur"] = [[ 308 | if i%2 == 1 then 309 | duplicate() 310 | modify("bord", replace(0)) 311 | if state.blur == 0 then modify("blur", replace(0.6)) end 312 | modify_line("layer",add(1)) 313 | remove("3c", "3a", "shad") 314 | else 315 | modify("c", replace(get("3c"))) 316 | modify("1a", replace(get("3a"))) 317 | if state.blur == 0 then modify("blur", replace(0.6)) end 318 | end]]} 319 | 320 | table_to_file(config_path,presets) 321 | end 322 | 323 | --Components of the dialog 324 | local preselector = { 325 | class="dropdown",items={}, 326 | name="pre_sel", 327 | x=0,y=7,width=20,height=1 328 | } 329 | local prenamer = { 330 | class="edit", 331 | name="new_prename", 332 | x=20,y=7,width=20,height=1 333 | } 334 | 335 | dialog_conf[3], dialog_conf[4] = preselector, prenamer 336 | 337 | local function make_name_list() 338 | preselector.items = {} 339 | local maxnew, p = 0, 1 340 | for k,_ in pairs(presets) do 341 | preselector.items[p], p = k, p+1 342 | num=k:match("New preset (%d+)") 343 | num=tonumber(num) or 0 344 | maxnew = math.max(num, maxnew) 345 | end 346 | table.sort(preselector.items) 347 | prenamer.value=string.format("New preset %d",maxnew+1) 348 | end 349 | 350 | make_name_list() 351 | 352 | --Show GUI 353 | local pressed, results 354 | repeat 355 | pressed,results=aegisub.dialog.display(dialog_conf, 356 | {"Run","Load","Save","Delete","Cancel"}) 357 | 358 | if pressed=="Cancel" then aegisub.cancel() end 359 | if pressed=="Load" then 360 | textbox.value=presets[results["pre_sel"]] 361 | preselector.value=results["pre_sel"] 362 | end 363 | if pressed=="Save" then 364 | textbox.value=results["code"] 365 | if presets[results["new_prename"]]~=nil then 366 | aegisub.dialog.display({{class="label",label="Name already in use!",x=0,y=0,width=1,height=1}}) 367 | else 368 | presets[results["new_prename"]]=results["code"] 369 | table_to_file(config_path,presets) 370 | make_name_list() 371 | end 372 | end 373 | if pressed=="Delete" then 374 | presets[results["pre_sel"]]=nil 375 | make_name_list() 376 | table_to_file(config_path,presets) 377 | preselector.value=nil 378 | end 379 | 380 | until pressed=="Run" 381 | 382 | local command, new_sel = results["code"], {} 383 | 384 | --Run for all lines in selection. Hard limit of 9001 just in case 385 | i, flags = 1, {} -- exposed to script 386 | while i<=#sel and #sel<=9001 do 387 | local li=sel[i] 388 | local line, line_table = libLyger.lines[li], {} 389 | 390 | aegisub.progress.set(100*i/#sel) 391 | 392 | --Alias maxi to the size of the selection 393 | maxi = #sel -- exposed to script 394 | --Break the line into a table 395 | 396 | if not line.text:match("^{") then 397 | line.text="{}"..line.text 398 | end 399 | line.text=line.text:gsub("}","}\t") 400 | local j = 1 401 | for thistag,thistext in line.text:gmatch("({[^{}]*})([^{}]*)") do 402 | line_table[j]={tag=thistag:gsub("\\1c","\\c"),text=thistext:gsub("^\t","")} 403 | j=j+1 404 | end 405 | line.text=line.text:gsub("}\t","}") 406 | 407 | --These functions are run at the end, at most once per line 408 | local tasklist = {} 409 | 410 | --Function to select line 411 | local function _select() 412 | tasklist[#tasklist+1] = function() 413 | new_sel[#new_sel+1] = li 414 | selected = true -- exposed to script 415 | end 416 | end 417 | 418 | --Function to duplicate lines 419 | local function _duplicate() 420 | table.insert(tasklist,1,function() 421 | table.insert(sel,i+1,li+1) 422 | libLyger:insert_line(util.copy(line), li+1) 423 | for x = i+2, #sel do 424 | sel[x] = sel[x]+1 425 | end 426 | 427 | for x = 1, #new_sel do 428 | if new_sel[x] > li+1 then 429 | new_sel[x] = new_sel[x] + 1 430 | end 431 | end 432 | 433 | duplicated = true -- exposed to script 434 | flags["duplicate"]=true 435 | end) 436 | end 437 | 438 | --Function to modify line properties 439 | local function _modify_line(prop,func) 440 | table.insert(tasklist, function() 441 | line[prop]=func(line[prop]) 442 | end) 443 | end 444 | 445 | --Create state table 446 | local state_table = {} 447 | for j,a in ipairs(line_table) do 448 | state_table[j]={} 449 | for b in a.tag:gmatch("(\\[^\\}]*)") do 450 | if b:match("\\fs%d") then 451 | state_table[j]["fs"]=b:match("\\fs([%d%.]+)") 452 | state_table[j]["fs"]=tonumber(state_table[j]["fs"]) 453 | elseif b:match("\\fn") then 454 | state_table[j]["fn"]=b:match("\\fn([^\\}]*)") 455 | elseif b:match("\\r") then 456 | state_table[j]["r"]=b:match("\\r([^\\}]*)") 457 | else 458 | local tag, param = b:match("\\([1-4]?%a+)(%A[^\\}]*)") 459 | state_table[j][tag] = tonumber(param) or param 460 | end 461 | end 462 | end 463 | 464 | --Create default state and current state 465 | state = libLyger:style_lookup(line) -- exposed to script 466 | dstate = util.copy(state) -- exposed to script 467 | 468 | --Define position and origin objects 469 | pos, org = {}, {} -- exposed to script 470 | pos.x,pos.y=libLyger:get_pos(line) 471 | org.x,org.y=libLyger:get_org(line) 472 | 473 | --Now cycle through all tag-text pairs 474 | for j,a in ipairs(line_table) do 475 | local fenv = getfenv(1) 476 | fenv.j=j 477 | fenv.line=line 478 | fenv.flags=flags 479 | fenv.maxj=#line_table 480 | 481 | --Wrappers for the once-per-line functions 482 | fenv.duplicate = function() 483 | if j==1 then _duplicate() end 484 | end 485 | fenv.select = function() 486 | if j==1 then _select() end 487 | end 488 | fenv.modify_line = function(prop,func) 489 | if j==1 then _modify_line(prop,func) end 490 | end 491 | 492 | local first = j==1 493 | 494 | --Define variables 495 | text, tag = a.text, a.tag -- exposed to script 496 | 497 | --Update state 498 | for tag, param in pairs(state_table[j]) do 499 | state[tag]= param 500 | dstate[tag]= param 501 | end 502 | 503 | --Get the parameter of the given tag 504 | fenv.get = function(b) 505 | local param = tostring(dstate[b]) 506 | if param:match("%b()") then 507 | local c = {} 508 | for d in param:gmatch("[^%(%),]+") do 509 | c[#c+1] = d 510 | end 511 | return unpack(c) 512 | end 513 | return param 514 | end 515 | 516 | --Modify the given tag 517 | fenv.modify = function(b,func) 518 | --Make sure once-per-lines are only modified once 519 | if opl[b] and j~=1 then return end 520 | 521 | local c, d = {get(b)} 522 | if #c==1 then c=c[1] end 523 | if type(c)=="table" then 524 | local e, h, f = {func(unpack(c))}, {"("}, "" 525 | --If modifying pos or org, store values in relevant objects 526 | if b=="pos" then pos.x,pos.y=unpack(e) end 527 | if b=="org" then org.x,org.y=unpack(e) end 528 | d = {"("} 529 | for i, g in ipairs(e) do 530 | d[2*i], d[2*i+1] = f, g 531 | h[2*i], h[2*i+1] = f, c[i] 532 | f = "," 533 | end 534 | d[#d+1], h[#h+1] = ")", ")" 535 | c, d = table.concat(h), table.concat(d) 536 | else 537 | d=func(c) 538 | if tonumber(d) then 539 | d = f2s(tonumber(d)) 540 | end 541 | -- if modifying a vector clip, wrap tags in parentheses 542 | if b == "clip" or b == "iclip" then 543 | c, d = "("..c..")", "("..d..")" 544 | end 545 | end 546 | --Prevent redundancy 547 | if state[b]~=d then 548 | local mod_tag, num = "\\"..b..esc(d) 549 | tag, num = tag:gsub("\\"..b..esc(c), mod_tag) 550 | if num<1 and not opl[b] then insert(mod_tag) end 551 | state[b]=d 552 | end 553 | end 554 | 555 | --Remove the given tags 556 | fenv.remove = function(...) 557 | tag = LibLyger.line_exclude(tag, table.pack(...)) 558 | end 559 | 560 | --Insert the given tag at the end 561 | fenv.insert = function(b) 562 | tag=tag:gsub("}$",b.."}") 563 | end 564 | 565 | --Select every 566 | fenv.isel = function(n) 567 | if i%n==1 then select() end 568 | end 569 | 570 | --Aliases for common functions 571 | fenv.mod=modify 572 | fenv.mul=multiply 573 | fenv.rep=replace 574 | fenv.app=append 575 | fenv.modln=modify_line 576 | fenv.rem=fenv.remove 577 | 578 | --Run the user's code 579 | local com, err = loadstring(command) 580 | 581 | if err then 582 | aegisub.log(err) 583 | aegisub.cancel() 584 | end 585 | 586 | setfenv(com, fenv)() 587 | 588 | a.text=text 589 | a.tag=tag 590 | end 591 | 592 | for _,task in ipairs(tasklist) do 593 | task() 594 | end 595 | 596 | --Rebuild 597 | local rebuilt_text = {} 598 | for r, a in ipairs(line_table) do 599 | rebuilt_text[r*2-1], rebuilt_text[r*2] = a.tag, a.text 600 | end 601 | line.text = table.concat(rebuilt_text):gsub("{}","") 602 | 603 | --Update position and org 604 | local px, py = libLyger:get_pos(line) 605 | if px ~= pos.x or py ~= pos.y then 606 | local ptag = string.format("\\pos(%s,%s)", f2s(pos.x), f2s(pos.y)) 607 | local num 608 | line.text, num = line.text:gsub("\\pos%b()", esc(ptag)) 609 | if num < 1 then 610 | line.text = line.text:gsub("{", "{"..esc(ptag), 1) 611 | end 612 | end 613 | local ox, oy = libLyger:get_org(line) 614 | if ox ~= org.x or oy ~= org.y then 615 | local otag = string.format("\\org(%s,%s)", f2s(org.x), f2s(org.y)) 616 | local num 617 | line.text, num = line.text:gsub("\\org%b()", esc(otag)) 618 | if num < 1 and (ox ~= pos.x or oy ~= pos.y) then 619 | line.text = line.text:gsub("{", "{"..esc(otag), 1) end 620 | end 621 | 622 | --Reinsert 623 | sub[li]=line 624 | 625 | --Increment 626 | i=i+1 627 | end 628 | 629 | aegisub.set_undo_point(script_name) 630 | 631 | --Return new selection or old selection 632 | if #new_sel>0 then return new_sel end 633 | return sel 634 | 635 | end 636 | 637 | rec:registerMacro(lua_interpret) -------------------------------------------------------------------------------- /macros/lyger.ModifyMocha.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | README 3 | 4 | Mass Modify Mocha Lines 5 | 6 | Basically a more robust, automatic find-and-replace that lets you modify the appearance of mocha-tracked 7 | frame-by-frame typesets, without having to re-apply the motion data. 8 | 9 | Duplicate the first line of the typeset you want to modify. In the actor field, mark one of the duplicates 10 | "orig" (for original) and one of the duplicates "mod" (for modified). 11 | 12 | You can comment out the "orig" line for now, and alter the "mod" line until it looks the way you want it 13 | to. DON'T TOUCH THE POSITIONING or anything else the mocha data might have affected. If you want to shift 14 | the position of your typeset, use the position shifter automation. 15 | 16 | Obviously, don't touch the "orig" line at all. The script will use that line for comparison, to determine 17 | what needs to be modified in the rest of the lines. 18 | 19 | Now highlight all the lines you want to change, as well as the "orig" and "mod" lines. Run this script. 20 | Now every frame of the mocha-tracked typeset will be altered to look like the "modified" line. You can 21 | add a letter-by-letter gradient, change the font size, even change the font. 22 | 23 | The text of the line (without the tags) must be EXACTLY THE SAME as it was before. If you want to change 24 | the text of the typeset, then for the love of god use find-and-replace. This script is meant for those 25 | occasions when you would have to find-and-replace for a dozen different color codes and you're getting a 26 | headache keeping track of them all. 27 | 28 | ]]-- 29 | 30 | script_name = "Mass modify mocha lines" 31 | script_description = "Allows you to quickly change the appearance of mocha tracked lines without reapplying motion data." 32 | script_version = "0.2.0" 33 | script_author = "lyger" 34 | script_namespace = "lyger.ModifyMocha" 35 | 36 | local DependencyControl = require("l0.DependencyControl") 37 | local rec = DependencyControl{ 38 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 39 | { 40 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, 41 | "aegisub.util" 42 | } 43 | } 44 | local LibLyger, util = rec:requireModules() 45 | local libLyger = LibLyger() 46 | local logger = rec:getLogger() 47 | 48 | --Tags that are not worth dealing with in the scope of this script 49 | local global_excludes = { 50 | "pos", 51 | "move", 52 | "org", 53 | "clip", 54 | "t", 55 | "r", 56 | "fad", 57 | "fade" 58 | } 59 | 60 | local function make_full_state_table(line_table) 61 | local this_state_table={} 62 | for i,val in ipairs(line_table) do 63 | this_state_table[i]={} 64 | local pstate = libLyger.line_exclude(val.tag,global_excludes) 65 | --\fn has special behavior, so check if it's there and if so, remove it 66 | --so the rest of the code doesn't have to deal with it 67 | pstate=pstate:gsub("\\fn([^\\{}]*)", function(a) 68 | this_state_table[i]["fn"]=a 69 | return "" 70 | end) 71 | for tagname,tagvalue in pstate:gmatch("\\([1-4]?%a+)([^\\{}]*)") do 72 | this_state_table[i][tagname]=tagvalue 73 | end 74 | end 75 | 76 | return this_state_table 77 | end 78 | 79 | 80 | --Modify the state tables so that all relevant tags in one table have corresponding partners in the other 81 | --If do_default is true, then it will draw from style defaults when not previously overridden 82 | local function match_state_tables(stable1,sstyle1,stable2,sstyle2,do_default) 83 | local current_state1={} 84 | local current_state2={} 85 | 86 | for i,val1 in ipairs(stable1) do 87 | --build current state tables 88 | for key1,param1 in pairs(val1) do 89 | current_state1[key1]=param1 90 | end 91 | for key2,param2 in pairs(stable2[i]) do 92 | current_state2[key2]=param2 93 | end 94 | 95 | --check if end is missing any tags that start has 96 | for key1,param1 in pairs(val1) do 97 | if stable2[i][key1]==nil then 98 | if current_state2[key1]==nil and do_default then 99 | stable2[i][key1]=sstyle2[key1] 100 | else 101 | stable2[i][key1]=current_state2[key1] 102 | end 103 | end 104 | end 105 | --check if start is missing any tags that end has 106 | for key2,param2 in pairs(stable2[i]) do 107 | if stable1[i][key2]==nil then 108 | if current_state1[key2]==nil and do_default then 109 | stable1[i][key2]=sstyle1[key2] 110 | else 111 | stable1[i][key2]=current_state1[key2] 112 | end 113 | end 114 | end 115 | end 116 | return stable1,stable2 117 | end 118 | 119 | --The main body that performs the modification 120 | function modify_mocha(sub,sel) 121 | libLyger:set_sub(sub, sel) 122 | 123 | --Find the "original" and "modified" lines 124 | local oline,mline 125 | local oindex,mindex 126 | 127 | for si,li in ipairs(sel) do 128 | checkline = libLyger.lines[li] 129 | if checkline.actor:lower():find("^orig$") then 130 | oline = libLyger.lines[li] 131 | oindex = li 132 | elseif checkline.actor:lower():find("^mod$") then 133 | mline = libLyger.lines[li] 134 | mindex = li 135 | end 136 | if oline and mline then break end 137 | end 138 | 139 | if not (oline and mline) then 140 | aegisub.dialog.display({ {class="label", label="Please mark the original line with \"orig\"\n".. 141 | "and the modified line with \"mod\" in the\nactor field"} }) 142 | return 143 | end 144 | 145 | --Break them into line tables and match the splits 146 | local otable, mtable = {}, {} 147 | 148 | local x = 1 149 | for thistag,thistext in oline.text:gmatch("({[^{}]*})([^{}]*)") do 150 | otable[x]={tag=thistag,text=thistext} 151 | x = x + 1 152 | end 153 | 154 | x = 1 155 | for thistag,thistext in mline.text:gmatch("({[^{}]*})([^{}]*)") do 156 | mtable[x]={tag=thistag,text=thistext} 157 | x = x + 1 158 | end 159 | 160 | otable, mtable = libLyger.match_splits(otable,mtable) 161 | --Parse the line tables into full state tables 162 | --(requires new code, since previous state table depended on a list of tags to search for) 163 | 164 | local o_state_table = make_full_state_table(otable) 165 | local m_state_table = make_full_state_table(mtable) 166 | 167 | --Compare the state tables and store the differences in a new state table 168 | --This state table will go along with the modified line's line table 169 | 170 | local ostyle=libLyger:style_lookup(oline) 171 | local mstyle=libLyger:style_lookup(mline) 172 | 173 | o_state_table, m_state_table = match_state_tables(o_state_table,ostyle,m_state_table,mstyle,true) 174 | 175 | local delta_state_table = {} 176 | 177 | --Find differences and add to delta 178 | for i,mval in ipairs(m_state_table) do 179 | delta_state_table[i]={} 180 | for mtag,mparam in pairs(mval) do 181 | if o_state_table[i][mtag]~=mparam or 182 | (m_state_table[i-1]~=nil and m_state_table[i-1][mtag]~=mparam) then 183 | delta_state_table[i][mtag]=mparam 184 | end 185 | end 186 | end 187 | 188 | --Now scan all the remaining lines 189 | --(being sure to store the indices of the original/modified lines so they can be skipped) 190 | for si,li in ipairs(sel) do 191 | if li~=mindex and li~=oindex then 192 | aegisub.progress.set((si-1)/#sel*100) 193 | local this_line = libLyger.lines[li] 194 | 195 | --Make sure this line starts with tags 196 | if this_line.text:find("^{")==nil then this_line.text="{}"..this_line.text end 197 | 198 | --Split it into a line table 199 | local this_table, x = {}, 1 200 | for thistag,thistext in this_line.text:gmatch("({[^{}]*})([^{}]*)") do 201 | this_table[x]={tag=thistag,text=thistext} 202 | x = x + 1 203 | end 204 | 205 | local mtable_copy = util.deep_copy(mtable) 206 | local delta_state_copy = util.deep_copy(delta_state_table) 207 | 208 | --Custom match split on the copied modified line and the current line, 209 | --which modifies state table too 210 | local j=1 211 | while(j<=#mtable_copy) do 212 | local mtext, mtag = mtable_copy[j].text, mtable_copy[j].tag 213 | local ttext, ttag = this_table[j].text, this_table[j].tag 214 | 215 | --If the mtable item has longer text, break it in two based on the text of this_table 216 | if mtext:len() > ttext:len() then 217 | local newtext = mtext:match(ttext.."(.*)") 218 | for k=#mtable_copy,j+1,-1 do 219 | mtable_copy[k+1]=mtable_copy[k] 220 | delta_state_copy[k+1]=delta_state_copy[k] 221 | end 222 | delta_state_copy[j]={} 223 | mtable_copy[j]={tag=mtag,text=ttext} 224 | mtable_copy[j+1]={tag="{}",text=newtext} 225 | --If the this_table item has longer text, break it in two based on the text of mtable 226 | elseif mtext:len() < ttext:len() then 227 | local newtext = ttext:match(mtext.."(.*)") 228 | for k=#this_table,j+1,-1 do 229 | this_table[k+1]=this_table[k] 230 | end 231 | this_table[j]={tag=ttag,text=mtext} 232 | this_table[j+1]={tag="{}",text=newtext} 233 | end 234 | j=j+1 235 | end 236 | 237 | --Generate state table 238 | local this_state_table = make_full_state_table(this_table) 239 | 240 | --Match state tables 241 | local this_style = libLyger:style_lookup(this_line) 242 | 243 | delta_state_copy,this_state_table= 244 | match_state_tables(delta_state_copy,mstyle,this_state_table,this_style,false) 245 | 246 | --[[DEBUG]]-- 247 | --[[for i,val in ipairs(delta_state_table) do 248 | for tag,param in pairs(val) do 249 | aegisub.log("In tag block "..i.." will replace "..tag.." tag with "..param.."\n") 250 | end 251 | end]]-- 252 | 253 | --For each tag block in the current line, remove the relevant tags in the delta state table 254 | local rebuilt_line = {} 255 | for i,tval in ipairs(this_table) do 256 | for dtag,dparam in pairs(delta_state_copy[i]) do 257 | if dtag=="fn" then tval.tag=tval.tag:gsub("\\"..dtag.."[^\\{}]*","") 258 | else tval.tag=tval.tag:gsub("\\"..dtag.."%A[^\\{}]*","") end 259 | tval.tag=tval.tag:gsub("}","\\"..dtag..dparam.."}") 260 | end 261 | rebuilt_line[i*2-1], rebuilt_line[i*2] = tval.tag, tval.text 262 | end 263 | 264 | --Re-insert 265 | this_line.text = table.concat(rebuilt_line):gsub("{}","") 266 | sub[li] = this_line 267 | end 268 | end 269 | 270 | oline.comment = true 271 | oline.actor = "*"..oline.actor 272 | sub[oindex] = oline 273 | end 274 | 275 | rec:registerMacro(modify_mocha) -------------------------------------------------------------------------------- /macros/lyger.MoveClip.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Move with Clip 5 | 6 | Turns lines with \pos and a rectangular \clip into lines with \move and \t that moves 7 | the clip correspondingly. 8 | 9 | Quick-and-dirty script with no failsafes. Requires \pos tag and rectangular \clip tag 10 | to be present in selected line(s) in order to work. 11 | 12 | ]] 13 | 14 | script_name = "Move with clip" 15 | script_description = "Moves both position and rectangular clip." 16 | script_version = "1.2.0" 17 | script_author = "lyger" 18 | script_namespace = "lyger.MoveClip" 19 | 20 | local DependencyControl = require("l0.DependencyControl") 21 | local rec = DependencyControl{ 22 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 23 | { 24 | {"lyger.LibLyger", version = "2.0.0", url = "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"} 25 | } 26 | } 27 | local LibLyger = rec:requireModules() 28 | local libLyger = LibLyger() 29 | 30 | local config = { 31 | {class="label",label="x change:",x=0,y=0,width=1,height=1}, 32 | {class="floatedit",name="d_x",x=1,y=0,width=1,height=1,value=0}, 33 | {class="label",label="y change:",x=0,y=1,width=1,height=1}, 34 | {class="floatedit",name="d_y",x=1,y=1,width=1,height=1,value=0} 35 | } 36 | 37 | function move_clip(sub, sel) 38 | local pressed, results = aegisub.dialog.display(config,{"Move","Cancel"}) 39 | local d_x, d_y = results["d_x"], results["d_y"] 40 | local f2s = libLyger.float2str 41 | libLyger:set_sub(sub, sel) 42 | 43 | for _,li in ipairs(sel) do 44 | local line = libLyger.lines[li] 45 | 46 | if line.text:match("\\clip%([%d%-%.]+,[%d%-%.]+,[%d%-%.]+,[%d%-%.]+%)") then 47 | local dur = line.end_time-line.start_time 48 | local ox, oy = libLyger:get_pos(line) 49 | 50 | line.text=line.text:gsub("\\pos%([%d%-%.]+,[%d%-%.]+%)","") 51 | line.text=line.text:gsub("\\move%([%d%-%.,]+%)","") 52 | line.text=line.text:gsub("{", 53 | function() 54 | local x1=tonumber(ox) 55 | local y1=tonumber(oy) 56 | return string.format("{\\move(%s,%s,%s,%s,%d,%d)", 57 | f2s(x1),f2s(y1),f2s(x1+d_x),f2s(y1+d_y),0,dur) 58 | end,1) 59 | 60 | line.text=line.text:gsub("\\clip%(([%d%-%.]+),([%d%-%.]+),([%d%-%.]+),([%d%-%.]+)%)", 61 | function(x1,y1,x2,y2) 62 | local x1=tonumber(x1) 63 | local x2=tonumber(x2) 64 | local y1=tonumber(y1) 65 | local y2=tonumber(y2) 66 | return string.format("\\clip(%s,%s,%s,%s)\\t(%d,%d,\\clip(%s,%s,%s,%s))", 67 | f2s(x1),f2s(y1),f2s(x2),f2s(y2),0,dur, 68 | f2s(x1+d_x),f2s(y1+d_y),f2s(x2+d_x),f2s(y2+d_y)) 69 | end) 70 | end 71 | 72 | sub[li]=line 73 | end 74 | 75 | return sel 76 | end 77 | 78 | aegisub.register_macro(script_name,script_description,move_clip) 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /macros/lyger.SemiColorCalc.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Semitransparent Color Calculator 3 | 4 | Does what it says. Use the eyedropper to select the background color 5 | and the target color (that is, the color the original sign looks like. 6 | For example, a semitransparent red sign on a white background will 7 | look pink. That pink is your target color). 8 | 9 | Then input your estimate of the opacity (which is in percent, and not 10 | the 0-255 scale the \alpha tag uses, because percentages are easier to 11 | get an intuition for. Also, 100% is solid alpha, not 0%). 12 | 13 | Check which colors to apply to, and the script does the rest. 14 | ]] 15 | 16 | script_name = "Semitransparent color calculator" 17 | script_description = "Input a target and background color to calculate the original color." 18 | script_version = "1.1.0" 19 | script_author = "lyger" 20 | script_namespace = "lyger.SemiColorCalc" 21 | 22 | local DependencyControl = require("l0.DependencyControl") 23 | local rec = DependencyControl{ 24 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 25 | { "aegisub.util" } 26 | } 27 | local util = rec:requireModules() 28 | 29 | local conf = 30 | { 31 | {x=0,y=0,width=4,height=1,class="label",label="Color(s):"}, 32 | ["c1"]={x=0,y=1,width=1,height=1,class="checkbox",name="c1",label="1",value=false}, 33 | ["c2"]={x=1,y=1,width=1,height=1,class="checkbox",name="c2",label="2",value=false}, 34 | ["c3"]={x=2,y=1,width=1,height=1,class="checkbox",name="c3",label="3",value=false}, 35 | ["c4"]={x=3,y=1,width=1,height=1,class="checkbox",name="c4",label="4",value=false}, 36 | {x=0,y=2,width=2,height=1,class="label",label="Background:"}, 37 | ["bg"]={x=2,y=2,width=2,height=1,class="color",name="bg"}, 38 | {x=0,y=3,width=2,height=1,class="label",label="Target:"}, 39 | ["tg"]={x=2,y=3,width=2,height=1,class="color",name="tg"}, 40 | {x=0,y=4,width=2,height=1,class="label",label="Opacity (%):"}, 41 | ["al"]={x=2,y=4,width=2,height=1,class="floatedit",max=100,min=0,name="al",value=50,step=1} 42 | } 43 | 44 | function choke(v, min, max, clamp_seen) 45 | if vmax then return max, true end 47 | return v, clamp_seen or false 48 | end 49 | 50 | function c_calc(sub,sel) 51 | local pressed,results=aegisub.dialog.display(conf,{"Go","Cancel"},{ok="Go",cancel="Cancel"}) 52 | if pressed=="Cancel" then aegisub.cancel() end 53 | 54 | --Update conf for convenience 55 | for k,v in pairs(results) do 56 | conf[k].value=v 57 | end 58 | 59 | --Calculate the color 60 | local bg = {util.extract_color(results.bg)} 61 | local tg = {util.extract_color(results.tg)} 62 | 63 | local f, c, warning = results.al/100, {}, false 64 | for i=1,3 do 65 | c[i], warning = choke((tg[i]-bg[i])/f+bg[i], 0, 255, warning) 66 | end 67 | 68 | local cstr = util.ass_color(unpack(c)) 69 | local at={} 70 | local astr, at = util.ass_alpha(255*(1-f)), {} 71 | 72 | for i=1,4 do 73 | if results["c"..i] then table.insert(at,i) end 74 | end 75 | 76 | if #at==0 then aegisub.cancel() end 77 | 78 | --Handle inserting into lines 79 | for si,li in ipairs(sel) do 80 | local line=sub[li] 81 | line.text= 82 | line.text:gsub("\\c","\\1c"):gsub("\\alpha[Hh&%x]+",""):gsub("\\[1-4]a[Hh&%x]+","") 83 | 84 | local tags, t = {"{"}, 2 85 | if #at == 4 then 86 | tags[2], tags[3], t = "\\alpha", astr, 4 87 | else 88 | for _,an in ipairs(at) do 89 | tags[t] = string.format("\\%da%s",an,astr) 90 | t = t + 1 91 | end 92 | end 93 | 94 | for _,an in ipairs(at) do 95 | line.text=line.text:gsub("\\"..an.."c[Hh&%x]+","") 96 | tags[t] = ("\\%dc%s"):format(an, cstr) 97 | t = t + 1 98 | end 99 | 100 | if not line.text:match("^{") then line.text="{}"..line.text end 101 | 102 | line.text = line.text:gsub("^{",table.concat(tags)):gsub("\\1c","\\c") 103 | 104 | sub[li]=line 105 | end 106 | 107 | if warning then 108 | aegisub.dialog.display( 109 | {{x=0,y=0,width=1,height=1,class="label", 110 | label="WARNING: Calculated color out of bounds.\n".. 111 | "Try increasing your opacity."}},{"OK"},{ok="OK"}) 112 | end 113 | 114 | aegisub.set_undo_point(script_name) 115 | end 116 | 117 | rec:registerMacro(c_calc) -------------------------------------------------------------------------------- /macros/lyger.VecClipGradient.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ==README== 3 | 4 | Clip Gradient 5 | 6 | Intersects a vector clip with the highlighted rectangular-clipped gradient. 7 | 8 | Only allows non-compound (i.e. only one "m") vector shapes with no bezier curves. 9 | 10 | It turns out it is possible to do this by using two \clip tags in the same line. 11 | This script can be better if you are concerned about lag in a big gradient, but 12 | yeah, in essence this automation is redundant. 13 | 14 | ]]-- 15 | 16 | script_name = "Vector-Clip Gradient" 17 | script_description = "Intersects the rectangular clips on a gradient with a specified vector clip." 18 | script_version = "1.1.0" 19 | script_author = "lyger" 20 | script_namespace = "lyger.VecClipGradient" 21 | 22 | local DependencyControl = require("l0.DependencyControl") 23 | local rec = DependencyControl{ 24 | feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 25 | {"aegisub.util"} 26 | } 27 | local util = rec:requireModules() 28 | 29 | --Distance between two points 30 | local function distance(x1,y1,x2,y2) 31 | return math.sqrt((x2-x1)^2+(y2-y1)^2) 32 | end 33 | 34 | --Sign of a value 35 | local function sign(n) 36 | return n/math.abs(n) 37 | end 38 | 39 | --Parses vector shape and makes it into a table 40 | --This modified version adds pointer fields to make the table into a circular linked list 41 | --It also ignores the exponential factor because who uses that anyway 42 | function make_linked_vector_table(vstring) 43 | local vtable, v = {}, 0 44 | for vtype,vcoords in vstring:gmatch("([mlb])([%d%s%-]+)") do 45 | for vx,vy in vcoords:gmatch("([%d%-]+)%s+([%d%-]+)") do 46 | v = v + 1 47 | vtable[v] = {class = vtype, x = tonumber(vx), y = tonumber(vy)} 48 | end 49 | end 50 | 51 | for i=1, v-1 do 52 | vtable[i].next=vtable[i+1] 53 | end 54 | vtable[v].next=vtable[1] 55 | 56 | return vtable 57 | end 58 | 59 | --Reverses a vector table object 60 | function reverse_vector_table(vtable) 61 | local nvtable={} 62 | if #vtable<1 then return nvtable end 63 | --Make sure vtable does not end in an m. I don't know why this would happen but still 64 | local maxi = #vtable 65 | while vtable[maxi].class=="m" do 66 | maxi=maxi-1 67 | end 68 | 69 | --All vector shapes start with m 70 | nvtable[1] = util.copy(vtable[maxi]) 71 | local tclass = nvtable[1].class 72 | nvtable[1].class = "m" 73 | 74 | --Reinsert coords in backwards order, but shift the class over by 1 75 | --because that's how vector shapes behave in aegi 76 | for i=maxi-1,1,-1 do 77 | local tcoord = util.copy(vtable[i]) 78 | tcoord.class, tclass = tclass, tcoord.tclass 79 | nvtable[#nvtable+1] = tcoord 80 | end 81 | 82 | return nvtable 83 | end 84 | 85 | --Turns vector table into string 86 | function vtable_to_string(vt) 87 | local result, cclass = {} 88 | 89 | for i=1,#vt,1 do 90 | if vt[i].class~=cclass then 91 | result[i] = string.format("%s %d %d ",vt[i].class,vt[i].x,vt[i].y) 92 | cclass = vt[i].class 93 | else 94 | result[i] = string.format("%d %d ",vt[i].x,vt[i].y) 95 | end 96 | end 97 | 98 | return table.concat(result) 99 | end 100 | 101 | --Rounds to the given number of decimal places 102 | function round(n,dec) 103 | dec=dec or 0 104 | return math.floor(n*10^dec+0.5)/(10^dec) 105 | end 106 | 107 | --Returns chirality of vector shape. +1 if counterclockwise, -1 if clockwise 108 | function get_chirality(vt) 109 | local wvt, trot = wrap(vt), 0 110 | for i = 2, #wvt - 1 do 111 | local rot1=math.atan2(wvt[i].y-wvt[i-1].y,wvt[i].x-wvt[i-1].x) 112 | local rot2=math.atan2(wvt[i+1].y-wvt[i].y,wvt[i+1].x-wvt[i].x) 113 | local drot=math.deg(rot2-rot1)%360 114 | if drot>180 then drot=360-drot else drot=-1*drot end 115 | trot=trot+drot 116 | end 117 | return sign(trot) 118 | end 119 | 120 | --Duplicates first and last coordinates at the end and beginning of shape, 121 | --to allow for wraparound calculations 122 | function wrap(vt) 123 | local wvt = {util.copy(vt[#vt])} 124 | for i = 1, #vt do 125 | wvt[i+1] = util.copy(vt[i]) 126 | end 127 | wvt[#vt+1] = util.copy(vt[1]) 128 | return wvt 129 | end 130 | 131 | --Cuts off the first and last coordinates, to undo the effects of "wrap" 132 | function unwrap(wvt) 133 | local vt={} 134 | for i = 2, #wvt - 1 do 135 | vt[i-1] = util.copy(wvt[i]) 136 | end 137 | return vt 138 | end 139 | 140 | --Returns v value of intersection at a given u 141 | function uintercept(u1,v1,u2,v2,uint) 142 | local m=(v2-v1)/(u2-u1) 143 | local c=v1-u1*m 144 | return m*uint+c 145 | end 146 | 147 | --Returns u value of intersection at a given v 148 | function vintercept(u1,v1,u2,v2,vint) 149 | local m=(u2-u1)/(v2-v1) 150 | local c=u1-v1*m 151 | return m*vint+c 152 | end 153 | 154 | 155 | --Intersects the vector in vt (a linked vector table) with the given rectangular coords 156 | function intersect(vt,tp,bm,lt,rt,vert,ch) 157 | 158 | --This is the function that's going to consume my soul =__= 159 | 160 | --CHIRALITY +1 161 | --Increasing y in the shape means inside is to the direction of increasing x 162 | --Increasing x in the shape means inside is to the direction of decreasing y 163 | --CHIRALITY -1 164 | --Increasing y in the shape means inside is to the direction of decreasing x 165 | --Increasing x in the shape means inside is to the direction of increasing y 166 | 167 | --Refactor into u and v coordinates, where u is the direction of the gradient 168 | --and v is the orthagonal 169 | --chmod is the chirality modifier. I'll figure out how it's used later. 170 | --ub and vb are the u and v bounds. 1 is lower, 2 is higher 171 | local u, v = "x", "y" 172 | local ub1, ub2, vb1, vb2 = lt, rt, tp, bm 173 | local chmod = -1 174 | if vert then 175 | u, v = "y", "x" 176 | chmod=1 177 | ub1, ub2, vb1, vb2 = tp, bm, lt, rt 178 | end 179 | 180 | --Find minimum v 181 | local minv, iminv = 10000, 0 182 | for i,vect in ipairs(vt) do 183 | if vect[v]=ub2 then czone="c" 229 | elseif curr[u]<=ub1 then czone="a" 230 | else czone="b" end 231 | 232 | if vnext[u]>=ub2 then nzone="c" 233 | elseif vnext[u]<=ub1 then nzone="a" 234 | else nzone="b" end 235 | 236 | --Check ALL the conditions 237 | --Staying between the lines 238 | if czone=="b" and nzone=="b" then 239 | if vert then 240 | nshape[n+1] = string.format(nclass.." %d %d ", curr[v], curr[u]) 241 | else 242 | nshape[n+1] = string.format(nclass.." %d %d ", curr[u], curr[v]) 243 | end 244 | n = n + 1 245 | --If a shape is not already open, abort 246 | if not open then abort = true end 247 | 248 | --Entering from above or below 249 | elseif (czone=="a" or czone=="c") and nzone=="b" then 250 | local uint = czone == "c" and ub2 or ub1 251 | 252 | local newv = round(uintercept(curr[u], curr[v], vnext[u], vnext[v], uint)) 253 | if vert then 254 | nshape[n+1] = string.format(nclass.." %d %d ", newv, uint) 255 | else 256 | nshape[n+1] = string.format(nclass.." %d %d ", uint, newv) 257 | end 258 | n = n + 1 259 | 260 | if open and sign(newv-exitv) ~= inside then 261 | --Abort if on the wrong side of the last exit v coordinate 262 | abort = true 263 | --Otherwise open a new shape 264 | elseif not open then 265 | open = true 266 | nclass="l" 267 | firstcross=uint 268 | inside=sign(vnext[u]-curr[u])*ch*chmod 269 | end 270 | --Exiting from above or below 271 | elseif czone=="b" and (nzone=="a" or nzone=="c") then 272 | local uint = nzone == "c" and ub2 or ub1 273 | 274 | local newv = round(uintercept(curr[u], curr[v], vnext[u], vnext[v], uint)) 275 | if vert then 276 | nshape[n+1] = string.format(nclass.." %d %d l %d %d ", 277 | curr[v], curr[u], newv, uint) 278 | else 279 | nshape[n+1] = string.format(nclass.." %d %d l %d %d ", 280 | curr[u], curr[v], uint, newv) 281 | end 282 | n = n + 1 283 | 284 | if open then 285 | --Recrossing the line initially crossed closes a shape 286 | if uint==firstcross then 287 | open=false 288 | nclass="m" 289 | --Otherwise, this is the last exit point 290 | else 291 | exitv=newv 292 | end 293 | 294 | --If a shape is not already open, abort 295 | else 296 | abort=true 297 | end 298 | --Crossing both lines from below or above 299 | elseif (czone=="c" and nzone=="a") or (czone=="a" and nzone=="c") then 300 | local uint1, uint2 = ub1, ub2 301 | if czone == "c" then 302 | uint1, uint2 = ub2, ub1 303 | end 304 | 305 | local newv1 = round(uintercept(curr[u], curr[v], vnext[u], vnext[v], uint1)) 306 | local newv2 = round(uintercept(curr[u], curr[v], vnext[u], vnext[v], uint2)) 307 | if vert then 308 | nshape[n+1] = string.format(nclass.." %d %d l %d %d ", 309 | newv1, uint1, newv2, uint2) 310 | else 311 | nshape[n+1] = string.format(nclass.." %d %d l %d %d ", 312 | uint1, newv1, uint2, newv2) 313 | end 314 | n = n +1 315 | 316 | --If it's already open, this should close the shape 317 | if open then 318 | --Abort if it crosses on the wrong side 319 | if sign(newv1-exitv)~=inside then abort=true end 320 | open=false 321 | nclass="m" 322 | 323 | --Otherwise open a new shape 324 | else 325 | open=true 326 | nclass="l" 327 | firstcross=uint1 328 | inside=sign(vnext[u]-curr[u])*ch*chmod 329 | end 330 | end 331 | 332 | curr=vnext 333 | count=count+1 334 | until count>=#vt or abort 335 | 336 | if abort then 337 | nshape = {} 338 | start=start.next 339 | end 340 | 341 | imaginebreaker=imaginebreaker+1 342 | until not abort or imaginebreaker>#vt 343 | 344 | return table.concat(nshape) 345 | end 346 | 347 | --Main execution function 348 | function clip_clip(sub,sel) 349 | 350 | --GUI config 351 | config= 352 | { 353 | { 354 | class="label", 355 | label="Gradient type:", 356 | x=0,y=0,width=1,height=1 357 | }, 358 | { 359 | class="dropdown", 360 | name="gtype", 361 | items={"horizontal","vertical"}, 362 | value="horizontal", 363 | x=1,y=0,width=1,height=1 364 | }, 365 | { 366 | class="label", 367 | label="Paste your clipping shape here:", 368 | x=0,y=1,width=2,height=1 369 | }, 370 | { 371 | class="textbox", 372 | name="shape", 373 | x=0,y=2,width=20,height=6 374 | } 375 | } 376 | 377 | --Show dialog 378 | local pressed, results = aegisub.dialog.display(config,{"Go","Cancel"}) 379 | if pressed=="Cancel" then aegisub.cancel() end 380 | 381 | --String of the vector shape 382 | local sshape = results.shape 383 | 384 | --Boolean that is true if the gradient is vertical, false if it's horizontal 385 | local vertical= results.gtype ~= "horizontal" 386 | 387 | --Enforce limitations on vector shape 388 | if sshape:match("b") then 389 | aegisub.dialog.display( 390 | {{class="label",x=0,y=0,width=1,height=1, 391 | label="This version does not support shapes with beziers."}}, 392 | {"OK"}) 393 | aegisub.cancel() 394 | end 395 | 396 | local _, mcount = sshape:gsub("m","m") 397 | if mcount>1 then 398 | aegisub.dialog.display( 399 | {{class="label",x=0,y=0,width=1,height=1, 400 | label="This version does not support compound shapes (more than one \"m\")."}}, 401 | {"OK"}) 402 | aegisub.cancel() 403 | end 404 | 405 | --Vector table object for this shape 406 | local svt = make_linked_vector_table(sshape) 407 | 408 | if #svt<3 then 409 | aegisub.dialog.display( 410 | {class="label",x=0,y=0,width=1,height=1, 411 | label="You're gonna need a bigger vector shape."}, 412 | {"OK"}) 413 | aegisub.cancel() 414 | 415 | end 416 | 417 | --Chirality 418 | local chir = get_chirality(svt) 419 | 420 | --Table of lines to delete 421 | local to_delete = {} 422 | 423 | --Process selected lines 424 | for si,li in ipairs(sel) do 425 | 426 | --Progress report 427 | aegisub.progress.task("Processing line "..si.."/"..#sel) 428 | aegisub.progress.set(100*si/#sel) 429 | 430 | --Read in the line 431 | local line = sub[li] 432 | 433 | --Find the clipping shape 434 | local ctype, tvector = line.text:match("\\(i?clip)%(([^%(%)]+)%)") 435 | 436 | --Error 437 | if not ctype then 438 | aegisub.dialog.display( 439 | {{class="label",x=0,y=0,width=1,height=1, 440 | label="Where is your \\clip, foo'?"}}, 441 | {"OK"}) 442 | aegisub.cancel() 443 | end 444 | 445 | --Get the coords 446 | local left, bottom, right, top = tvector:match("([%d%-]+),([%d%-]+),([%d%-]+),([%d%-]+)") 447 | 448 | left = tonumber(left) 449 | bottom = tonumber(nottom) 450 | right = tonumber(right) 451 | top = tonumber(top) 452 | 453 | --Error 454 | if not right then 455 | aegisub.dialog.display( 456 | {{class="label",x=0,y=0,width=1,height=1, 457 | label="Rectangular clipped gradients only."}}, 458 | {"OK"}) 459 | aegisub.cancel() 460 | end 461 | 462 | --Make sure coords are correct 463 | if top > bottom then 464 | top, bottom = bottom, top 465 | end 466 | 467 | if left > right then 468 | left, right = right, left 469 | end 470 | 471 | --Calculate the new clip 472 | local newclip = intersect(svt, top, bottom, left, right, vertical, chir) 473 | 474 | --Substitute 475 | line.text = line.text:gsub(ctype.."%(([^%(%)]+)%)", ctype.."("..newclip..")") 476 | 477 | if newclip == "" then 478 | to_delete[#to_delete] = li 479 | end 480 | 481 | --Put the line back 482 | sub[li]=line 483 | 484 | end 485 | 486 | --Cleanup 487 | sub.delete(to_delete) 488 | 489 | aegisub.set_undo_point(script_name) 490 | end 491 | 492 | rec:registerMacro(clip_clip) -------------------------------------------------------------------------------- /macros/split-tags.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | README 3 | 4 | Split at tags 5 | 6 | Pretty self-explanatory. If you ever had a typeset with effects on each character and you wished 7 | each character was on a separate line, you're in luck. This script splits the line into a new 8 | line for each block of tags, with the appropriate position and appearance. For example: 9 | 10 | {\pos(x,y)\c&H0000FF&}This {\c&H0000DD&}is {\c&H0000BB&}a {\c&H000099&}test 11 | 12 | would get split into 13 | 14 | {\pos(x1,y1)\c&H0000FF&}This 15 | {\pos(x2,y2)\c&H0000DD&}is 16 | {\pos(x3,y3)\c&H0000BB&}a 17 | {\pos(x4,y4)\c&H000099&}test 18 | 19 | In theory, after running this script, the appearance of the typeset will be exactly the same, but 20 | every section will be on a different line, allowing you to work with them separately. 21 | 22 | Doesn't support newlines (\N) and at this rate, never will. If someone teaches me about how .ass 23 | calculates newline heights, I might write a separate script to split at newlines. 24 | 25 | 26 | ]]-- 27 | 28 | script_name="Split at tags" 29 | script_description="Splits the line into separate lines based on tag boundaries" 30 | script_version="0.3" 31 | 32 | include("karaskel.lua") 33 | 34 | --Convert float to neatly formatted string 35 | local function float2str(f) return string.format("%.3f",f):gsub("%.(%d-)0+$","%.%1"):gsub("%.$","") end 36 | 37 | --Creates a deep copy of the given table 38 | local function deep_copy(source_table) 39 | new_table={} 40 | for key,value in pairs(source_table) do 41 | --Let's hope the recursion doesn't break things 42 | if type(value)=="table" then value=deep_copy(value) end 43 | new_table[key]=value 44 | end 45 | return new_table 46 | end 47 | 48 | --Creates a slightly less deep copy of the given table 49 | local function shallow_copy(source_table) 50 | new_table={} 51 | for key,value in pairs(source_table) do 52 | new_table[key]=value 53 | end 54 | return new_table 55 | end 56 | 57 | --[[ 58 | Tags that can have any character after the tag declaration: 59 | \r 60 | \fn 61 | Otherwise, the first character after the tag declaration must be: 62 | a number, decimal point, open parentheses, minus sign, or ampersand 63 | ]]-- 64 | 65 | --Remove listed tags from the given text 66 | local function line_exclude(text, exclude) 67 | remove_t=false 68 | local new_text=text:gsub("\\([^\\{}]*)", 69 | function(a) 70 | if a:find("^r")~=nil then 71 | for i,val in ipairs(exclude) do 72 | if val=="r" then return "" end 73 | end 74 | elseif a:find("^fn")~=nil then 75 | for i,val in ipairs(exclude) do 76 | if val=="fn" then return "" end 77 | end 78 | else 79 | _,_,tag=a:find("^([1-4]?%a+)") 80 | for i,val in ipairs(exclude) do 81 | if val==tag then 82 | --Hacky exception handling for \t statements 83 | if val=="t" then 84 | remove_t=true 85 | return "\\"..a 86 | end 87 | return "" 88 | end 89 | end 90 | end 91 | return "\\"..a 92 | end) 93 | if remove_t then 94 | text=text:gsub("\\t%b()","") 95 | end 96 | return new_text 97 | end 98 | 99 | --Returns the position of a line 100 | local function get_pos(line) 101 | local _,_,posx,posy=line.text:find("\\pos%(([%d%.%-]*),([%d%.%-]*)%)") 102 | if posx==nil then 103 | _,_,posx,posy=line.text:find("\\move%(([%d%.%-]*),([%d%.%-]*),") 104 | if posx==nil then 105 | _,_,align_n=line.text:find("\\an([%d%.%-]*)") 106 | if align_n==nil then 107 | _,_,align_dumb=line.text:find("\\a([%d%.%-]*)") 108 | if align_dumb==nil then 109 | --If the line has no alignment tags 110 | posx=line.x 111 | posy=line.y 112 | else 113 | --If the line has the \a alignment tag 114 | vid_x,vid_y=aegisub.video_size() 115 | align_dumb=tonumber(align_dumb) 116 | if align_dumb>8 then 117 | posy=vid_y/2 118 | elseif align_dumb>4 then 119 | posy=line.eff_margin_t 120 | else 121 | posy=vid_y-line.eff_margin_b 122 | end 123 | _temp=align_dumb%4 124 | if _temp==1 then 125 | posx=line.eff_margin_l 126 | elseif _temp==2 then 127 | posx=line.eff_margin_l+(vid_x-line.eff_margin_l-line.eff_margin_r)/2 128 | else 129 | posx=vid_x-line.eff_margin_r 130 | end 131 | end 132 | else 133 | --If the line has the \an alignment tag 134 | vid_x,vid_y=aegisub.video_size() 135 | align_n=tonumber(align_n) 136 | _temp=align_n%3 137 | if align_n>6 then 138 | posy=line.eff_margin_t 139 | elseif align_n>3 then 140 | posy=vid_y/2 141 | else 142 | posy=vid_y-line.eff_margin_b 143 | end 144 | if _temp==1 then 145 | posx=line.eff_margin_l 146 | elseif _temp==2 then 147 | posx=line.eff_margin_l+(vid_x-line.eff_margin_l-line.eff_margin_r)/2 148 | else 149 | posx=vid_x-line.eff_margin_r 150 | end 151 | end 152 | end 153 | end 154 | return tonumber(posx),tonumber(posy) 155 | end 156 | 157 | --Returns the origin of a line 158 | local function get_org(line) 159 | local _,_,orgx,orgy=line.text:find("\\org%(([%d%.%-]*),([%d%.%-]*)%)") 160 | if orgx==nil then 161 | return get_pos(line) 162 | end 163 | return tonumber(orgx),tonumber(orgy) 164 | end 165 | 166 | --Returns a table of tag-value pairs 167 | --Supports fn but ignores r because fuck r 168 | local function full_state_subtable(tag) 169 | --Store time tags in their own table, so they don't interfere 170 | time_tags={} 171 | for ttag in tag:gmatch("\\t%b()") do 172 | table.insert(time_tags,ttag) 173 | end 174 | 175 | --Remove time tags from the string so we don't have to deal with them 176 | tag=tag:gsub("\\t%b()","") 177 | 178 | state_subtable={} 179 | 180 | for t in tag:gmatch("\\[^\\{}]*") do 181 | ttag,tparam="","" 182 | if t:match("\\fn")~=nil then 183 | ttag,tparam=t:match("\\(fn)(.*)") 184 | else 185 | ttag,tparam=t:match("\\([1-4]?%a+)(%A.*)") 186 | end 187 | state_subtable[ttag]=tparam 188 | end 189 | 190 | --Dump the time tags back in 191 | if #time_tags>0 then 192 | state_subtable["t"]=time_tags 193 | end 194 | 195 | return state_subtable 196 | end 197 | 198 | local function split_tag(sub,sel) 199 | --Read in styles and meta 200 | local meta,styles = karaskel.collect_head(sub, false) 201 | 202 | --How far to offset the next line read 203 | lines_added=0 204 | 205 | for si,li in ipairs(sel) do 206 | 207 | --Progress report 208 | aegisub.progress.task("Processing line "..si.."/"..#sel) 209 | aegisub.progress.set(100*si/#sel) 210 | 211 | --Read in the line 212 | line=sub[li+lines_added] 213 | 214 | --Comment it out 215 | line.comment=true 216 | sub[li+lines_added]=line 217 | line.comment=false 218 | 219 | --Preprocess 220 | karaskel.preproc_line(sub,meta,styles,line) 221 | 222 | --Get position and origin 223 | px,py=get_pos(line) 224 | ox,oy=get_org(line) 225 | 226 | --If there are rotations in the line, then write the origin 227 | do_org=false 228 | 229 | if line.text:match("\\fr[xyz]")~=nil then do_org=true end 230 | 231 | --Turn all \Ns into the newline character 232 | --line.text=line.text:gsub("\\N","\n") 233 | 234 | --Make sure any newline followed by a non-newline character has a tag afterwards 235 | --(i.e. force breaks at newlines) 236 | --line.text=line.text:gsub("\n([^\n{])","\n{}%1") 237 | 238 | --Make line table 239 | line_table={} 240 | for thistag,thistext in line.text:gmatch("({[^{}]*})([^{}]*)") do 241 | table.insert(line_table,{tag=thistag,text=thistext}) 242 | end 243 | 244 | --Stores current state of the line as style table 245 | current_style=deep_copy(line.styleref) 246 | 247 | --Stores the width of each section 248 | substr_data={} 249 | 250 | --Total width of the line 251 | cum_width=0 252 | --Total height of the line 253 | --cum_height=0 254 | --Stores the various cumulative widths for each linebreak 255 | --subs_width={} 256 | --subs_index=1 257 | 258 | --First pass to collect size data 259 | for i,val in ipairs(line_table) do 260 | 261 | --Create state subtable 262 | subtable=full_state_subtable(val.tag) 263 | 264 | --Fix style tables to reflect override tags 265 | current_style.fontname=subtable["fn"] or current_style.fontname 266 | current_style.fontsize=tonumber(subtable["fs"]) or current_style.fontsize 267 | current_style.scale_x=tonumber(subtable["fscx"]) or current_style.scale_x 268 | current_style.scale_y=tonumber(subtable["fscy"]) or current_style.scale_y 269 | current_style.spacing=tonumber(subtable["fsp"]) or current_style.spacing 270 | current_style.align=tonumber(subtable["an"]) or current_style.align 271 | if subtable["b"]~=nil then 272 | if subtable["b"]=="1" then current_style.bold=true 273 | else current_style.bold=false end 274 | end 275 | if subtable["i"]~=nil then 276 | if subtable["i"]=="1" then current_style.italic=true 277 | else current_style.italic=false end 278 | end 279 | if subtable["a"]~=nil then 280 | dumbalign=tonumber(subtable["a"]) 281 | halign=dumbalign%4 282 | valign=0 283 | if dumbalign>8 then valign=3 284 | elseif dumbalign>4 then valign=6 285 | end 286 | current_style.align=valign+halign 287 | end 288 | 289 | --Store this style table 290 | val.style=deep_copy(current_style) 291 | 292 | --Get extents of the section. _sdesc is not used 293 | --Temporarily remove all newlines first 294 | swidth,sheight,_sdesc,sext=aegisub.text_extents(current_style,val.text:gsub("\n","")) 295 | 296 | --aegisub.log("Text: %s\n--w: %.3f\n--h: %.3f\n--d: %.3f\n--el: %.3f\n\n", 297 | -- val.text, swidth, sheight, _sdesc, sext) 298 | 299 | --Add to cumulative width 300 | cum_width=cum_width+swidth 301 | 302 | --Total height of the line 303 | --theight=0 304 | 305 | --Handle tasks for a line that has a newline 306 | --[[if val.text:match("\n")~=nil then 307 | --Add sheight for each newline, if any 308 | for nl in val.text:gmatch("\n") do 309 | theight=theight+sheight 310 | end 311 | 312 | --Add the external lead to account for the line of normal text 313 | --theight=theight+sext 314 | 315 | --Store the current cumulative width and reset it to zero 316 | subs_width[subs_index]=cum_width 317 | subs_index=subs_index+1 318 | cum_width=0 319 | 320 | --Add to cumulative height 321 | cum_height=cum_height+theight 322 | else 323 | theight=sheight+sext 324 | end]]-- 325 | 326 | --Add data to data table 327 | table.insert(substr_data, 328 | {["width"]=swidth,["height"]=theight,["subtable"]=subtable}) 329 | 330 | end 331 | 332 | --Store the last cumulative width 333 | --subs_width[subs_index]=cum_width 334 | 335 | --Add the last cumulative height 336 | --cum_height=cum_height+substr_data[#substr_data].height 337 | 338 | --Stores current state of the line as a state subtable 339 | current_subtable={} 340 | --[[current_subtable=shallow_copy(substr_data[1].subtable) 341 | if current_subtable["t"]~=nil then 342 | current_subtable["t"]=shallow_copy(substr_data[1].subtable["t"]) 343 | end]] 344 | 345 | --How far to offset the x coordinate 346 | xoffset=0 347 | 348 | --How far to offset the y coordinate 349 | --yoffset=0 350 | 351 | --Newline index 352 | --nindex=1 353 | 354 | --Ways of calculating the new x position 355 | xpos_func={} 356 | --Left aligned 357 | xpos_func[1]=function(w) 358 | return px+xoffset 359 | end 360 | --Center aligned 361 | xpos_func[2]=function(w) 362 | return px-cum_width/2+xoffset+w/2 363 | end 364 | --Right aligned 365 | xpos_func[0]=function(w) 366 | return px-cum_width+xoffset+w 367 | end 368 | 369 | --Ways of calculating the new y position 370 | --[[ypos_func={} 371 | --Bottom aligned 372 | ypos_func[1]=function(h) 373 | return py-cum_height+yoffset+h 374 | end 375 | --Middle aligned 376 | ypos_func[2]=function(h) 377 | return py-cum_height/2+yoffset+w/2 378 | end 379 | --Top aligned 380 | ypos_func[3]=function(h) 381 | return py+yoffset 382 | end]]-- 383 | 384 | --Second pass to generate lines 385 | for i,val in ipairs(line_table) do 386 | 387 | --Here's where the action happens 388 | new_line=shallow_copy(line) 389 | 390 | --Fix state table to reflect current state 391 | for tag,param in pairs(substr_data[i].subtable) do 392 | if tag=="t" then 393 | if current_subtable["t"]==nil then 394 | current_subtable["t"]=shallow_copy(param) 395 | else 396 | --current_subtable["t"]={unpack(current_subtable["t"]),unpack(param)} 397 | for _,subval in ipairs(param) do 398 | table.insert(current_subtable["t"],subval) 399 | end 400 | end 401 | else 402 | current_subtable[tag]=param 403 | end 404 | end 405 | 406 | --Figure out where the new x and y coords should be 407 | new_x=xpos_func[current_style.align%3](substr_data[i].width) 408 | --new_y=ypos_func[math.ceil(current_style.align/3)](substr_data[i].height) 409 | 410 | --Check if the text ends in whitespace 411 | wsp=val.text:gsub("\n",""):match("%s+$") 412 | 413 | --Modify positioning accordingly 414 | if wsp~=nil then 415 | wsp_width=aegisub.text_extents(val.style,wsp) 416 | if current_style.align%3==2 then new_x=new_x-wsp_width/2 417 | elseif current_style.align%3==0 then new_x=new_x-wsp_width end 418 | end 419 | 420 | --Increase x offset 421 | xoffset=xoffset+substr_data[i].width 422 | 423 | --Handle what happens in the line contains newlines 424 | --[[if val.text:match("\n")~=nil then 425 | --Increase index and reset x offset 426 | nindex=nindex+1 427 | xoffset=0 428 | --Increase y offset 429 | yoffset=yoffset+substr_data[i].height 430 | 431 | --Remove the last newline and convert back to \N 432 | val.text=val.text:gsub("\n$","") 433 | val.text=val.text:gsub("\n","\\N") 434 | end]]-- 435 | 436 | --Start rebuilding text 437 | rebuilt_tag=string.format("{\\pos(%s,%s)}",float2str(new_x),float2str(py)) 438 | 439 | --Add the remaining tags 440 | for tag,param in pairs(current_subtable) do 441 | if tag=="t" then 442 | for k,ttime in ipairs(param) do 443 | rebuilt_tag=rebuilt_tag:gsub("}",ttime.."}") 444 | end 445 | elseif tag~="pos" and tag~="org" then 446 | rebuilt_tag=rebuilt_tag:gsub("{","{\\"..tag..param) 447 | end 448 | end 449 | 450 | if do_org then 451 | rebuilt_tag=rebuilt_tag:gsub("{",string.format("{\\org(%s,%s)",float2str(ox),float2str(oy))) 452 | end 453 | 454 | new_line.text=rebuilt_tag..val.text 455 | 456 | --Insert the new line 457 | sub.insert(li+lines_added+1,new_line) 458 | lines_added=lines_added+1 459 | 460 | end 461 | 462 | end 463 | 464 | aegisub.set_undo_point(script_name) 465 | end 466 | 467 | 468 | aegisub.register_macro(script_name,script_description,split_tag) 469 | 470 | -------------------------------------------------------------------------------- /modules/LibLyger.moon: -------------------------------------------------------------------------------- 1 | [[ 2 | README 3 | 4 | This file is a library of commonly used functions across all my automation 5 | scripts. This way, if there are errors or updates for any of these functions, 6 | I'll only need to update one file. 7 | 8 | The filename is a bit vain, perhaps, but I couldn't come up with anything else. 9 | 10 | ]] 11 | 12 | DependencyControl = require("l0.DependencyControl") 13 | version = DependencyControl{ 14 | name: "LibLyger", 15 | version: "2.0.3", 16 | description: "Library of commonly used functions across all of lyger's automation scripts.", 17 | author: "lyger", 18 | url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts", 19 | moduleName: "lyger.LibLyger", 20 | feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", 21 | { 22 | "aegisub.util", "karaskel" 23 | } 24 | } 25 | util = version\requireModules! 26 | 27 | class LibLyger 28 | msgs = { 29 | preproc_lines: { 30 | bad_type: "Error: argument #1 must be either a line object, an index into the subtitle object or a table of indexes; got a %s." 31 | } 32 | } 33 | new: (sub, sel, generate_furigana) => 34 | @set_sub sub, sel, generate_furigana if sub 35 | 36 | set_sub: (@sub, @sel = {}, generate_furigana = false) => 37 | @script_info, @lines, @dialogue, @dlg_cnt = {}, {}, {}, 0 38 | for i, line in ipairs sub 39 | @lines[i] = line 40 | switch line.class 41 | when "info" then @script_info[line.key] = line.value 42 | when "dialogue" 43 | @dlg_cnt += 1 44 | @dialogue[@dlg_cnt], line.i = line, i 45 | 46 | @meta, @styles = karaskel.collect_head @sub, generate_furigana 47 | @preproc_lines @sel 48 | 49 | insert_line: (line, i = #@lines + 1) => 50 | table.insert(@lines, i, line) 51 | @sub.insert(i, line) 52 | 53 | preproc_lines: (lines) => 54 | val_type = type lines 55 | -- indexes into the subtitles object 56 | if val_type == "number" 57 | lines, val_type = {@lines[lines]}, "table" 58 | assert val_type == "table", msgs.preproc_lines.bad_type\format val_type 59 | 60 | -- line objects 61 | if lines.raw and lines.section and not lines.duration 62 | karaskel.preproc_line @sub, @meta, @styles, lines 63 | -- tables of line numbers/objects such as the selection 64 | else @preproc_lines line for line in *lines 65 | 66 | -- returns a "Lua" portable version of the string 67 | exportstring: (s) -> string.format "%q", s 68 | 69 | --Lookup table for the nature of each kind of parameter 70 | param_type: { 71 | alpha: "alpha" 72 | "1a": "alpha" 73 | "2a": "alpha" 74 | "3a": "alpha" 75 | "4a": "alpha" 76 | c: "color" 77 | "1c": "color" 78 | "2c": "color" 79 | "3c": "color" 80 | "4c": "color" 81 | fscx: "number" 82 | fscy: "number" 83 | frz: "angle" 84 | frx: "angle" 85 | fry: "angle" 86 | shad: "number" 87 | bord: "number" 88 | fsp: "number" 89 | fs: "number" 90 | fax: "number" 91 | fay: "number" 92 | blur: "number" 93 | be: "number" 94 | xbord: "number" 95 | ybord: "number" 96 | xshad: "number" 97 | yshad: "number" 98 | pos: "point" 99 | org: "point" 100 | clip: "clip" 101 | } 102 | 103 | --Convert float to neatly formatted string 104 | float2str: (f) -> "%.3f"\format(f)\gsub("%.(%d-)0+$","%.%1")\gsub "%.$", "" 105 | 106 | --Escapes string for use in gsub 107 | esc: (str) -> str\gsub "([%%%(%)%[%]%.%*%-%+%?%$%^])","%%%1" 108 | 109 | [[ 110 | Tags that can have any character after the tag declaration: \r, \fn 111 | Otherwise, the first character after the tag declaration must be: 112 | a number, decimal point, open parentheses, minus sign, or ampersand 113 | ]] 114 | 115 | -- Remove listed tags from the given text 116 | line_exclude: (text, exclude) -> 117 | remove_t = false 118 | new_text = text\gsub "\\([^\\{}]*)", (a) -> 119 | if a\match "^r" 120 | for val in *exclude 121 | return "" if val == "r" 122 | elseif a\match "^fn" 123 | for val in *exclude 124 | return "" if val == "fn" 125 | else 126 | tag = a\match "^[1-4]?%a+" 127 | for val in *exclude 128 | if val == tag 129 | --Hacky exception handling for \t statements 130 | if val == "t" 131 | remove_t = true 132 | return "\\#{a}" 133 | elseif a\match "%)$" 134 | return a\match("%b()") and "" or ")" 135 | else 136 | return "" 137 | return "\\"..a 138 | 139 | if remove_t 140 | new_text = new_text\gsub "\\t%b()", "" 141 | 142 | return new_text\gsub "{}", "" 143 | 144 | -- Remove all tags except the given ones 145 | line_exclude_except: (text, exclude) -> 146 | remove_t = true 147 | new_text = text\gsub "\\([^\\{}]*)", (a) -> 148 | if a\match "^r" 149 | for val in *exclude 150 | return "\\#{a}" if val == "r" 151 | elseif a\match "^fn" 152 | for val in *exclude 153 | return "\\#{a}" if val == "fn" 154 | else 155 | tag = a\match "^[1-4]?%a+" 156 | for val in *exclude 157 | if val == tag 158 | remove_t = false if val == "t" 159 | return "\\#{a}" 160 | 161 | if a\match "^t" 162 | return "\\#{a}" 163 | elseif a\match "%)$" 164 | return a\match("%b()") and "" or ")" 165 | else return "" 166 | 167 | if remove_t 168 | new_text = new_text\gsub "\\t%b()", "" 169 | 170 | return new_text 171 | 172 | -- Returns the position of a line 173 | get_default_pos: (line, align_x, align_y) => 174 | @preproc_lines line 175 | x = { 176 | @script_info.PlayResX - line.eff_margin_r, 177 | line.eff_margin_l, 178 | line.eff_margin_l + (@script_info.PlayResX - line.eff_margin_l - line.eff_margin_r) / 2 179 | } 180 | y = { 181 | @script_info.PlayResY - line.eff_margin_b, 182 | @script_info.PlayResY / 2 183 | line.eff_margin_t 184 | } 185 | return x[align_x], y[align_y] 186 | 187 | get_pos: (line) => 188 | posx, posy = line.text\match "\\pos%(([%d%.%-]*),([%d%.%-]*)%)" 189 | unless posx 190 | posx, posy = line.text\match "\\move%(([%d%.%-]*),([%d%.%-]*)," 191 | return tonumber(posx), tonumber(posy) if posx 192 | 193 | -- \an alignment 194 | if align = tonumber line.text\match "\\an([%d%.%-]+)" 195 | return @get_default_pos line, align%3 + 1, math.ceil align/3 196 | -- \a alignment 197 | elseif align = tonumber line.text\match "\\a([%d%.%-]+)" 198 | return @get_default_pos line, align%4, 199 | align > 8 and 2 or align> 4 and 3 or 1 200 | -- no alignment tags (take karaskel values) 201 | else return line.x, line.y 202 | 203 | -- Returns the origin of a line 204 | get_org: (line) => 205 | orgx, orgy = line.text\match "\\org%(([%d%.%-]*),([%d%.%-]*)%)" 206 | if orgx 207 | return orgx, orgy 208 | else return @get_pos line 209 | 210 | -- Returns a table of default values 211 | style_lookup: (line) => 212 | @preproc_lines line 213 | return { 214 | alpha: "&H00&" 215 | "1a": util.alpha_from_style line.styleref.color1 216 | "2a": util.alpha_from_style line.styleref.color2 217 | "3a": util.alpha_from_style line.styleref.color3 218 | "4a": util.alpha_from_style line.styleref.color4 219 | c: util.color_from_style line.styleref.color1 220 | "1c": util.color_from_style line.styleref.color1 221 | "2c": util.color_from_style line.styleref.color2 222 | "3c": util.color_from_style line.styleref.color3 223 | "4c": util.color_from_style line.styleref.color4 224 | fscx: line.styleref.scale_x 225 | fscy: line.styleref.scale_y 226 | frz: line.styleref.angle 227 | frx: 0 228 | fry: 0 229 | shad: line.styleref.shadow 230 | bord: line.styleref.outline 231 | fsp: line.styleref.spacing 232 | fs: line.styleref.fontsize 233 | fax: 0 234 | fay: 0 235 | xbord: line.styleref.outline 236 | ybord: line.styleref.outline 237 | xshad: line.styleref.shadow 238 | yshad: line.styleref.shadow 239 | blur: 0 240 | be: 0 241 | } 242 | 243 | -- Modify the line tables so they are split at the same locations 244 | match_splits: (line_table1, line_table2) -> 245 | for i=1, #line_table1 246 | text1 = line_table1[i].text 247 | text2 = line_table2[i].text 248 | 249 | insert = (target, text, i) -> 250 | for j = #target, i+1, -1 251 | target[j+1] = target[j] 252 | 253 | target[i+1] = tag: "{}", text: target[i].text\match "#{LibLyger.esc(text)}(.*)" 254 | target[i] = tag: target[i].tag, :text 255 | 256 | if #text1 > #text2 257 | -- If the table1 item has longer text, break it in two based on the text of table2 258 | insert line_table1, text2, i 259 | elseif #text2 > #text1 260 | -- If the table2 item has longer text, break it in two based on the text of table1 261 | insert line_table2, text1, i 262 | 263 | return line_table1, line_table2 264 | 265 | -- Remove listed tags from any \t functions in the text 266 | time_exclude: (text, exclude) -> 267 | text = text\gsub "(\\t%b())", (a) -> 268 | b = a 269 | for tag in *exclude 270 | if a\match "\\#{tag}" 271 | b = b\gsub(tag == "clip" and "\\#{tag}%b()" or "\\#{tag}[^\\%)]*", "") 272 | return b 273 | 274 | -- get rid of empty blocks 275 | return text\gsub "\\t%([%-%.%d,]*%)", "" 276 | 277 | -- Returns a state table, restricted by the tags given in "tag_table" 278 | -- WILL NOT WORK FOR \fn AND \r 279 | make_state_table: (line_table, tag_table) -> 280 | this_state_table = {} 281 | for i, val in ipairs line_table 282 | temp_line_table = {} 283 | pstate = LibLyger.line_exclude_except val.tag, tag_table 284 | for j, ctag in ipairs tag_table 285 | -- param MUST start in a non-alpha character, because ctag will never be \r or \fn 286 | -- If it is, you fucked up 287 | param = pstate\match "\\#{ctag}(%A[^\\{}]*)" 288 | temp_line_table[ctag] = param if param 289 | 290 | this_state_table[i] = temp_line_table 291 | return this_state_table 292 | interpolate: (this_table, start_state_table, end_state_table, factor, preset) -> 293 | this_current_state = {} 294 | 295 | rebuilt_text = for k, val in ipairs this_table 296 | temp_tag = val.tag 297 | -- Cycle through all the tag blocks and interpolate 298 | for ctag, param in pairs start_state_table[k] 299 | temp_tag = "{}" if #temp_tag == 0 300 | temp_tag = temp_tag\gsub "}", -> 301 | tval_start, tval_end = start_state_table[k][ctag], end_state_table[k][ctag] 302 | tag_type = LibLyger.param_type[ctag] 303 | ivalue = switch tag_type 304 | when "alpha" 305 | util.interpolate_alpha factor, tval_start, tval_end 306 | when "color" 307 | util.interpolate_color factor, tval_start, tval_end 308 | when "number", "angle" 309 | nstart, nend = tonumber(tval_start), tonumber(tval_end) 310 | if tag_type == "angle" and preset.c.flip_rot 311 | nstart %= 360 312 | nend %= 360 313 | ndelta = nend - nstart 314 | if 180 < math.abs ndelta 315 | nstart += ndelta * 360 / math.abs ndelta 316 | 317 | nvalue = util.interpolate factor, nstart, nend 318 | nvalue += 360 if tag_type == "angle" and nvalue < 0 319 | 320 | LibLyger.float2str nvalue 321 | when "point", "clip" then nil -- not touched by this function 322 | else "" 323 | 324 | -- check for redundancy 325 | if this_current_state[ctag] == ivalue 326 | return "}" 327 | this_current_state[ctag] = ivalue 328 | return "\\#{ctag..ivalue}}" 329 | temp_tag .. val.text 330 | 331 | return table.concat(rebuilt_text)\gsub "{}", "" 332 | 333 | write_table: (my_table, file, indent) -> 334 | indent or= "" 335 | charS, charE = " ", "\n" 336 | 337 | --Opening brace of the table 338 | file\write "#{indent}{#{charE}" 339 | 340 | for key,val in pairs my_table 341 | file\write switch type key 342 | when "number" then indent..charS 343 | when "string" then table.concat {indent, charS, "[", LibLyger.exportstring(key), "]="} 344 | else "#{indent}#{charS}#{key}=" 345 | 346 | switch type val 347 | when "table" 348 | file\write charE 349 | LibLyger.write_table val, file, indent..charS 350 | file\write indent..charS 351 | when "string" then file\write LibLyger.exportstring val 352 | when "number" then file\write tostring val 353 | when "boolean" then file\write val and "true" or "false" 354 | 355 | file\write ","..charE 356 | 357 | -- Closing brace of the table 358 | file\write "#{indent}}#{charE}" 359 | 360 | :version 361 | 362 | return version\register LibLyger --------------------------------------------------------------------------------