(function (me) {
//==================================================
// 1) applyKeyframes: Undoは呼び出し側に任せる
//==================================================
function applyKeyframes(layer, propertyName, keyframes) {
var prop = layer.property(propertyName);
if (!prop) {
alert("Layer \"" + layer.name + "\" does not have \"" + propertyName + "\" property.");
return;
}
var comp = layer.containingComp;
var frameDuration = 1 / comp.frameRate;
var currentTime = comp.time;
// ここでは UndoGroup は行わず呼び出し側でまとめる。
for (var i = 0; i < keyframes.length; i++) {
var keyTime = currentTime + (keyframes[i].frame * frameDuration);
prop.setValueAtTime(keyTime, keyframes[i].value);
}
}
//==================================================
// 2) applyRotationKeyframesFromSelection - 複数レイヤー + 一括Undo
//==================================================
function applyRotationKeyframesFromSelection() {
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Please open a composition.");
return;
}
var layers = comp.selectedLayers;
if (layers.length === 0) {
alert("Please select at least one layer.");
return;
}
app.beginUndoGroup("Rotation Shake (multi-layer)");
for (var i = 0; i < layers.length; i++) {
var layer = layers[i];
var prop = layer.property("Rotation");
if (!prop) {
alert("Layer \"" + layer.name + "\" does not have a Rotation property.");
continue;
}
var currentRotation = prop.value;
var keyframes = [
{ frame: 0, value: currentRotation },
{ frame: 5, value: currentRotation + 25 },
{ frame: 10, value: currentRotation - 25 },
{ frame: 15, value: currentRotation + 25 },
{ frame: 20, value: currentRotation - 25 },
{ frame: 25, value: currentRotation + 25 },
{ frame: 30, value: currentRotation - 25 },
{ frame: 35, value: currentRotation }
];
applyKeyframes(layer, "Rotation", keyframes);
}
app.endUndoGroup();
}
//==================================================
// 3) applyKeyframesFromSelection - 複数レイヤー + 一括Undo (Scale, Opacityなど)
//==================================================
function applyKeyframesFromSelection(property, keyframes) {
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Please open a composition.");
return;
}
var layers = comp.selectedLayers;
if (layers.length === 0) {
alert("Please select at least one layer.");
return;
}
app.beginUndoGroup("Apply Keyframes (multi-layer)");
for (var i = 0; i < layers.length; i++) {
var layer = layers[i];
applyKeyframes(layer, property, keyframes);
}
app.endUndoGroup();
}
//==================================================
// 4) applyShakeKeyframesFromSelection - 複数レイヤー + 一括Undo
//==================================================
function applyShakeKeyframesFromSelection(isHorizontal) {
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Please open a composition.");
return;
}
var layers = comp.selectedLayers;
if (layers.length === 0) {
alert("Please select at least one layer.");
return;
}
app.beginUndoGroup("Shake (multi-layer)");
for (var i = 0; i < layers.length; i++) {
var layer = layers[i];
var prop = layer.property("Position");
if (!prop) {
alert("Layer \"" + layer.name + "\" does not have a Position property.");
continue;
}
var basePos = prop.value;
var keyframes = [
{ frame: 0, value: isHorizontal ? [basePos[0] + 50, basePos[1]] : [basePos[0], basePos[1] + 50] },
{ frame: 5, value: isHorizontal ? [basePos[0] - 50, basePos[1]] : [basePos[0], basePos[1] - 50] },
{ frame: 10, value: isHorizontal ? [basePos[0] + 50, basePos[1]] : [basePos[0], basePos[1] + 50] },
{ frame: 15, value: isHorizontal ? [basePos[0] - 50, basePos[1]] : [basePos[0], basePos[1] - 50] },
{ frame: 20, value: isHorizontal ? [basePos[0] + 50, basePos[1]] : [basePos[0], basePos[1] + 50] },
{ frame: 25, value: isHorizontal ? [basePos[0] - 50, basePos[1]] : [basePos[0], basePos[1] - 50] },
{ frame: 30, value: isHorizontal ? [basePos[0] + 50, basePos[1]] : [basePos[0], basePos[1] + 50] },
{ frame: 35, value: isHorizontal ? [basePos[0] - 50, basePos[1]] : [basePos[0], basePos[1] - 50] }
];
applyKeyframes(layer, "Position", keyframes);
}
app.endUndoGroup();
}
//==================================================
// 5) Custom Keyframes: 複数レイヤー + Positionならオフセット加算
//==================================================
function createCustomKeyframeUI(parent) {
var panel = parent.add("panel", undefined, "Custom Keyframes");
panel.orientation = "column";
var defaultSettings = {
"Scale": {
frames: "0,10",
values: "0,0, 100,100"
},
"Opacity": {
frames: "0,10",
values: "0,100"
},
"Rotation": {
frames: "0,10",
values: "0,25"
},
"Position": {
frames: "0,10",
values: "0,0, 100,100"
}
};
panel.add("statictext", undefined, "Property:");
var propertyDropdown = panel.add("dropdownlist", undefined, ["Scale", "Opacity", "Rotation", "Position"]);
propertyDropdown.selection = 0;
panel.add("statictext", undefined, "Frames (comma separated):");
var frameInput = panel.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].frames);
frameInput.characters = 25;
panel.add("statictext", undefined, "Values (comma separated):");
var valueInput = panel.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].values);
valueInput.characters = 30;
valueInput.preferredSize.height = 40;
// Property変更時にデフォルト更新
propertyDropdown.onChange = function () {
var pName = propertyDropdown.selection.text;
if (defaultSettings[pName]) {
frameInput.text = defaultSettings[pName].frames;
valueInput.text = defaultSettings[pName].values;
}
};
var applyButton = panel.add("button", undefined, "Apply Custom Keyframes");
applyButton.onClick = function () {
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Please open a composition.");
return;
}
var layers = comp.selectedLayers;
if (layers.length === 0) {
alert("Please select at least one layer.");
return;
}
var selectedProperty = propertyDropdown.selection.text;
// Frames
var frameStrArray = frameInput.text.split(",");
var frames = [];
for (var i = 0; i < frameStrArray.length; i++) {
var fNum = parseFloat(frameStrArray[i]);
if (isNaN(fNum)) {
alert("Invalid frame value: " + frameStrArray[i]);
return;
}
frames.push(fNum);
}
// Values
var valueStrArray = valueInput.text.split(",");
for (var j = 0; j < valueStrArray.length; j++) {
valueStrArray[j] = valueStrArray[j].replace(/^\s+|\s+$/g, "");
}
var is2D = (selectedProperty === "Scale" || selectedProperty === "Position");
if (is2D) {
if (valueStrArray.length !== frames.length * 2) {
alert("For 2D property, need frames.length x 2 values.\nCheck your input.");
return;
}
} else {
if (valueStrArray.length !== frames.length) {
alert("For 1D property, need frames.length values.\nCheck your input.");
return;
}
}
// ベースのキーフレーム配列 (Positionならオフセット値、他は絶対値)
var baseKeyframes = [];
var idx = 0;
for (var k = 0; k < frames.length; k++) {
if (is2D) {
var vx = parseFloat(valueStrArray[idx]);
var vy = parseFloat(valueStrArray[idx + 1]);
if (isNaN(vx) || isNaN(vy)) {
alert("Invalid numeric value in 2D property at index " + idx);
return;
}
baseKeyframes.push({ frame: frames[k], value: [vx, vy] });
idx += 2;
} else {
var v1 = parseFloat(valueStrArray[idx]);
if (isNaN(v1)) {
alert("Invalid numeric value in property at index " + idx);
return;
}
baseKeyframes.push({ frame: frames[k], value: v1 });
idx += 1;
}
}
app.beginUndoGroup("Custom Keyframes (multi-layer)");
// レイヤーごとに処理
for (var m = 0; m < layers.length; m++) {
var layer = layers[m];
if (selectedProperty === "Position") {
// Positionは現在の値にオフセット加算
var propPos = layer.property("Position");
if (!propPos) {
alert("Layer \"" + layer.name + "\" does not have a Position property.");
continue;
}
var basePos = propPos.value; // CTI時点のPosition
var offsetKeyframes = [];
for (var n = 0; n < baseKeyframes.length; n++) {
var offX = baseKeyframes[n].value[0];
var offY = baseKeyframes[n].value[1];
offsetKeyframes.push({
frame: baseKeyframes[n].frame,
value: [basePos[0] + offX, basePos[1] + offY]
});
}
applyKeyframes(layer, "Position", offsetKeyframes);
} else {
// Scale / Opacity / Rotation ⇒ 従来どおり絶対値
applyKeyframes(layer, selectedProperty, baseKeyframes);
}
}
app.endUndoGroup();
};
}
//==================================================
// 6) Repeated Keyframes: 複数レイヤー対応 + Positionならオフセット加算
//==================================================
function createRepeatedKeyframeUI(parent) {
var panel = parent.add("panel", undefined, "Repeated Keyframes");
panel.orientation = "column";
// デフォルト設定 (Positionはオフセット想定)
var defaultSettings = {
"Scale": {
frames: "0,5",
values: "0,0, 100,100"
},
"Opacity": {
frames: "0,5",
values: "100,50"
},
"Rotation": {
frames: "0,5",
values: "0,25"
},
"Position": {
frames: "0,5",
values: "0,0, 100,0"
}
};
panel.add("statictext", undefined, "Property (Scale / Opacity / Rotation / Position):");
var propertyDropdown = panel.add("dropdownlist", undefined, ["Scale", "Opacity", "Rotation", "Position"]);
propertyDropdown.selection = 1; // 例: Opacityを初期
panel.add("statictext", undefined, "Frames (comma separated):");
var frameInput = panel.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].frames);
frameInput.characters = 25;
panel.add("statictext", undefined,
"Values (comma separated):\n(1D for Opacity/Rotation, 2D for Scale/Position)\n(Position is offset from current pos)");
var valueInput = panel.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].values);
valueInput.characters = 30;
valueInput.preferredSize.height = 40;
panel.add("statictext", undefined, "Repeat count:");
var repeatInput = panel.add("edittext", undefined, "3");
repeatInput.characters = 5;
// Property切り替え時にデフォルト更新
propertyDropdown.onChange = function () {
var pName = propertyDropdown.selection.text;
if (defaultSettings[pName]) {
frameInput.text = defaultSettings[pName].frames;
valueInput.text = defaultSettings[pName].values;
}
};
var applyButton = panel.add("button", undefined, "Apply Repeated Keyframes");
applyButton.onClick = function () {
var comp = app.project.activeItem;
if (!(comp && comp instanceof CompItem)) {
alert("Please open a composition.");
return;
}
var layers = comp.selectedLayers;
if (layers.length === 0) {
alert("Please select at least one layer.");
return;
}
var selectedPropName = propertyDropdown.selection.text;
// Frames
var frameStrArray = frameInput.text.split(",");
var frames = [];
for (var i = 0; i < frameStrArray.length; i++) {
var fNum = parseFloat(frameStrArray[i]);
if (isNaN(fNum)) {
alert("Invalid frame value: " + frameStrArray[i]);
return;
}
frames.push(fNum);
}
// Values
var valueStrArray = valueInput.text.split(",");
for (var j = 0; j < valueStrArray.length; j++) {
valueStrArray[j] = valueStrArray[j].replace(/^\s+|\s+$/g, "");
}
var is2D = (selectedPropName === "Scale" || selectedPropName === "Position");
if (is2D) {
if (valueStrArray.length !== frames.length * 2) {
alert("For 2D property, you need frames.length x 2 values.\nCheck your input.");
return;
}
} else {
if (valueStrArray.length !== frames.length) {
alert("For 1D property, you need frames.length values.\nCheck your input.");
return;
}
}
var repeatCount = parseInt(repeatInput.text, 10);
if (isNaN(repeatCount) || repeatCount < 1) {
alert("Invalid repeat count.");
return;
}
// 1セット分のキーフレーム (Positionならオフセット値)
var baseKeyframes = [];
var idx = 0;
for (var k = 0; k < frames.length; k++) {
if (is2D) {
var vx = parseFloat(valueStrArray[idx]);
var vy = parseFloat(valueStrArray[idx + 1]);
if (isNaN(vx) || isNaN(vy)) {
alert("Invalid numeric value in 2D property at index " + idx);
return;
}
baseKeyframes.push({ frame: frames[k], value: [vx, vy] });
idx += 2;
} else {
var v1 = parseFloat(valueStrArray[idx]);
if (isNaN(v1)) {
alert("Invalid numeric value in property at index " + idx);
return;
}
baseKeyframes.push({ frame: frames[k], value: v1 });
idx += 1;
}
}
// 繰り返し適用
var minFrame = frames[0];
var maxFrame = frames[frames.length - 1];
var chunkSpan = maxFrame - minFrame;
var offsetSpan = chunkSpan * 2;
// allKeyframesには「オフセット前」の相対値が入る(= frames だけずらした形)
var allKeyframes = [];
for (var r = 0; r < repeatCount; r++) {
var offset = r * offsetSpan;
for (var t = 0; t < baseKeyframes.length; t++) {
allKeyframes.push({
frame: baseKeyframes[t].frame + offset,
value: baseKeyframes[t].value
});
}
}
// まとめてUndo
app.beginUndoGroup("Repeated Keyframes (multi-layer)");
// レイヤーごとに適用
for (var m = 0; m < layers.length; m++) {
var layer = layers[m];
if (selectedPropName === "Position") {
// Positionは現在のposに対してオフセットを加算
var propPos = layer.property("Position");
if (!propPos) {
alert("Layer \"" + layer.name + "\" does not have a Position property.");
continue;
}
var layerBasePos = propPos.value; // CTI時点のPosition
// オフセット分を足したキーフレームを作成
var offsetKeyframes = [];
for (var u = 0; u < allKeyframes.length; u++) {
var offX = allKeyframes[u].value[0];
var offY = allKeyframes[u].value[1];
offsetKeyframes.push({
frame: allKeyframes[u].frame,
value: [layerBasePos[0] + offX, layerBasePos[1] + offY]
});
}
applyKeyframes(layer, "Position", offsetKeyframes);
} else {
// Scale / Opacity / Rotation ⇒ 絶対値で適用
applyKeyframes(layer, selectedPropName, allKeyframes);
}
}
app.endUndoGroup();
};
}
//==================================================
// 7) Basicウィンドウ (Scale, Shake, Opacity, Rotation)
//==================================================
function buildBasicWindow(thisObj) {
var w = (thisObj instanceof Panel)
? thisObj
: new Window("palette", "Basic Animations", undefined, { resizeable: true });
var mainGroup = w.add("group");
mainGroup.orientation = "column";
mainGroup.alignChildren = ["fill", "top"];
var rowGroup = mainGroup.add("group");
rowGroup.orientation = "row";
rowGroup.alignChildren = ["fill","top"];
var leftGroup = rowGroup.add("group");
leftGroup.orientation = "column";
leftGroup.alignChildren = ["fill","top"];
var rightGroup = rowGroup.add("group");
rightGroup.orientation = "column";
rightGroup.alignChildren = ["fill","top"];
//--- Scale Panel
var scalePanel = leftGroup.add("panel", undefined, "Scale");
scalePanel.orientation = "column";
var btnUp = scalePanel.add("button", undefined, "Scale Up");
btnUp.onClick = function() {
applyKeyframesFromSelection("Scale", [
{ frame: 0, value: [0, 0] },
{ frame: 10, value: [100, 100] }
]);
};
var btnDown = scalePanel.add("button", undefined, "Scale Down");
btnDown.onClick = function() {
applyKeyframesFromSelection("Scale", [
{ frame: 0, value: [100, 100] },
{ frame: 10, value: [0, 0] }
]);
};
//--- Shake Panel
var shakePanel = leftGroup.add("panel", undefined, "Shake");
shakePanel.orientation = "column";
var btnH = shakePanel.add("button", undefined, "Horizontal Shake");
btnH.onClick = function() {
applyShakeKeyframesFromSelection(true);
};
var btnV = shakePanel.add("button", undefined, "Vertical Shake");
btnV.onClick = function() {
applyShakeKeyframesFromSelection(false);
};
//--- Opacity Panel
var opacityPanel = rightGroup.add("panel", undefined, "Opacity");
opacityPanel.orientation = "column";
var btnFadeIn = opacityPanel.add("button", undefined, "Fade In");
btnFadeIn.onClick = function() {
applyKeyframesFromSelection("Opacity", [
{ frame: 0, value: 0 },
{ frame: 10, value: 100 }
]);
};
var btnFadeOut = opacityPanel.add("button", undefined, "Fade Out");
btnFadeOut.onClick = function() {
applyKeyframesFromSelection("Opacity", [
{ frame: 0, value: 100 },
{ frame: 10, value: 0 }
]);
};
var btnFadeIO = opacityPanel.add("button", undefined, "Fade In & Out");
btnFadeIO.onClick = function() {
applyKeyframesFromSelection("Opacity", [
{ frame: 0, value: 0 },
{ frame: 10, value: 100 },
{ frame: 30, value: 100 },
{ frame: 40, value: 0 }
]);
};
var btnBlink = opacityPanel.add("button", undefined, "Blink");
btnBlink.onClick = function() {
applyKeyframesFromSelection("Opacity", [
{ frame: 0, value: 100 },
{ frame: 5, value: 50 },
{ frame: 10, value: 100 },
{ frame: 15, value: 50 },
{ frame: 20, value: 100 },
{ frame: 25, value: 50 },
{ frame: 30, value: 100 },
{ frame: 35, value: 50 }
]);
};
//--- Rotation Panel
var rotationPanel = rightGroup.add("panel", undefined, "Rotation");
rotationPanel.orientation = "column";
var btnRotShake = rotationPanel.add("button", undefined, "Rotation Shake");
btnRotShake.onClick = function() {
applyRotationKeyframesFromSelection();
};
if (w instanceof Window) {
w.center();
w.show();
} else {
w.layout.layout(true);
}
return w;
}
//==================================================
// 8) Customウィンドウ (Custom Keyframes, Repeated Keyframes)
//==================================================
function buildCustomWindow(thisObj) {
var w = (thisObj instanceof Panel)
? thisObj
: new Window("palette", "Custom Animations", undefined, { resizeable: true });
var mainGroup = w.add("group");
mainGroup.orientation = "column";
mainGroup.alignChildren = ["fill","top"];
// ① Custom Keyframes
createCustomKeyframeUI(mainGroup);
// ② Repeated Keyframes
createRepeatedKeyframeUI(mainGroup);
if (w instanceof Window) {
w.center();
w.show();
} else {
w.layout.layout(true);
}
return w;
}
//==================================================
// 9) 実行: 2つのウィンドウを作る
//==================================================
var basicWin = buildBasicWindow(me);
var customWin = buildCustomWindow(me);
if (basicWin instanceof Window) {
basicWin.show();
}
if (customWin instanceof Window) {
customWin.show();
}
})(this);