Created
May 7, 2025 22:51
-
-
Save WamWooWam/639db1d489823b7f0c85282c62e25046 to your computer and use it in GitHub Desktop.
Panels UI tool to bake cubic-bezier curves into After Effects keyframes
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
(function (thisObj) { | |
// Create the UI panel | |
var win = (thisObj instanceof Panel) ? thisObj : new Window("palette", "Bezier Curve Tool", undefined, { resizeable: true }); | |
win.orientation = "column"; | |
win.alignChildren = ["fill", "top"]; | |
// Cubic Bezier inputs | |
win.add("statictext", undefined, "Cubic Bezier: x1, y1, x2, y2"); | |
var bezierInputGroup = win.add("group"); | |
bezierInputGroup.orientation = "row"; | |
var bezierInput = bezierInputGroup.add("edittext", undefined, "0.85, 0, 0.15, 1"); | |
bezierInput.preferredSize = [270, 20] | |
// Value inputs | |
win.add("statictext", undefined, "Start Values:"); | |
var valueInputGroup = win.add("group"); | |
valueInputGroup.orientation = "row"; | |
var startInputs = []; | |
var endInputs = []; | |
for (var i = 0; i < 3; i++) { | |
var input = valueInputGroup.add("edittext", undefined, "0"); | |
input.preferredSize = [60, 20] | |
startInputs.push(input); | |
} | |
win.add("statictext", undefined, "End Values:"); | |
var endValueInputGroup = win.add("group"); | |
endValueInputGroup.orientation = "row"; | |
for (var i = 0; i < 3; i++) { | |
var input = endValueInputGroup.add("edittext", undefined, "100"); | |
input.preferredSize = [60, 20] | |
endInputs.push(input); | |
} | |
// Duration & Framerate | |
win.add("statictext", undefined, "Duration (s) and Framerate:"); | |
var durationGroup = win.add("group"); | |
durationGroup.orientation = "row"; | |
var durationInput = durationGroup.add("edittext", undefined, "2"); | |
durationInput.preferredSize = [60, 20] | |
var fpsInput = durationGroup.add("edittext", undefined, "60"); | |
fpsInput.preferredSize = [60, 20] | |
// Bake button | |
var bakeBtn = win.add("button", undefined, "Bake Bezier Curve"); | |
bakeBtn.onClick = function () { | |
try { | |
// Check if a comp and property are selected | |
if (app.project.activeItem == null || !(app.project.activeItem instanceof CompItem)) { | |
alert("Please select a comp and a property."); | |
return; | |
} | |
var comp = app.project.activeItem; | |
var selectedProperties = comp.selectedProperties; | |
if (!(selectedProperties[selectedProperties.length - 1] instanceof Property)) { | |
alert("Please select a single keyframeable property."); | |
return; | |
} | |
var prop = selectedProperties[selectedProperties.length - 1]; | |
var dimensions = prop.value instanceof Array ? prop.value.length : 1; | |
var parts = []; | |
var split = bezierInput.text.split(","); | |
for (var i = 0; i < split.length; i++) { | |
parts.push(parseFloat(split[i])); | |
} | |
if (parts.length < 4) { | |
alert("You must enter 8 comma-separated values."); | |
return; | |
} | |
// Read input values | |
var x1 = parts[0]; | |
var y1 = parts[1]; | |
var x2 = parts[2]; | |
var y2 = parts[3]; | |
var startX = parseFloat(startInputs[0].text); | |
var startY = parseFloat(startInputs[1].text); | |
var startZ = parseFloat(startInputs[2].text); | |
var endX = parseFloat(endInputs[0].text); | |
var endY = parseFloat(endInputs[1].text); | |
var endZ = parseFloat(endInputs[2].text); | |
var duration = parseFloat(durationInput.text); | |
var fps = parseFloat(fpsInput.text); | |
// Cubic bezier math | |
function cubicBezier(t, x1, y1, x2, y2) { | |
function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } | |
function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } | |
function C(aA1) { return 3.0 * aA1; } | |
function calcBezier(t, aA1, aA2) { | |
return ((A(aA1, aA2) * t + B(aA1, aA2)) * t + C(aA1)) * t; | |
} | |
function getSlope(t, aA1, aA2) { | |
return 3.0 * A(aA1, aA2) * t * t + 2.0 * B(aA1, aA2) * t + C(aA1); | |
} | |
function getTForX(x) { | |
var t = x; | |
for (var i = 0; i < 5; ++i) { | |
var currentX = calcBezier(t, x1, x2) - x; | |
var currentSlope = getSlope(t, x1, x2); | |
if (Math.abs(currentSlope) < 1e-6) break; | |
t -= currentX / currentSlope; | |
} | |
return t; | |
} | |
var tApprox = getTForX(t); | |
return calcBezier(tApprox, y1, y2); | |
} | |
app.beginUndoGroup("Bake Cubic Bezier"); | |
var steps = Math.ceil(duration * fps); | |
var timeStart = comp.time; | |
if (dimensions == 3) { | |
prop.setValueAtTime(timeStart, [startX, startY, startZ]); | |
} | |
else if (dimensions == 2) { | |
prop.setValueAtTime(timeStart, [startX, startY]); | |
} | |
else { | |
prop.setValueAtTime(timeStart, [startX]); | |
} | |
for (var i = 0; i <= steps; i++) { | |
var t = i / steps; | |
var easedT = cubicBezier(t, x1, y1, x2, y2); | |
var keyTime = timeStart + t * duration; | |
if (dimensions == 3) { | |
var valueX = startX + (endX - startX) * easedT; | |
var valueY = startY + (endY - startY) * easedT; | |
var valueZ = startZ + (endZ - startZ) * easedT; | |
prop.setValueAtTime(keyTime, [valueX, valueY, valueZ]); | |
} | |
else if (dimensions == 2) { | |
var valueX = startX + (endX - startX) * easedT; | |
var valueY = startY + (endY - startY) * easedT; | |
prop.setValueAtTime(keyTime, [valueX, valueY]); | |
} | |
else { | |
var valueX = startX + (endX - startX) * easedT; | |
prop.setValueAtTime(keyTime, [valueX]); | |
} | |
} | |
app.endUndoGroup(); | |
} | |
catch (e) { | |
alert(e) | |
} | |
}; | |
win.layout.layout(true); | |
})(this); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment