-
-
Save CHFR-wide/2c1fa3596b4bc2d02b5c2759bc1dab96 to your computer and use it in GitHub Desktop.
| -- 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
does it work in latest davinci 20
I'm on the free version of Davinci resolve 20, on MacOS, and after putting the script into the scripts folder, when ever i launch it it tells me about the studio version and tells me to pay the 250$, does this script only work on the studio version of Davinci?
I'm on the free version of Davinci resolve 20, on MacOS, and after putting the script into the scripts folder, when ever i launch it it tells me about the studio version and tells me to pay the 250$, does this script only work on the studio version of Davinci?
For the free version of resolve, it will only work for versions strictly below 19.1. In 19.1, they made scripts that use the UI manager only work with the studio version.
Hello, I can't save the animation curve. DaVinci Resolve Studio 21, W11.
Also, if a keyframe is created when there isn't one, it's necessary to reopen the tool to see it in the updated Target list. In other words, the Target list doesn't seem to update automatically, requiring reopening.
Also, congratulations on creating the script; I used it a few times and it was very useful, although I can't use it now.
Hello, I can't save the animation curve. DaVinci Resolve Studio 21, W11.
Also, if a keyframe is created when there isn't one, it's necessary to reopen the tool to see it in the updated Target list. In other words, the Target list doesn't seem to update automatically, requiring reopening.
Also, congratulations on creating the script; I used it a few times and it was very useful, although I can't use it now.
Hi, Two things look a bit off in this image:
- Is your cursor between the two keyframes? I don't see it in the spline panel. The script can only look at keyframes the cursor is contained in.
- It looks like the first and second keyframes have exactly the same value, when the ease is being saved, there's a normalization step that needs to be done, this step divides by the value difference, so if the two keyframes have the exact same value it will cause a division by zero (no matter the easing).
- (yapping a bit:) The script could potentially make it work by skipping normalization when there's no value difference, but without normalization, you'd be looking at the eases' raw values, and for example, your curve that jumps from 0.5 to 0.8 (so a 0.3 difference) would be tiny on a parameter like rotation, where one spin is 360 whole degrees.
The error messages seem unrelated to the script itself, also, this is not quite between, the cursor can be on top of the first keyframe, but it can't be on top of the last, It's an [in, out[ range for implementation reasons.
As mensagens de erro parecem não ter relação com o script em si. Além disso, isso não é exatamente o meio termo. O cursor pode estar em cima do primeiro quadro-chave, mas não pode estar em cima do último. É um intervalo [dentro, fora[ por motivos de implementação.
The error in the console is generated after clicking the save ease button.
I also tried positioning the playhead above the first keyframe, but it doesn't work.
That's a bit of a problem, I am still on 18.6.6 so I am not be able to test on versions past 19.1, one more thing that you could try, is to use a version of the script that was modified by someone called paperclipfx, they shared it in a public discord server so I'll relay the script with attribution.
It seems to have a few more improvements like easier access to presets and stuff. I don't know if this version will work for you, but I think it has better chances than my version, which I haven't touched in at least a year.
Once again, all credits for this modified version goes to them. I'm just pasting it here as they don't seem to have publicized it anywhere.
-- Easecopy v1.0.9 - Adjusted Header and Button Layout
-- Changelog:
-- v1.0.9 : Moved Apply header to top, restored Apply button to original position
-- v1.0.8 : Made cycle buttons compact, increased dropdown space, moved Apply button to top
-- v1.0.7 : Moved cycle buttons beside Ease and Target dropdowns, adjusted cycling functionality
-- v1.0.6 : Added cycle buttons for navigation without applying, moved headers to top
-- v1.0.5 : Fixed target dropdown issues with enhanced debugging and manual refresh
-- v1.0.4 : Added numbered buttons for quick preset access + rename functionality
-- v1.0.3 : Makes the script composition-agnostic
-- v1.0.2 : Fixes an issue where number-only ease names would crash the script
-- Modified: Moved Apply header to top, restored Apply button
----------------------
-- UI_MANAGER SETUP --
----------------------
local ui = fu.UIManager
local disp = bmd.UIDispatcher(ui)
local width,height = 275,400
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:VGroup{
Weight = 0,
ui:HGroup{
Weight = 0,
ui:Label{ Weight = 0.5, Text = 'Ease', Alignment = {AlignHCenter = true}, },
ui:ComboBox{ Weight = 2, ID = 'qEase', Text = '', },
ui:Button{ Weight = 0.3, ID = 'qCyclePrevEase', Text = '<<', MinimumSize = {20, 20}, },
ui:Button{ Weight = 0.3, ID = 'qCycleNextEase', Text = '>>', MinimumSize = {20, 20}, },
},
ui:HGroup{
Weight = 0,
ui:Label{ Weight = 0.5, Text = 'Target', Alignment = {AlignHCenter = true}, },
ui:ComboBox{ Weight = 2, ID = 'qTargetProp', Text = '', },
ui:Button{ Weight = 0.3, ID = 'qCyclePrevTarget', Text = '<<', MinimumSize = {20, 20}, },
ui:Button{ Weight = 0.3, ID = 'qCycleNextTarget', Text = '>>', MinimumSize = {20, 20}, },
},
ui:Button{ Weight = 0, ID = 'qApplyBtn', Text = 'Apply ease' },
ui:Button{ Weight = 0, ID = 'qRefreshTargets', Text = 'Refresh Targets' },
ui:TabBar{},
},
ui:VGroup{
Weight = 0,
ui:Label{ Weight = 0, Text = 'Save', Alignment = {AlignHCenter = true}, },
ui:HGroup{
Weight = 0,
ui:ComboBox{ Weight = 0.3, ID = 'qSaveMode', Text = 'save', },
ui:LineEdit{ Weight = 1.7, ID = 'qSaveEaseText' },
ui:Button{ Weight = 1, ID = 'qSaveBtn', Text = 'Save ease' },
},
ui:HGroup{
Weight = 0,
ui:Button{ Weight = 1, ID = 'qApplyPrevious', Text = 'Apply previous' },
ui:Button{ Weight = 1, ID = 'qApplyNext', Text = 'Apply next' },
},
},
ui:VGroup{
Weight = 1,
ID = 'ButtonsArea',
-- Row 1
ui:HGroup{
Weight = 0,
ID = 'ButtonRow1',
ui:Button{ ID = 'qBtn1', Text = '1', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn2', Text = '2', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn3', Text = '3', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn4', Text = '4', Weight = 1, MinimumSize = {40, 30}, Visible = false },
},
-- Row 2
ui:HGroup{
Weight = 0,
ID = 'ButtonRow2',
ui:Button{ ID = 'qBtn5', Text = '5', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn6', Text = '6', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn7', Text = '7', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn8', Text = '8', Weight = 1, MinimumSize = {40, 30}, Visible = false },
},
-- Row 3
ui:HGroup{
Weight = 0,
ID = 'ButtonRow3',
ui:Button{ ID = 'qBtn9', Text = '9', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn10', Text = '10', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn11', Text = '11', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn12', Text = '12', Weight = 1, MinimumSize = {40, 30}, Visible = false },
},
-- Row 4
ui:HGroup{
Weight = 0,
ID = 'ButtonRow4',
ui:Button{ ID = 'qBtn13', Text = '13', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn14', Text = '14', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn15', Text = '15', Weight = 1, MinimumSize = {40, 30}, Visible = false },
ui:Button{ ID = 'qBtn16', Text = '16', Weight = 1, MinimumSize = {40, 30}, Visible = false },
},
},
ui:TabBar{},
ui:VGroup{
Weight = 0,
ui:HGroup{
Weight = 0,
ui:ComboBox{ Weight = 0.3, ID = 'qDeleteProtection', Text = 'off', },
ui:Button{ Weight = 0.7, ID = 'qDeleteOne', Text = 'Delete selected ease' },
},
},
}
})
selectedTool = tool
itm = win:GetItems()
notify = ui:AddNotify('Comp_Activate_Tool')
-- Store preset to button mapping
presetButtons = {}
maxButtons = 16 -- Maximum number of buttons we have
-- Initialize the dropdowns
itm.qDeleteProtection:AddItem('off')
itm.qDeleteProtection:AddItem('on')
itm.qDeleteProtection:SetCurrentText('off')
-- Initialize save mode dropdown
itm.qSaveMode:AddItem('save')
itm.qSaveMode:AddItem('rename')
itm.qSaveMode:SetCurrentText('save')
-- EVENT BINDING --
function win.On.MyWin.Close(ev)
disp:ExitLoop()
end
function disp.On.Comp_Activate_Tool(ev)
ReloadTargetComboBox()
end
function win.On.qRefreshTargets.Clicked(ev)
ReloadTargetComboBox()
print("Target dropdown refreshed")
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 saveMode = itm.qSaveMode.CurrentText
local presetName = itm.qSaveEaseText.Text
local targetProp = itm.qTargetProp.CurrentText
if (presetName ~= '') then
if saveMode == 'save' then
if EaseCopy(presetName, targetProp, true) then
itm.qSaveEaseText.Text = ''
ReloadEaseComboBox(presetName)
UpdateNumberedButtons()
end
elseif saveMode == 'rename' then
HandleRename(presetName)
end
end
end
function HandleRename(newPresetName)
local oldPresetName = itm.qEase.CurrentText
if not oldPresetName or oldPresetName == '' then
print('No ease selected to rename.')
return
end
if oldPresetName == newPresetName then
print('New name is the same as current name.')
return
end
local savedEases = fusion:GetData("easeCopy.presets")
if savedEases and savedEases[newPresetName] then
local overwriteConfirm = currentComp:AskUser("Preset Already Exists", {
{"Message", Name="Message", "Text", ReadOnly = true,
Default = "A preset with the name '" .. newPresetName .. "' already exists. Do you want to overwrite it?",
Lines = 2}
})
if not overwriteConfirm then
return
end
end
local oldPresetData = fusion:GetData("easeCopy.presets." .. oldPresetName)
if not oldPresetData then
print('Selected ease preset not found.')
return
end
fusion:SetData("easeCopy.presets." .. newPresetName, oldPresetData)
fusion:SetData("easeCopy.presets." .. oldPresetName, nil)
currentComp:AskUser("Rename Successful", {
{"Message", Name="Message", "Text", ReadOnly = true,
Default = "Ease preset renamed from '" .. oldPresetName .. "' to '" .. newPresetName .. "'",
Lines = 2}
})
itm.qSaveEaseText.Text = ''
itm.qSaveMode:SetCurrentText('save')
ReloadEaseComboBox(newPresetName)
UpdateNumberedButtons()
print('Ease preset renamed from "' .. oldPresetName .. '" to "' .. newPresetName .. '"')
end
function win.On.qSaveMode.CurrentIndexChanged(ev)
local saveMode = itm.qSaveMode.CurrentText
if saveMode == 'save' then
itm.qSaveBtn.Text = 'Save ease'
itm.qSaveEaseText.Text = ''
elseif saveMode == 'rename' then
itm.qSaveBtn.Text = 'Rename ease'
local currentEase = itm.qEase.CurrentText
if currentEase and currentEase ~= '' then
itm.qSaveEaseText.Text = currentEase
else
itm.qSaveEaseText.Text = ''
end
end
end
function win.On.qEase.CurrentIndexChanged(ev)
local saveMode = itm.qSaveMode.CurrentText
if saveMode == 'rename' then
local currentEase = itm.qEase.CurrentText
if currentEase and currentEase ~= '' then
itm.qSaveEaseText.Text = currentEase
end
end
end
function win.On.qApplyPrevious.Clicked(ev)
local savedEases = fusion:GetData("easeCopy.presets")
if not savedEases then
print('No ease presets available.')
return
end
local presets = GetKeys(savedEases)
local presetCount = #presets
if presetCount == 0 then
print('No ease presets available.')
return
end
local currentPreset = itm.qEase.CurrentText
local currentIndex = 1
for i, preset in ipairs(presets) do
if preset == currentPreset then
currentIndex = i
break
end
end
local previousIndex = currentIndex - 1
if previousIndex < 1 then
previousIndex = presetCount
end
local newPreset = presets[previousIndex]
itm.qEase:SetCurrentText(newPreset)
local targetProp = itm.qTargetProp.CurrentText
EaseCopy(newPreset, targetProp)
print('Applied previous ease: ' .. newPreset)
end
function win.On.qApplyNext.Clicked(ev)
local savedEases = fusion:GetData("easeCopy.presets")
if not savedEases then
print('No ease presets available.')
return
end
local presets = GetKeys(savedEases)
local presetCount = #presets
if presetCount == 0 then
print('No ease presets available.')
return
end
local currentPreset = itm.qEase.CurrentText
local currentIndex = 1
for i, preset in ipairs(presets) do
if preset == currentPreset then
currentIndex = i
break
end
end
local nextIndex = currentIndex + 1
if nextIndex > presetCount then
nextIndex = 1
end
local newPreset = presets[nextIndex]
itm.qEase:SetCurrentText(newPreset)
local targetProp = itm.qTargetProp.CurrentText
EaseCopy(newPreset, targetProp)
print('Applied next ease: ' .. newPreset)
end
function win.On.qDeleteOne.Clicked(ev)
local deleteProtection = itm.qDeleteProtection.CurrentText
if deleteProtection == 'off' then
print('Delete function is disabled. Set dropdown to "on" to enable deletion.')
return
end
local presetName = itm.qEase.CurrentText
if (presetName == '') then
print('No ease selected to delete.')
itm.qDeleteProtection:SetCurrentText('off')
return
end
local confirmDelete = currentComp:AskUser("Delete selected ease?", {
{"Message", Name="Message", "Text", ReadOnly = true, Default = "Are you sure you want to delete the ease '" .. presetName .. "'?", Lines = 2}
})
if confirmDelete then
fusion:SetData("easeCopy.presets." .. presetName, nil)
ReloadEaseComboBox()
UpdateNumberedButtons()
print('Ease: ' .. presetName .. ' deleted.')
end
itm.qDeleteProtection:SetCurrentText('off')
end
function win.On.qCyclePrevEase.Clicked(ev)
local savedEases = fusion:GetData("easeCopy.presets")
if not savedEases then
print('No ease presets available.')
return
end
local presets = GetKeys(savedEases)
local presetCount = #presets
if presetCount == 0 then
print('No ease presets available.')
return
end
local currentPreset = itm.qEase.CurrentText
local currentIndex = 1
for i, preset in ipairs(presets) do
if preset == currentPreset then
currentIndex = i
break
end
end
local previousIndex = currentIndex - 1
if previousIndex < 1 then
previousIndex = presetCount
end
local newPreset = presets[previousIndex]
itm.qEase:SetCurrentText(newPreset)
print('Cycled to previous ease: ' .. newPreset)
end
function win.On.qCycleNextEase.Clicked(ev)
local savedEases = fusion:GetData("easeCopy.presets")
if not savedEases then
print('No ease presets available.')
return
end
local presets = GetKeys(savedEases)
local presetCount = #presets
if presetCount == 0 then
print('No ease presets available.')
return
end
local currentPreset = itm.qEase.CurrentText
local currentIndex = 1
for i, preset in ipairs(presets) do
if preset == currentPreset then
currentIndex = i
break
end
end
local nextIndex = currentIndex + 1
if nextIndex > presetCount then
nextIndex = 1
end
local newPreset = presets[nextIndex]
itm.qEase:SetCurrentText(newPreset)
print('Cycled to next ease: ' .. newPreset)
end
function win.On.qCyclePrevTarget.Clicked(ev)
local targets = GetTargetList()
local targetCount = #targets
if targetCount == 0 then
print('No targets available.')
return
end
local currentTarget = itm.qTargetProp.CurrentText
local currentIndex = 1
for i, target in ipairs(targets) do
if target == currentTarget then
currentIndex = i
break
end
end
local previousIndex = currentIndex - 1
if previousIndex < 1 then
previousIndex = targetCount
end
local newTarget = targets[previousIndex]
itm.qTargetProp:SetCurrentText(newTarget)
print('Cycled to previous target: ' .. newTarget)
end
function win.On.qCycleNextTarget.Clicked(ev)
local targets = GetTargetList()
local targetCount = #targets
if targetCount == 0 then
print('No targets available.')
return
end
local currentTarget = itm.qTargetProp.CurrentText
local currentIndex = 1
for i, target in ipairs(targets) do
if target == currentTarget then
currentIndex = i
break
end
end
local nextIndex = currentIndex + 1
if nextIndex > targetCount then
nextIndex = 1
end
local newTarget = targets[nextIndex]
itm.qTargetProp:SetCurrentText(newTarget)
print('Cycled to next target: ' .. newTarget)
end
function GetTargetList()
local selectedTools = currentComp:GetToolList(true)
local targets = {'ALL'}
if not selectedTools or #selectedTools == 0 then
return targets
end
local eligibleInputs = FindEligibleInputs(selectedTools)
for _, target in pairs(eligibleInputs) do
table.insert(targets, target)
end
return targets
end
function UpdateNumberedButtons()
for i = 1, maxButtons do
local buttonId = 'qBtn' .. i
if itm[buttonId] then
itm[buttonId].Visible = false
end
end
presetButtons = {}
local savedEases = fusion:GetData("easeCopy.presets")
if not savedEases then
return
end
local presets = GetKeys(savedEases)
if not presets or #presets == 0 then
return
end
for i, presetName in ipairs(presets) do
if i <= maxButtons then
local buttonId = 'qBtn' .. i
if itm[buttonId] then
itm[buttonId].Visible = true
presetButtons[i] = presetName
end
end
end
print('Updated numbered buttons for ' .. #presets .. ' presets')
end
function win.On.qBtn1.Clicked(ev) ApplyPresetByNumber(1) end
function win.On.qBtn2.Clicked(ev) ApplyPresetByNumber(2) end
function win.On.qBtn3.Clicked(ev) ApplyPresetByNumber(3) end
function win.On.qBtn4.Clicked(ev) ApplyPresetByNumber(4) end
function win.On.qBtn5.Clicked(ev) ApplyPresetByNumber(5) end
function win.On.qBtn6.Clicked(ev) ApplyPresetByNumber(6) end
function win.On.qBtn7.Clicked(ev) ApplyPresetByNumber(7) end
function win.On.qBtn8.Clicked(ev) ApplyPresetByNumber(8) end
function win.On.qBtn9.Clicked(ev) ApplyPresetByNumber(9) end
function win.On.qBtn10.Clicked(ev) ApplyPresetByNumber(10) end
function win.On.qBtn11.Clicked(ev) ApplyPresetByNumber(11) end
function win.On.qBtn12.Clicked(ev) ApplyPresetByNumber(12) end
function win.On.qBtn13.Clicked(ev) ApplyPresetByNumber(13) end
function win.On.qBtn14.Clicked(ev) ApplyPresetByNumber(14) end
function win.On.qBtn15.Clicked(ev) ApplyPresetByNumber(15) end
function win.On.qBtn16.Clicked(ev) ApplyPresetByNumber(16) end
function ApplyPresetByNumber(buttonNumber)
local presetName = presetButtons[buttonNumber]
if presetName then
local targetProp = itm.qTargetProp.CurrentText
itm.qEase:SetCurrentText(presetName)
EaseCopy(presetName, targetProp)
print('Applied preset "' .. presetName .. '" via button ' .. buttonNumber)
end
end
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 and newSelected ~= '' then
itm.qEase:SetCurrentText(newSelected)
end
end
function ReloadTargetComboBox()
currentComp = fu:GetCurrentComp()
local selectedTools = currentComp:GetToolList(true)
local currentSelection = itm.qTargetProp.CurrentText
itm.qTargetProp:Clear()
itm.qTargetProp:AddItem('ALL')
if not selectedTools or #selectedTools == 0 then
print("No tools selected. Please select a tool in the composition.")
itm.qTargetProp:SetCurrentText('ALL')
return
end
local eligibleInputs = FindEligibleInputs(selectedTools)
local selectionStillValid = false
for _, target in pairs(eligibleInputs) do
itm.qTargetProp:AddItem(target)
if target == currentSelection then
selectionStillValid = true
end
end
if selectionStillValid and currentSelection ~= '' then
itm.qTargetProp:SetCurrentText(currentSelection)
else
itm.qTargetProp:SetCurrentText('ALL')
end
print("Target dropdown populated with " .. itm.qTargetProp:Count() .. " items")
end
function FindEligibleInputs(tools)
local eligibleInputs = {}
for _, tool in pairs(tools) do
print("Processing tool: " .. tool:GetAttrs().TOOLS_Name)
for _, input in pairs(tool:GetInputList()) do
if IsViableInput(input) then
print(" Viable input: " .. input:GetAttrs().INPS_Name)
local inputTool = GetTool(input)
if inputTool then
if IsModifier(inputTool) then
if IsBezierSpline(inputTool) then
local savedTarget = tool:GetAttrs().TOOLS_Name .. ":" .. input:GetAttrs().INPS_ID
print(" Adding target: " .. savedTarget)
table.insert(eligibleInputs, savedTarget)
else
print(" Recursing into modifier: " .. inputTool:GetAttrs().TOOLS_Name)
for _, v in ipairs(FindEligibleInputs({inputTool})) do
table.insert(eligibleInputs, v)
end
end
end
else
print(" No tool found for input: " .. input:GetAttrs().INPS_Name)
end
else
print(" Input not viable: " .. input:GetAttrs().INPS_Name)
end
end
end
return eligibleInputs
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)
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 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
local t = Split(targetProp, ':')
return input:GetTool():GetAttrs().TOOLS_Name == t[1] and input:GetAttrs().INPS_ID == t[2]
end
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
print("Found modifier: " .. toolAttrs.TOOLS_Name)
return true
end
end
print("Not a modifier: " .. toolAttrs.TOOLS_Name)
return false
end
function IsBezierSpline(tool)
local isBezier = tool:GetAttrs().TOOLS_RegID == "BezierSpline"
print("Tool " .. tool:GetAttrs().TOOLS_Name .. " is BezierSpline: " .. tostring(isBezier))
return isBezier
end
function GetTool(input)
local output = input:GetConnectedOutput()
if output 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 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()
UpdateNumberedButtons()
win:Show()
disp:RunLoop()
win:Hide()
collectgarbage()Thanks, it's working!
Excuse me for asking, which Discord server did you mention?
Why are you still using Resolve version 18, besides the fact that some plugins aren't compatible with recent versions?
Thanks, it's working! Excuse me for asking, which Discord server did you mention?
Why are you still using Resolve version 18, besides the fact that some plugins aren't compatible with recent versions?
- PeejENT's discord: https://discord.com/invite/RkEEEnXQnZ, they have a davinci discorvery channel where people post stuff, and that's where someone posted a reworked version of my script.
- I use 18.6.6 because 19.1 made script UIs studio-only, I don't use any 19.X version before 19.1 because early major davinci version are often said to be unstable, so I use the last 18.X version instead. As of now, I feel like blackmagic has yet to give fusion a feature good enough to make losing script UIs worth it. And while I could easily buy studio, I'm still hesitating because I might want to introduce other people to davinci, and it would have to be the free version.




Sadly it isn't working for me (i'm on 18.6.6) whenever I press save it just doesn't do anything...