*追記 より高速な改訂版を作成しました。
toComp,fromCompは便利ですが、シェイプのトランスフォームには対応していません。シェイプトランスフォームの位置や角度などが変わっていたらtoComp,fromCompで座標変換ができないんですね。なのでその架け橋となる関数を作りました。
■toShapeLayerCoord
シェイプグループ座標(パスポイントなど)を、そのシェイプレイヤーのレイヤー座標に変換します。
function mToShpLyrCoord(aPath, aTgtPt) {
//パスパラメータの1つ上のパスプロパティを得る。
var mProp = aPath.propertyGroup(1);
//パスプロパティの2つ上がグループではなくレイヤーならば、
//パスプロパティは直下コンテンツにあるのでターゲットPtをそのままリターンする。
if( mProp.propertyGroup(2) instanceof Layer ){
return aTgtPt;
}
var mTfms = [];
for(var i = 0; i <100; i++){
//mProp(パスプロパティあるいはグループ)の2つ上がグループではなくレイヤーならば検索終了。
if( mProp.propertyGroup(2) instanceof Layer ){ break; }
//パスPropの上は隠しコンテンツ、その上はグループなので『2』でmPropをグループへ変える。
//そしてグループのトランスフォームを得る。
mProp = mProp.propertyGroup(2);
mTfms.push(mProp.transform);
}
//先祖から順に処理するので、反転する。
mTfms.reverse();
//-------------------------------------------------------------------------
//非歪位置(各トランスフォームのApのコンポ座標)を親から順に割り出して、mEachPts配列に入れていく。
var mEachPts = [];
for (var i = 0; i < mTfms.length; i++) {
if (i === 0) {
var mXY = mTfms[0].position;
mEachPts.push(mXY);
} else {
//子の位置は親の“原点からの”位置なので、前回算出位置から現状の親のアンカーポイントを引いて、現状ループの位置を足す。
mXY = mXY - mTfms[i - 1].anchorPoint + mTfms[i].position;
mEachPts.push(mXY);
}
}
//TgtPtの非歪コンポ座標を出す。
//ループ後のXYはラストレイヤーのアンカーポイント位置になるので、アンカーポイントを引いて原点にし、そこにTgtPtを足す。
var mTgtXY = (mXY - mTfms[mTfms.length - 1].anchorPoint) + aTgtPt;
//-------------------------------------------------------------------------
//スケール、スキュー、回転を子から順に設定していく。まずは子~親順にする。
mTfms.reverse();
mEachPts.reverse();
//自身が所属するトランスフォームも含めて処理するので、length分全てを回す。
for (var i = 0; i < mTfms.length; i++) {
//スケールパート。計算しやすいように原点からの位置にして、スケーリングして元の位置に戻す。
var mVecXY = mTgtXY - mEachPts[i];
var mScldX = mVecXY[0] * (mTfms[i].scale[0] / 100);
var mScldY = mVecXY[1] * (mTfms[i].scale[1] / 100);
mTgtXY = [mEachPts[i][0] + mScldX, mEachPts[i][1] + mScldY];
//スキューパート。もしもスキューが0でなければ、スキューした値を出す。
if (mTfms[i].skew !== 0) {
mTgtXY = mGetSkewedPt(mEachPts[i], mTgtXY, mTfms[i].skew, mTfms[i].skewAxis);
}
//回転パート。親とTgtPt間の現状の角度(元々の角度もあるが、上記のスケーリングでもまた変わっている)と、
//親が設定している角度を足したものから、その結果の位置を割り出す(現状の角度は関数内で処理されている)。
mTgtXY = mGetRotPt(mEachPts[i], mTgtXY, mTfms[i].rotation);
}
return mTgtXY;
//以下、関数内で使用している関数。
//------------------------------------
//2点から、基準点を中心に目標点を回転させた値を得る(現状の2点の角度は入れなくてよい)。
function mGetRotPt(aStdPt, aTgtPt, aStdRot ) {
var mTgtPtZr = aTgtPt - aStdPt;
var mRotRd = aStdRot * (Math.PI / 180);
var mRstX = mTgtPtZr[0] * Math.cos(mRotRd) - mTgtPtZr[1] * Math.sin(mRotRd);
var mRstY = mTgtPtZr[0] * Math.sin(mRotRd) + mTgtPtZr[1] * Math.cos(mRotRd);
return [mRstX,mRstY]+aStdPt;
}
//------------------------------------
function mGetSkewedPt(aOriginPt, aVtx, aRtn, aAxis) {
//回転軸方向へスキューさせたいので、一度、回転軸に基づいてVtxを回転させて、X方向スキューで済むようにする。
var mRtdXY = mGetRotPt(aOriginPt, aVtx, aAxis);
//スキュー計算は原点からのもののため、原点に持ってくる。
var mVecXY = mRtdXY - aOriginPt;
//回転させたものをスキューする(式はX+(Y*タンジェントシータ)。グラフのY方向が数学と違うのでシータは-する。
var mRad = (aRtn) * (Math.PI / 180);
var mTgt = - Math.tan(mRad);
var mSkewX = mVecXY[0] + (mVecXY[1] * mTgt);
var mSkwdPt = [mSkewX, mVecXY[1]];
//位置を元に戻す。
var mVecFromOrgXY = mSkwdPt + aOriginPt;
//回転させた方向を元に戻してリターンする。
return mGetRotPt(aOriginPt, mVecFromOrgXY, -aAxis);
}
}
■fromShapeLayerCoord関数
シェイプレイヤーのレイヤー座標を、シェイプグループ座標(パスポイントなど)に変換します。
function mFromShpLyrCoord(aPath, aTgtPt) {
//パスパラメータの1つ上のパスプロパティを得る。
var mProp = aPath.propertyGroup(1);
//パスプロパティの2つ上がグループではなくレイヤーならば、
//パスプロパティは直下コンテンツにあるのでターゲットPtをそのままリターンする。
if( mProp.propertyGroup(2) instanceof Layer ){
return aTgtPt;
}
var mTfms = [];
for(var i = 0; i <100; i++){
//mProp(パスプロパティあるいはグループ)の2つ上がグループではなくレイヤーならば検索終了。
if( mProp.propertyGroup(2) instanceof Layer ){ break; }
//パスPropの上は隠しコンテンツ、その上はグループなので『2』でmPropをグループへ変える。
//そしてグループのトランスフォームを得る。
mProp = mProp.propertyGroup(2);
mTfms.push(mProp.transform);
}
//先祖から順に処理するので、反転する。
mTfms.reverse();
//-------------------------------------------------------------------------
//非歪位置(各トランスフォームのApのコンポ座標)を親から順に割り出して、mEachPts配列に入れていく。
var mEachPts = [];
for (var i = 0; i < mTfms.length; i++) {
if (i === 0) {
var mXY = mTfms[0].position;
mEachPts.push(mXY);
} else {
//子の位置は親の“原点からの”位置なので、前回算出位置から現状の親のアンカーポイントを引いて、現状ループの位置を足す。
mXY = mXY - mTfms[i - 1].anchorPoint + mTfms[i].position;
mEachPts.push(mXY);
}
}
//TgtPtはコンポ座標なのでそのまま使う。
var mTgtXY = aTgtPt;
//一番深いVgp座標を出したいので、その非歪座標の原点を取っておく。
//ループ後のXYはラストレイヤーのアンカーポイント位置になるので、アンカーポイントを引いて原点にする。
var mOglPt = (mXY - mTfms[mTfms.length - 1].anchorPoint);
//-------------------------------------------------------------------------
//回転、スキュー、スケールを親から順に戻していく。
//自身が所属するトランスフォームも含めて処理するので、length分全てを回す。
for (var i = 0; i < mTfms.length; i++) {
//回転パート。親とTgtPt間の現状の角度と、
//親が設定している角度を足したものから、その結果の位置を割り出す(現状の角度は関数内で処理されている)。
mTgtXY = mGetRotPt(mEachPts[i], mTgtXY, (mTfms[i].rotation)*-1);
//スキューパート。もしもスキューが0でなければ、スキューした値を出す。
if (mTfms[i].skew !== 0) {
mTgtXY = mGetSkewedPt(mEachPts[i], mTgtXY, (mTfms[i].skew * -1 ), mTfms[i].skewAxis);
}
//スケールパート。計算しやすいように原点からの位置にして、スケーリングして元の位置に戻す。逆数は1/割合。
var mVecXY = mTgtXY - mEachPts[i];
var mScldX = mVecXY[0] * ( 1/(mTfms[i].scale[0] / 100));
var mScldY = mVecXY[1] * ( 1/(mTfms[i].scale[1] / 100));
mTgtXY = [mEachPts[i][0] + mScldX, mEachPts[i][1] + mScldY];
}
//この時点でmTgtXYは、レイヤー座標を非歪にしてグローバル座標と1マス単位を同じにした場合の
//グローバル位置となったので、一番深いVgp座標の非歪位置を原点とした値がVgp座標(レイヤー座標)となる。
return mTgtXY - mOglPt;
//以下、関数内で使用している関数。
//------------------------------------
//2点から、基準点を中心に目標点を回転させた値を得る(現状の2点の角度は入れなくてよい)。
function mGetRotPt(aStdPt, aTgtPt, aStdRot ) {
var mTgtPtZr = aTgtPt - aStdPt;
var mRotRd = aStdRot * (Math.PI / 180);
var mRstX = mTgtPtZr[0] * Math.cos(mRotRd) - mTgtPtZr[1] * Math.sin(mRotRd);
var mRstY = mTgtPtZr[0] * Math.sin(mRotRd) + mTgtPtZr[1] * Math.cos(mRotRd);
return [mRstX,mRstY]+aStdPt;
}
//------------------------------------
function mGetSkewedPt(aOriginPt, aVtx, aRtn, aAxis) {
//回転軸方向へスキューさせたいので、一度、回転軸に基づいてVtxを回転させて、X方向スキューで済むようにする。
var mRtdXY = mGetRotPt(aOriginPt, aVtx, aAxis);
//スキュー計算は原点からのもののため、原点に持ってくる。
var mVecXY = mRtdXY - aOriginPt;
//回転させたものをスキューする(式はX+(Y*タンジェントシータ)。グラフのY方向が数学と違うのでシータは-する。
var mRad = (aRtn) * (Math.PI / 180);
var mTgt = - Math.tan(mRad);
var mSkewX = mVecXY[0] + (mVecXY[1] * mTgt);
var mSkwdPt = [mSkewX, mVecXY[1]];
//位置を元に戻す。
var mVecFromOrgXY = mSkwdPt + aOriginPt;
//回転させた方向を元に戻してリターンする。
return mGetRotPt(aOriginPt, mVecFromOrgXY, -aAxis);
}
}
使い方
関数なので、呼び出ししない限り処理されません。事前準備です。
例えば『パスポイントをヌル位置に変換』であれば、ヌル位置のエクスプレッション欄に
//この上にmToShpLyrCoordがコピペしてある。
var mShpLyr = thisComp.layer("シェイプレイヤー 1");
var mPath = mShpLyr.content("シェイプ 1").content("パス 1").path;
var mTgtPt = mPath.points()[0];
//以下が変換部分。
var mShpLyrCoord = mToShpLyrCoord(mPath, mTgtPt);
mShpLyr.toComp(mShpLyrCoord);
と書けばパスポイント(0番目)の位置にヌルが来ます。
*シェイプレイヤー名やパスの場所は例です。ピックウィップなどで適宜変えてください。
逆に『ヌル位置をパスポイントに変換』であれば、パスのエクスプレッション欄に
//この上にmFromShpLyrCoordがコピペしてある。
//親子対策のためpositionではなくanchorPointからヌル位置を得る。
var mNull = thisComp.layer("ヌル 1");
mNullPos = mNull.toComp(mNull.anchorPoint);
mPath = thisProperty;
mVtx = mPath.points();
//以下が変換部分。
mNullPosShpLyrCoord = fromComp( mNullPos );
mNullPosShpGpCoord = mFromShpLyrCoord( mPath , mNullPosShpLyrCoord )
//spliceはゼロ番目から1個抜いてmNullPosShpGpCoordと置き換える、という意味。
//つまりパスポイント(0番目)に変換したヌル位置が入る。
mVtx.splice( 0 , 1 , mNullPosShpGpCoord );
createPath(mVtx, mPath.inTangents(), mPath.outTangents(), mPath.isClosed() );
と書けばヌル位置にパスポイント(0番目)が来ます。
*ヌル名は例です。ピックウィップなどで適宜変えてください。
どちらもtoComp、fromCompと組み合わせて使っていることに注意してください。toCompの使い方はこちら。
引数にaPathと書いてありますが、そこの地点から階層を上がってシェイプのトランスフォームを集めているだけなので、実はシェイプグループ内にあるパラメータプロパティならば『線』の『線幅』でも『塗り』の『カラー』でもなんでもかまいません。ただし階層的に、『線』や『塗り』自体だと『パス』ではなくその上の『パス 1』に対応しているので、うまくいかないことに注意してください。
また、toShapeLayerCoordとfromShapeLayerCoord、どちらも戻り値(変換結果)がXYの配列であることに注意してください。XYZではありません。toWorld、fromWorldと組み合わせる場合や、変数の節約のために代入する変数と引数を一緒にする場合(例えば『〇〇=toShapeLayerCoord(パス,〇〇)』のような形)には配列の数によるエラーに注意してください。特に、createPathのパスポイントの要素にはXY配列しか入らないという特徴があります。
使いどころ
座標変換関数なので色々なことに使えますが、例えばヌルとパスポイントの位置を合わせたいけど、シェイプのトランスフォームを変えてしまっていたり、シェイプのトランスフォームを複数作ってしまっている場合などに使えます。
また「このエクスプレッションを適用して値を得て、なにかする」というパスポイント関連のスクリプトにも活用できます。スクリプトにする場合はトランスフォームを集める作業をスクリプト用に書き換えれば直接、関数としても使えますね。
座標を得るために一時的に適用する場合もですが、適用してそのまま作業する場合は特に以下にご注意ください。
- 速度はそこそこなので、例えばこのエクスプレッションを適用したヌルが多すぎると重くなるやもです。
- 十分なテストをしたわけではないので、使用にあたっては自己責任でお願いいたします。
解説
エクスプレッションの内容としては、この記事で紹介した方法とほぼ一緒です。違いは『トランスフォームプロパティの集め方』と『レイヤートランスフォームにはない歪曲プロパティの処理を追加』ですね。
*追記 トランスフォームプロパティの集め方を再度修正しました。
階層と位置関係でたどり、存在確認しなくて済む形にしました。
■トランスフォームプロパティの集め方トランスフォームプロパティの集め方ですが、これが「自己責任で」とわざわざ強調した理由です。スクリプトと違い、エクスプレッションにはプロパティの『種類』を調べるすべが、提供されているメソッドには無いです。なので最初は以下のように書きました。
if ( mPrtPpty.transform !== undefined ){ mPrtPpty.transformは存在しているから集める }
『存在していないものはundefinedになるから、undefinedでないならば存在している』ということですね。が、これでごくまれに『内部エラー』が出てしまったんですね…。
謎の『内部エラー』は単純なキャッシュ消去と再起動で直りましたが、「そこの行でエラーが出ている」とAEが指し示したのは間違いないので原因を調べてみました。
すると『undefinedで存在チェック』はJavaScriptであまり推奨されていないらしいことがわかりました。が、その理由は『undefinedは実はただの変数で上書きできるから、もしも事前にundefined = 〇〇と書いてしまった場合、想定する結果にならないから』というものです。『内部エラー』と関係あるかわからんなあ…といったところです。
解決策として、undefinedを使わない以下の方法
if ( mPrtPpty.hasOwnProperty(“transform”) ){ mPrtPpty.transformは存在しているから集める }
に書き換えました。これなら、存在していないプロパティを記述しておらず、文字列として判断しているので大丈夫そうです。それ以来『内部エラー』は出ていません。が、本当に原因が解決したのかは不明なので、「自己責任で」と強調した、ということです…。
■歪曲プロパティの処理を追加
歪曲(skew)に関しては、アフィン変換で調べると行列ですが式が出てきます。ただ、AEには『回転軸』というプロパティがあるので、なにをやってるんだか調べるテストに苦労しました…。詳しくはエクスプレッション内のコメントをご覧ください。
参考サイト
画像処理ソリューション:アフィン変換