【AEスクリプト】『Create Nulls From Paths』でズレたヌル位置を修正するスクリプト

*追記 コードを一部修正しました。詳しくはこの記事の『解説』へ。

Create Nulls From PathsはAEの標準で入っている、パスポイントに合ったヌルを作る便利スクリプトです。が、シェイプのトランスフォームが変化していたり、複数あったりすると位置がズレるので、それを修正するスクリプトを書きました。

CnfpFix.jsx
以下コードをテキストにコピペして、名前を『CnfpFix』拡張子を『.jsx』にすれば出来上がりです。
使い方はこのページの下のほうに書いてあります。また、その下の『重要』という項は必ずお読みください。

(function (aGbl) {
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    function mCreateUI(aObj) {
        var mPorW = (aObj instanceof Panel) ? aObj : new Window("palette", "CnfpFix", undefined);
        mPorW.preferredSize = [200, 200];
        mPorW.margins = [10, 10, 10, 10];
        mPorW.spacing = 20;

        mPorW.mBt1 = mPorW.add("button { preferredSize : [-1,20]  , text : 'Del Shape Exp' ,alignment :  [ 'fill','top' ] }");
        mPorW.mBt2 = mPorW.add("button { preferredSize : [-1,20]  , text : 'Adjust Nulls' ,alignment :  [ 'fill','top' ] }");
        mPorW.mBt3 = mPorW.add("button { preferredSize : [-1,20]  , text : 'Apply Fixed Exp' ,alignment :  [ 'fill','top' ] }");
        //各種文字列保存用変数。
        mPorW.ExpFstLine = "";
        mPorW.mLyrName = "";
        mPorW.mPathAdrs = "";

        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);
    }
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    //メイン処理パート。
    /*
        シェイプレイヤーのエクスプレッションを取っておきつつ消す。
    ついでにエクスプレッションの1行目と
    シェイプレイヤーの名前とパスのアドレスを得ておく。
*/
    mPnl.mBt1.onClick = function () {
        var mAi = app.project.activeItem;
        var mSl = mAi.selectedLayers[0];
        var mSps = mSl.selectedProperties;

        var mPath;
        for (var i = 0; i < mSps.length; i++) {
            if (mSps[i].matchName === "ADBE Vector Shape") {
                mPath = mSps[i];
            }
        }
        if (mPath === undefined) { return; }


        app.beginUndoGroup("Del");

        var mExpTmp = mPath.expression;
        var mLineEndNum = mExpTmp.indexOf(';');
        mPnl.ExpFstLine = mExpTmp.slice(0, mLineEndNum + 1);

        mPnl.mLyrName = mSl.name;
        var mPathAdrsTmp = mGetSelPptyAdrs(mPath);
        mPathAdrsTmp.shift();
        var mPathAdrsStr = 'srcLayer(';
        for (var i = 0; i < mPathAdrsTmp.length; i++) {
            if (i !== mPathAdrsTmp.length - 1) {
                if (isNaN(mPathAdrsTmp[i]) === false) {
                    mPathAdrsStr = mPathAdrsStr + mPathAdrsTmp[i] + ')(';
                } else {
                    mPathAdrsStr = mPathAdrsStr + '"' + mPathAdrsTmp[i] + '")(';
                }
            } else {
                if (isNaN(mPathAdrsTmp[i]) === false) {
                    mPathAdrsStr = mPathAdrsStr + mPathAdrsTmp[i] + ');';
                } else {
                    mPathAdrsStr = mPathAdrsStr + '"' + mPathAdrsTmp[i] + '");';
                }
            }
        }
        mPnl.mPathAdrs = mPathAdrsStr;

        mPath.expression = "";

        app.endUndoGroup();
    }
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    /*
        ヌル全てを選んで、ヌルの数ぶんをfor文で回して
パス位置へ追従のエクスプレッションを適用。
シェイプレイヤーの名前とパスのアドレスを使う。
エクスプレッション後の値を残してエクスプレッションを消す。
*/
    mPnl.mBt2.onClick = function () {
        var mAi = app.project.activeItem;
        var mSls = mAi.selectedLayers;

        if (parseInt(mSls[0].name.replace(/[^0-9]/g, '')) > parseInt(mSls[1].name.replace(/[^0-9]/g, ''))) {
            mSls.reverse();
        }

        var mExpForNullStr = mExpForNull.toString();
        var mFstNum = mExpForNullStr.indexOf('{');
        var mLstNum = mExpForNullStr.lastIndexOf('}');
        mExpForNullStr = mExpForNullStr.slice(mFstNum + 2, mLstNum);


        app.beginUndoGroup("Adjust");
        for (var i = 0; i < mSls.length; i++) {
            var mNulPt = mSls[i].position;
            mNulPt.expression =
                'var srcLayer = thisComp.layer("' + mPnl.mLyrName + '");' + '\n' +
                'var srcPath = ' + mPnl.mPathAdrs + '\n' +
                'var mTgtPt = srcPath.points()[' + i + '];' + '\n' +
                mExpForNullStr;
        }

        for (var i = 0; i < mSls.length; i++) {
            var mNulPt = mSls[i].position;
            var mPtValTmp = mNulPt.valueAtTime(mAi.time, false);
            mNulPt.expression = "";
            mNulPt.setValue(mPtValTmp);
        }
        app.endUndoGroup();
    }

    //-------------------------------------------------------------------------------------------------------------------------------------------------- 
    /*
        パスにヌルへ追従エクスプレッションを適用。
取っておいたエクスプレッションの一行目のみを加える。
*/
    mPnl.mBt3.onClick = function () {
        var mAi = app.project.activeItem;
        var mSl = mAi.selectedLayers[0];
        var mSps = mSl.selectedProperties;

        var mPath;
        for (var i = 0; i < mSps.length; i++) {
            if (mSps[i].matchName === "ADBE Vector Shape") {
                mPath = mSps[i];
            }
        }
        if (mPath === undefined) { return; }


        app.beginUndoGroup("Apply");

        var mExpForPathStr = mExpForPath.toString();
        var mFstNum = mExpForPathStr.indexOf('{');
        var mLstNum = mExpForPathStr.lastIndexOf('}');
        mExpForPathStr = mExpForPathStr.slice(mFstNum + 2, mLstNum);

        mPath.expression = mPnl.ExpFstLine + '\n' + mExpForPathStr;

        app.endUndoGroup();

    }
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    //選択Propから [ 選択レイヤー(これのみ番号等ではなくObj)~選択Prop ] の階層別idx配列を得る関数。
    function mGetSelPptyAdrs(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(mSp.propertyGroup(mSp.propertyDepth));
            }else{
                mAdrs.push(mPropTmp.propertyIndex);
            }
        }
        //自身の情報を入れる。
        mAdrs.push(mSp.propertyIndex);
        return mAdrs;
    }
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    function mExpForNull() {
        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);
            }
        }

        var mShpLyrCoord = mToShpLyrCoord(srcPath, mTgtPt);

        srcLayer.toComp(mShpLyrCoord);
    }
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    //--------------------------------------------------------------------------------------------------------------------------------------------------
    function mExpForPath() {
        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);
            }
        }

        var origPath = thisProperty;
        var origPoints = origPath.points();
        var origInTang = origPath.inTangents();
        var origOutTang = origPath.outTangents();
        var getNullLayers = [];
        for (var i = 0, il = nullLayerNames.length; i < il; i++) {
            try {
                getNullLayers.push(effect(nullLayerNames[i])("ADBE Layer Control-0001"));
            } catch (err) {
                getNullLayers.push(null);
            }
        }
        for (var i = 0, il = getNullLayers.length; i < il; i++) {
            if (getNullLayers[i] != null && getNullLayers[i].index != thisLayer.index) {
                origPoints[i] = mFromShpLyrCoord(thisProperty, fromCompToSurface(getNullLayers[i].toComp(getNullLayers[i].anchorPoint)));
            }
        }
        createPath(origPoints, origInTang, origOutTang, origPath.isClosed());
    }

    //-------------------------------------------------------------------------------------------------------------------------------------------------- 
})(this);

