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"