Separating Dimensions on multidimensional property doesn't transfer easing values but resets to linear
it should work like this:
(function () {
app.beginUndoGroup("Separate Dimensions + Preserve Easing");
try {
var comp = app.project.activeItem;
if (!(comp instanceof CompItem)) {
throw new Error("Keine aktive Komposition.");
}
// ------------------------------------------------------------
// Helpers
// ------------------------------------------------------------
function vectorLength(v) {
var sum = 0;
for (var i = 0; i < v.length; i++) {
sum += v[i] * v[i];
}
return Math.sqrt(sum);
}
function normalize(v) {
var len = vectorLength(v);
var result = [];
if (len < 0.0000001) {
for (var i = 0; i < v.length; i++) {
result.push(0);
}
return result;
}
for (var j = 0; j < v.length; j++) {
result.push(v[j] / len);
}
return result;
}
function subtract(a, b) {
var result = [];
for (var i = 0; i < a.length; i++) {
result.push(a[i] - b[i]);
}
return result;
}
function invert(v) {
var result = [];
for (var i = 0; i < v.length; i++) {
result.push(-v[i]);
}
return result;
}
function zeroVector(dimensions) {
var result = [];
for (var i = 0; i < dimensions; i++) {
result.push(0);
}
return result;
}
function clampInfluence(value) {
// ExtendScript KeyframeEase only accepts 0.1 ... 100
if (value < 0.1) return 0.1;
if (value > 100) return 100;
return value;
}
// Determine actual direction of motion immediately before/after
// a keyframe.
//
// valueAtTime(..., true) evaluates the keyframed property while
// ignoring an expression.
function getDirection(prop, keyIndex, incoming, dimensions) {
var keyTime = prop.keyTime(keyIndex);
var keyValue = prop.keyValue(keyIndex);
var neighbourTime;
var span;
var epsilon;
var sample;
var direction;
if (incoming) {
if (keyIndex <= 1) {
return zeroVector(dimensions);
}
neighbourTime = prop.keyTime(keyIndex - 1);
span = keyTime - neighbourTime;
epsilon = Math.min(0.001, span * 0.001);
epsilon = Math.max(epsilon, 0.000001);
sample = prop.valueAtTime(keyTime - epsilon, true);
direction = subtract(keyValue, sample);
// If sampling is numerically too small, use the
// actual spatial tangent supplied by AE.
if (vectorLength(direction) < 0.0000001 && prop.isSpatial) {
try {
direction = invert(prop.keyInSpatialTangent(keyIndex));
} catch (e) {}
}
// Final fallback.
if (vectorLength(direction) < 0.0000001) {
direction = subtract(
keyValue,
prop.keyValue(keyIndex - 1)
);
}
} else {
if (keyIndex >= prop.numKeys) {
return zeroVector(dimensions);
}
neighbourTime = prop.keyTime(keyIndex + 1);
span = neighbourTime - keyTime;
epsilon = Math.min(0.001, span * 0.001);
epsilon = Math.max(epsilon, 0.000001);
sample = prop.valueAtTime(keyTime + epsilon, true);
direction = subtract(sample, keyValue);
if (vectorLength(direction) < 0.0000001 && prop.isSpatial) {
try {
direction = prop.keyOutSpatialTangent(keyIndex);
} catch (e2) {}
}
if (vectorLength(direction) < 0.0000001) {
direction = subtract(
prop.keyValue(keyIndex + 1),
keyValue
);
}
}
return normalize(direction);
}
// ------------------------------------------------------------
// Process one multidimensional property
// ------------------------------------------------------------
function processProperty(prop) {
if (!prop.isSeparationLeader) {
throw new Error(prop.name + " kann nicht getrennt werden.");
}
if (prop.dimensionsSeparated) {
throw new Error(prop.name + " ist bereits getrennt.");
}
if (prop.numKeys < 1) {
throw new Error(prop.name + " hat keine Keyframes.");
}
var firstValue = prop.keyValue(1);
if (!(firstValue instanceof Array)) {
throw new Error(prop.name + " ist nicht mehrdimensional.");
}
var dimensions = firstValue.length;
var keys = [];
// --------------------------------------------------------
// CAPTURE EVERYTHING BEFORE SEPARATING
// --------------------------------------------------------
for (var k = 1; k <= prop.numKeys; k++) {
var inEase = prop.keyInTemporalEase(k);
var outEase = prop.keyOutTemporalEase(k);
var data = {
time: prop.keyTime(k),
inType: prop.keyInInterpolationType(k),
outType: prop.keyOutInterpolationType(k),
selected: false,
inSpeeds: [],
outSpeeds: [],
inInfluences: [],
outInfluences: []
};
try {
data.selected = prop.keySelected(k);
} catch (selectionError) {}
/*
Spatial Position normally has ONE temporal ease:
speed = magnitude of velocity along motion path
So convert:
velocityVector =
normalizedDirection * originalSpeed
Then each component becomes the speed for the
corresponding separated property.
*/
if (inEase.length === 1) {
var inDirection =
getDirection(prop, k, true, dimensions);
var originalInSpeed = inEase[0].speed;
var originalInInfluence =
clampInfluence(inEase[0].influence);
for (var d = 0; d < dimensions; d++) {
data.inSpeeds[d] =
originalInSpeed * inDirection[d];
data.inInfluences[d] =
originalInInfluence;
}
} else {
// Generic multidimensional temporal property:
// AE already supplies one ease per dimension.
for (var di = 0; di < dimensions; di++) {
var sourceIn =
inEase[Math.min(di, inEase.length - 1)];
data.inSpeeds[di] = sourceIn.speed;
data.inInfluences[di] =
clampInfluence(sourceIn.influence);
}
}
if (outEase.length === 1) {
var outDirection =
getDirection(prop, k, false, dimensions);
var originalOutSpeed = outEase[0].speed;
var originalOutInfluence =
clampInfluence(outEase[0].influence);
for (var d2 = 0; d2 < dimensions; d2++) {
data.outSpeeds[d2] =
originalOutSpeed * outDirection[d2];
data.outInfluences[d2] =
originalOutInfluence;
}
} else {
for (var dj = 0; dj < dimensions; dj++) {
var sourceOut =
outEase[Math.min(dj, outEase.length - 1)];
data.outSpeeds[dj] = sourceOut.speed;
data.outInfluences[dj] =
clampInfluence(sourceOut.influence);
}
}
keys.push(data);
}
// --------------------------------------------------------
// SEPARATE DIMENSIONS
// --------------------------------------------------------
prop.dimensionsSeparated = true;
// --------------------------------------------------------
// RESTORE EASING PER AXIS
// --------------------------------------------------------
for (var dim = 0; dim < dimensions; dim++) {
var follower = prop.getSeparationFollower(dim);
for (var key = 0; key < keys.length; key++) {
var keyData = keys[key];
var targetIndex =
follower.nearestKeyIndex(keyData.time);
if (
Math.abs(
follower.keyTime(targetIndex) -
keyData.time
) > 0.00001
) {
throw new Error(
"Keyframe bei " +
keyData.time +
"s konnte auf " +
follower.name +
" nicht eindeutig gefunden werden."
);
}
var newInEase = new KeyframeEase(
keyData.inSpeeds[dim],
keyData.inInfluences[dim]
);
var newOutEase = new KeyframeEase(
keyData.outSpeeds[dim],
keyData.outInfluences[dim]
);
// Numeric easing first
follower.setTemporalEaseAtKey(
targetIndex,
[newInEase],
[newOutEase]
);
// Then restore Linear / Bezier / Hold state
follower.setInterpolationTypeAtKey(
targetIndex,
keyData.inType,
keyData.outType
);
// Restore keyframe selection
try {
follower.setSelectedAtKey(
targetIndex,
keyData.selected
);
} catch (selectionError2) {}
}
}
}
// ------------------------------------------------------------
// FIND TARGETS
// ------------------------------------------------------------
var targets = [];
var selectedProps = comp.selectedProperties;
// Prefer explicitly selected properties.
for (var p = 0; p < selectedProps.length; p++) {
var candidate = selectedProps[p];
try {
if (
candidate.propertyType === PropertyType.PROPERTY &&
candidate.isSeparationLeader &&
!candidate.dimensionsSeparated &&
candidate.numKeys > 0
) {
targets.push(candidate);
}
} catch (ignoreProperty) {}
}
// If no Position property is selected, use Position from
// selected layers instead.
if (targets.length === 0) {
var selectedLayers = comp.selectedLayers;
for (var l = 0; l < selectedLayers.length; l++) {
var transform =
selectedLayers[l].property("ADBE Transform Group");
if (!transform) continue;
var position =
transform.property("ADBE Position");
if (!position) continue;
try {
if (
position.isSeparationLeader &&
!position.dimensionsSeparated &&
position.numKeys > 0
) {
targets.push(position);
}
} catch (ignoreLayer) {}
}
}
if (targets.length === 0) {
throw new Error(
"Wähle eine ungetrennte Position mit Keyframes " +
"oder einfach den entsprechenden Layer aus."
);
}
// ------------------------------------------------------------
// GO
// ------------------------------------------------------------
for (var t = 0; t < targets.length; t++) {
processProperty(targets[t]);
}
} catch (err) {
alert(
"Separate Dimensions + Preserve Easing\n\n" +
err.toString()
);
} finally {
app.endUndoGroup();
}
})();
