Skip to content

Instantly share code, notes, and snippets.

@WamWooWam
Created May 7, 2025 22:51
Show Gist options
  • Save WamWooWam/639db1d489823b7f0c85282c62e25046 to your computer and use it in GitHub Desktop.
Save WamWooWam/639db1d489823b7f0c85282c62e25046 to your computer and use it in GitHub Desktop.
Panels UI tool to bake cubic-bezier curves into After Effects keyframes
(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