「エクスプレッションでレイヤーを円状に並べる」で作ったエクスプレッション適用をスクリプト化。さらに盛り込みたいアイディアがあったので拡張しています。細かい気配りがドリルのように循環する改良版です。
「これ以上ヌルレイヤーを増やしたくない人へのスクリプト「smartNuller.jsx」」の処理も含んでいるためフッテージに無闇にヌルレイヤーを増やしません。
[概要]
複数のレイヤーを放射状に整列させるエクスプレッションを仕込むスクリプト。
[使い方]
- 並べたいレイヤーを全選択する
- スクリプトを実行する
- コントロールレイヤーで全体の調整をする
[オプション]
- コントローラーとしてオレンジのヌル「circle_Control」レイヤーが追加される。
- コントローラーが一番上、その後選択順に下に向かって並び変える。
- 「オフセット」で12時方向のレイヤーを指定。
- 「circleサイズ」で配置する円のサイズ調整。
- 「個別サイズ(%)」で全レイヤーのサイズを一括調整。
- 「広げる角度」で整列の距離を指定(180で12時から18時までの均等配置)。
- 「ジャストフィット」ONで360°に広げても1個目と最後のレイヤーが被らない。
- 「角度をキープ」ONでレイヤーの天地が固定。観覧車状態。
- 「全体回転」でぐるぐる回す。「オフセット」とやっていることは同じ。
- 「3D-X回転」で全レイヤーをその場でピッチ回転。3Dレイヤーのときのみ。
- 「3D-Y回転」で全レイヤーをその場でヨー回転。3Dレイヤーのときのみ。
- 「3D-Z回転」で全レイヤーをその場でロール回転。
- 全体の移動は「circle_Control」レイヤーを移動させる。
- レイヤー個別に移動など調整もできる(わかりやすく[0,0,0]が所定の位置)。
[解説]
//////////////////////////////////////////////////
// 放射状に配置するスクリプト
//////////////////////////////////////////////////
var actCmp=app.project.activeItem;
var sel=actCmp.selectedLayers;
function f_posCircleAlign(){
if(sel.length>0){
for(i=0;i<sel.length-1;i++){
sel[i].moveBefore(sel[sel.length-1]);
}
var topLyr=actCmp.layer(Math.min(sel[0].index,sel[sel.length-1].index));
var bottomLyr=actCmp.layer(Math.max(sel[0].index,sel[sel.length-1].index));
var nullLyr1=f_smartNuller();
nullLyr1.moveBefore(topLyr);
nullLyr1.name="circle_Control";
var nullLyr2=f_smartNuller();
nullLyr2.moveBefore(topLyr);
nullLyr2.name="circle_START";
nullLyr2.enabled=false;
var nullLyr3=f_smartNuller();
nullLyr3.moveAfter(bottomLyr);
nullLyr3.name="circle_END";
nullLyr3.enabled=false;
nullLyr1.label=11;//オレンジ
nullLyr2.label=2;//黄色
nullLyr3.label=2;//黄色
var efAry=["オフセット","circleサイズ","個別サイズ(%)","広げる角度","┗ジャストフィット","角度をキープ","全体回転","個別-X回転","個別-Y回転","個別-Z回転"];
var efAry2=["ADBE Slider Control","ADBE Slider Control","ADBE Slider Control","ADBE Angle Control","ADBE Checkbox Control","ADBE Checkbox Control","ADBE Angle Control","ADBE Angle Control","ADBE Angle Control","ADBE Angle Control"];
var addAry=[];
for(i=0;i<efAry.length;i++){
addAry[i]=nullLyr1.property("ADBE Effect Parade").addProperty(efAry2[i]);
addAry[i].enabled=false;
addAry[i].name=efAry[i];
}//for
nullLyr1.property("ADBE Effect Parade")("circleサイズ")(1).setValue(200);
nullLyr1.property("ADBE Effect Parade")("個別サイズ(%)")(1).setValue(100);
nullLyr1.property("ADBE Effect Parade")("広げる角度")(1).setValue(360);
nullLyr1.property("ADBE Effect Parade")("┗ジャストフィット")(1).setValue(1);
nullLyr1.property("ADBE Effect Parade")("全体回転")(1).expression=
'var qty=thisComp.layer("circle_END").index-thisComp.layer("circle_START").index-1;\r'+
'var mxSp=effect("広げる角度")(1);\r'+
'var adjustSp=1-effect("┗ジャストフィット")(1);\r'+
'var aida=mxSp/(qty-adjustSp);\r'+
'var now=effect("オフセット")(1);\r'+
'(aida*now)-value;\r';
var threeDflag=0;
for(i=0;i<sel.length;i++){
threeDflag=0;
sel[i].position.setValue([0,0,0]);
sel[i].position.expression=
'var ctrl=thisComp.layer("circle_Control")\r'+
'var center=ctrl.transform.position\r'+
'var circleSize=ctrl.effect("circleサイズ")(1);\r'+
'var qty=thisComp.layer("circle_END").index-thisComp.layer("circle_START").index-1;\r'+
'var now=index-thisComp.layer("circle_START").index;\r'+
'var mxSp=ctrl.effect("広げる角度")(1);\r'+
'var adjustSp=1-ctrl.effect("┗ジャストフィット")(1);\r'+
'var aida=mxSp/(qty-adjustSp);\r'+
'var thisAngle=degreesToRadians((now-1)*-aida+ctrl.effect("全体回転")(1)+90);\r'+
'var x=circleSize*Math.cos(thisAngle);\r'+
'var y=-circleSize*Math.sin(thisAngle);\r'+
'center+[x,y,0]+value;';
sel[i].scale.expression=
'var a=thisComp.layer("circle_Control").effect("個別サイズ(%)")(1)-100;\r'+
'value+[a,a,a];';
if(sel[i].threeDLayer==true){
threeDflag=1;
}
sel[i].threeDLayer=true;
sel[i].orientation.expression=
'var ctrl=thisComp.layer("circle_Control");\r'+
'var center=ctrl.transform.position;\r'+
'var a=center-position;\r'+
'var angle=Math.atan2(a[1],a[0]);\r'+
'var autoRt=(thisComp.layer("circle_Control").effect("角度をキープ")(1)==1)?rotation:radiansToDegrees(angle)-90;\r'+
'var z=value+autoRt;\r'+
'[value[0],value[1],value[2]+z[0]]';
sel[i].rotationX.expression=
'value+thisComp.layer("circle_Control").effect("個別-X回転")(1)';
sel[i].rotationY.expression=
'value+thisComp.layer("circle_Control").effect("個別-Y回転")(1)';
sel[i].rotationZ.expression=
'value+thisComp.layer("circle_Control").effect("個別-Z回転")(1)';
if(threeDflag==0){
sel[i].threeDLayer=false;
}
}//for
}
}
function f_smartNuller(){
var addLyr=app.project.activeItem.layers.addNull(); // nullレイヤー追加
for(i=1;i<=app.project.items.length;i++){
//[平面]か[ヌル]であることを判別
if( (app.project.items[i].mainSource == "[object SolidSource]")&&(app.project.items[i].name=="ヌル 1") ){
addLyr.replaceSource(app.project.items[i],true);
break;
}
}
for(i=app.project.items.length;i>=1;i--){
//使用されていない[ヌル n]のみ削除
if( (app.project.items[i].mainSource == "[object SolidSource]")&&(app.project.items[i].name.indexOf("ヌル")!=-1)&&(app.project.items[i].name!="ヌル 1")&&(app.project.items[i].usedIn=="") ){
app.project.items[i].remove();
}
}
return addLyr;
}
app.beginUndoGroup("posCircleAlign");
f_posCircleAlign();
app.endUndoGroup();
全体を挟む処理
if(sel.length>0){
~
}
10行目、98行目はただレイヤーが選択されていなければ処理をしない分岐です。
選択順に重ね順を整える
for(i=0;i<sel.length-1;i++){
sel[i].moveBefore(sel[sel.length-1]);
}
11行目のfor文はタイムライン上で選択順に上から並び替えます。エクスプレッションで配置したいレイヤー数を把握し続けられない都合上、「circle_START」「circle_END」の間に挟まれているレイヤー数で管理するためです。
.moveBefore()で選択レイヤーを最後に選択したレイヤーの1つ上に重ね順変更を繰り返します。これで次々と最後の選択したレイヤーの上に差し込む形で、上から下へ、選択順に重なります。
コントローラーなど管理用レイヤーの準備
15行目からです。
var topLyr=actCmp.layer(Math.min(sel[0].index,sel[sel.length-1].index));
var bottomLyr=actCmp.layer(Math.max(sel[0].index,sel[sel.length-1].index));
var nullLyr1=f_smartNuller();
nullLyr1.moveBefore(topLyr);
nullLyr1.name="circle_Control";
var nullLyr2=f_smartNuller();
nullLyr2.moveBefore(topLyr);
nullLyr2.name="circle_START";
nullLyr2.enabled=false;
var nullLyr3=f_smartNuller();
nullLyr3.moveAfter(bottomLyr);
nullLyr3.name="circle_END";
nullLyr3.enabled=false;
nullLyr1.label=11;//オレンジ
nullLyr2.label=2;//黄色
nullLyr3.label=2;//黄色
ヌルで管理用のレイヤーを作ります。3つ必要です。コントローラーと、レイヤーの数を把握するために対象レイヤー群の上下に配置するヌル2つです。
視認性を確保するために、コントローラーはオレンジ、レイヤー数把握のやつは黄色にラベルカラーを変更しておきます。
ヌルは増やせば増やすほどプロジェクトパネルの平面フォルダにヌル 1、ヌル 2、ヌル 3…と増えるので、「これ以上ヌルレイヤーを増やしたくない人へのスクリプト「smartNuller.jsx」」で無闇に増やさない処理も含めています。
f_smartNuller()で呼び出している関数ですね。100行目からのfunctionがまるまるそれです。
function f_smartNuller(){
var addLyr=app.project.activeItem.layers.addNull(); // nullレイヤー追加
for(i=1;i<=app.project.items.length;i++){
//[平面]か[ヌル]であることを判別
if( (app.project.items[i].mainSource == "[object SolidSource]")&&(app.project.items[i].name=="ヌル 1") ){
addLyr.replaceSource(app.project.items[i],true);
break;
}
}
for(i=app.project.items.length;i>=1;i--){
//使用されていない[ヌル n]のみ削除
if( (app.project.items[i].mainSource == "[object SolidSource]")&&(app.project.items[i].name.indexOf("ヌル")!=-1)&&(app.project.items[i].name!="ヌル 1")&&(app.project.items[i].usedIn=="") ){
app.project.items[i].remove();
}
}
return addLyr;
}
この処理内でreturnしたのを関数の外のnullLyr1で受けることで、その後の並べ替えやリネームで指定できるようにしています。
コントローラーにエフェクトを次々適用
var efAry=["オフセット","circleサイズ","個別サイズ(%)","広げる角度","┗ジャストフィット","角度をキープ","全体回転","個別-X回転","個別-Y回転","個別-Z回転"];
var efAry2=["ADBE Slider Control","ADBE Slider Control","ADBE Slider Control","ADBE Angle Control","ADBE Checkbox Control","ADBE Checkbox Control","ADBE Angle Control","ADBE Angle Control","ADBE Angle Control","ADBE Angle Control"];
var addAry=[];
for(i=0;i<efAry.length;i++){
addAry[i]=nullLyr1.property("ADBE Effect Parade").addProperty(efAry2[i]);
addAry[i].enabled=false;
addAry[i].name=efAry[i];
}//for
33行目以降、ここは処理をまとめました。
配列を用意して、それぞれefAry[]にエフェクト名を、efAry2[]に対応するエクスプレッション制御を格納しておいて、for文で回して適用→リネームを繰り返します。
addAry[i].enabled=false;はおそらく不要ですが、だいぶ前のバージョンでエフェクトがoffの方がメモリを食わないという説を信じているので、エクスプレッション制御はエフェクトoffでもできることは変わらないために.enabledはfalseにしています。
当サイトの他のスクリプトもいちいちfalseにしているのですが、なくてもいい処理です。
コントローラーにデフォルト値とエクスプレッションを仕込む
nullLyr1.property("ADBE Effect Parade")("circleサイズ")(1).setValue(200);
nullLyr1.property("ADBE Effect Parade")("個別サイズ(%)")(1).setValue(100);
nullLyr1.property("ADBE Effect Parade")("広げる角度")(1).setValue(360);
nullLyr1.property("ADBE Effect Parade")("┗ジャストフィット")(1).setValue(1);
nullLyr1.property("ADBE Effect Parade")("全体回転")(1).expression=
'var qty=thisComp.layer("circle_END").index-thisComp.layer("circle_START").index-1;\r'+
'var mxSp=effect("広げる角度")(1);\r'+
'var adjustSp=1-effect("┗ジャストフィット")(1);\r'+
'var aida=mxSp/(qty-adjustSp);\r'+
'var now=effect("オフセット")(1);\r'+
'(aida*now)-value;\r';
42~52行目がコントローラー最後の仕掛けです。
「circleサイズ」エフェクトにデフォルトの200をセット。中心点からの距離px。半径。
「個別サイズ(%)」エフェクトは対象レイヤーのスケールにプラスするパーセント。スケールに仕込むエクスプレッションで後述します。
「広げる角度」は1回転させるケースが多いかなとデフォルト360にしています。ご自身のよく使う数値があれば書き換えていただければ。
「┗ジャストフィット」はデフォルトでONに。名前から機能が想像しずらいので詳細を後述します。
「全体回転」には値ではなくエクスプレッションをセットします。これを回せば全体が回ればいいだけであればエクスプレッションいらないのですが、オフセットという機能を実装してしまったために必要です。
オフセットは基準となる12時方向に何個目のレイヤーが来るかオフセットできるものです。隣のレイヤーまでの角度を追従しなくてはならないので、「広げる角度」も引っ張り、いくつオフセットしているかの数値と自分の数値を足すようにしました。
配置用レイヤーへのセッティング
var threeDflag=0;
for(i=0;i<sel.length;i++){
threeDflag=0;
sel[i].position.setValue([0,0,0]);
sel[i].position.expression=
'var ctrl=thisComp.layer("circle_Control")\r'+
'var center=ctrl.transform.position\r'+
'var circleSize=ctrl.effect("circleサイズ")(1);\r'+
'var qty=thisComp.layer("circle_END").index-thisComp.layer("circle_START").index-1;\r'+
'var now=index-thisComp.layer("circle_START").index;\r'+
'var mxSp=ctrl.effect("広げる角度")(1);\r'+
'var adjustSp=1-ctrl.effect("┗ジャストフィット")(1);\r'+
'var aida=mxSp/(qty-adjustSp);\r'+
'var thisAngle=degreesToRadians((now-1)*-aida+ctrl.effect("全体回転")(1)+90);\r'+
'var x=circleSize*Math.cos(thisAngle);\r'+
'var y=-circleSize*Math.sin(thisAngle);\r'+
'center+[x,y,0]+value;';
sel[i].scale.expression=
'var a = thisComp.layer("circle_Control").effect("個別サイズ(%)")(1)-100;\r'+
'value+[a,a,a];';
if(sel[i].threeDLayer==true){
threeDflag=1;
}
sel[i].threeDLayer=true;
sel[i].orientation.expression=
'var ctrl=thisComp.layer("circle_Control");\r'+
'var center=ctrl.transform.position;\r'+
'var a=center-position;\r'+
'var angle=Math.atan2(a[1],a[0]);\r'+
'var plusAngleZ=ctrl.effect("個別-X回転")(1);\r'+
'var autoRt=(thisComp.layer("circle_Control").effect("角度をキープ")(1)==1)?rotation:radiansToDegrees(angle)+plusAngleZ-90;\r'+
'var z=value+autoRt+plusAngleZ;\r'+
'[value[0],value[1],value[2]+z[0]]';
sel[i].rotationX.expression=
'value+thisComp.layer("circle_Control").effect("個別-X回転")(1)';
sel[i].rotationY.expression=
'value+thisComp.layer("circle_Control").effect("個別-Y回転")(1)';
sel[i].rotationZ.expression=
'value+thisComp.layer("circle_Control").effect("個別-Z回転")(1)';
if(threeDflag==0){
sel[i].threeDLayer=false;
}
}//for
ラストの54行目から選択した配置用レイヤーのセッティングです。
threeDfrag、これは方向、X回転、Y回転にもエクスプレッションを仕込んでおきたいのですが、2Dレイヤーにスクリプトからエクスプレッションを入れたくとも2D状態なので方向もX回転もY回転もないよ、とエラーが出るため、75行目でレイヤーが2Dか3Dかをチェックし、78行目で一旦強制的に3Dレイヤーにしてエクスプレッションを入れ、元の状態に戻すためのフラグとして使っています。元が2Dなら3Dスイッチをオフにし直すために3DスイッチをONにする前に状態を保存しておかないと戻せないためです。
後は結構見た通りです。エクスプレッションによって配置レイヤーの位置は決定されるのですが、個別にズラしたいこともあるので位置は計算結果+valueとしました。
元のvalueに半端な位置が入るとずらした状態から整列状態に戻すようなキーフレームアニメーションを設定する際に難儀するため、valueは[0,0,0]でいいようにしてあります。配置段階で[0,0,0]にしてやります。
次に59行目、位置へのエクスプレッションではコントローラー位置をcenterとして、「circleサイズ」での調整、自分が何個目のレイヤーかで角度を決定するためのもろもろです。ここでジャストフィットに関する処理を入れています。一旦このまま読み進めて後述のジャストフィットについてで言及します。
72行目、スケールへのエクスプレッションでは全スケールを一括調整するための「個別サイズ(%)」と、レイヤー自体のスケールを単体で変えるためのvalueを両方生かします。
「個別サイズ(%)」の数値を-100しているのは、スケール100が元のサイズという方が直感的と思いましたのでこうしています。特に「個別サイズ(%)」は0開始でいい場合は-100にしなくていいのと、「個別サイズ(%)」にセットするデフォルト値は0(処理を消してOK)に変えても良いです。
79行目、方向にエクスプレッションを仕込みますが、X,Y,Z回転に自動処理を入れてしまうと、個別に回転させたいときに軸がズレるからです。こうしておくと、レイヤー単体の回転はX,Y,Z回転を使えば直感的な回転をしてくれます。
87行目以降はX,Y,Z回転にそれぞれコントローラーの値をリンクさせます。単体で回転させたい場合に備えvalueも生かします。
備考
オフセットいつつかうの?
キャラクター選択画面みたいな、聖剣伝説2のリングコマンドみたいなことをしたいときでしょうね。
回転させるにしても隣のレイヤーまでの角度を毎回計算するのは不憫なので、全体回転ではなくオフセットを使います。これにキーフレームで簡単にレイヤー単位で回転できます。
ジャストフィットについて
最後に「┗ジャストフィット」機能の解説です。
まずこれを見てください。レイヤーは0~9の10個あります。
違和感ないですよね。
ジャストフィットをOFFにしてみましょう。
レイヤーが1個足りないですよね。
これ、360°、720°など循環する角度の場合、最後のレイヤーが最初のレイヤーと被るので、カード0と同じ位置にカード9があります。わかりやすいように加工すると、
こうなっています。
この循環する角度の場合には1個分距離を戻してやると直感的な角度になります。このときはジャストフィットをONにします。
もしこの処理がないと、今回は配置レイヤーが10個なので360/10=36°マイナスして、広げる角度は324°に設定しないといけません。面倒ですよね。どういたしまして。
続いて、ジャストフィットをON(1個分戻す)にしたまま広げる角度を180°に絞ってみます。
1個分距離が足りないですよね。
180°の場合はカード9は真下の6時方向に欲しいわけです。このときはジャストフィットをOFFにします。
度の値が0のときはジャストフィットON、それ以外はジャストフィットOFFが直感的になるので、使い分けできるようにしました。
この記事へのコメントはありません。