(function (me) {
//ウィンドウを保持するグローバル配列を用意
var customWindows = [];
//==================================================
// 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);
}
}
//==================================================
// ) Custom Keyframes: 複数レイヤー + Positionならオフセット加算
//==================================================
function createCustomKeyframeUI(parent) {
var panel = parent.add("panel", undefined, "Custom Keyframes");
panel.orientation = "column";
panel.alignChildren = "fill";
var subGroup1 = panel.add("group");
subGroup1.orientation = "row";
subGroup1.alignChildren = ["fill", "center"];
var subGroup2 = panel.add("group");
subGroup2.orientation = "row";
subGroup2.alignChildren = ["fill", "center"];
var subGroup3 = panel.add("group");
subGroup3.orientation = "row";
subGroup3.alignChildren = ["fill", "center"];
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"
}
};
subGroup1.add("statictext", undefined, "Property:");
var propertyDropdown = subGroup1.add("dropdownlist", undefined, ["Scale", "Opacity", "Rotation", "Position"]);
propertyDropdown.selection = 0;
subGroup2.add("statictext", undefined, "Frames (comma separated):");
var frameInput = subGroup2.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].frames);
frameInput.characters = 30;
subGroup3.add("statictext", undefined, "Values (comma separated):");
var valueInput = subGroup3.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();
};
}
//==================================================
// ) Repeated Keyframes: 複数レイヤー対応 + Positionならオフセット加算
//==================================================
function createRepeatedKeyframeUI(parent) {
var panel = parent.add("panel", undefined, "Repeated Keyframes");
panel.orientation = "column";
panel.alignChildren = "fill";
var subGroup1 = panel.add("group");
subGroup1.orientation = "row";
subGroup1.alignChildren = ["fill", "center"];
var subGroup2 = panel.add("group");
subGroup2.orientation = "row";
subGroup2.alignChildren = ["fill", "center"];
var subGroup3 = panel.add("group");
subGroup3.orientation = "row";
subGroup3.alignChildren = ["fill", "center"];
var subGroup4 = panel.add("group");
subGroup4.orientation = "row";
subGroup4.alignChildren = ["fill", "center"];
// デフォルト設定 (Positionはオフセット想定)
var defaultSettings = {
"Scale": {
frames: "0,5",
values: "0,0, 100,100"
},
"Opacity": {
frames: "0,5",
values: "100,0"
},
"Rotation": {
frames: "0,5",
values: "0,25"
},
"Position": {
frames: "0,5",
values: "0,0, 100,0"
}
};
subGroup1.add("statictext", undefined, "Property (Scale / Opacity / Rotation / Position):");
var propertyDropdown = subGroup1.add("dropdownlist", undefined, ["Scale", "Opacity", "Rotation", "Position"]);
propertyDropdown.selection = 1; // 例: Opacityを初期
subGroup2.add("statictext", undefined, "Frames (comma separated):");
var frameInput = subGroup2.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].frames);
frameInput.characters = 30;
subGroup3.add("statictext", undefined,
"Values (comma separated):\n(1D for Opacity/Rotation, 2D for Scale/Position)\n(Position is offset from current pos)");
var valueInput = subGroup3.add("edittext", undefined, defaultSettings[propertyDropdown.selection.text].values);
valueInput.characters = 30;
// valueInput.preferredSize.height = 40;
subGroup4.add("statictext", undefined, "Repeat count:");
var repeatInput = subGroup4.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();
};
}
//==================================================
// ) Customウィンドウ (Custom Keyframes, Repeated Keyframes)
//==================================================
function buildCustomKeyframeWindow() {
var w = new Window("palette", "Custom Animations", undefined, { resizeable: true });
var mainGroup = w.add("group");
mainGroup.orientation = "column";
mainGroup.alignChildren = ["left", "top"];
// ① Custom Keyframes
createCustomKeyframeUI(mainGroup);
if (!(w instanceof Window)) {
w.layout.layout(true);
}
return w;
}
function buildRepeatedKeyframeWindow() {
var w = new Window("palette", "Custom Animations", undefined, { resizeable: true });
var mainGroup = w.add("group");
mainGroup.orientation = "column";
mainGroup.alignChildren = ["left", "top"];
// ② Repeated Keyframes
createRepeatedKeyframeUI(mainGroup);
if (!(w instanceof Window)) {
w.layout.layout(true);
}
return w;
}
//==================================================
// ) Basicウィンドウ
//==================================================
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"];
// カスタムウィンドウ(Custom Keyframes)を開くボタンを追加
var openCustomButton = mainGroup.add("button", undefined, "Add Custom Keyframes Window");
// クリックするたびに新しいウィンドウを開く
openCustomButton.onClick = function () {
var newWin = buildCustomKeyframeWindow();
customWindows.push(newWin);
newWin.onClose = function () {
var index = customWindows.indexOf(newWin);
if (index !== -1) customWindows.splice(index, 1);
};
newWin.show();
};
// カスタムウィンドウ(Repeat Keyframes)を開くボタンを追加
var openCustomButton2 = mainGroup.add("button", undefined, "Add Repeat Keyframes Window");
// クリックするたびに新しいウィンドウを開く
openCustomButton2.onClick = function () {
var newWin = buildRepeatedKeyframeWindow();
customWindows.push(newWin);
newWin.onClose = function () {
var index = customWindows.indexOf(newWin);
if (index !== -1) customWindows.splice(index, 1);
};
newWin.show();
};
if (w instanceof Window) {
// w.center();
w.show();
} else {
w.layout.layout(true);
}
return w;
}
//==================================================
// ) 実行: ウィンドウを作る
//==================================================
var basicWin = buildBasicWindow(me);
if (basicWin instanceof Window) {
basicWin.show();
}
})(this);