bounceKeyframe_simple.jsx

(function () {
    app.beginUndoGroup("Apply Bounce Animation");

    var comp = app.project.activeItem;
    if (!(comp instanceof CompItem)) {
        alert("コンポジションを開いてください。");
        return;
    }

    var selectedLayers = comp.selectedLayers;
    if (selectedLayers.length === 0) {
        alert("レイヤーを選択してください。");
        return;
    }

    for (var i = 0; i < selectedLayers.length; i++) {
        var layer = selectedLayers[i];
        var position = layer.property("Position");

        if (!position.canSetExpression) {
            alert("このレイヤーの位置にキーフレームを設定できません。");
            continue;
        }

        var startTime = comp.time;
        var duration = 1.2; // バウンスの持続時間
        var bounces = 4; // バウンス回数
        var amplitude = 200; // 初期振幅
        var damping = 2.5; // 減衰率

        var initialPos = position.value;
        var endPos = [initialPos[0], initialPos[1] + amplitude, initialPos[2] || 0];

        position.setValueAtTime(startTime, initialPos);
        position.setValueAtTime(startTime + 0.1, endPos);

        for (var j = 1; j <= bounces; j++) {
            var t = startTime + j * (duration / bounces);
            var bounceHeight = amplitude / Math.pow(damping, j);
            var bouncePos = [initialPos[0], initialPos[1] + (j % 2 === 0 ? bounceHeight : -bounceHeight), initialPos[2] || 0];

            position.setValueAtTime(t, bouncePos);
        }

        // 最終的な位置
        position.setValueAtTime(startTime + duration, initialPos);
    }

    app.endUndoGroup();
})();

bounceKeyframe.jsx

