dotfiles/.aegisub/automation/autoload/lyger.FbfTransform.moon

398 lines
17 KiB
Plaintext

[[
==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