Skip to main content
formzuform
Known Participant
October 7, 2026
Open for Voting

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();

    }

})();

    2 replies

    Mathias Moehl
    Community Expert
    Community Expert
    October 16, 2026

    It is not possible to preserve all the easing and the motion path when separating dimensions.

    Translate
    Mathias Möhl - Developer of tools like BeatEdit and Automation Blocks for Premiere Pro and After Effects
    formzuform
    Known Participant
    October 16, 2026

    Of course you can’t keep a single motion path, how would you? You are going to end up with 3 separate values, that it the point, isn’t it?
    But you can transfer the easing so that X Y and Z have exactly the same value curves as they did before separating the dimensions.
    This not being the case by default has been an issue for me ever since and this finally solves it. I tested it and it does precisely what I want it to. Separate the dimensions but keep my animation intact.

    Worth considering for sure. 

    Translate