スクリプトとエクスプレッションを組み合わせると、こんなことができるんだというサンプルです。
今後も他のスクリプト・エクスプレッション記事を読んで頂きながら、今回のスクリプトを読み返した際、読み解けるようになっている瞬間が来ます。そのとき、あなたのスクリプト・エクスプレッションコーディング力は相当なレベルになっていることでしょう。
具体的には、自身でスクリプトをゼロから作れるようになっているはずです。さらに作業効率化のアイディアも、順序立てて具現化できるようになっているでしょう。
あなたがスクリプトに触れ始めた段階であれば、決してこの記事だけで全て理解しようとしないでください。遠回りになります。
まずはスクリプトでできること、効率化できることに気づく力、問題へのアプローチ方法に触れて頂くため、今回の記事はサラッと読み飛ばすくらいの気楽さで読んでください。この規模のスクリプトを第一の目標として頭の片隅に起きながら、初めは短い単機能のスクリプトを学んでいくと、ゴールがイメージできている分理解が進みます。
今回作るスクリプト
SMSやLINE、Facebook messengerなどのメッセージングアプリ風のやりとり画面をAfter Effectsで作りたいことも多いでしょう。今回の記事ではこれらを模したやりとり画面をスクリプトでどう作るか模索しながら、スクリプトで出来る下記の内容を集約しています。
- シェイプレイヤーの生成方法
- フォント関連の設定方法
- エクスプレッションの設定方法
- 一括制御用コントローラーヌルのアイディア
- 修正を考慮したアイディア出し
- テキストエリアのあるウィンドウの作成方法
- ボタンの設定、処理方法
個々の細かな解説は改めて今後のスクリプト記事で行いますので、ご安心ください。まずはこれらの知識をある程度持っていることとして扱い、スクリプトを作る際の思考の一例をご紹介します。
ではまず目的を整理しましょう。作りたい画面を分析して言葉に落とし込み、式に変換していくことにします。
作るものを整理する
- 2者の対話がある
- 相手のメッセージは左配置左寄せ
- 自分の発言は右配置左寄せ
- 文章量に合わせ吹き出しサイズが追従
- フキダシのしっぽは常に発言側の上
画像添付の送信フキダシは今後のバージョンアップに託すということにして、今回は対応しません。
ここから、静的な処理で済む部分と、動的な処理が必要になる部分を仕分けしながら、さらに案出ししてみましょう。
静的な処理
静的な処理、つまり一括設定で済む部分は、
- テキストのインポート
- 相手のフキダシの左位置
- 自分のフキダシの右位置
- 両者のフキダシのしっぽの位置
- フキダシの丸み
- フォントサイズ
フキダシのサイズを文字数に追従させたい場合、1文字の横幅、高さ、カーニング、行の高さが必要です。日本語で注意したいのは、半角と全角が混在するので、半角・全角を個別に文字数カウントし、それぞれ異なる横幅を設定しなくてはなりません。
さらに、文字によってサイズがバラバラでは、文章ごとにサイズの設定が強いられます。そんなことしてられませんので、フォントサイズは固定で等幅フォントを使用しましょう。目的はAfter Effects上で動く完璧なメッセージングアプリシステムを作ることではありません。メッセージングアプリ「風」のやりとり画面を手軽に作ることです。
動的な処理
フキダシごとに動的な処理が必要な部分は…
- 相手、自分のフキダシ左右配置振り分け
- 文字数によるフキダシ幅(全角、半角)
- 複数行になった際のフキダシ高さ
- 配置までしたいので前のフキダシとの距離
- テキストの追加削除への対応
フキダシの左右振り分けは、そのセリフが相手のメッセージか、自分のメッセージかを判定するトリガーが必要です。
では今回、自分のメッセージには、冒頭に”<me>”と付いた文章をトリガーとしましょう。テキスト準備段階でこのトリガーを仕込むことをルールとします。
また、行ごとに1メッセージに区切り、1行を1フキダシに配置します。ただし複数行のメッセージも入力したいので、「<br>」で改行しても1メッセージとして認識するようスクリプトでの文章の区切り方を工夫します。
完成品
このようなメッセージを用意したら、下記のように出力できるのがゴールです。
完成したスクリプトが下記です。
var scName="faceLine_v1";
/*
===============
--- GUI準備 ---
===============
*/
// テキストエリアと「Export」ボタンの設置
function f_showDialog(){
this.w = new Window("palette{properties:{resizeable:false, }}",scName);
this.w.center();
this.ed = this.w.add("edittext",[0,20,360,320],"",{multiline: true, alignChildren: "fill",});
this.go = this.w.add("button",[0,20,220,40],"Export");
this.show = function()
{
return this.w.show();
}
}
var wobj = new f_showDialog;
// 設置したウィンドウを表示
wobj.show();
wobj.onResizing = wobj.onResize = function(){ this.layout.resize()};
/*
===============
--- ボタン操作 ---
===============
*/
// 「Export」ボタンをクリック時の処理
wobj.go.onClick = function(){
app.beginUndoGroup("faceLine create");
var Itm = app.project.items; //全てのアイテムを取得
var actCmp = app.project.activeItem; //選択コンポを取得
var lineTxt,txArbr,txAr=[];
var tag1=/^<me>/;
// まず"<br>改行"を"<brbbr>"に変換し、1メッセージ内での改行と認識させる
txArbr=wobj.ed.text.replace(/<br>\r|<br>\r\n|<br>\n/g, '<brbrr>');
// 改行ごとにメッセージに分ける
txAr = txArbr.split(/\r\n|\r|\n/);
/*----------コントロール用ヌル設定----------*/
var nullLyr=actCmp.layers.addNull(),nullFx=[];
nullLyr.name="faceLine Control";
// フキダシは上から下に増えるので、基準となるヌルの位置は画面上部に設定
nullLyr.position.setValue([nullLyr.position.value[0],50]);
nullLyr.label=11;//ラベルをオレンジに
nullFx[0]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
// メモリ節約のためにエクスプレッション制御エフェクトは以降も無効化する
//(数値をエクスプレッションで引っ張るだけなので、エフェクト自体は無効で問題なし)
nullFx[0].enabled=false;
nullFx[0].name="width";
nullFx[0](1).setValue(500);
nullFx[1]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Checkbox Control");
nullFx[1].enabled=false;
nullFx[1].name="tale";
nullFx[1](1).setValue(1);
nullFx[2]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[2].enabled=false;
nullFx[2].name="my font-color";
nullFx[2](1).setValue([0,0,0]);
nullFx[3]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[3].enabled=false;
nullFx[3].name="my balloon-color";
nullFx[3](1).setValue([133/255,226/255,73/255]);
nullFx[4]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[4].enabled=false;
nullFx[4].name="other font-color";
nullFx[4](1).setValue([0,0,0]);
nullFx[5]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[5].enabled=false;
nullFx[5].name="other balloon-color";
nullFx[5](1).setValue([1,1,1]);
nullFx[6]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[6].enabled=false;
nullFx[6].name="2byte font-width";
nullFx[6](1).setValue(50);
nullFx[7]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[7].enabled=false;
nullFx[7].name="1byte font-width";
nullFx[7](1).setValue(33.3);
nullFx[8]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[8].enabled=false;
nullFx[8].name="font-height";
nullFx[8](1).setValue(50);
nullFx[9]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[9].enabled=false;
nullFx[9].name="line-height";
nullFx[9](1).setValue(10);
nullFx[10]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[10].enabled=false;
nullFx[10].name="balloon X-padding";
nullFx[10](1).setValue(50);
nullFx[11]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[11].enabled=false;
nullFx[11].name="balloon padding-top";
nullFx[11](1).setValue(10);
nullFx[12]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[12].enabled=false;
nullFx[12].name="balloon padding-bottom";
nullFx[12](1).setValue(15);
nullFx[13]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[13].enabled=false;
nullFx[13].name="balloon Y-interval";
nullFx[13](1).setValue(100);
nullFx[14]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[14].enabled=false;
nullFx[14].name="balloon round";
nullFx[14](1).setValue(40);
for(i=0;i<txAr.length;i++){
/*----------フキダシシェイプ設定----------*/
// シェイプレイヤー準備
var myShapeLayer = actCmp.layers.addShape();// 空のシェイプレイヤーが作られる
myShapeLayer.name="Balloon"+(i+1);
// 配置振り分け用「my_balloon?」チェックボックスを設置
myShapeFx=myShapeLayer.property("ADBE Effect Parade").addProperty("ADBE Checkbox Control");
myShapeFx.name="my_balloon?";
myShapeFx.enabled=false;
var lineTxt=actCmp.layers.addText(); // テキストレイヤー準備
lineTxt.moveBefore(myShapeLayer); // シェイプレイヤーの1つ上に
var shapeProperty = myShapeLayer.property('ADBE Root Vectors Group'); // これにシェイプを追加していく
// 長方形パス準備
var myShapePath1 = shapeProperty.addProperty('ADBE Vector Shape - Rect');
// サイズは可変なので数値指定でなくエクスプレションで検知
myShapePath1(2).expression=
'ctrl=thisComp.layer("faceLine Control");\r'+
'tx=thisComp.layer(index-1).text.sourceText;\r'+
'gyousu=tx.split(/\\r/g);\r\r'+
'gyoukan=(gyousu.length-1)*ctrl.effect("line-height")(1).value\r\r'+
'function f_maxLength(str) {\r'+ // 文字数が一番多い行を検知
' var longestText = gyousu.sort(function(a, b) { return b.length - a.length; });\r'+
' return longestText[0];\r'+
'}\r\r'+
'maxLength=f_maxLength(gyousu)\r'+
'function f_hanzen(tx) {\r'+// 文字列を渡して半角と全角文字数を取得する関数
' hankaku=0;zenkaku=0;\r'+
' for (i=0; i<tx.length; i++) {\r'+
' var chr = tx.charCodeAt(i);\r'+
' if((chr >= 0x00 && chr < 0x81) || (chr === 0xf8f0) ||\r'+
' (chr >= 0xff61 && chr < 0xffa0) || (chr >= 0xf8f1 && chr < 0xf8f4)){\r'+
' hankaku += 1;\r'+
' } else {\r'+
' zenkaku += 1;\r'+
' }\r'+
' }\r'+
'return [hankaku,zenkaku];\r'+ // [半角,全角] の文字数が返ってくる
'}\r'+
// txNumに一番文字数の多い行の[半角文字数 ,全角文字数]が配列で入る
'txNum=f_hanzen(maxLength);\r\r'+
'x=(txNum[0]*ctrl.effect("1byte font-width")(1).value)+(txNum[1]*ctrl.effect("2byte font-width")(1).value)'+
'+ctrl.effect("balloon X-padding")(1).value;\r'+
'y=(gyousu.length*ctrl.effect("font-height")(1).value)'+
'+gyoukan+(ctrl.effect("balloon padding-top")(1).value/2)+gyoukan+ctrl.effect("balloon padding-bottom")(1).value;\r'+ // サイズ
'[x,y]';
myShapePath1(3).expression= // 位置 x,yのサイズを「/2」した位置とはすると、結果
'[content("長方形パス 1").size[0]/2,content("長方形パス 1").size[1]/2];';
myShapePath1(4).expression= // 角丸
'ctrl=thisComp.layer("faceLine Control");\r'+
'ctrl.effect("balloon round")(1).value';
// 色設定
var fill1 = shapeProperty.addProperty('ADBE Vector Graphic - Fill'); //塗り
fill1(4).expression=
'if(effect("my_balloon?")(1)==1){\r'+
' thisComp.layer("faceLine Control").effect("my balloon-color")(1).value;\r'+
'} else {\r'+
' thisComp.layer("faceLine Control").effect("other balloon-color")(1).value;\r'+
'}';
// パス - しっぽ
var myShapePath2 = shapeProperty.addProperty('ADBE Vector Shape - Group');
var myShape = new Shape();
myShape.vertices = [[10,25],[-20,0],[20,10]];
myShape.inTangents = [[20,0],[0,0],[-20,0]];
myShape.outTangents = [[-20,0],[0,0],[20,0]];
myShape.closed = true;
myShapePath2(2).setValue(myShape);
// チェックボックスで左右振り分け、前のフキダシからの距離はエクスプレッションで
myShapeLayer.position.setValue([0,0]);
myShapeLayer.position.expression=
'x=0;y=0;\r'+
'ctrl=thisComp.layer("faceLine Control");\r'+
'try {\r'+
' gyousu=thisComp.layer(index+1).text.sourceText.split(/\\r/g).length;\r'+
'} catch(e) {\r'+
' gyousu=1;\r'+
'}\r\r'+
'if(effect("my_balloon?")(1)==1){\r'+
' x=ctrl.position[0]+ctrl.effect("width")(1).value;\r'+
'} else {\r'+
' x=ctrl.position[0]-ctrl.effect("width")(1).value;\r'+
'}\r'+
'if(thisComp.layer(index+1).name=="faceLine Control"){\r'+
' y=ctrl.position[1];\r'+
'} else {\r'+
' y=thisComp.layer(index+2).position[1]+(gyousu*ctrl.effect("font-height")(1).value)'+
'+ctrl.effect("balloon Y-interval")(1).value'+
'+(ctrl.effect("balloon padding-top")(1).value/2)+ctrl.effect("balloon padding-bottom")(1).value;\r'+
'}\r\r'+
'value+[x,y];';
myShapeLayer.scale.expression=
'if(effect("my_balloon?")(1)==1){\r'+
' [-100,100];\r'+
'} else {\r'+
' [100,100];\r'+
'}';
// 相手のフキダシか自分のフキダシか判定
if(txAr[i].match(tag1)){ // <me>を消しながらマッチで相手のフキダシへ
myShapeLayer.property("ADBE Effect Parade")("my_balloon?")(1).setValue(1);
lineTxt.property("Source Text").setValue(txAr[i].replace(/<me>/g,"").replace(/<brbrr>/g, '\r\n'));
} else {
myShapeLayer.property("ADBE Effect Parade")("my_balloon?")(1).setValue(0);
lineTxt.property("Source Text").setValue(txAr[i].replace(/<brbrr>/g, '\r\n'));
}
// 色設定
var fill2 = shapeProperty.addProperty('ADBE Vector Graphic - Fill'); //塗り
fill2(4).expression=
'if(effect("my_balloon?")(1)==1){\r'+
' thisComp.layer("faceLine Control").effect("my balloon-color")(1).value;\r'+
'} else {\r'+
' thisComp.layer("faceLine Control").effect("other balloon-color")(1).value;\r'+
'}';
fill2(5).expression=
'if(thisComp.layer("faceLine Control").effect("tale")(1)==1){\r'+
' 100;\r'+
'} else {\r'+
' 0;\r'+
'}';
/*----------テキスト設定----------*/
var textProp = lineTxt.property("Source Text");//テキストレイヤーのどのプロパティなのか指定
var textDocument = textProp.value;//プロパティ内のそれぞれに値を指定する用意
textDocument.justification = ParagraphJustification.LEFT_JUSTIFY;
textDocument.font = 'SourceHanCodeJP-Normal';
textDocument.fontSize = 50;
textDocument.strokeWidth=0;
textDocument.fillColor=[0,0,0];
textProp.setValue(textDocument);
var txtFx=[];
txtFx[0]=lineTxt.property("ADBE Effect Parade").addProperty("ADBE Tint");
txtFx[0].name="link_font-color";
txtFx[0]("ADBE Tint-0001").expression=
'if(thisComp.layer(index+1).effect("my_balloon?")(1)==1){\r'+
' thisComp.layer("faceLine Control").effect("my font-color")(1)\r'+
'} else {\r'+
' thisComp.layer("faceLine Control").effect("other font-color")(1)'+
'}';
lineTxt.position.setValue([0,0]);
lineTxt.position.expression= // 配置はフキダシ位置からエクスプレッションで
'ctrl=thisComp.layer("faceLine Control");\r'+
'tx=text.sourceText;\r'+
'gyousu=tx.split(/\\r/g);\r\r'+
'function f_maxLength(str) {\r'+ // 文字数が一番多い行を検知
' var longestText = gyousu.sort(function(a, b) { return b.length - a.length; });\r'+
' return longestText[0];\r'+
'}\r\r'+
'maxLength=f_maxLength(gyousu)\r'+
'function f_hanzen(tx) { // 文字列渡して[半角,全角]文字数を返す\r'+
' hankaku=0;zenkaku=0;\r'+
' for (i=0; i<tx.length; i++) {\r'+
' var chr = tx.charCodeAt(i);\r'+
' if((chr >= 0x00 && chr < 0x81) || (chr === 0xf8f0) ||\r'+
' (chr >= 0xff61 && chr < 0xffa0) || (chr >= 0xf8f1 && chr < 0xf8f4)){\r'+
' hankaku += 1;\r'+
' } else {\r'+
' zenkaku += 1;\r'+
' }\r'+
' }\r'+
'return [hankaku,zenkaku];\r'+
'}\r'+
'txNum=f_hanzen(maxLength);\r\r'+ // [半角文字数 ,全角文字数]
'if(thisComp.layer(index+1).effect("my_balloon?")(1)==1){\r'+
' x=thisComp.layer(index+1).position[0]'+
'-(ctrl.effect("1byte font-width")(1).value*txNum[0])'+
'-(ctrl.effect("2byte font-width")(1).value*txNum[1])'+
'-(ctrl.effect("balloon X-padding")(1).value/2);\r'+
'} else {\r'+
' x=thisComp.layer(index+1).position[0]+(ctrl.effect("balloon X-padding")(1).value/2);\r'+
'}\r\r'+
'y=thisComp.layer(index+1).position[1]+ctrl.effect("font-height")(1).value+(ctrl.effect("balloon padding-top")(1).value/2);\r'+
'value+[x,y];';
lineTxt.label=(txAr[i]=="")?0:1; // テキストなしならラベル黒
lineTxt.name="";
app.project.activeItem.selectedLayers[0].selected=false;
lineTxt.selected=true;
}// for
app.project.activeItem.selectedLayers[0].selected=false;
nullLyr.selected=true;
app.endUndoGroup();
}
ルールを固める
相手のフキダシは左向きに、自分のフキダシは右向きに、色が異なり、複数行にも対応したいです。
フキダシは高さこそ下方向へ伸び縮みしますが、形は一定なのと、左から右に読む横書きの特性から、相手のメッセージがベースで、アンカーポイントがしっぽ側にあると都合が良さそうなので、自分用のフキダシは相手用のフキダシを反転して使います。
自分のメッセージは右配置の左揃えのため、右配置後に文字数分だけ左に移動が必要ですね。
どうせなら面倒な処理は全てスクリプトで済ませてしまいましょう。配置まで終わらせたいので、前のフキダシが複数行であれば、次のフキダシはY方向に行数分プラスされるようにエクスプレッションを仕込みます。
さらに、文字の修正やセリフの追加・削除へ即対応できるよう、前のフキダシとの間隔と、左右配置は切り替えられるようにしておきたいですね。後ほどチェックボックス制御とエクスプレッションを仕込みましょう。
フキダシの準備方法
フキダシはサイズが数値で指定できるよう、シェイプレイヤーで作ることにします。念の為長文の場合、ズレがないとも限りません。サイズの微調整が出来たほうがいいと思われますので、フキダシ単体で簡単に横幅を変えられるよう、エクスプレッションとスライダー制御を噛ませると良さそうです。
また、同様に長い複数行になれば縦位置も微調整できるよう、同じくエクスプレッションを仕込んでおきたいです。この際、以降のフキダシ全て下へズレが必要です。これも考慮します。
作るシェイプは2つです。
メッセージの座布団となる長方形パス(サイズ可変)、フキダシのしっぽ(サイズ・位置固定)です。スクリプトで描きます。
スクリプトへ置き換えていく
それでは上記のルールが整理できたので、順番にスクリプトを組み立てていきましょう。
テキストを読み込む
まずはテキストの読み込み方法です。
1.テキストファイルを読み込む、2.テキストレイヤーから読み取る、3.スクリプト上のテキストエリアからインポートする、といろいろやり方は考えられますが、今回は文章を確認しながらそのまま実行ボタンが押せるよう、3.スクリプト上のテキストエリアからインポートしたいと思います。
テキストをコピペしたり、調整できるテキストエリアを表示するスクリプトを作ります。こうしたユーザーインターフェースは、Graphical User Interface(グラフィカル・ユーザー・インターフェース)と呼ばれ、略して「GUI」と表記されることが多いです。
var scName="faceLine";
/*
===============
--- GUI準備 ---
===============
*/
// ウィンドウ表示関数を設定
function f_showDialog(){
// パレットタイプでリサイズ不可、タイトルバーにスクリプト名
this.w = new Window("palette{properties:{resizeable:false, }}",scName);
// ウィンドウは中央配置
this.w.center();
// 入力可能なテキストエリアをサイズ指定して設置
// multiline:trueで複数行可という意味
this.ed = this.w.add("edittext",[0,20,360,320],"",{multiline: true, alignChildren: "fill",});
// Exportボタンを設置。今はまだただのボタン
// 後ほどクリックされた時の処理を設定する
this.go = this.w.add("button",[0,20,220,40],"Export");
// ウィンドウを表示する下準備
this.show = function()
{
return this.w.show();
}
}
// 上の関数を実行
var wobj = new f_showDialog;
// もろもろ配置されたウィンドウ全体を「表示」
// わざわざこの「表示」をしないと出てこない
wobj.show();
// パネルとして作成したので、必ず入れるリサイズ関数
wobj.onResizing = wobj.onResize = function(){ this.layout.resize()};
説明はプログラム内のコメントが全て。
最後の行のみ、おまじないのようなもので、パネルとしてウィンドウを表示する以上、必ず設定しておくものとのこと。
「パネルの時は指定しなくても最初からリサイズ可能状態。onResizeコールバックを必ず設定しておく」
http://shadeco.video/youuu4-zft1
このようにウィンドウが表示されれば準備完了です。
「Export」ボタンはまだ作っただけの状態のため、押しても何も起こりません。この後、ボタンをクリックされた時の処理をプログラミングします。
ボタンを押したときの処理
/*
===============
--- ボタン操作 ---
===============
*/
// 「Export」ボタンをクリック時の処理
wobj.go.onClick = function(){
app.beginUndoGroup("faceLine create");
var Itm = app.project.items; //全てのアイテムを取得
var actCmp = app.project.activeItem; //選択コンポを取得
var lineTxt,txArbr,txAr=[];
var tag1=/^<me>/;
// まず"<br>改行"を"<brbbr>"に変換し、1メッセージ内での改行と認識させる
txArbr=wobj.ed.text.replace(/<br>\r|<br>\r\n|<br>\n/g, '<brbrr>');
// 改行ごとにメッセージに分ける
txAr = txArbr.split(/\r\n|\r|\n/);
ボタンをクリックされたらonClickで拾い、function内の処理を実行します。
var tag1=/^<me>/;で左右振り分けのトリガー「文頭の<me>」を検知し、.replace(/<br>\r|<br>\r\n|<br>\n/g, ‘<brbrr>’);で「文末の<br>改行」を、'<brbrr>’という文字列に変換します。
これで複数行も「1行目<brbrr>2行目」と変換され、次の処理.split(/\r\n|\r|\n/);ではメッセージ内改行は区切られず、メッセージの切り替わりのみで区切られます。
.split(/\r\n|\r|\n/);は受け取った文字列の「改行でsplit(切り分ける)」する処理で、10メッセージあれば10個の配列で返ってきます。
その後例えば2つ目のメッセージは、txAr[2]で取り出せます。
改行の判定はOSやコピペ元のエディタにより「\r\n」「|(もしくは)」「\r」「|」「\n」の3種類の方式があることを考慮し、どれであってもsplitするよう指定します。
左右の振り分け方
次に、先ほど触れたように、自分と相手のフキダシを認識するためのルールを設定します。今回は準備するセリフの頭に”<me>”という目印を付けておきますので、スクリプトで左右の判別→フキダシに「自分のセリフかどうか」のチェックボックス制御を追加・設定→テキストから目印<me>の削除という流れをとります。
この場合、振り分け処理の一例としては下記のように作れます。
/*----------フキダシ設定----------*/
var myShapeLayer = actCmp.layers.addShape(); // シェイプレイヤー準備
myShapeLayer.name="Balloon"+(i+1);
// 配置振り分け用
myShapeFx=myShapeLayer.property("ADBE Effect Parade").addProperty("ADBE Checkbox Control");
myShapeFx.name="my_balloon?";
myShapeFx.enabled=false;
addShape()で空のシェイプレイヤーが生成されます。
通常のシェイプレイヤーツールでシェイプレイヤーを作った際は、既にコンテンツ>長方形パス>サイズなどが自動で作られますが、スクリプトでは一つ一つ設定していかなければなりません。
新規作成したシェイプレイヤーに「my balloon?」というチェックボックス制御を追加し、判定に使います。<me>を付け忘れた場合や、メッセージを途中に追加したい場合などに、レイヤーをコピペした後にこのチェックボックスをいじれば、振り分けが簡単になります。
一括設定できるコントローラーが便利そう
こうなると全体のエクスプレッション配置を統括するコントロールレイヤーが欲しいですね。
コントロールレイヤーを準備
全体に一括で適用したい調整項目を統括するため、ヌルオブジェクトをコントローラーとして準備することにします。自分と相手のフキダシの開きなど、統括できて欲しいコントロールは
- 全体の開き(width)
- フキダシのしっぽ有無(tale)
- 自分のフォントの色(my font-color)
- 自分のフキダシの色(my balloon-color)
- 相手のフォントの色(other font-color)
- 相手のフキダシの色(other balloon-color)
- 全角文字の横幅(2byte font-width)
- 半角文字の横幅(1byte font-width)
- フォントの高さ(font-height)
- 行間(line-height)
- フキダシ内ヨコの隙間(balloon X-padding)
- フキダシ内上の隙間(balloon padding-top)
- フキダシ内下の隙間(balloon padding-bottom)
- フキダシ同士のタテ間隔(balloon Y-interval)
- フキダシの角丸(balloon round)
このあたりですね。わかりづらそうな表現は下記の図参照。
「開き」はフキダシの左右の距離、「隙間(padding)」はフキダシ外側とテキストの間、しっぽ(tale)はフキダシの発言者側にちょろっと飛び出す○で囲った部分のことをこう呼ぶそうです。LINEではしっぽがありますが、メッセンジャーなどしっぽがないデザインもあるので、選択できるようにします。
シェイプ自体を無くすことはできませんので、「tale」チェックボックスがONの際はしっぽの塗りの不透明度を100に、OFFの際は0にして、擬似的に無くなったように見せます。
ヌルオブジェクトをコントローラーとして作成し、統括用のエクスプレッション制御エフェクトを黙々と設定します。
/*----------コントロール用ヌル設定----------*/
var nullLyr=actCmp.layers.addNull()
var nullFx=[];
// エフェクトを多数追加して、ついでに名前を付けたりデフォルトの値を設定したいので、変数用の配列だけ用意
nullLyr.name="faceLine Control";
nullLyr.position.setValue([nullLyr.position.value[0],50]);
// 中央画面上部に基準となるヌルオブジェクトを移動
nullLyr.label=11;//ラベルの色をオレンジ
に
nullFx[0]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");// スライダー制御を追加
nullFx[0].enabled=false;
// メモリ消費を抑えるためエフェクト無効化(数値をエクスプレッションで引っ張るので無効化で問題なし)
nullFx[0].name="width";
(スライダーの名前を変更)
nullFx[0](1).setValue(500);
(デフォルトの値をセット)
nullFx[1]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Checkbox Control");
nullFx[1].enabled=false;
nullFx[1].name="tale";
nullFx[1](1).setValue(1);
nullFx[2]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[2].enabled=false;
nullFx[2].name="my font-color";
nullFx[2](1).setValue([0,0,0]);
nullFx[3]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[3].enabled=false;
nullFx[3].name="my balloon-color";
nullFx[3](1).setValue([133/255,226/255,73/255]);
nullFx[4]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[4].enabled=false;
nullFx[4].name="other font-color";
nullFx[4](1).setValue([0,0,0]);
nullFx[5]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Color Control");
nullFx[5].enabled=false;
nullFx[5].name="other balloon-color";
nullFx[5](1).setValue([1,1,1]);
nullFx[6]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[6].enabled=false;
nullFx[6].name="2byte font-width";
nullFx[6](1).setValue(50);
nullFx[7]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[7].enabled=false;
nullFx[7].name="1byte font-width";
nullFx[7](1).setValue(33.3);
nullFx[8]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[8].enabled=false;
nullFx[8].name="font-height";
nullFx[8](1).setValue(50);
nullFx[9]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[9].enabled=false;
nullFx[9].name="line-height";
nullFx[9](1).setValue(10);
nullFx[10]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[10].enabled=false;
nullFx[10].name="balloon X-padding";
nullFx[10](1).setValue(50);
nullFx[11]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[11].enabled=false;
nullFx[11].name="balloon padding-top";
nullFx[11](1).setValue(10);
nullFx[12]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[12].enabled=false;
nullFx[12].name="balloon padding-bottom";
nullFx[12](1).setValue(15);
nullFx[13]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[13].enabled=false;
nullFx[13].name="balloon Y-interval";
nullFx[13](1).setValue(100);
nullFx[14]=nullLyr.property("ADBE Effect Parade").addProperty("ADBE Slider Control");
nullFx[14].enabled=false;
nullFx[14].name="balloon round";
nullFx[14](1).setValue(40);
フキダシの生成
左右の振り分け処理は作りましたので、それぞれのフキダシとしてシェイプレイヤーを作ります。幅、高さの微調整を考慮したいので、固定となるフキダシしっぽ、左フキダシの左位置、右フキダシの右位置を設定します。
固定とはいえ、開きはデザインによって好みで変えたいですよね。スライダー制御とエクスプレッションでの制御にしましょう。
/*----------フキダシシェイプ設定----------*/
// シェイプレイヤー準備
var myShapeLayer = actCmp.layers.addShape();// 空のシェイプレイヤーが作られる
myShapeLayer.name="Balloon"+(i+1);
// 配置振り分け用「my_balloon?」チェックボックスを設置
myShapeFx=myShapeLayer.property("ADBE Effect Parade").addProperty("ADBE Checkbox Control");
myShapeFx.name="my_balloon?";
myShapeFx.enabled=false;
var lineTxt=actCmp.layers.addText(); // テキストレイヤー準備
lineTxt.moveBefore(myShapeLayer); // シェイプレイヤーの1つ上に
var shapeProperty = myShapeLayer.property('ADBE Root Vectors Group'); // これにシェイプを追加していく
// 長方形パス準備
var myShapePath1 = shapeProperty.addProperty('ADBE Vector Shape - Rect');
// パス - しっぽ
var myShapePath2 = shapeProperty.addProperty('ADBE Vector Shape - Group');
var myShape = new Shape();
myShape.vertices = [[10,25],[-20,0],[20,10]];
myShape.inTangents = [[20,0],[0,0],[-20,0]];
myShape.outTangents = [[-20,0],[0,0],[20,0]];
myShape.closed = true;
myShapePath2(2).setValue(myShape);
// 相手のフキダシか自分のフキダシか判定
if(txAr[i].match(tag1)){ // <me>を消しながらマッチで相手のフキダシへ
myShapeLayer.property("ADBE Effect Parade")("my_balloon?")(1).setValue(1);
lineTxt.property("Source Text").setValue(txAr[i].replace(/<me>/g,"").replace(/<brbrr>/g, '\r\n'));
} else {
myShapeLayer.property("ADBE Effect Parade")("my_balloon?")(1).setValue(0);
lineTxt.property("Source Text").setValue(txAr[i].replace(/<brbrr>/g, '\r\n'));
}
ここで特筆すべきは、シェイプレイヤーのパスをスクリプトで描く方法です。
‘ADBE Vector Shape – Group’でパスグループを作り、new Shape()でパスを作ったら、
- .vertices…パスの頂点を[x,y]で指定していく
- .inTangents…各頂点の進行方向逆のハンドルを[x,y]で指定していく
- .outTangents…各頂点の進行方向のハンドルを[x,y]で指定していく
- .closed…パスを閉じるかどうかtrueかfalseで指定する
- .setValue()…これまで設定した値をパスにブチ込む
これで自由なパスをスクリプトで描くことが出来ます。頂点やパスの曲がり具合を数値で入力しなくてはならないため、スクリプトだけで複雑な形状を描くのは難しいでしょう。
今回はフキダシのしっぽという単純な形だからこそ実現できました。複雑なパスを描きたい場合は、aiファイルやaepファイルを配布するほうが簡単です。なにも全てスクリプトで行う必要はありません。一番作業を効率化できる方法がスクリプトであればスクリプトを使えば良いのです。
案件によっては、テキストはエクセルからコピペのほうが早い場合は、スクリプトを考える時間のほうが無駄です。効率化の手段の一つにスクリプトがあることを忘れないでください。
エクスプレッションを考える
ここから調整用のエクスプレッションを作ります。
スクリプトからエクスプレッションを書き込む際は、プロパティ名.expression=で直接文字列を入力するのですが、文字列を指定する際はシングルクオーテーションもしくはダブルクオーテーションで囲みます。
例としては
thisComp.layer("サンプル").position.expression='ctrl=thisComp.layer("faceLine Control");
\r'+
'tx=thisComp.layer(index-1).text.sourceText;
\r'+
...
このような書き方になります。
シングルクオーテーションとダブルクオーテーションは入れ子に出来ますが、必ずセットで(‘…”…”…’) (“…’…’…”)使用しなくてはなりません。
(‘…”…’…”)これはエラーとなります。
また、エクスプレッション内の改行もそのまま改行を入力してはエラーとなります。明確的に「改行」を指定する「エスケープシーケンス」と呼ばれる特殊文字が用意されており、「改行」であれば「\r」と打ちます。これがスクリプトから設定するエクスプレッション内での改行となります。
下記のコードで確認してください。個人的にエクスプレッションではダブルクオーテーション「”」で文字列を囲みたいため、スクリプト内の文字列はシングルクオーテーション「’」で囲んでいます。
下記のコードではシングルクオーテーション「’」でスクリプト内の文字列を囲み、ダブルクオーテーション「”」でエクスプレッション内の文字列を囲んでいます。改行させたい位置に「\r」を打っています。
※「\」はWindowsでは「半角円マーク」、Macでは「バックスラッシュ」で表示されます。
フキダシ用長方形パスのサイズのエクスプレッション設定
myShapePath1(2).expression=
'ctrl=thisComp.layer("faceLine Control");\r'+
'tx=thisComp.layer(index-1).text.sourceText;\r'+
'gyousu=tx.split(/\\r/g);\r\r'+
'gyoukan=(gyousu.length-1)*ctrl.effect("line-height")(1).value\r\r'+
'function f_maxLength(str) {\r'+ // 文字数が一番多い行を検知
' var longestText = gyousu.sort(function(a, b) { return b.length - a.length; });\r'+
' return longestText[0];\r'+
'}\r\r'+
'maxLength=f_maxLength(gyousu)\r'+
'function f_hanzen(tx) {\r'+// 文字列を渡して半角と全角文字数を取得する関数
' hankaku=0;zenkaku=0;\r'+
' for (i=0; i<tx.length; i++) {\r'+
' var chr = tx.charCodeAt(i);\r'+
' if((chr >= 0x00 && chr < 0x81) || (chr === 0xf8f0) ||\r'+
' (chr >= 0xff61 && chr < 0xffa0) || (chr >= 0xf8f1 && chr < 0xf8f4)){\r'+
' hankaku += 1;\r'+
' } else {\r'+
' zenkaku += 1;\r'+
' }\r'+
' }\r'+
'return [hankaku,zenkaku];\r'+ // [半角,全角] の文字数が返ってくる
'}\r'+
// txNumに一番文字数の多い行の[半角文字数 ,全角文字数]が配列で入る
'txNum=f_hanzen(maxLength);\r\r'+
'x=(txNum[0]*ctrl.effect("1byte font-width")(1).value)+(txNum[1]*ctrl.effect("2byte font-width")(1).value)'+
'+ctrl.effect("balloon X-padding")(1).value;\r'+
'y=(gyousu.length*ctrl.effect("font-height")(1).value)'+
'+gyoukan+(ctrl.effect("balloon padding-top")(1).value/2)+gyoukan+ctrl.effect("balloon padding-bottom")(1).value;\r'+ // サイズ
'[x,y]';
フキダシの横幅は「全ての文字数分」ではなく、「一番長い行の文字数分」です。改行で区切り、文字数の長い順に並べ替え、一番長い行の文字数を取得します。
また全角文字と半角文字の横幅をそれぞれコントロールレイヤーのエフェクトとしてセットしておき、全角文字数×全角横幅、半角文字数と半角横幅でフキダシの横幅が正しくセットできます。
最後にフキダシ内の隙間(padding)を反映させてデザインが整うようにします。
フキダシ用長方形パスの位置のエクスプレッション設定
myShapePath1(3).expression= // x,yのサイズの半分を位置に入れると、結果右、下にのみ伸び縮みさせられます。
'[content("長方形パス 1").size[0]/2,content("長方形パス 1").size[1]/2];';
これで伸び縮みしても、フキダシのアンカーポイントが左上に固定されます。
フキダシ用長方形パスの角丸のエクスプレッション設定
myShapePath1(4).expression= // 角丸
'ctrl=thisComp.layer("faceLine Control");\r'+
'ctrl.effect("balloon round")(1).value';
フキダシ用長方形パスの塗りのエクスプレッション設定
var fill1 = shapeProperty.addProperty('ADBE Vector Graphic - Fill'); //塗り
fill1(4).expression=
'if(effect("my_balloon?")(1)==1){\r'+
' thisComp.layer("faceLine Control").effect("my balloon-color")(1).value;\r'+
'} else {\r'+
' thisComp.layer("faceLine Control").effect("other balloon-color")(1).value;\r'+
'}';
自分のメッセージならコントロールレイヤーの「my balloon-color」に、相手のメッセージなら「other balloon-color」に切り替えます。
フキダシレイヤーの位置のエクスプレッション設定
// チェックボックスで左右振り分け、前のフキダシからの距離はエクスプレッションで
myShapeLayer.position.setValue([0,0]);
myShapeLayer.position.expression=
'x=0;y=0;\r'+
'ctrl=thisComp.layer("faceLine Control");\r'+
'try {\r'+
' gyousu=thisComp.layer(index+1).text.sourceText.split(/\\r/g).length;\r'+
'} catch(e) {\r'+
' gyousu=1;\r'+
'}\r\r'+
'if(effect("my_balloon?")(1)==1){\r'+
' x=ctrl.position[0]+ctrl.effect("width")(1).value;\r'+
'} else {\r'+
' x=ctrl.position[0]-ctrl.effect("width")(1).value;\r'+
'}\r'+
'if(thisComp.layer(index+1).name=="faceLine Control"){\r'+
' y=ctrl.position[1];\r'+
'} else {\r'+
' y=thisComp.layer(index+2).position[1]+(gyousu*ctrl.effect("font-height")(1).value)'+
'+ctrl.effect("balloon Y-interval")(1).value'+
'+(ctrl.effect("balloon padding-top")(1).value/2)+ctrl.effect("balloon padding-bottom")(1).value;\r'+
'}\r\r'+
'value+[x,y];';
一つ下のレイヤーがテキストレイヤーと仮定して処理をさせます。1つ目のメッセージの場合は、一つ下のレイヤーはコントロールレイヤーとなり、ソーステキストが見つからずエラーになるため、try{}catch(e){}でエラーとなっても無視して処理が続くようにします。
一つ下のレイヤーが正常にテキストレイヤーであれば、行数を取得して自身のフキダシ位置を調整します。一つ前のメッセージが10行あれば、自身は10行分下に下がらなければなりません。
ついでに一つ前のメッセージの高さを考慮して配置されなければならないので、フキダシとテキストの隙間(padding)とフォントの高さ(font-height)など一つ前のメッセージの大きさを考慮した位置に配置すればOKです。
フキダシレイヤーのサイズのエクスプレッション設定
// 自分のフキダシは左右反転すればOK
myShapeLayer.scale.expression=
'if(effect("my_balloon?")(1)==1){\r'+
' [-100,100];\r'+
'} else {\r'+
' [100,100];\r'+
'}';
サイズのxをマイナスにするだけで、レイヤーを左右反転することができます。
アンカーポイントをしっぽ側にしていた意味がここでも効いてきます。サイズによってしっぽの位置を変えなくて済む他、相手の吹き出しをベースとして、反転するだけで他の様々なプロパティをそのまま自分のフキダシとして使えます。
フキダシ用しっぽパスの塗りのエクスプレッション設定
// 自分のフキダシかどうかで色を振り分け
if(effect("my_balloon?")(1)==1){
thisComp.layer("faceLine Control").effect("my balloon-color")(1).value;
} else {
thisComp.layer("faceLine Control").effect("other balloon-color")(1).value;
};
自分のフキダシか相手のフキダシかにより色を変えたいので、フキダシ自身に「チェックボックス制御」エフェクトを追加し、振り分けます。
フキダシ用しっぽパスの不透明度のエクスプレッション設定
//塗り
var fill2 = shapeProperty.addProperty('ADBE Vector Graphic - Fill');
// しっぽチェックボックスがOFFなら不透明度を0に
fill2(5).expression=
'if(thisComp.layer("faceLine Control").effect("tale")(1)==1){\r'+
' 100;\r'+
'} else {\r'+
' 0;\r'+
'}';
デザイン上、しっぽの有無を選択したいので、不透明度にエクスプレッションを仕込みます。
テキストレイヤーの「色かぶり補正」エフェクト「ブラックをマップ」のエクスプレッション設定
// フォントの色は「色かぶり補正」エフェクトでリンクさせる
txtFx[0]=lineTxt.property("ADBE Effect Parade").addProperty("ADBE Tint");
txtFx[0].name="link_font-color";
txtFx[0]("ADBE Tint-0001").expression=
'if(thisComp.layer(index+1).effect("my_balloon?")(1)==1){\r'+
' thisComp.layer("faceLine Control").effect("my font-color")(1)\r'+
'} else {\r'+
' thisComp.layer("faceLine Control").effect("other font-color")(1)'+
'}';
' thisComp.layer("faceLine Control").effect("other balloon-color")(1).value;\r'+
'}';
フォントの色自体にエクスプレッションが仕込めないため、フォントを黒にし、「色かぶり補正」エフェクトの「ブラックをマップ」の色をコントロールレイヤーの「font-color」とリンクさせます。
対応するフキダシ(一つ下のレイヤー)の「my balloon?」チェックボックスがONであれば、自分のメッセージの色、OFFなら相手のメッセージの色に切り替えます。
テキストレイヤーの位置のエクスプレッション設定
lineTxt.position.expression= // 配置はフキダシ位置からエクスプレッションで
'ctrl=thisComp.layer("faceLine Control");\r'+
'tx=text.sourceText;\r'+
'gyousu=tx.split(/\\r/g);\r\r'+
'function f_maxLength(str) {\r'+ // 文字数が一番多い行を検知
' var longestText = gyousu.sort(function(a, b) { return b.length - a.length; });\r'+
' return longestText[0];\r'+
'}\r\r'+
'maxLength=f_maxLength(gyousu)\r'+
'function f_hanzen(tx) { // 文字列渡して[半角,全角]文字数を返す\r'+
' hankaku=0;zenkaku=0;\r'+
' for (i=0; i<tx.length; i++) {\r'+
' var chr = tx.charCodeAt(i);\r'+
' if((chr >= 0x00 && chr < 0x81) || (chr === 0xf8f0) ||\r'+
' (chr >= 0xff61 && chr < 0xffa0) || (chr >= 0xf8f1 && chr < 0xf8f4)){\r'+
' hankaku += 1;\r'+
' } else {\r'+
' zenkaku += 1;\r'+
' }\r'+
' }\r'+
'return [hankaku,zenkaku];\r'+
'}\r'+
'txNum=f_hanzen(maxLength);\r\r'+ // [半角文字数 ,全角文字数]
'if(thisComp.layer(index+1).effect("my_balloon?")(1)==1){\r'+
' x=thisComp.layer(index+1).position[0]'+
'-(ctrl.effect("1byte font-width")(1).value*txNum[0])'+
'-(ctrl.effect("2byte font-width")(1).value*txNum[1])'+
'-(ctrl.effect("balloon X-padding")(1).value/2);\r'+
'} else {\r'+
' x=thisComp.layer(index+1).position[0]+(ctrl.effect("balloon X-padding")(1).value/2);\r'+
'}\r\r'+
'y=thisComp.layer(index+1).position[1]+ctrl.effect("font-height")(1).value+(ctrl.effect("balloon padding-top")(1).value/2);\r'+
'value+[x,y];';
“フキダシ用長方形パスのサイズのエクスプレッション”と原理は同じです。
自分のフキダシの場合、右配置なのに左揃えなので、そのままだと右にあるフキダシの右端から右に向かってテキストが始まってしまいます。右配置の自分のメッセージの場合は文字数分左に戻してやる必要があります。
そこで全角、半角文字数を追いかけ、一番文字数の多い行を横幅に変換し、その分を左に移動させフキダシに収めます。
これらをスクリプトでのフキダシ生成時の処理として繋げれば完成
これらを繋げれば、冒頭の完成したスクリプトができあがります。
コントローラーである「faceLine Control」レイヤー位置に他が全て追従してきますので、上にスクロールさせながらテキストと吹き出しをセットで表示させていくことで、やりとり風のアニメーションが作れます。
これでメッセージングアプリ風の動画が手軽に作れるようになりました。手作業でも作れますが、単純な作業が大量に必要なりますし、ルールが決まった作り方なので、プログラム化が正しい判断だったと思います。
ダウンロード
最後に
今回のスクリプトでは、フキダシの準備、配置にかかる膨大な時間を短縮し、本編の制作というクリエイティブな作業に時間を使えるようになります。
この記事を読まれた方も、是非プログラムで効率化できそうな処理に気付いたら、スクリプト化に挑戦してみてはいかがでしょうか。
次回予告
次のスクリプト記事では、実際にスクリプトを書くために必要なものを紹介します。
下記リンクから飛べます。
この記事へのコメントはありません。