演算子のオーバーロード
会社で演算子のオーバーロードの話になったのですが、ExtendScriptでは演算子のオーバーロードがサポートされていたので、試してみました。
アニメならではの時間表記
アニメではカット等の時間表記が‟3+18”のように秒と、コマ数で表記されるのが一般的です。
1秒あたりのコマ数は24コマとなっているので、この場合コマ数でいうと90コマになります。
また、コマ数同士の計算もよく行われます。
例えば‟3+18”のカットと‟1+12”のカットの時間の合計は‟5+6”です、アニメ撮影マンは24進数がわかるよう訓練されているので簡単に暗算できますが、これをAfterEffectsのスクリプトでやろうとすると少し面倒です。
コマ数にしてしまえば普通に加算、減算ができるので以下のような関数を書けばいいでしょう。
function frame2koma(frame, fps){
var sec = Math.floor(frame / fps);
var koma = frame % fps;
return sec + '+' + koma;
}
function koma2frame(koma, fps){
var split_koma = koma.split('+');
var sec = Number(split_koma[0]);
var koma = Number(split_koma[1]);
return sec * fps + koma;
}
function add(operandA, operandB, fps){
return frame2koma(koma2frame(operandA, fps) + koma2frame(operandB, fps), fps);
}
var a = '3+18';
var b = '1+12';
var c = add(a, b, 24);
$.writeln(c); //'5+6'
これでも問題なく機能していますが、もっと直感的にa + bのように出来ないでしょうか。
そこでESTKでサポートされている演算子オーバーロードを使用してみます。
ExtendScriptの演算子オーバーロードサポート
javaScript Tools Guide CC.pdfによると
// define the constructor method
function MyClass (initialValue) {
this.value = initialValue;
}
// define the addition operator
MyClass.prototype [“+”] = function (operand) {
return this.value + operand;
}
This allows you to perform the "+" operation with any object of this class:
var obj = new MyClass (5);
Result: [object Object] obj + 10;
Result: 15
とのことです。
なるほど、prototypeに生やせばOKらしいですね。ということはクラスを作ってしまえばOKなのでゴリっとクラスを作って、そこで演算子をオーバーロードしていきます。
var KomaTime = (function(){
function KomaTime(initValue, fps){
var frame = initValue;
if (String(initValue).match(/\+/)){
frame = koma2frame (initValue, fps);
}
this.frame = Number(frame);
this.fps = fps;
this.koma = frame2koma (frame, fps);
}
function frame2koma(frame, fps){
var sign = (frame < 0) ? '-' : '';
var sec = Math.floor(Math.abs(frame) / fps);
var koma = Math.abs(frame) % fps;
return sign + sec + '+' + koma;
}
function koma2frame(koma, fps){
var split_koma = koma.split('+');
var sec = Number(split_koma[0]);
var koma = Number(split_koma[1]);
if(sec < 0) return sec * fps - koma;
return sec * fps + koma;
}
function correctOperand(operandValue, fps){
var value = (operandValue instanceof KomaTime) ? String(operandValue) : operandValue;
if (String(value).match(/\+/)){
return koma2frame (value, fps);
}
return Number(value);
}
var p = KomaTime.prototype;
p.toString = function(){
return this.koma;
}
p ["+"] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
var new_frame = this.frame + corrected_Operand;
return new KomaTime(new_frame, this.fps);
};
p ["-"] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
var new_frame = this.frame - corrected_Operand;
return new KomaTime(new_frame, this.fps);
};
p ["<"] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame < corrected_Operand) return true;
return false;
};
p [">"] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame > corrected_Operand) return true;
return false;
};
p ["<="] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame <= corrected_Operand) return true;
return false;
};
p [">="] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame >= corrected_Operand) return true;
return false;
};
p ["=="] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame == corrected_Operand) return true;
return false;
};
p ["!="] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame != corrected_Operand) return true;
return false;
};
p ["==="] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame === corrected_Operand) return true;
return false;
};
p ["!=="] = function(operand){
var corrected_Operand = correctOperand(operand, this.fps);
if (this.frame !== corrected_Operand) return true;
return false;
};
return KomaTime;
}());
ExtendScriptにはassertがないので、簡単な比較でチェックしてみます。
(function (){
#include 'KomaTime.jsxinc'
function assert(actual, expected) {
$.writeln(actual === expected, '\tactual : ', String(actual), '\texpected : ', String(expected));
}
var kt1 = new KomaTime('3+18', 24);
var kt2 = new KomaTime('1+12', 24);
assert (kt1, '3+18');
assert (kt2, '1+12');
assert (kt1 + kt2, '5+6');
assert (kt2 - kt1, '-2+6');
assert ('-2+6' + kt1, kt2);
assert (kt1 > 0, true);
assert (kt1 < 0, false);
assert (kt1 - kt2 > 0, true);
assert (kt1 < 91, true);
assert (kt1 <= 90, true);
assert (kt1 >= 90, true);
assert (kt1 == 90, true);
assert (kt1 === 90, true);
assert (kt2 != kt1, true);
}());
true actual : 3+18 expected : 3+18
true actual : 1+12 expected : 1+12
true actual : 5+6 expected : 5+6
true actual : -2+6 expected : -2+6
true actual : 1+12 expected : 1+12
true actual : true expected : true
true actual : false expected : false
true actual : true expected : true
true actual : true expected : true
true actual : true expected : true
true actual : true expected : true
true actual : true expected : true
true actual : true expected : true
true actual : true expected : true
結果 : undefined
クラス自体のコードは長くなりましたが、これで加算だけではなく、減算、比較もできるようになったので、色々な計算が楽になりました。
オーバーロードできる演算子は以下のようです。
Unary +, ~
Binary +, -,
*, /, %, ^
<, <=, ==
<<, >>, >>>
&, |, ===
今回は乗算、除算、インクリメント、デクリメントはどう扱った方がいいのか考えて見送っているので、運用に合わせて機能を追加してみるのもいいと思います。
まとめ
意外に便利。
その他
ExtendScriptでクラスを書くときは、今は大体こんな感じで書いています。使用する際には即時関数化されたスコープ内で#includeすると、グローバル汚染もしないので#includeはコードの一番上に書かないようにしています。
ExtendScriptはES3準拠のAdobe独自言語ですが、標準でファイルアクセスができたり、簡単なSocketが実装されていたり、地味に面白い機能がいろいろあるのですが、ES3準拠のままアップデートがなく結構大変。とはいえAdobe製品の自動化には欠かせない機能です。