使い方

STEP
『Create Nulls From Paths』でヌル生成。

パス(名前が変えられる『パス 1』ではなくその下の『パス』)を選び『ポイントはヌルに従う』ボタンでヌル生成。

STEP
『CnfpFix』の『Del Shape Exp』ボタンでパスのエクスプレッションを消す。

パスを選び、ボタンを押してエクスプレッションが消えたら成功です。
結果がわかりにくいですが、パス横の>マークが消えていたらOKです。

*実際は消しているだけではなく、エクスプレッションやレイヤー名などの各種文字情報をスクリプト内に貯める処理もしています。

STEP
『Adjust Nulls』ボタンでヌルを移動させる。

生成されたヌルを全て選び、ボタンを押せばズレていたヌルがパスポイントへ移動します。
*『生成されたヌルのみ』を『全て』選ばないとうまくいきません。

STEP
Apply Fixed Exp』ボタンでパス修正エクスプレッションを適用する。

再びパスを選び、ボタンを押せば修正されたエクスプレッションが適用されます。
結果がわかりにくいですが、パス横の>マークが付いたら成功です。

重要

独自作成した座標変換関数によるエクスプレッションを使用しており、かつ一時的ではなく作業中は使い続けるので、使用は自己責任でお願い致します。

条件が合えばこちらのスクリプトを使ったほうがより安全です。

