[[ ==README== Frame-by-Frame Transform Automation Script Smoothly transforms various parameters across multi-line, frame-by-frame typesets. Useful for adding smooth transitions to frame-by-frame typesets that cannot be tracked with mocha, or as a substitute for the stepped \t transforms generated by the Aegisub-Motion.lua script, which may cause more lag than hard-coded values. First generate the frame-by-frame typeset that you will be adding the effect to. Find the lines where you want the effect to begin and the effect to end, and visually typeset them until they look the way you want them to. These lines will act as "keyframes", and the automation will modify all the lines in between so that the appearance of the first line smoothly transitions into the appearance of the last line. Simply highlight the first line, the last line, and all the lines in between, and run the automation. It will only affect the tags that are checked in the popup menu when you run the automation. If you wish to save specific sets of parameters that you would like to run together, you can use the presets manager. For example, you can go to the presets manager, check all the color tags, and save a preset named "Colors". The next time you want to transform all the colors, just select "Colors" from the preset dropdown menu. The "All" preset is included by default and cannot be deleted. If you want a specific preset to be loaded when you start the script, name it "Default" when you define the preset. This may be obvious, but this automation only works on one layer or one component of a frame-by-frame typeset at a time. If you have a frame-by-frame typeset that has two lines per frame, which looks like: A1 B1 A2 B2 A3 B3 etc. Then this automation will not work. The lines must be organized as: A1 A2 A3 etc. B1 B2 B3 etc. And you would have to run the automation twice, once on A and once on B. Furthermore, the text of each line must be exactly the same once all tags are removed. You can have as many tag blocks as you want in whatever positions you want for the "keyframe" lines (the first and the last). But once the tags are taken out, the text of the lines must be identical, down to the last space. If you are using ctrl-D or copy-pasting, this should be a given, but it's worth a warning. The lines in between can have any tags you want in them. So long as the automation is not transforming those particular tags, they will be left untouched. If you need the typeset to suddenly turn huge for one frame, simply uncheck "fscx" and "fscy" when you run the automation, and the size of the line won't be touched. If you are transforming rotations, there is something to watch out for. If you want a line to start with \frz10 and rotate to \frz350, then with default options, the line will rotate 340 degrees around the circle until it gets to 350. You probably wanted it to rotate only 20 degrees, passing through 0. The solution is to check the "Rotate in shortest direction" checkbox from the popup window. This will cause the line to always pick the rotation direction that has a total rotation of less than 180 degrees. New feature: ignore text. Requires you to only have one tag block in each line, at the beginning. Comes with an extra automation "Remove tags" that utilizes functions that were written for the main automation. You can comment out (two dashes) the line at the bottom that adds this automation if you don't want it. TODO: Check that all lines text match iclip support ]] export script_name = "Frame-by-frame transform" export script_description = "Smoothly transforms between the first and last selected lines." export script_version = "2.0.1" export script_namespace = "lyger.FbfTransform" DependencyControl = require "l0.DependencyControl" rec = DependencyControl{ feed: "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json", { {"lyger.LibLyger", version: "2.0.0", url: "http://github.com/TypesettingTools/lyger-Aegisub-Scripts"}, {"l0.Functional", version: "0.3.0", url: "https://github.com/TypesettingTools/ASSFoundation", feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"}, } } LibLyger, Functional = rec\requireModules! import list, math, string, table, unicode, util, re from Functional logger, libLyger = rec\getLogger!, LibLyger! -- tag list, grouped by dialog layout tags_grouped = { {"c", "2c", "3c", "4c"}, {"alpha", "1a", "2a", "3a", "4a"}, {"fscx", "fscy", "fax", "fay"}, {"frx", "fry", "frz"}, {"bord", "shad", "fs", "fsp"}, {"xbord", "ybord", "xshad", "yshad"}, {"blur", "be"}, {"pos", "org", "clip"} } tags_flat = list.join unpack tags_grouped -- default settings for every preset preset_defaults = { skiptext: false, flip_rot: false, accel: 1.0, tags: {tag, false for tag in *tags_flat } } -- the default preset must always be available and cannot be deleted config = rec\getConfigHandler { presets: { Default: {} "[Last Settings]": {description: "Repeats the last #{script_name} operation"} } startupPreset: "Default" } unless config\load! -- write example preset on first time load config.c.presets["All"] = tags: {tag, true for tag in *tags_flat} config\write! create_dialog = (preset) -> config\load! preset_names = [preset for preset, _ in pairs config.c.presets] table.sort preset_names dlg = { -- Flip rotation { name: "flip_rot", class: "checkbox", x: 0, y: 9, width: 3, height: 1, label: "Rotate in shortest direction", value: preset.c.flip_rot }, { name: "skiptext", class: "checkbox", x: 3, y: 9, width: 2, height: 1, label: "Ignore text", value: preset.c.skiptext }, -- Acceleration { class: "label", x: 0, y: 10, width: 2, height: 1, label: "Acceleration: ", }, { name: "accel", class:"floatedit", x: 2, y: 10, width: 3, height: 1, value: preset.c.accel, hint: "1 means no acceleration, >1 starts slow and ends fast, <1 starts fast and ends slow" }, { class: "label", x: 0, y: 11, width: 2, height: 1, label: "Preset: " }, { name: "preset_select", class: "dropdown", x: 2, y: 11, width: 2, height: 1, items: preset_names, value: preset.section[#preset.section] }, { name: "preset_modify", class: "dropdown", x: 4, y: 11, width: 2, height: 1, items: {"Load", "Save", "Delete", "Rename"}, value: "Load" } } -- generate tag checkboxes for y, group in ipairs tags_grouped dlg[#dlg+1] = { name: tag, class: "checkbox", x: x-1, y: y, width: 1, height: 1, label: "\\#{tag}", value: preset.c.tags[tag] } for x, tag in ipairs group btn, res = aegisub.dialog.display dlg, {"OK", "Cancel", "Mod Preset", "Create Preset"} return btn, res, preset save_preset = (preset, res) -> preset\import res, nil, true if res.__class != DependencyControl.ConfigHandler preset.c.tags[k] = res[k] for k in *tags_flat preset\write! create_preset = (settings, name) -> msg = if not name "Onii-chan, what name would you like your preset to listen to?" elseif name == "" "Onii-chan, did you forget to name the preset?" elseif config.c.presets[name] "Onii-chan, it's not good to name a preset the same thing as another one~" if msg btn, res = aegisub.dialog.display { { class: "label", x: 0, y: 0, width: 2, height: 1, label: msg } { class: "label", x: 0, y: 1, width: 1, height: 1, label: "Preset Name: " }, { class: "edit", x: 1, y: 1, width: 1, height: 1, name: "name", text: name } } return btn and create_preset settings, res.name preset = config\getSectionHandler {"presets", name}, preset_defaults save_preset preset, settings return name prepare_line = (i, preset) -> line = libLyger.lines[i] -- Figure out the correct position and origin values posx, posy = libLyger\get_pos line orgx, orgy = libLyger\get_org line -- Look for clips clip = {line.text\match "\\clip%(([%d%.%-]*),([%d%.%-]*),([%d%.%-]*),([%d%.%-]*)%)"} -- Make sure each line starts with tags line.text = "{}#{line.text}" unless line.text\find "^{" -- Turn all \1c tags into \c tags, just for convenience line.text = line.text\gsub "\\1c", "\\c" --Separate line into a table of tags and text line_table = if preset.c.skiptext while not line.text\match "^{[^}]+}[^{]" line.text = line.text\gsub "}{", "", 1 tag, text = line.text\match "^({[^}]+})(.+)$" {{:tag, :text}} else [{:tag, :text} for tag, text in line.text\gmatch "({[^}]*})([^{]*)"] return line, line_table, posx, posy, orgx, orgy, #clip > 0 and clip --The main body of code that runs the frame transform frame_transform = (sub, sel, res) -> -- save last settings preset = config\getSectionHandler {"presets", "[Last Settings]"}, preset_defaults save_preset preset, res libLyger\set_sub sub -- Set the first and last lines in the selection first_line, start_table, sposx, sposy, sorgx, sorgy, sclip = prepare_line sel[1], preset last_line, end_table, eposx, eposy, eorgx, eorgy, eclip = prepare_line sel[#sel], preset -- If either the first or last line do not contain a rectangular clip, -- you will not be clipping today preset.c.tags.clip = false unless sclip and eclip -- These are the tags to transform transform_tags = [tag for tag in *tags_flat when preset.c.tags[tag]] -- Make sure both lines have the same splits LibLyger.match_splits start_table, end_table -- Tables that store tables for each tag block, consisting of the state of all relevant tags -- that are in the transform_tags table start_state_table = LibLyger.make_state_table start_table, transform_tags end_state_table = LibLyger.make_state_table end_table, transform_tags -- Insert default values when not included for the state of each tag block, -- or inherit values from previous tag block start_style = libLyger\style_lookup first_line end_style = libLyger\style_lookup last_line current_end_state, current_start_state = {}, {} for k, sval in ipairs start_state_table -- build current state tables for skey, sparam in pairs sval current_start_state[skey] = sparam for ekey, eparam in pairs end_state_table[k] current_end_state[ekey] = eparam -- check if end is missing any tags that start has for skey, sparam in pairs sval end_state_table[k][skey] or= current_end_state[skey] or end_style[skey] -- check if start is missing any tags that end has for ekey, eparam in pairs end_state_table[ k] start_state_table[k][ekey] or= current_start_state[ekey] or start_style[ekey] -- Insert proper state into each intervening line for i = 2, #sel-1 aegisub.progress.set 100 * (i-1) / (#sel-1) this_line = libLyger.lines[sel[i]] -- Turn all \1c tags into \c tags, just for convenience this_line.text = this_line.text\gsub "\\1c","\\c" -- Remove all the relevant tags so they can be replaced with their proper interpolated values this_line.text = LibLyger.time_exclude this_line.text, transform_tags this_line.text = LibLyger.line_exclude this_line.text, transform_tags this_line.text = this_line.text\gsub "{}","" -- Make sure this line starts with tags this_line.text = "{}#{this_line.text}" unless this_line.text\find "^{" -- The interpolation factor for this particular line factor = (i-1)^preset.c.accel / (#sel-1)^preset.c.accel -- Handle pos transform if preset.c.tags.pos then x = LibLyger.float2str util.interpolate factor, sposx, eposx y = LibLyger.float2str util.interpolate factor, sposy, eposy this_line.text = this_line.text\gsub "^{", "{\\pos(#{x},#{y})" -- Handle org transform if preset.c.tags.org then x = LibLyger.float2str util.interpolate factor, sorgx, eorgx y = LibLyger.float2str util.interpolate factor, sorgy, eorgy this_line.text = this_line.text\gsub "^{", "{\\org(#{x},#{y})" -- Handle clip transform if preset.c.tags.clip then clip = [util.interpolate factor, ord, eclip[i] for i, ord in ipairs sclip] logger\dump{clip, sclip, eclip} this_line.text = this_line.text\gsub "^{", "{\\clip(%d,%d,%d,%d)"\format unpack clip -- Break the line into a table local this_table if preset.c.skiptext while not this_line.text\match "^{[^}]+}[^{]" this_line.text = this_line.text\gsub "}{", "", 1 tag, text = this_line.text\match "^({[^}]+})(.+)$" this_table = {{:tag, :text}} else this_table = [{:tag, :text} for tag, text in this_line.text\gmatch "({[^}]*})([^{]*)"] -- Make sure it has the same splits j = 1 while j <= #start_table stext = start_table[j].text ttext, ttag = this_table[j].text, this_table[j].tag -- ttext might contain miscellaneous tags that are not being checked for, -- so remove them temporarily ttext_temp = ttext\gsub "{[^{}]*}", "" -- If this table item has longer text, break it in two based on -- the text of the start table if #ttext_temp > #stext newtext = ttext_temp\match "#{LibLyger.esc stext}(.*)" for i = #this_table, j+1,-1 this_table[i+1] = this_table[i] this_table[j] = tag: ttag, text: ttext\gsub "#{LibLyger.esc newtext}$","" this_table[j+1] = tag: "{}", text: newtext -- If the start table has longer text, then perhaps ttext was split -- at a tag that's not being transformed if #ttext < #stext -- It should be impossible for this to happen at the end, but check anyway assert this_table[j+1], "You fucked up big time somewhere. Sorry." this_table[j].text = table.concat {ttext, this_table[j+1].tag, this_table[j+1].text} if this_table[j+2] this_table[i] = this_table[i+1] for i = j+1, #this_table-1 this_table[#this_table] = nil j -= 1 j += 1 --Interpolate all the relevant parameters and insert this_line.text = LibLyger.interpolate this_table, start_state_table, end_state_table, factor, preset sub[sel[i]] = this_line validate_fbf = (sub, sel) -> #sel >= 3 load_tags_remove = (sub, sel) -> pressed, res = aegisub.dialog.display { { class: "label", label: "Enter the tags you would like to remove: ", x: 0, y: 0, width: 1,height: 1 }, { class: "textbox", name: "tag_list", text: "", x: 0, y: 1,width: 1, height: 1 }, { class: "checkbox", label: "Remove all EXCEPT", name: "do_except", value: false, x: 0,y: 2, width: 1, height: 1 } }, {"Remove","Cancel"}, {ok: "Remove", cancel: "Cancel"} return if pressed == "Cancel" tag_list = [tag for tag in res.tag_list\gmatch "\\?(%w+)[%s\\n,;]*"] --Remove or remove except the tags in the table for li in *sel line = sub[li] f = res.do_except and LibLyger.line_exclude_except or LibLyger.line_exclude line.text = f(line.text, tag_list)\gsub "{}", "" sub[li] = line fbf_gui = (sub, sel, _, preset_name = config.c.startupPreset) -> preset = config\getSectionHandler {"presets", preset_name}, preset_defaults btn, res = create_dialog preset switch btn when "OK" do frame_transform sub, sel, res when "Create Preset" do fbf_gui sub, sel, nil, create_preset res when "Mod Preset" if preset_name != res.preset_select preset = config\getSectionHandler {"presets", res.preset_select}, preset_defaults preset_name = res.preset_select switch res.preset_modify when "Delete" preset\delete! preset_name = nil when "Save" do save_preset preset, res when "Rename" preset_name = create_preset preset.userConfig, preset_name preset\delete! fbf_gui sub, sel, nil, preset_name -- register macros rec\registerMacros { {script_name, nil, fbf_gui, validate_fbf}, {"Remove tags", "Remove or remove all except the input tags.", load_tags_remove} } for name, preset in pairs config.c.presets f = (sub, sel) -> frame_transform sub, sel, config\getSectionHandler {"presets", name} rec\registerMacro "Presets/#{name}", preset.description, f, validate_fbf, nil, true