Adobe ExtendScriptにES2015で現代秩序をもたらす

  • 12
    いいね
  • 0
    コメント

この記事は AfterEffects Advent Calendar 2016 の 9日目の記事です。
昨日の記事は @wlor_ さんによる「MOT1995∞2016」用に作成したAfterEffectsスクリプト紹介でした。

AfterEffectsアドベントカレンダーですが、Adobe製品全般に使えるExtendScirptの話をします。

ExtendScirpt、使っていますでしょうか。
簡単なところだと、数十あるレイヤを数フレームずつランダムにずらしたい時とか、
スクリーンショット 2016-12-08 23.27.34.png

app.beginUndoGroup('randomize'); // 以降の処理のhistoryをまとめる
var layers = app.project.activeItem.selectedLayers; // 現在選択しているレイヤー
for (var i = 0; i < layers.length; i++) { // その数だけ
 layers[i].startTime += (Math.random()) * 0.1; // 開始時間に0.01~0.1秒足す
}
app.endUndoGroup(); // まとめ解除

スクリーンショット 2016-12-08 23.27.53.png

だけでいくらでも処理できますので、楽ですね。
(追記2016/12/09:AfterEffectsCC2015(13.6)ではMath.random()ではなくgenerateRandomNumber()が推奨されてるようです)

他にはMIDIを解釈してAfterEffectsのコンポジションに配置しまくるだとか、歌詞の全文字を1文字ずつコンポジションに分けて動かすだとか、やりたくないことは全部スクリプトに任せたいところです。

さて、普段WEBのお仕事でjavascriptを書く傍ら何件かExtendScriptを書いてて思うのですが、ExtendScirptはかなり古いjavascriptのコード(ES3/1999年12月の仕様)しか使えず、しんどいです。配列操作が面倒だったり、クラスが書けなかったりJSONライブラリもないです。旧石器時代です。

参考:ECMAScript - Wikipedia

ES3の策定時とはjavascriptもかなり状況が変わってきて、サーバーでも動かせるようになったりjsだけでアプリも作れるようになったり。Node.jsはAdobeの拡張機能でも使用できます。

ExtendScirptも中身はjsなのだから、最近のフロントエンド進化で変わった諸々でES2015で書けるようにしてやればもっと書きやすくなって最高なのでは?

ということで、今日の記事を書きます。

前提

  • ExtendScriptと書いてますが、私が主に使っているのはAfterEffectsですのでそれで使った知見で書いてます。他のアプリケーションでも使えるとは思います
  • コマンドラインで色々やります(黒い画面に白い文字をうちます)
  • Mac OSでやっています。Windowsの場合は適宜読み替えて下さい。
  • 「ぼくのしってるjsと違う」な人はnpm とか bower とか一体何なんだよ!Javascript 界隈の文脈を理解しようを読むとだいたいわかる

やりたいこと

  • ES5追加の配列系関数を使いたい
  • 既存のnpmモジュール(underscore.js)を使いたい
  • クラスで書きたい
  • ESTK以外のエディタで書きたい(Atom)
  • なにかしらのAltJSで書きたい

やること

Node.jsをインストール

Node.jsが入っていない(使った覚えがない)人はNode.jsをインストール

または

$ brew install node

(バージョン管理を気にする人はnodebrewでNode.jsをバージョン管理等をご参考に)

スクリプト作業用フォルダを作成して移動

$ mkdir -p my-script/{dist,src,lib}
$ cd my-script

package.jsonを作成

作業用フォルダ直下にnpmで使用するpackage.jsonを作成、以下を書き込みます。

package.json
{
  "scripts": {
    "start": "gulp",
    "clean": "gulp clean"
  },
  "dependencies": {
    "JSON2": "^0.1.0",
    "babel-preset-es2015": "^6.16.0",
    "babelify": "^7.3.0",
    "browserify": "^13.1.0",
    "es5-shim": "^4.5.9",
    "gulp": "^3.9.1",
    "rimraf": "^2.5.4",
    "run-sequence": "^1.2.2",
    "underscore": "^1.8.3",
    "vinyl-source-stream": "^1.1.0"
  }
}

