diff --git a/automation/autoload/complex-movement.lua b/automation/autoload/complex-movement.lua new file mode 100644 index 0000000000000000000000000000000000000000..19e2ceb64c82265ee3636ec5a43ff85741a2a369 --- /dev/null +++ b/automation/autoload/complex-movement.lua @@ -0,0 +1,226 @@ +include("karaskel.lua") + +script_name = "Complex movements" +script_description = "Create complex (non-linear) movement effects defined by a cubic Bezier curve" +script_author = "Sting" +script_version = "1.1" + +function movement(subs) + aegisub.progress.task("Retrieving header data...") + aegisub.progress.task("Applying effect...") + local i, ai, maxi, maxai = 1, 1, #subs, #subs -- Initial number of lines to process + while i <= maxi do -- For each line in the subs + aegisub.progress.task(string.format("Applying effect (%d/%d)...", ai, maxai)) + aegisub.progress.set((ai-1)/maxai*100) + local l = subs[i] + if l.class == "dialogue" and -- If the line is a non-comment non-header line + not l.comment then + apply_cmove(subs, l) -- Apply the complex move transform + subs.delete(i) -- Delete this line (or comment it instead if you prefer) + maxi = maxi - 1 -- We deleted a line, so the loop upper bound must be decreased (to avoid endless loops) + else + i = i + 1 -- Nothing to see, skip to next line + end + ai = ai + 1 + end + aegisub.progress.task("Finished!") + aegisub.progress.set(100) + aegisub.set_undo_point("Complex movement") -- Set undo point (for Aegisub) after the task is completed. +end + +-- Transform complex \cmove in a series of simpler \move +function apply_cmove(subs, line) + local l = table.copy(line) -- Deep copy of the original line + local cmove = get_cmove_args(l) -- Retrieve the \cmove tag with its parameters + if cmove == nil then + subs.append(l) -- If no \cmove on that line, leave it unchanged + else + local time_array = {} + local real_time_array = {} + local curve_points = {} + local n = tonumber(cmove["steps"]) -- Number of point to approximate the curve with + local animations = get_anims(l) -- Retrieve all \t animation tags + local speed_profile, err = load(cmove["speed"]) + local ok, apply_accel = pcall(speed_profile) + + for i=0, n do -- Create arrays necessary for movement approximation : + time_array[i] = i/n + real_time_array[i] = (tonumber(cmove["t2"]) - tonumber(cmove["t1"]))*apply_accel(i/n) + tonumber(cmove["t1"]) -- Time interval subdivisions + curve_points[i] = {} + curve_points[i][1], curve_points[i][2] = bezier(time_array[i], get_bezier_points(cmove["command"])) -- Curve approximation points + end + + + -- For each subdivision of the movement approximation, create a new line between times t[i] and t[i+1], and moving between points i and i+1 + for i=0, (n - 1) do + -- Copy the line text. We do not want to change whatever else is there. + l.text = line.text + -- Assembling the \move tag for the current subdivision. No timestamps specified, move duration is the whole subdivision duration + local move = string.format("\\move(%f,%f,%f,%f)", curve_points[i][1], curve_points[i][2], curve_points[i+1][1], curve_points[i+1][2]) + -- Set start/end times of the line to the start/end times of the subdivision + l.start_time, l.end_time = line.start_time + real_time_array[i], line.start_time + real_time_array[i+1] + -- Replace the \cmove in the line text with the appropriate \move tag we just created + l.text, nb = string.gsub(l.text,'\\cmove%(([+-]?[%d%.]+)%s*,([+-]?[%d%.]+)%s*,([+-]?[%d%.]+)%s*,([e%d%a%s%-%.]+)%s*,".+"%)',move) + + + -- If there are any \t animations, we have to shift their timings : + -- we want those animations to go once, over the entire complex movement, not n times, over every subdivision. + if animations ~= nil then + local j = 1 + -- For each \t tag on the line + while animations[j] ~= nil do + local anim = animations[j] + -- Set new timestamps for the animation : + -- by deducing the start time of the subdivision relatively to the global line start time, + -- we ensure that all \t timestamps on all subdivisions are relative to the global line start time. + -- That way, we have the expected animation once, over the whole line. + local new_start_anim, new_end_anim = to_int(anim["t1"]) - real_time_array[i], to_int(anim["t2"]) - real_time_array[i] + + if math.abs(new_end_anim) < 1 then + if new_end_anim >= 0 then -- This is to go around an annoying unconsistent behavior : if the \t end time is 0 (or rounded to 0), + new_end_anim = new_end_anim + 1 -- it will be treated as a \t with no timestamp, therefore over the whole subdivision line. + else + new_end_anim = new_end_anim - 1 -- So if we end up with 0, deduce or add 1 ms, no one will notice it, and it works as we want it to. + end + end + + -- Lua standard regex is limited, this is just a workaround for nested \clip() in \t tags. + if anim["clip"] ~= nil then + local clip_params = string.match(anim["clip"], '\\i?clip%((.-)%)') + l.text, nb = string.gsub(l.text, '\\i?clip%(' .. clip_params .. '%)', '') + -- Build the new \t tag with shifted timestamps + local new_anim_tag = string.format("\\t(%f,%f,%s)", new_start_anim, new_end_anim, anim["clip"] .. anim["tags"]) + -- Replace the \t tag with the new one + l.text, nb = string.gsub(l.text, '\\t%(' .. anim["t1"] .. '%s*,%s*' .. anim["t2"] .. '%s*,%s*' .. anim["tags"] .. '%)', new_anim_tag) + else + -- Same, but simpler since there's no \clip + local new_anim_tag = string.format("\\t(%f,%f,%s)", new_start_anim, new_end_anim, anim["tags"]) + l.text, nb = string.gsub(l.text, '\\t%(' .. anim["t1"] .. '%s*,%s*' .. anim["t2"] .. '%s*,%s*' .. anim["tags"] .. '%)', new_anim_tag) + end + j = j + 1 + end + end + subs.append(l) -- Finally, append the modified subdivision line + end + + + -- For cases when the \cmove start/end is not the line start/end + -- Add a static line from the cmove end to the end of the line. + l.text = line.text + local pos = string.format("\\pos(%f,%f)", curve_points[n][1], curve_points[n][2]) + l.start_time, l.end_time = line.start_time + real_time_array[n], line.end_time + l.text, nb = string.gsub(l.text,'\\cmove%([+-]?[%d%.]+%s*,[+-]?[%d%.]+%s*,[+-]?[%d%.]+%s*,[e%d%a%s%-%.]+%s*,".+"%)', pos) + -- Not forgetting to shift animations timestamps here too + if animations ~= nil then + local j = 1 + while animations[j] ~= nil do + local anim = table.copy(animations[j]) + local new_start_anim, new_end_anim = to_int(anim["t1"]) - real_time_array[n], to_int(anim["t2"]) - real_time_array[n] + + if math.abs(new_end_anim) < 1 then + if new_end_anim >= 0 then + new_end_anim = new_end_anim + 1 + else + new_end_anim = new_end_anim - 1 + end + end + + + + if anim["clip"] ~= nil then + local clip_params = string.match(anim["clip"], '\\i?clip%((.-)%)') + l.text, nb = string.gsub(l.text, '\\i?clip%(' .. clip_params .. '%)', '') + local new_anim_tag = string.format("\\t(%f,%f,%s)", new_start_anim, new_end_anim, anim["clip"] .. anim["tags"]) + l.text, nb = string.gsub(l.text, '\\t%(' .. anim["t1"] .. '%s*,%s*' .. anim["t2"] .. '%s*,%s*' .. anim["tags"] .. '%)', new_anim_tag) + else + local new_anim_tag = string.format("\\t(%f,%f,%s)", new_start_anim, new_end_anim, anim["tags"]) + l.text, nb = string.gsub(l.text, '\\t%(' .. anim["t1"] .. '%s*,%s*' .. anim["t2"] .. '%s*,%s*' .. anim["tags"] .. '%)', new_anim_tag) + end + j = j + 1 + end + end + subs.append(l) + + + -- And add a static line before the cmove. + -- Since that line's start time is the global line start time, no need to shift the \t timestamps (or rather, shift by 0ms) + l.text = line.text + pos = string.format("\\pos(%f,%f)", curve_points[0][1], curve_points[0][2]) + l.start_time, l.end_time = line.start_time, line.start_time + real_time_array[0] + l.text, nb = string.gsub(l.text,'\\cmove%(([+-]?[%d%.]+)%s*,([+-]?[%d%.]+)%s*,([+-]?[%d%.]+)%s*,([e%d%a%s%-%.]+)%s*,".+"%)', pos) + subs.append(l) + end + l = nil + cmove = nil + animations = nil +end + + + + +-- Retrieve parameters of the \cmove tag in a table +function get_cmove_args(line) + -- match the \cmove tag if it exists + local cmove_tag = string.match(line.text, '\\cmove%([+-]?[%d%.%s]+,[+-]?[%d%.%s]+,%s?[+-]?[%d%.%s]+,[e%d%a%s%-%.]+,".+"%)') + if cmove_tag == nil then + return nil + else + local cmove = {} + -- If there is a \cmove, retrieve its parameters in a table + cmove["t1"], cmove["t2"], cmove["steps"], cmove["command"], cmove["speed"] = string.match(cmove_tag, '\\cmove%(([+-]?[%d%.]+)%s*,%s*([+-]?[%d%.]+)%s*,%s*([+-]?[%d%.]+)%s*,%s*([e%d%a%s%-%.]+)%s*,%s?"(.+)"%)') + return cmove + end +end + + + +-- Retrieve all \t tags in the line with params in a table anims{anim1 {[t1,] [t2,] [accel,] tags}, ... } +function get_anims(line) + local anims = {} + local remaining_text = line.text + local i = 1 + while string.match(remaining_text, '\\t%(.*%)') ~= nil do -- While there are \t tags we haven't seen yet + local anim = {} + local insert_clip = string.match(remaining_text, '\\t%([^()]-(\\i?clip%(.-%))[^()]-%)') -- Trying to find a nested \clip + if insert_clip ~= nil then + remaining_text, nb1 = string.gsub(remaining_text, '\\i?clip%(.-%)', '') -- If there is one, get it out + anim["clip"] = insert_clip -- And keep it somewhere + end + anim["t1"], anim["t2"], anim["tags"] = string.match(remaining_text, '\\t%(([+-]?[%d%.]+)%s*,%s*([+-]?[%d%.]+)%s*,%s*(.-)%)') -- Get the \t params + local anim_tag = '\\t%(' .. anim["t1"] .. '%s*,%s*' .. anim["t2"] .. '%s*,%s*' .. anim["tags"] .. '%)' + remaining_text, nb2 = string.gsub(remaining_text, anim_tag, '') -- We got that one, remove it from what we have yet to check + anim["remain"] = remaining_text -- Debug leftover, I'll remove it eventually. + anims[i] = table.copy(anim) + i = i + 1 + end + return anims +end + + + + +-- Convert string to signed int +function to_int(str) + if string.sub(str, 1, 1) == "-" then + return (- tonumber(string.sub(str,2))) + else + return tonumber(str) + end +end + + +-- Get Bezier curve definition points from ASSDraw command +function get_bezier_points(command) + local x1, y1, x2, y2, x3, y3, x4, y4 = string.match(command, '^m%s(%--[e%-%d%.]+)%s(%--[e%-%d%.]+)%sb%s(%--[e%-%d%.]+)%s(%--[e%-%d%.]+)%s(%--[e%-%d%.]+)%s(%--[e%-%d%.]+)%s(%--[e%-%d%.]+)%s(%--[e%-%d%.]+)$') + return to_int(x1), to_int(y1), to_int(x2), to_int(y2), to_int(x3), to_int(y3), to_int(x4), to_int(y4) +end + + +-- Calculate point x,y at time t in [0,1] of a cubic Bezier curve defined by four (xi,yi) points +function bezier(t, x1, y1, x2, y2, x3, y3, x4, y4) + local x = (1-t)*(1-t)*(1-t)*x1 + 3*t*(1-t)*(1-t)*x2 + 3*t*t*(1-t)*x3 + t*t*t*x4 + local y = (1-t)*(1-t)*(1-t)*y1 + 3*t*(1-t)*(1-t)*y2 + 3*t*t*(1-t)*y3 + t*t*t*y4 + return x, y +end + +aegisub.register_macro("Complex movements", "Create complex movement effects", movement) diff --git a/automation/meson.build b/automation/meson.build index 4e4f7507ac9ba8a8d989812b7fc0d15dbd39fc88..a09afc5aca05d985a618d089e3720d5214c51b24 100644 --- a/automation/meson.build +++ b/automation/meson.build @@ -2,6 +2,7 @@ automation_dir = dataroot / 'automation' install_data( 'autoload/cleantags-autoload.lua', + 'autoload/complex-movement.lua', 'autoload/duetto-meika.lua', 'autoload/kara-templater.lua', 'autoload/karaoke-adjust-1sec.lua', diff --git a/src/dialog_about.cpp b/src/dialog_about.cpp index efbc0080ffa8102d19063d38b63da7f6df9b6552..9dc8b57d5e244afc28fc4ceb14062271abdb5a06 100644 --- a/src/dialog_about.cpp +++ b/src/dialog_about.cpp @@ -71,6 +71,7 @@ void ShowAboutDialog(wxWindow *parent) { " Rodrigo Braz Monteiro\n" " Simone Cociancich\n" " Thomas Goyne\n" + " Maël Martin\n" "User manual written by:\n" " Karl Blomster\n" " Niels Martin Hansen\n"