398 lines
17 KiB
Plaintext
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 |