今回はUnderscore.jsも使ってみます
遅すぎたUnderscore.js入門 - 全体像

依存ファイルをインストール

$ npm install

gulpfile.jsを作成

タスクランナーのgulpを使用し、コンパイルの自動化等を行います。

gulpfile.js
'use strict';
const gulp         = require('gulp');
const browserify   = require('browserify');
const source       = require('vinyl-source-stream');
const rimraf       = require('rimraf');
const sequence     = require('run-sequence');

const BUNDLENAME = 'bundle.jsx';
const DIST = './dist/';

gulp.task('default', () => {
  sequence('clean', 'build', 'watch');
});

gulp.task('clean', (cb) => {
  rimraf(DIST, cb);
});

gulp.task('build', () => {
  browserify({
    entries: ['./lib/entry.js']
  })
  .transform("babelify", {presets: ["es2015"]})
  .bundle()
  .on('error', function(e){
    console.log(e.message);
    console.log(e.stack);
    this.emit("end");
  })
  .pipe(source(BUNDLENAME))
  .pipe(gulp.dest(DIST));
});

gulp.task('watch', () => {
  gulp.watch(['src/**/*.js','lib/**/*.js'], ['build']);
});

lib/entry.jsを作成

続いて、browserifyの開始点になる entry.js を作成します。

entry.js
'use strict';

try {
  require('es5-shim/es5-shim.min.js');
  require('es5-shim/es5-sham.min.js');
} catch (error) {
  // ExtendScriptはすべてのグローバル変数を次回実行時も記憶している。
  // es5-shimでグローバルのDateオブジェクトをprototype拡張するが、次回実行時も保持したままになっている。
  // その関係で、2度目の読み込みで一部関数が例外を投げる。既にグローバルに読み込めてはいるので使える。
  // $.writeln('Caught an error:', error);
}
// JSONを使う
$.global.JSON = require('JSON2');

// underscoreを使う
$.global._ = require('underscore');

// メイン処理の開始
require('../src/main.js');

(ESTKを使うとglobal空間に定義された変数をデータオブジェクトから一覧可能になる上、global空間はユーザーが使用する他のスクリプトと共用になるため、他スクリプトとの衝突を避けるならglobal以下に別の名前空間を作ったほうが良いです。書きやすさと安全性のバランスが取れたうまい解決方法を探している)

src/main.jsに処理を書く

main.jsに実装したいコードを書いていきます。ためしに上で入れた諸々が使えるかを確認するmain.jsを書いてみましょう。

main.js
'use strict';

// ES6文法で書ける
const yaru = ['やるかー', 'やるしかないかー。'];
const pr = obj => $.writeln(JSON.stringify(obj));

// 配列系が使える
yaru.forEach(y => alert(y)); // alert('やるかー');alert('やるしかないかー。');


const apps = [
  {
    abbr:"AE",
    name:"AfterEffects",
    color:"purple"
  },
  {
    abbr:"PS",
    value:"Photoshop",
    color:"blue"
  }
];

pr(apps);

// underscoreを使う
const ae = _.findWhere(apps, {abbr:"AE"});
pr(ae);

if(!_.isUndefined(ae)){
  alert(ae.color); // purple
}


完成です

ここまでやれば、ES2015でExtendScriptを書くことができます。
sh
$ npm start

でsrc以下がdist/bundle.jsに変換され、変更を監視して自動で更新されるようになっています。
ESTKを起動して読み込み → my-script/dist/bundle.js を選択して実行してみましょう。

(何度か実行していると、たまにバグなのか異様にキャッシュが効いて変わらないことがある。一度ファイルを閉じて再読込すれば良い)

(ESTKで実行せずにCLIから実行する方法もあるんですが、環境ごとの安定性に欠けたりデータの確認がしにくかったりでなんだかんだこちらに落ち着きました。)

coffeescriptやtypescriptを使いたい場合はgulpfile.jsのbabelifyにしているところをtsifyやcoffeeifyにすればよいです。