(function () {
    function createUI(thisObj) {
        var win = (thisObj instanceof Panel) ? thisObj : new Window("palette", "Bounce Animation", undefined, {resizeable: true});
        
        win.orientation = "column";
        win.alignChildren = "left";

        // バウンスの設定
        win.add("statictext", undefined, "バウンスの設定:");

        var bounceGroup = win.add("group");
        bounceGroup.orientation = "row";
        bounceGroup.add("statictext", undefined, "回数:");
        var bounceInput = bounceGroup.add("edittext", undefined, "4");
        bounceInput.characters = 3;

        var amplitudeGroup = win.add("group");
        amplitudeGroup.orientation = "row";
        amplitudeGroup.add("statictext", undefined, "振幅:");
        var amplitudeInput = amplitudeGroup.add("edittext", undefined, "200");
        amplitudeInput.characters = 5;

        var dampingGroup = win.add("group");
        dampingGroup.orientation = "row";
        dampingGroup.add("statictext", undefined, "減衰率:");
        var dampingInput = dampingGroup.add("edittext", undefined, "2.5");
        dampingInput.characters = 3;

        var durationGroup = win.add("group");
        durationGroup.orientation = "row";
        durationGroup.add("statictext", undefined, "持続時間:");
        var durationInput = durationGroup.add("edittext", undefined, "1.2");
        durationInput.characters = 4;

        var frequencyGroup = win.add("group");
        frequencyGroup.orientation = "row";
        frequencyGroup.add("statictext", undefined, "波の周期:");
        var frequencyInput = frequencyGroup.add("edittext", undefined, "1.0");
        frequencyInput.characters = 3;

        var easingGroup = win.add("group");
        easingGroup.orientation = "row";
        easingGroup.add("statictext", undefined, "イージング:");
        var easingDropdown = easingGroup.add("dropdownlist", undefined, ["リニア", "イーズイン", "イーズアウト", "イーズイン・アウト"]);
        easingDropdown.selection = 0;

        // バウンス方向
        var directionGroup = win.add("group");
        directionGroup.orientation = "row";
        directionGroup.add("statictext", undefined, "方向:");
        var directionDropdown = directionGroup.add("dropdownlist", undefined, ["下 → 上", "上 → 下"]);
        directionDropdown.selection = 0;

        // 軸の選択
        var axisGroup = win.add("group");
        axisGroup.orientation = "row";
        axisGroup.add("statictext", undefined, "適用軸:");
        var xCheckbox = axisGroup.add("checkbox", undefined, "X軸");
        var yCheckbox = axisGroup.add("checkbox", undefined, "Y軸");
        yCheckbox.value = true; // デフォルトでY軸に適用
        var rotationCheckbox = axisGroup.add("checkbox", undefined, "回転");

        // 適用ボタン
        var applyButton = win.add("button", undefined, "バウンスを適用");

        applyButton.onClick = function () {
            var bounces = parseInt(bounceInput.text, 10);
            var amplitude = parseFloat(amplitudeInput.text);
            var damping = parseFloat(dampingInput.text);
            var duration = parseFloat(durationInput.text);
            var frequency = parseFloat(frequencyInput.text);
            var direction = directionDropdown.selection.index; // 0: 下 → 上, 1: 上 → 下
            var easingType = easingDropdown.selection.index; // 0: リニア, 1: イーズイン, 2: イーズアウト, 3: イーズイン・アウト
            var applyX = xCheckbox.value;
            var applyY = yCheckbox.value;
            var applyRotation = rotationCheckbox.value;

            if (isNaN(bounces) || isNaN(amplitude) || isNaN(damping) || isNaN(duration) || isNaN(frequency)) {
                alert("すべての値を正しく入力してください!");
                return;
            }

            applyBounceAnimation(bounces, amplitude, damping, duration, frequency, direction, easingType, applyX, applyY, applyRotation);
        };

        if (win instanceof Window) {
            win.center();
            win.show();
        }
    }

    function applyBounceAnimation(bounces, amplitude, damping, duration, frequency, direction, easingType, applyX, applyY, applyRotation) {
        app.beginUndoGroup("Apply Bounce Animation");

        var comp = app.project.activeItem;
        if (!(comp instanceof CompItem)) {
            alert("コンポジションを開いてください。");
            return;
        }

        var selectedLayers = comp.selectedLayers;
        if (selectedLayers.length === 0) {
            alert("レイヤーを選択してください。");
            return;
        }

        for (var i = 0; i < selectedLayers.length; i++) {
            var layer = selectedLayers[i];
            var position = layer.property("Position");
            var rotation = layer.property("Rotation");

            var startTime = comp.time;
            var initialPos = position.value;
            var initialRotation = rotation.value;

            var endPos = [
                applyX ? initialPos[0] + (direction === 0 ? amplitude : -amplitude) : initialPos[0],
                applyY ? initialPos[1] + (direction === 0 ? -amplitude : amplitude) : initialPos[1],
                initialPos[2] || 0
            ];

            var endRotation = applyRotation ? initialRotation + 20 : initialRotation;

            function getEasedTime(t, type) {
                switch (type) {
                    case 1: return Math.pow(t, 3);  // イーズイン
                    case 2: return 1 - Math.pow(1 - t, 3); // イーズアウト
                    case 3: return t < 0.5 ? 4 * Math.pow(t, 3) : 1 - Math.pow(-2 * t + 2, 3) / 2; // イーズイン・アウト
                    default: return t; // リニア
                }
            }

            if (applyX || applyY) {
                position.setValueAtTime(startTime, initialPos);
                position.setValueAtTime(startTime + 0.1, endPos);

                for (var j = 1; j <= bounces; j++) {
                    var t = startTime + j * (duration / bounces);
                    var easedT = getEasedTime(j / bounces, easingType) * frequency;
                    var bounceHeight = amplitude / Math.pow(damping, j);
                    var bouncePos = [
                        applyX ? initialPos[0] + (j % 2 === 0 ? bounceHeight : -bounceHeight) : initialPos[0],
                        applyY ? initialPos[1] + (j % 2 === 0 ? -bounceHeight : bounceHeight) : initialPos[1],
                        initialPos[2] || 0
                    ];

                    position.setValueAtTime(t * easedT, bouncePos);
                }

                position.setValueAtTime(startTime + duration, initialPos);
            }
        }

        app.endUndoGroup();
    }

    createUI(this);
})();

