Posted at

ExtendScriptで演算子のオーバーロード

More than 1 year has passed since last update.


演算子のオーバーロード

会社で演算子のオーバーロードの話になったのですが、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なのでゴリっとクラスを作って、そこで演算子をオーバーロードしていきます。


KomaTime.jsxinc

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がないので、簡単な比較でチェックしてみます。


test.jsx

(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);

}());



JavaScriptコンソール

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製品の自動化には欠かせない機能です。