細かい説明

ExtendScriptはES3基準

AdobeのExtendScriptはjavascriptのES3基準で作られています。
具体的に面倒なところでは、JSONが使えない、forEach / map / filterあたりの配列系が使えない、 Object.keys が使えない等。

ES5系をes5-shimを入れて解決します。
es5-shimは、ES5で追加されたメソッド等を擬似的に再現してくれます。

JSONの方はJSON2を入れて解決します。
(これもJSON3のほうがいいんだろうなと思いつつ、構文エラーが出たのでJSON2に逃げています。同様に現在のところ困ってはいないのですが、困ったタイミングとかで更新するかも)

もっと言えば最初素直にbabelでES3トランスパイルしようとしたがうまく行かなかったのでこの方法に逃げた。

ファイルを分割したい

一応ExtendScriptにも#includeというものがあるにはあるらしいですが、CommonJSっぽいrequireを使いたい。

複数のファイルを一つにまとめ、requireを擬似的に使えるようにしてくれます。

クラスを書きたい

ファイルを分けられるようになったので、それぞれをclassとして管理したい。

どちらかを使用すれば良さそう。

TypeScriptを使いたい

まだ試せていないです。
型定義ファイル(.d.ts)と併用することで、コードを書くときに補完が効くようになって便利。

型定義ファイル

AfterEffects

https://github.com/atarabi/aftereffects.d.ts

Photoshop

https://www.npmjs.com/package/photoshop.d.ts

所感

メリット

  • 大規模な開発をするならclass分けれて良さそう。汎用処理とかも分けれそう。
  • JSONオブジェクトが使えるのがうれしい。
  • underscore等の一部モジュールが使える。
  • ES2015なりCoffeeScriptなり書き慣れたものが使えそう。

デメリット

  • 書くコードは減るが、書き出されるコード量はめちゃくちゃに増える。実際の処理が数行のものでも、バンドル後のファイルは5000行程度になる。
  • 残念ながらnpmのモジュールは動かないものが多そう。moment.jsを利用しようとしたがエラーが出たので保留した。(そもそもそこまで使うのかという感じではあるが)
  • es5-shim等で拡張するグローバル周りの扱いがつらい。グローバル環境がアプリ起動している限り永遠に残り続けるので、毎回上書きしたりしないとXSS(クロスすくりぷとスクリプティング)なチャンスがありそう。

個人的にはクラスの継承やファイル分割周りとunderscoreが嬉しいので、所々に気をつけつつこの方法を使っていきたい。

余談:拡張機能(HTML5パネル)の話

拡張機能(HTML5パネル)はCEF(Chromium Embedded Framework)でできている。つまりChromeである。また、CSInterface.jsのevalScriptを使えば、拡張機能側からアプリケーション側に処理を投げ、その結果を取得することができる。なので、ExtendScriptに高度な処理を持たせず、拡張機能側に主な処理系を持たせて実際のExtendScriptにはCSInterfaceを通して簡単なメソッド操作のみを送る、という仕組みを作ることもできそう・・・?

CSInterface
(HTML5パネル周りもっと知りたいので、よろしければご教授下さい。)

おわりに

大まかにではありますが、Node.jsやフロント系の諸々を使ってExtendScriptを書く話をしましたがいかがでしょうか。
私が実際に使用している範囲では良い感じに使えていますが、もっと高度なことをする場合は色々変える必要がありそうです。

明日のAfterEffectsアドベントカレンダーは@smlifcaticさんの「クリスマスなのに卵を掘る話。」です!どういうことだ・・・?

宣伝

まつらい(@matsurai25)と言います。普段はフロントよりのWEB系のエンジニアですが最近PHPばかり書いている気がします。趣味でAfterEffectsを使ってモーショングラフィックスの作成をしたりAfterEffectsのスクリプト芸をします。EverydayOneMotionとか。

今回の内容+じゃあ軽く何か作ってみようかというコラムも載っているEverydayOneMotionメイキング本を冬コミで出します。よろしければどうぞ。
https://twitter.com/motions_work/status/805384084855758849