(function () {
    function createUI(thisObj) {
        var win = (thisObj instanceof Panel) ? thisObj : new Window("palette", "Bounce Animation", undefined, {resizeable: true});
        
        win.orientation = "column";
        win.alignChildren = "left";

        // バウンスの設定
        win.add("statictext", undefined, "バウンスの設定:");

        var bounceGroup = win.add("group");
        bounceGroup.orientation = "row";
        bounceGroup.add("statictext", undefined, "回数:");
        var bounceInput = bounceGroup.add("edittext", undefined, "4");
        bounceInput.characters = 3;

        var amplitudeGroup = win.add("group");
        amplitudeGroup.orientation = "row";
        amplitudeGroup.add("statictext", undefined, "振幅:");
        var amplitudeInput = amplitudeGroup.add("edittext", undefined, "200");
        amplitudeInput.characters = 5;

        var dampingGroup = win.add("group");
        dampingGroup.orientation = "row";
        dampingGroup.add("statictext", undefined, "減衰率:");
        var dampingInput = dampingGroup.add("edittext", undefined, "2.5");
        dampingInput.characters = 3;

        var durationGroup = win.add("group");
        durationGroup.orientation = "row";
        durationGroup.add("statictext", undefined, "持続時間:");
        var durationInput = durationGroup.add("edittext", undefined, "1.2");
        durationInput.characters = 4;

        // バウンス方向
        var directionGroup = win.add("group");
        directionGroup.orientation = "row";
        directionGroup.add("statictext", undefined, "方向:");
        var directionDropdown = directionGroup.add("dropdownlist", undefined, ["下 → 上", "上 → 下"]);
        directionDropdown.selection = 0;

        // 適用軸の選択
        var axisGroup = win.add("group");
        axisGroup.orientation = "row";
        axisGroup.add("statictext", undefined, "適用軸:");
        var xCheckbox = axisGroup.add("checkbox", undefined, "X軸");
        var yCheckbox = axisGroup.add("checkbox", undefined, "Y軸");
        yCheckbox.value = true; // デフォルトでY軸に適用
        var rotationCheckbox = axisGroup.add("checkbox", undefined, "回転");

        // 非線形バウンスオプション
        var nonLinearGroup = win.add("group");
        nonLinearGroup.orientation = "row";
        var nonLinearCheckbox = nonLinearGroup.add("checkbox", undefined, "後半のキーフレームを多くする");
        nonLinearCheckbox.value = true;

        // 適用ボタン
        var applyButton = win.add("button", undefined, "バウンスを適用");

        applyButton.onClick = function () {
            var bounces = parseInt(bounceInput.text, 10);
            var amplitude = parseFloat(amplitudeInput.text);
            var damping = parseFloat(dampingInput.text);
            var duration = parseFloat(durationInput.text);
            var direction = directionDropdown.selection.index; // 0: 下 → 上, 1: 上 → 下
            var applyX = xCheckbox.value;
            var applyY = yCheckbox.value;
            var applyRotation = rotationCheckbox.value;
            var nonLinear = nonLinearCheckbox.value;

            if (isNaN(bounces) || isNaN(amplitude) || isNaN(damping) || isNaN(duration)) {
                alert("すべての値を正しく入力してください!");
                return;
            }

            applyBounceAnimation(bounces, amplitude, damping, duration, direction, applyX, applyY, applyRotation, nonLinear);
        };

        if (win instanceof Window) {
            win.center();
            win.show();
        }
    }

    function applyBounceAnimation(bounces, amplitude, damping, duration, direction, applyX, applyY, applyRotation, nonLinear) {
        app.beginUndoGroup("Apply Bounce Animation");

        var comp = app.project.activeItem;
        if (!(comp instanceof CompItem)) {
            alert("コンポジションを開いてください。");
            return;
        }

        var selectedLayers = comp.selectedLayers;
        if (selectedLayers.length === 0) {
            alert("レイヤーを選択してください。");
            return;
        }

        for (var i = 0; i < selectedLayers.length; i++) {
            var layer = selectedLayers[i];
            var position = layer.property("Position");
            var rotation = layer.property("Rotation");

            var startTime = comp.time;
            var initialPos = position.value;
            var initialRotation = rotation.value;

            function getNonLinearTime(index) {
                if (nonLinear) {
                    return startTime + duration * (1 - Math.exp(-index / (bounces / 2)));
                } else {
                    return startTime + index * (duration / bounces);
                }
            }

            var endPos = [
                applyX ? initialPos[0] + (direction === 0 ? amplitude : -amplitude) : initialPos[0],
                applyY ? initialPos[1] + (direction === 0 ? -amplitude : amplitude) : initialPos[1],
                initialPos[2] || 0
            ];

            var endRotation = applyRotation ? initialRotation + 20 : initialRotation; // 20度回転

            if (applyX || applyY) {
                position.setValueAtTime(startTime, initialPos);
                position.setValueAtTime(getNonLinearTime(0.1), endPos);

                for (var j = 1; j <= bounces; j++) {
                    var t = getNonLinearTime(j);
                    var bounceHeight = amplitude / Math.pow(damping, j);
                    var bouncePos = [
                        applyX ? initialPos[0] + (j % 2 === 0 ? bounceHeight : -bounceHeight) : initialPos[0],
                        applyY ? initialPos[1] + (j % 2 === 0 ? -bounceHeight : bounceHeight) : initialPos[1],
                        initialPos[2] || 0
                    ];

                    position.setValueAtTime(t, bouncePos);
                }

                position.setValueAtTime(getNonLinearTime(bounces), initialPos);
            }

            if (applyRotation) {
                rotation.setValueAtTime(startTime, initialRotation);
                rotation.setValueAtTime(getNonLinearTime(0.1), endRotation);

                for (var j = 1; j <= bounces; j++) {
                    var t = getNonLinearTime(j);
                    var bounceRotation = initialRotation + (j % 2 === 0 ? 10 : -10) / Math.pow(damping, j);
                    rotation.setValueAtTime(t, bounceRotation);
                }

                rotation.setValueAtTime(getNonLinearTime(bounces), initialRotation);
            }
        }

        app.endUndoGroup();
    }

    createUI(this);
})();