標準機能ではできない「複数レイヤーキーのコピペ」をするスクリプトを作ってみました。色々と制限があるんですが、使えるときは使えると思います。制限の内容についてはコードの次の『使い方』を参照ください。
以下のコードをテキストにコピーして、拡張子を.jsxにすれば完成です。
右上のマークをクリックすれば全文をコピーできます。
(function (aGbl) {
var mTjs = {};
var mScrName = "MassiveKeyPaste";
function mCreateUI(aObj) {
var mPorW = (aObj instanceof Panel) ? aObj : new Window("palette",mScrName, undefined);
mPorW.preferredSize = [200, 200];
mPorW.margins = [10, 10, 10, 10];
mPorW.spacing = 5;
mPorW.mBtCopy = mPorW.add("button { preferredSize : [120,20] ,alignment : [ 'fill','top' ] ,text : 'Copy' }");
mPorW.mBtPaste = mPorW.add("button { preferredSize : [120,20] ,alignment : [ 'fill','top' ] ,text : 'Paste' }");
mPorW.mCbExp = mPorW.add("checkbox { preferredSize : [120,20] ,alignment : [ 'left','top' ] ,text : 'With Expression' }");
mPorW.mCbIndc = mPorW.add("checkbox { preferredSize : [120,20] ,alignment : [ 'left','top' ] ,text : 'At Indicator' }");
return mPorW;
}
//mPnlという名でメインウインドウを作成。
var mPnl = mCreateUI(aGbl);
if (mPnl instanceof Window) {
mPnl.center();
mPnl.show();
} else if (mPnl instanceof Panel) {
//UIパネルの場合は以下をしないと自動レイアウトされない。
mPnl.layout.layout(true);
}
//----------------------------------------------------------------------------------------------------------------------
//チェックボックスはオンクリックで設定保存する。
mPnl.mCbExp.onClick = function () {
app.settings.saveSetting(mScrName, "mCbExp", mPnl.mCbExp.value.toString());
}
mPnl.mCbIndc.onClick = function () {
app.settings.saveSetting(mScrName, "mCbIndc", mPnl.mCbIndc.value.toString());
}
//あれば設定を読み込んで上書きする。
if (app.settings.haveSetting(mScrName, "mCbExp")) {
mPnl.mCbExp.value = (app.settings.getSetting(mScrName, "mCbExp") === "true");
}
if (app.settings.haveSetting(mScrName, "mCbIndc")) {
mPnl.mCbIndc.value = (app.settings.getSetting(mScrName, "mCbIndc") === "true");
}
//----------------------------------------------------------------------------------------------------------------------
//レイヤー数に応じたキー情報を得ておく。
//必要なのはレイヤー数(配列の数)、各レイヤーの各プロップスの種類&キー情報。
//コピー先に各プロップが無かったら作る?
//ペースト時に階層とプロップインデックスが必要。
mPnl.mBtCopy.onClick = function (){
try{
var mAi = app.project.activeItem;
var mSls = mAi.selectedLayers;
var mAllSelProps = mAi.selectedProperties;
//キーがあるプロップのみにする。
var mKeyProps = mGetKeyProps(mAllSelProps);
//プロップの親レイヤー番号でソートしとく。
mKeyProps.sort( function( a , b ){ return a.propertyGroup(a.propertyDepth).index - b.propertyGroup(b.propertyDepth).index; } );
//まずは先頭キー時間を得る。
var mSttTime = mGetSttTime( mKeyProps );
//プロップごとに、キー情報Obj配列を得る。
var mkeyInfoss = [];
for( var i =0; i<mKeyProps.length; i++ ){
mkeyInfoss.push( mGetKeyInfos( mKeyProps[i], mSttTime));
}
//プロップごとのアドレスも得ておく。
var mAdrss = [];
for( var i =0; i<mKeyProps.length; i++ ){
mAdrss.push( mGetSelPptyAdrsLyrIdx(mKeyProps[i]));
}
//もしもエクスプレッションチェックがあればプロップごとのエクスプレッションも保存しておく。
var mExps = [];
if( mPnl.mCbExp.value ){
for( var i =0; i<mKeyProps.length; i++ ){
mExps.push(mKeyProps[i].expression);
}
}
//選択レイヤーが何個かも得ておく。
var mLyrNum = mSls.length;
//キーがあるレイヤーのみを集める。同レイヤーの別プロップがある場合はレイヤーが重複する。
var mKeyLyrs = [];
for( var i =0; i<mAdrss.length; i++ ){
mKeyLyrs.push(mAi.layers[mAdrss[i][0]]);
}
//最先頭キーから最インポイントまでの尺も得ておく。
var mDurFromFInP = mGetDurFromFInP( mKeyLyrs, mSttTime );
//グローバル要素に保存しておく。
mTjs.mkeyInfoss = mkeyInfoss;
mTjs.mAdrss = mAdrss;
mTjs.mLyrNum = mLyrNum;
mTjs.mExps = mExps;
mTjs.mDurFromFInP = mDurFromFInP;
}catch(e){
alert(e.message + e.line);
}
}
//--------------------------------------------------------
//グローバルObjに保存したキー情報を元に、キーを入れる。
mPnl.mBtPaste.onClick = function (){
try{
app.beginUndoGroup("CopyCutPasteKeys");
var mAi = app.project.activeItem;
var mSls = mAi.selectedLayers;
//グローバル要素を得る。
var mkeyInfoss = mTjs.mkeyInfoss;
var mAdrss = mTjs.mAdrss;
var mLyrNum = mTjs.mLyrNum;
var mExps = mTjs.mExps;
var mDurFromFInP = mTjs.mDurFromFInP;
//エクスプレッション配列が空でも一応””を入れておく。
if( mExps.length === 0 ){
for( var i =0; i<mkeyInfoss.length; i++ ){
mExps.push("");
}
}
//選択レイヤーをインデックス順に並び変える。
mSls.sort( function( a , b ){ return a.index - b.index; } );
//複数レイヤーに対応させる。mLyrNumで選択レイヤー群を分ける。
var mCntr = 1;
var mLyrGps = [[mSls[0]]];
//1つ入っている状態からスタートなので、カウンターは1スタートにする。
for( var i =1; i<mSls.length; i++ ){
var mSl = mSls[i];
if( mCntr < mLyrNum ){
mLyrGps[mLyrGps.length-1].push( mSl );
mCntr++;
}else{
mLyrGps.push( [mSl] );
mCntr = 1;
}
}
//各配列を、レイヤー番号が同じものでまとめる。
var mAdrsss = [[mAdrss[0]]];
var mkeyInfosss = [[mkeyInfoss[0]]];
var mExpss = [[mExps[0]]];
for( var i =1; i<mAdrss.length; i++ ){
var mAdrs = mAdrss[i];
var mkeyInfos = mkeyInfoss[i];
var mExp = mExps[i];
var mLast = mAdrsss[mAdrsss.length-1];
var mLast2 = mkeyInfosss[mkeyInfosss.length-1];
var mLast3 = mExpss[mExpss.length-1];
if( mLast[mLast.length-1][0] === mAdrs[0] ){
mLast.push( mAdrs );
mLast2.push( mkeyInfos );
mLast3.push( mExp );
}else{
mAdrsss.push( [ mAdrs ] );
mkeyInfosss.push( [ mkeyInfos ] );
mExpss.push( [ mExp ] );
}
}
//適用する。
for( var k =0; k<mLyrGps.length; k++ ){
var mLyrGp = mLyrGps[k];
if( mLyrGp.length !== mLyrNum ){ continue;}
//レイヤーGpの最先頭インポイントを出しておく。
var mInPs = [];
for( var i =0; i<mLyrGp.length; i++ ){
mInPs.push( mLyrGp[i].inPoint );
}
mInPs.sort( function( a , b ){ return a - b; } );
var mRstInP = mInPs[0];
for( var i =0; i<mLyrGp.length; i++ ){
var mSl = mLyrGp[i];
var mAdrss = mAdrsss[i];
var mkeyInfoss = mkeyInfosss[i];
var mExps = mExpss[i];
//レイヤーにあるはずの各プロップについて処理する。
for( var j =0; j<mkeyInfoss.length; j++ ){
var mkeyInfos = mkeyInfoss[j];
var mAdrs = mAdrss[j];
var mExp = mExps[j];
//アドレスをmSlのものに書き換える。
mAdrs.splice(0,1,mSl.index);
var mTgtProp = mGetPptyFromAdrsLyrIdx(mAdrs, 0);
//インジケータチェックがあればインジケータ時間ででペーストする。
if( mPnl.mCbIndc.value ){ var mPasteTime = mAi.time;}
//最先頭キーからのベクトルで適用されるので、各レイヤーの最先頭インポイントに、元レイヤーから元キーへの距離を足せばよい。
else{ var mPasteTime = mRstInP + mDurFromFInP;}
mAddKeys( mTgtProp, mkeyInfos, mPasteTime );
//エクスプレッションチェックがあればエクスプレッションを適用する。
if( mPnl.mCbExp.value ){ mTgtProp.expression = mExp;}
}
}
}
app.endUndoGroup();
}catch(e){
app.endUndoGroup();
alert(e.message + e.line);
}
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
//プロップスからキーがあるものを集める関数。
function mGetKeyProps(aProps){
var mProps = aProps;
var mKeyProps = [];
for( var i = 0; i<mProps.length ; i++ ){
var mProp = mProps[i];
if(mProp.canVaryOverTime && mProp.numKeys !== 0 && !mIsHiddenForKey(mProp)){
mKeyProps.push( mProp );
}
}
return mKeyProps;
}
//---------------------------------------------------------------
//hiddenかどうかを返す関数。
//selectedで判別だとキーが全選択されてしまうので、キー選択で行う。
function mIsHiddenForKey(aProp){
try{
aProp.setSelectedAtKey(1 , aProp.keySelected(1));
return false;
}catch(e){
return(/hidden/.test(e.message));
}
}
//----------------------------------------------------------------------------------------------------------------------
//選択キープロップの選択先頭キー時間を得る関数。
function mGetSttTime( aKeyProps ){
var mKeyProps = aKeyProps;
var mTimes = [];
for( var i = 0; i<mKeyProps.length ; i++ ){
var mKeyProp = mKeyProps[i];
var mSelKeys = mKeyProp.selectedKeys;
for( var j = 0; j<mSelKeys.length ; j++ ){
mTimes.push( mKeyProp.keyTime( mSelKeys[j] ) );
}
}
mTimes.sort( function( a , b ){ return a - b; } );
if( mTimes.length === 0 ){ return null;}
else{ return mTimes[0];}
}
//----------------------------------------------------------------------------------------------------------------------
//最先頭キーから最インポイントまでの尺を得る関数。
function mGetDurFromFInP( aSls, aSttTime ){
var mSls = aSls;
var mSttTime = aSttTime;
var mInPs = [];
for( var i = 0; i<mSls.length ; i++ ){
mInPs.push(mSls[i].inPoint);
}
mInPs.sort( function( a , b ){ return a - b; } );
return mSttTime - mInPs[0];
}
//----------------------------------------------------------------------------------------------------------------------
//プロップの選択キー群からキー情報objsを作る関数。
function mGetKeyInfos( aSp, aSttTime){
var mSp = aSp;
var mSelKeys = mSp.selectedKeys;
var mSttTime = aSttTime;
var mRstObjs = [];
for( var i = 0; i<mSelKeys.length ; i++ ){
var mSelIdx = mSelKeys[i];
var mObj = {};
mObj.mVal = mSp.keyValue(mSelIdx);
//時間補完法関連。
mObj.mTenEaseIO = [ mSp.keyInTemporalEase(mSelIdx) , mSp.keyOutTemporalEase(mSelIdx) ];
mObj.mTenCont = mSp.keyTemporalContinuous(mSelIdx);
mObj.mTenAuto = mSp.keyTemporalAutoBezier(mSelIdx);
//空間補完法関連&ロービング。
var mPropType = mSp.propertyValueType;
if( mPropType.propertyValueType === PropertyValueType.TwoD_SPATIAL
|| mPropType.propertyValueType === PropertyValueType.ThreeD_SPATIAL){
mObj.mSpaTngtIO = [ mSp.keyInSpatialTangent(mSelIdx) , mSp.keyOutSpatialTangent(mSelIdx) ] ;
mObj.mSpaCont = mSp.keySpatialContinuous(mSelIdx);
mObj.mSpaAuto = mSp.keySpatialAutoBezier(mSelIdx);
mObj.mRoving = mSp.keyRoving(i);
}
//キー補完種類。
mObj.mItpTypeIO = [ mSp.keyInInterpolationType(mSelIdx) , mSp.keyOutInterpolationType(mSelIdx) ];
//先頭時間からのベクトル。
mObj.mVec = mSp.keyTime(mSelIdx) - mSttTime;
mRstObjs.push( mObj );
}
return mRstObjs;
}
//-------------------------------------------------------------
//aSpにインジケータ時間からキーを複数打ち、イーズ情報も加える関数。
function mAddKeys( aSp, aObjs, aTime ){
var mSp = aSp;
var mObjs = aObjs;
for( var i = 0; i<mObjs.length ; i++ ){
var mObj = mObjs[i];
var mKeyIdx = mSp.addKey( aTime + mObj.mVec );
mSp.setValueAtKey( mKeyIdx, mObj.mVal );
//時間補完法関連。
mSp.setTemporalEaseAtKey(mKeyIdx, mObj.mTenEaseIO[0], mObj.mTenEaseIO[1]);
mSp.setTemporalContinuousAtKey(mKeyIdx, mObj.mTenCont);
mSp.setTemporalAutoBezierAtKey(mKeyIdx, mObj.mTenAuto);
//空間補完法関連&ロービング。
var mPropType = mSp.propertyValueType;
if( mPropType.propertyValueType === PropertyValueType.TwoD_SPATIAL
|| mPropType.propertyValueType === PropertyValueType.ThreeD_SPATIAL){
mSp.setSpatialTangentsAtKey(mKeyIdx, mObj.mSpaTngtIO[0], mObj.mSpaTngtIO[1]);
mSp.setSpatialContinuousAtKey(mKeyIdx, mObj.mSpaCont);
mSp.setSpatialAutoBezierAtKey(mKeyIdx, mObj.mSpaAuto);
mSp.setRovingAtKey(mKeyIdx, mObj.mRoving);
}
//キー補完種類は最後に適用する。
mSp.setInterpolationTypeAtKey(mKeyIdx, mObj.mItpTypeIO[0], mObj.mItpTypeIO[1]);
}
}
//----------------------------------------------------------------------------------------------------------------------
//キーフレーム関連プロパティ集
/*
numKeys
selectedKeys
setValueAtKey()
nearestKeyIndex()
keyTime()
keyValue()
addKey()
removeKey()
ease関連一式
isInterpolationTypeValid()
setSelectedAtKey()
keySelected()
ease関連一式
setInterpolationTypeAtKey(keyIndex, inType, outType) //キーフレームの補間の種類を設定。
keyInInterpolationType(keyIndex) //キーフレームの「イン」側の補間方法を取得。
keyOutInterpolationType(keyIndex) //キーフレームの「アウト」側の補間方法を取得。
//プロパティが_SPATIALでないとエラーとなる。
setSpatialTangentsAtKey(keyIndex, inTangent, outTangent) // キーフレームの空間補間法のイン/アウトの接線のベクトルを設定。
keyInSpatialTangent(keyIndex) //キーフレームの空間補間法のイン/アウトの接線のベクトルを取得。
keyOutSpatialTangent(keyIndex) //キーフレームの空間補間法のアウト側の接線を取得。
//イーズは配列。
setTemporalEaseAtKey(keyIndex, inTemporalEase, outTemporalEase) //キーフレームの時間補間法のイージーイーズを設定。
keyInTemporalEase(keyIndex) //キーフレームの時間補間法のイン側のイージーイーズを取得。
keyOutTemporalEase(keyIndex) //キーフレームの時間補間法のアウト側のイージーイーズを取得。
setTemporalContinuousAtKey(keyIndex, newVal) //キーフレームの時間補間法の連続ベジェモードを設定。
keyTemporalContinuous(keyIndex) //キーフレームの時間補間法が連続ベジェモードになっているか。インアウトのInterpolationTypeがBEZIER でないと影響が無い。
setTemporalAutoBezierAtKey(keyIndex, newVal) //キーフレームの時間補間法の自動ベジェモードを設定。
keyTemporalAutoBezier(keyIndex) //キーフレームの時間補間法が自動ベジェモードになっているか。インアウトのInterpolationTypeがBEZIER でないと影響が無い。
//プロパティが_SPATIALでないとエラーとなる。
setSpatialContinuousAtKey(keyIndex, newVal) //キーフレームの空間補間法の連続ベジェモードを設定。
keySpatialContinuous(keyIndex) //キーフレームの空間補間法が連続ベジェモードになっているか。
setSpatialAutoBezierAtKey(keyIndex, newVal) // キーフレームの空間補間法の自動ベジェモードを設定。
keySpatialAutoBezier(keyIndex) // キーフレームの空間補間法が自動ベジェモードになっているか。keySpatialContinuousがtrueでないと影響が無い。
setRovingAtKey(keyIndex, newVal) //キーフレームの時間ロービングモードの設定。
keyRoving(keyIndex) //キーフレームが時間ロービングモードになっているか。
*/
//----------------------------------------------------------------------------------------------------------------------
//選択Propから [ 選択レイヤー~選択Prop ] の階層別idx配列を得る関数。レイヤーも番号にしてあるバージョン。
function mGetSelPptyAdrsLyrIdx(aSp) {
var mSp = aSp;
var mAdrs = [];
//親プロパティ情報を上から入れていく。
for (var i = mSp.propertyDepth; i >= 1; i--) {
var mPropTmp = mSp.propertyGroup(i);
if (i === mSp.propertyDepth) {
mAdrs.push(mPropTmp.index);
} else {
mAdrs.push(mPropTmp.propertyIndex);
}
}
//自身の情報を入れる。
mAdrs.push(mSp.propertyIndex);
return mAdrs;
}
//----------------------------------------------------------------------------------------------------------------------
//階層別idx配列から、該当Pptyを得る(引数2は得る予定の最新深層Pptyからの上層数)。レイヤーを番号で取得するバージョン。
function mGetPptyFromAdrsLyrIdx(aAdrs, aNum) {
var mAdrs = aAdrs;
//まずはレイヤーが直接入っている0番目を参照する。
var mSpfydPpty = app.project.activeItem.layers[mAdrs[0]];
//階層を下って、選択pptyを変えていく。
for (var i = 1; i < mAdrs.length - aNum; i++) {
mSpfydPpty = mSpfydPpty(mAdrs[i]);
}
return mSpfydPpty;
}
//----------------------------------------------------------------------------------------------------------------------
})(this);
使い方
複数レイヤーのキー群を選び、Copyを押してコピーします。
そして”同じプロパティ構造”の複数レイヤーを選んでペーストを押すとそれぞれのプロパティにキーがペーストされます。
また、例えば2レイヤーのキーをコピーしたならば、レイヤー2つを1セットとみなします。そしてその後のペースト時に
4レイヤーなら2セット、6レイヤーなら3セットとして、何レイヤーでも選択でき、一括ペーストできます。
*同じプロパティ構造とは、コピー元レイヤーのキープロパティをペースト先レイヤーがすでに持っている必要がある、ということです。トランスフォーム系プロパティ(位置など)はたいがいのレイヤーが持っているので問題ないですが、マスクやシェイプパスなどが無いレイヤーに、マスクやシェイプパスキーはペーストできません(キーが無くともプロパティがあれば大丈夫です)。
*With Expressionでエクスプレッションも一緒にコピペできますが、元のコードのままなので、特定のレイヤーなどを参照していた場合はそれをそのまま引き継いでしまうことに注意してください。
*At Indicatorチェックを外すと、コピー元レイヤーの最先頭インポイントを基準位置として、ペースト時には各レイヤーの最先頭インポイントからの相対位置でキーがペーストされます。「複数レイヤーセットをちょっとずつずらして出現させている」ときなど、ペーストしてからいちいちキーをずらさずに済むので便利です。
ただし、一度複数ペーストした後で値を微調整する場合は以下に注意してください。
コピー元レイヤーのキーフレ時間を動かしてまたペーストすると、すでにペースト済みのキーフレは上書きされずに残るので、意図しない動きになる可能性があります。
解説
複数レイヤーのキーフレコピペはたまに必要になるので、制限が多いとはいえなかな使えるんではないでしょうか?
位置プロパティは分割しているとxPositonやyPositionが表示され、positionはhiddenとなるんですが、キーフレは”ある”と判定されてしまうため、キーフレありプロパティを探す関数でhidden対策が必要となります。
いつもなら○○.selected = ○○.selectedで出るエラーメッセージにhiddenが含まれているかで識別できるんですが、じつはこれでは「選択していないキーも選択状態になる」という欠点がありました。
なので今回は、代わりにkeySelectedを使用しています。
コードの最後にまとめてあるコメントは、プロパティオブジェクトのキーフレーム関連プロパティとメソッドをまとめたものです。