Last active
February 16, 2025 17:48
-
-
Save CHFR-wide/2c1fa3596b4bc2d02b5c2759bc1dab96 to your computer and use it in GitHub Desktop.
Script for saving and applying easing presets within davinci fusion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Easecopy v1.0.3 | |
-- Changelog: | |
-- v1.0.3 : Makes the script composition-agnostic | |
-- v1.0.2 : Fixes an issue where number-only ease names would crash the script | |
---------------------- | |
-- UI_MANAGER SETUP -- | |
---------------------- | |
local ui = fu.UIManager | |
local disp = bmd.UIDispatcher(ui) | |
local width,height = 275,300 | |
local positionX,positionY = 800,400 | |
local currentComp = fu:GetCurrentComp() | |
win = disp:AddWindow({ | |
ID = 'MyWin', | |
TargetID = 'MyWin', | |
WindowTitle = 'Ease Copy', | |
Geometry = {positionX, positionY, width, height}, | |
Spacing = 0, | |
ui:VGroup{ | |
ID = 'root', | |
ui:Label{ | |
Weight = 0, | |
Text = 'Apply', | |
Alignment = {AlignHCenter = true}, | |
}, | |
ui:HGroup{ | |
Weight = 0, | |
ui:Label { Weight = 0.5, Text = 'Ease', }, | |
ui:ComboBox { Weight = 2, ID = 'qEase', Text = '', }, | |
}, | |
ui:HGroup{ | |
Weight = 0, | |
ui:Label { Weight = 0.5, Text = 'Target', }, | |
ui:ComboBox { Weight = 2, ID = 'qTargetProp', Text = '', }, | |
}, | |
ui:Button { Weight=0, ID = 'qApplyBtn', Text = 'Apply ease' }, | |
ui:TabBar{}, | |
ui:Label{ | |
Weight = 0, | |
Text = 'Save', | |
Alignment = {AlignHCenter = true}, | |
}, | |
ui:HGroup{ | |
Weight = 0, | |
ui:LineEdit { ID = 'qSaveEaseText' }, | |
ui:Button { ID = 'qSaveBtn', Text = 'Save ease' }, | |
}, | |
ui:TabBar{}, | |
ui:Label{ | |
Weight = 0, | |
Text = 'Delete', | |
Alignment = {AlignHCenter = true}, | |
}, | |
ui:HGroup{ | |
Weight = 0, | |
ui:Button { | |
Weight = 0.5, | |
ID = 'qDeleteOne', | |
Text = 'Selected ease' | |
}, | |
ui:Button { | |
Weight = 0.5, | |
ID = 'qDeleteAll', | |
Text = 'All eases' | |
}, | |
}, | |
} | |
}) | |
selectedTool = tool | |
itm = win:GetItems() | |
notify = ui:AddNotify('Comp_Activate_Tool') | |
-- EVENT BINDING -- | |
function win.On.MyWin.Close(ev) | |
disp:ExitLoop() | |
end | |
function disp.On.Comp_Activate_Tool(ev) | |
ReloadTargetComboBox() | |
end | |
function win.On.qApplyBtn.Clicked(ev) | |
local presetName = itm.qEase.CurrentText | |
local targetProp = itm.qTargetProp.CurrentText | |
if (presetName ~= '') then | |
EaseCopy(presetName, targetProp) | |
end | |
end | |
function win.On.qSaveBtn.Clicked(ev) | |
local presetName = itm.qSaveEaseText.Text | |
local targetProp = itm.qTargetProp.CurrentText | |
if (presetName ~= '') then | |
if EaseCopy(presetName, targetProp, true) then | |
itm.qSaveEaseText.Text = '' | |
ReloadEaseComboBox(presetName) | |
end | |
end | |
end | |
function win.On.qDeleteOne.Clicked(ev) | |
local presetName = itm.qEase.CurrentText | |
fusion:SetData("easeCopy.presets." .. presetName, nil) | |
ReloadEaseComboBox() | |
print('Ease: ' .. presetName .. ' deleted.') | |
end | |
function win.On.qDeleteAll.Clicked(ev) | |
local confirmClear = currentComp:AskUser("Delete all eases?", {}) | |
if not confirmClear then return end | |
fusion:SetData("easeCopy", nil) | |
ReloadEaseComboBox() | |
print('All eases have been deleted.') | |
end | |
-- DISPLAY UPDATE -- | |
function ReloadEaseComboBox(newSelected) | |
dump('reloading') | |
local savedEases = fusion:GetData("easeCopy.presets") | |
local presets = {} | |
if savedEases then; | |
presets = GetKeys(fusion:GetData("easeCopy.presets")); | |
end | |
itm.qEase:Clear() | |
for _, preset in pairs(presets) do | |
itm.qEase:AddItem(preset) | |
end | |
if newSelected ~= '' then | |
itm.qEase:SetCurrentText(newSelected) | |
end | |
end | |
function ReloadTargetComboBox() | |
currentComp = fu:GetCurrentComp() | |
itm.qTargetProp:Clear() | |
itm.qTargetProp:AddItem('ALL') | |
for _, target in pairs(FindEligibleInputs(currentComp:GetToolList(true))) do | |
itm.qTargetProp:AddItem(target) | |
end | |
end | |
---------------- | |
-- MAIN LOGIC -- | |
---------------- | |
function EaseCopy(presetName, targetProp, copy) | |
currentComp:StartUndo("EaseCopy") | |
currentComp:Lock() | |
for k,v in pairs(currentComp:GetToolList(true)) do | |
local endExecutionEarly = EaseCopyTool(v, presetName, targetProp, copy) | |
if (endExecutionEarly) then | |
currentComp:Unlock() | |
currentComp:EndUndo(true) | |
return true | |
end | |
end | |
currentComp:Unlock() | |
currentComp:EndUndo(true) | |
end | |
function EaseCopyTool(tool, presetName, targetProp, copy) | |
for k,v in pairs(tool:GetInputList()) do | |
local endExecutionEarly = EaseCopyInput(v, presetName, targetProp, copy) | |
if (endExecutionEarly) then | |
return true | |
end | |
end | |
end | |
function EaseCopyInput(input, presetName, targetProp, copy) | |
if not IsViableInput(input) then return end | |
local inputTool = GetTool(input) | |
if not inputTool then; return; end | |
if (IsModifier(inputTool) and not IsBezierSpline(inputTool)) then | |
return EaseCopyTool(inputTool, presetName, targetProp, copy) | |
end | |
if not IsTargetInput(input, targetProp) then return end | |
local keyframes = inputTool:GetKeyFrames() | |
if not keyframes then; return; end | |
local adjacentKeyframes = GetAdjacentKeyframes(keyframes) | |
if not adjacentKeyframes then | |
return | |
end | |
print("Found valid property: " .. input:GetAttrs().INPS_Name) | |
if copy then | |
CopyEase(presetName, adjacentKeyframes) | |
return true | |
else | |
local hardReplace = input:GetAttrs().INPS_ID ~= "Displacement" | |
PasteEase(inputTool, presetName, adjacentKeyframes, hardReplace) | |
end | |
end | |
function CopyEase(presetName, adjacentKeyframes) | |
local normalized = NormalizeKeyframePairHandles(adjacentKeyframes) | |
print("copying ease as " .. presetName) | |
fusion:SetData("easeCopy.presets." .. presetName, normalized) | |
end | |
function PasteEase(tool, presetName, adjacentKeyframes, hardReplace) | |
local ease = fusion:GetData("easeCopy.presets." .. presetName) | |
if ease then | |
print("pasting ease preset " .. presetName) | |
local denormalized = DenormalizeKeyframePairHandles(adjacentKeyframes, ease) | |
local oldKf = tool:GetKeyFrames() | |
local newKf = PatchExistingKeyFrames(oldKf, denormalized) | |
if hardReplace then | |
tool:DeleteKeyFrames(currentComp:GetAttrs().COMPN_GlobalStart, currentComp:GetAttrs().COMPN_GlobalEnd) | |
tool:SetKeyFrames(newKf, false) | |
else | |
ShowDisplacementWarning() | |
tool:SetKeyFrames(newKf, false) | |
-- This is not a mistake, for some reason, running this twice on Displacement properties | |
-- gives better (though still inconsistent) results | |
tool:SetKeyFrames(newKf, false) | |
end | |
end | |
end | |
function ShowDisplacementWarning() | |
if fusion:GetData('easeCopy.displacementWarning.doNotShow') == true then return end | |
local warnMessage = 'Pasting eases on a Displacement property can give inconsistent results, if it doesn\'t work as intended, please use an XY path modifier instead.' | |
local warnText = {"Message", Name="Message", "Text", ReadOnly = true, Wrap = true, Default = warnMessage, Lines = 5} | |
local checkBox = {"DoNotShowAgain", Name="Don\'t show this message again", "Checkbox"} | |
local dialog = currentComp:AskUser("Warning", {warnText, checkBox}) | |
dump(dialog) | |
if dialog and dialog.DoNotShowAgain == 1 then | |
dump('Saving preferences') | |
fusion:SetData('easeCopy.displacementWarning.doNotShow', true) | |
end | |
end | |
---------------------------------- | |
-- INPUT PARSING AND VALIDATION -- | |
---------------------------------- | |
function FindEligibleInputs(tools) | |
local eligibleInputs = {} | |
for _, tool in pairs(tools) do | |
for _, input in pairs(tool:GetInputList()) do | |
if IsViableInput(input) then | |
local inputTool = GetTool(input) | |
if IsModifier(inputTool) then | |
if IsBezierSpline(inputTool) then | |
local savedTarget = tool:GetAttrs().TOOLS_Name .. ":" .. input:GetAttrs().INPS_ID | |
table.insert(eligibleInputs, savedTarget ) | |
else | |
for _, v in ipairs(FindEligibleInputs({inputTool})) do | |
table.insert(eligibleInputs, v) | |
end | |
end | |
end | |
end | |
end | |
end | |
return eligibleInputs | |
end | |
function IsViableInput(input) | |
return input:GetAttrs("INPB_Connected") and input:GetAttrs("INPS_DataType") ~= "LookUpTable" | |
end | |
function IsTargetInput(input, targetProp) | |
if targetProp == 'ALL' then return true end | |
t = Split(targetProp, ':') | |
dump(input:GetAttrs().INPS_ID) | |
dump(input:GetTool():GetAttrs().TOOLS_Name == t[1] and input:GetAttrs().INPS_ID == t[2]) | |
return input:GetTool():GetAttrs().TOOLS_Name == t[1] and input:GetAttrs().INPS_ID == t[2] | |
end | |
-- thanks. https://www.steakunderwater.com/wesuckless/viewtopic.php?p=45445#p45445 | |
function IsModifier(tool) | |
local regModifiers = fusion:GetRegList(fusion.CT_Modifier) | |
local toolAttrs = tool:GetAttrs() | |
for _,v in pairs(regModifiers) do | |
if v:GetAttrs().REGS_ID == toolAttrs.TOOLS_RegID then | |
return true | |
end | |
end | |
return false | |
end | |
function IsBezierSpline(tool) | |
return tool:GetAttrs().TOOLS_RegID == "BezierSpline" | |
end | |
function GetTool(input) | |
local output = input:GetConnectedOutput() | |
if (output ~= nil) then | |
return output:GetTool() | |
end | |
end | |
---------------------------- | |
-- KEYFRAMES MANIPULATION -- | |
---------------------------- | |
function GetAdjacentKeyframes(keyframes) | |
local closestLeft = nil | |
local closestRight = nil | |
for k,v in pairs(keyframes) do | |
if k <= currentComp.CurrentTime and (closestLeft == nil or k > closestLeft) then | |
closestLeft = k | |
end | |
if k > currentComp.CurrentTime and (closestRight == nil or k < closestRight) then | |
closestRight = k | |
end | |
end | |
if (closestLeft and closestRight) then | |
return {[closestLeft] = keyframes[closestLeft], [closestRight] = keyframes[closestRight]} | |
end | |
end | |
function IsolateAdjacentKeyframes(keyframes, adjacent) | |
return { | |
[adjacent.Left] = keyframes[adjacent.Left], | |
[adjacent.Right] = keyframes[adjacent.Right] | |
} | |
end | |
function NormalizeKeyframePairHandles(adjacentKeyframes) | |
local tLeft, hLeft, tRight, hRight = SortAdjacentFrames(adjacentKeyframes) | |
local timeDiff = tRight - tLeft | |
local valueDiff = hRight[1] - hLeft[1] | |
if valueDiff == 0 then; return nil; end | |
local RH = hLeft.RH | |
local LH = hRight.LH | |
return { | |
RH = { RH[1] / timeDiff, RH[2] / valueDiff }, | |
LH = { LH[1] / timeDiff, LH[2] / valueDiff }, | |
} | |
end | |
function DenormalizeKeyframePairHandles(adjacentKeyframes, normalized) | |
local tLeft, hLeft, tRight, hRight = SortAdjacentFrames(adjacentKeyframes) | |
local timeDiff = tRight - tLeft | |
local valueDiff = hRight[1] - hLeft[1] | |
local RH = normalized.RH | |
local LH = normalized.LH | |
adjacentKeyframes[tLeft].RH = { RH[1] * timeDiff, RH[2] * valueDiff } | |
adjacentKeyframes[tRight].LH = { LH[1] * timeDiff, LH[2] * valueDiff } | |
adjacentKeyframes[tLeft].Flags = { RH[1] * timeDiff, RH[2] * valueDiff } | |
adjacentKeyframes[tRight].Flags = { LH[1] * timeDiff, LH[2] * valueDiff } | |
return adjacentKeyframes | |
end | |
function SortAdjacentFrames(adjacentKeyframes) | |
local tLeft, hLeft = next(adjacentKeyframes) | |
local tRight, hRight = next(adjacentKeyframes, tLeft) | |
if tLeft < tRight then | |
return tLeft, hLeft, tRight, hRight | |
else | |
return tRight, hRight, tLeft, hLeft | |
end | |
end | |
function PatchExistingKeyFrames(keyframes, denormalized) | |
local k1, v1 = next(denormalized) | |
local k2, v2 = next(denormalized, k1) | |
keyframes[k1] = v1 | |
keyframes[k2] = v2 | |
return keyframes | |
end | |
---------------------- | |
-- HELPER FUNCTIONS -- | |
---------------------- | |
function PairsByKeys (t, f) | |
local a = {} | |
for n in pairs(t) do table.insert(a, tostring(n)) end | |
table.sort(a, f) | |
local i = 0 | |
local iter = function () | |
i = i + 1 | |
if a[i] == nil then return nil | |
else return a[i], t[a[i]] | |
end | |
end | |
return iter | |
end | |
function GetKeys(t) | |
if t == nil then; return; end | |
local keys={} | |
for key,_ in PairsByKeys(t) do | |
table.insert(keys, key) | |
end | |
return keys | |
end | |
function Split (inputstr, sep) | |
if sep == nil then | |
sep = "%s" | |
end | |
local t={} | |
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do | |
table.insert(t, str) | |
end | |
return t | |
end | |
---------------------- | |
ReloadEaseComboBox() | |
ReloadTargetComboBox() | |
win:Show() | |
disp:RunLoop() | |
win:Hide() | |
collectgarbage() |
Hello @infinitypacific.
Does the console show any kind of error when you try to press save? Also could you attach an image of what your Resolve screen looks like when that happens?
Sorry for the late response, it doesn't show anything really it just stays there.
Your playhead is at the wrong spot, the script looks for the keyframe directly on our before the playhead for the "left" keyframe, then it looks to any keyframe strictly after the playhead, if you move one frame before your ease should be saved normally
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sadly it isn't working for me (i'm on 18.6.6) whenever I press save it just doesn't do anything...