解説

詳しい処理内容についてはコメントに書いてありますので『なぜズレるのか?』の原因を書いていきます。

『Create Nulls From Paths』の『ポイントはヌルに従う』ボタンで為される処理はおそらく以下の通りです。

  • まずはヌルをパスポイント分作成する。
  • 『toComp()』を使ってパスポイントをコンポ座標に変換。
  • 変換した位置にヌルを持ってくる。
  • 『fromComp()』(実際は『fromCompToSurface()』)のエクスプレッションをパスに適用してヌル位置にパスポイントを追従させる。

問題なのは、変換にtoComp()fromComp()を使っており、それらで変換できる範囲はコンポ座標からレイヤー座標まで(あるいはその逆)だということです。つまり、シェイプのトランスフォームは加味されないのでズレるんですね。これが原因です。座標系についてはこの記事をご覧ください。

発展

よくよく考えてみれば、逆に言えばtoComp()fromComp()で完結するような作りになっていれば、シェイプのトランスフォームを加味しなくてよい、ということですね。そのような条件は以下となります。

  • パスがシェイプレイヤーの直下コンテンツに入っている。
  • あるいは、パスに到達するまでのシェイプのトランスフォームが全てデフォルトのまま。

直下コンテンツにはシェイプのトランスフォームが含まれていない、つまり『座標系はシェイプレイヤーのレイヤー座標』なので問題なし、ということです。
あるいは、シェイプのトランスフォームがデフォルトのままだと、シェイプレイヤーのレイヤー座標から『変化なし』なので実質、『座標系はシェイプレイヤーのレイヤー座標』なので問題なしですね。

最初から気を付けてそのようなシェイプレイヤー組みをしていれば問題ないのですが、さまざまな理由でそうもできないことがあり、ズレてしまうことがあります。

そんなときにまず試してみてほしい手法があります。
もしも「シェイプレイヤーの追加プロパティを複雑に組んでいて、描画が変わるからシェイプグループからパスを移動させられない」といったことが無ければ、
『手動でパスを直下コンテンツまで移動してみる』ですね。スケール、回転、歪曲はかかっていないというのが条件ですが、手動で移動すると座標変換しつつの移動をしてくれる場合があります。

それでうまくいかない場合はこのスクリプトを使ってみてください。


よかったらシェアしてね!
  • URLをコピーしました!