吉里吉里用のVisual Studio Codeプラグインを作った

Visual Studio Code用の拡張機能を作りました。
TJS用とKAG/KAGEX用があります。

TJS : プラグイン 説明書
KAG/KAGEX : プラグイン 説明書

今のところ以下の機能が使えます。

1.シンタックスハイライト

tjsのソースコードやタグを色分け表示できます。

2.定義へジャンプ・定義をここに表示

クラス、関数、変数やマクロの定義などにジャンプできます。
この機能を使うにはctagsとctagsxを入れる必要があります。詳しくは説明書を見てください。

※colorRectの定義を表示しているところ

3.リファレンスパレット

クラス名やタグ名からリファレンスを検索できます。

4.スニペット

forやfunctionなどのスニペットが使えます。

吉里吉里のバージョンを調べる

吉里吉里の質問をする時は使用している吉里吉里のバージョンを最初に伝えましょう。
単に吉里吉里といっても吉里吉里2安定版/吉里吉里2開発版と吉里吉里Zの各種バージョンで様々な違いがあります。
さらにKAG3/KAGEX2/KAGEX3などKAGのバージョンの違いがあります。
これらを書かない場合、バージョンを質問し返すのが面倒なためにスルーされる確率が上がります。

吉里吉里のバージョン

吉里吉里を起動してCtrl+F12を押すと以下のような画面が表示されます。
一番最初の行の「吉里吉里[きりきり] 2 実行コア version 2.31.2012.831 ( TJS version 2.4.28 )」が吉里吉里のバージョンになります。
自分の吉里吉里の同じ部分をコピーして送れば大体わかります。
krkr-version

KAGのバージョン

KAGのバージョンは簡単に調べる方法がありません。
しかし最後に更新されたのが5年前になるので普通は最新版だと思います。
KAGとだけ分かれば十分です。

KAGEXの場合は以下の3つのどこからダウンロードしたかでバージョンが変わります。
kag3ex3が最新版です。自分がどれを使っているかくらいは覚えておきましょう。
https://sv.kikyou.info/svn/kirikiri2/branches/kag3ex3/
https://sv.kikyou.info/svn/kirikiri2/branches/kag3ex2/
https://sv.kikyou.info/svn/kirikiri2/branches/kag3ex1/

KAGEX拡張例(既読テキストの色変更)

KAGEXには既読テキストと未読テキストで色を変える機能がありません。少しtjsで書き換えるだけで簡単に実現できます。

書き換えるのはMessageLayer.tjsの2476行目です。drawTextToLayerでテキストを描画していますが、chColorが色の設定です。ルビがある場合は2497行目でまたdrawTextToLayerを使って描画しています。この2箇所を適当に書き換えればいいです。

		drawTextToLayer(ll, dx, dy, ch, chColor);

		if(currentRuby != "")
		{
			// ルビがある
			var cw = llfont.getTextWidth(ch);
			var orgsize = llfont.height;
			llfont.height = rubySize;
			var rw = llfont.getTextWidth(currentRuby);
			var rx,ry;
			if(!vert)
			{
				rx = int(dx + (cw>>1) - (rw>>1));
				ry = int(dy - rubySize - rubyOffset);
			}
			else
			{
				rx = int(dx + rubySize + rubyOffset);
				ry = int(dy + (cw>>1) - (rw>>1));
			}

			drawTextToLayer(ll, rx, ry, currentRuby, chColor);

			llfont.height = orgsize;
			currentRuby = '';
		}

既読かどうかはkag.getCurrentRead()でわかります。現在のテキストが既読ならtrue、未読ならfalseになります。以下では既読なら色を0xFF0000、未読ならchColorそのままになるように変更しました。

		if (kag.getCurrentRead()) {
			drawTextToLayer(ll, dx, dy, ch, 0xFF0000);
		} else {
			drawTextToLayer(ll, dx, dy, ch, chColor);
		}

		if(currentRuby != "")
		{
			// ルビがある
			var cw = llfont.getTextWidth(ch);
			var orgsize = llfont.height;
			llfont.height = rubySize;
			var rw = llfont.getTextWidth(currentRuby);
			var rx,ry;
			if(!vert)
			{
				rx = int(dx + (cw>>1) - (rw>>1));
				ry = int(dy - rubySize - rubyOffset);
			}
			else
			{
				rx = int(dx + rubySize + rubyOffset);
				ry = int(dy + (cw>>1) - (rw>>1));
			}

			if (kag.getCurrentRead()) {
				drawTextToLayer(ll, dx, dy, ch, 0xFF0000);
			} else {
				drawTextToLayer(ll, dx, dy, ch, chColor);
			}

			llfont.height = orgsize;
			currentRuby = '';
		}

KAGEX拡張例(アクションの停止)

アクションの実行の続きです。

kag.stopAction

kag.stopActionを使うと実行中のアクションを停止できます。引数に渡したオブジェクトを対象としたアクションが全て停止されます。そのオブジェクトのアクションならnowaitのアクションも含めて停止されます。

// レイヤを生成して"star.png"を表示
var layer = new Layer(kag, kag.primaryLayer);
layer.loadImages("star.png");
layer.setSizeToImageSize();
layer.visible = true;

// アクションを実行
kag.beginAction(layer, %[
    left : %[
        handler : "MoveAction",
        value : 300,
        time : 5000,
    ]
]);

// 左クリックされたらアクションを停止
kag.addHook("leftClick", function() {
    kag.stopAction(layer);
});

この例では左クリックされたらkag.stopAction(layer);を呼んでlayerを対象としたアクションを停止しています。アクションが停止されるとアクション終了時の状態になります。この例ではlayerはleft=300の状態になります。

kag.stopAllActions

kag.stopAllActionsを使うと実行中のアクションを全て停止できます。kag.stopActionのように対象となるオブジェクトを渡す必要はありません。

// レイヤを生成して"star.png"を表示
var layer = new Layer(kag, kag.primaryLayer);
layer.loadImages("star.png");
layer.setSizeToImageSize();
layer.visible = true;

// アクションを実行
kag.beginAction(layer, %[
    left : %[
        handler : "MoveAction",
        value : 300,
        time : 5000,
    ]
]);

// 左クリックされたら全てのアクションを停止
kag.addHook("leftClick", function() {
    kag.stopAllActions();
});

kag.stopActionと違い、kag.stopAllActionsの場合はnowaitのアクションは停止されません。nowaitのアクションも含めて停止したい場合はkag.stopAllActions(true);のように引数にtrueを指定してください。

KAGEX拡張例(アクションの実行2)

KAGEX拡張例(アクションの実行)の続きです。

onCompleted, nowait

前回はbeginActionの引数を2つしか使いませんでしたが本当は4つあります。後半の2つは省略可能なので前回は使いませんでした。

第1引数はアクションの対象となるオブジェクト、第2引数はアクション定義と説明しました。

そして第3引数には関数を指定します。その関数はアクションが終了した時に呼ばれます。その関数の引数にはアクションの対象となっていたオブジェクトが渡されます。

そして第4引数にはtrueまたはfalseを指定します。trueに指定するとnowaitの動作になります。デフォルトではfalseです。

// レイヤを生成して"star.png"を表示
var layer = new Layer(kag, kag.primaryLayer);
layer.loadImages("star.png");
layer.setSizeToImageSize();
layer.visible = true;

// アクション終了時に呼ばれる関数を定義
function onCompleted(target) {
    System.inform(target.left);
}

// アクションを実行
kag.beginAction(layer, %[
    left : %[
        handler : "MoveAction",
        value : 300,
        time : 3000,
    ]
], onCompleted, true);

この例では終了時にonCompletedが呼ばれるようにしています。その引数targetはアクションの対象なのでlayerになります。なのでSystem.inform(target.left)ではアクション終了時のレイヤのleft座標が表示されます。

beginActionの第4引数にtrueを指定しているのでこのアクションはnowait指定のアクションになります。よってページ送りなどではスキップされません。

次回はアクションの停止の方法を説明します。

KAGEX拡張例(GenericFlip)

GenericFlipの機能を利用すると環境レイヤに独自の属性を追加できます。特殊な画像やエフェクトなどをtjsで描画したいときに便利です。初期状態のKAGEXでも動画を再生するGFX_Movie, フラッシュを再生するGFX_Flash, パーティクルを表示するGFX_Particleなどが用意されています。今回は自前でGenericFlipを作成してみます。

GenericFlipの実装例

今回作成するGenericFlipは、指定された画像をインターバルごとに繰り返し表示するものです。以下がそのスクリプトとその使用例です。

/**
 * 指定された画像を一定時間ごとに切り替えて表示するGenericFlip
 *     layerタグにswitch属性とinterval属性を追加します。
 *         switch   : 表示する画像をコンマで区切って指定します
 *         interval : 画像を切り替える間隔をms単位で指定します
 * 例) @layer name="テスト" switch="image1,image2" interval=1000
 */

// 必ずGenericFlipクラスを継承する
class ImageSwitcherGenericFlip extends GenericFlip 
{
    var interval; // 画像を切り替える間隔
    var files;    // 切り替えて表示する画像ファイルが入った配列

    var startTick;        // 開始時の時間
    var currentFileIndex; // 現在読み込んでいるファイルの番号

    var targetLayer; // 画像を読み込むレイヤ

    // コンストラクタにはKAGWindowオブジェクト(kag)が渡される
    function ImageSwitcherGenericFlip(window) {
        super.GenericFlip(window);
        targetLayer = new Layer(window, window.primaryLayer);
    }

    // フリップ開始時に呼ばれる
    // 第1引数には登録属性の属性値、第2引数には全ての属性が入った辞書が渡される
    function flipStart(files, elm) {
        // コンマで区切って配列に変換する
        var files = files.split(",");

        // 表示を開始する
        start((int)elm.interval, files);
    }

    // フリップ再生中に毎フレーム呼ばれる
    // 引数には現在のSystem.getTickCount()の返り値が渡される
    function flipUpdate(currentTick) {
        // 現在の時間から表示するファイル番号を計算する
        var elapsedTick = currentTick - startTick;
        var fileIndex = (elapsedTick \ interval) % files.count;

        // 現在表示している画像と同じなら何もしない
        if (currentFileIndex === fileIndex) { return; }
        currentFileIndex = fileIndex;

        // targetLayerに現在の画像を読み込み
        targetLayer.loadImages(files[fileIndex]);
        targetLayer.setSizeToImageSize();

        // flipAssignでtargetLayerを対象のレイヤにコピー
        flipAssign(targetLayer);
    }

    // フリップ停止時に呼ばれる
    function flipStop() {
        this.interval = 0;
        this.files = void;
        super.flipStop(); // 必ずsuper.flipStopを呼び出す
    }

    // セーブする際に呼ばれる
    // 引数には保存用の辞書が渡される
    function flipStore(dic) {
        dic.interval = interval;
        dic.files = files;
    }

    // ロードする際に呼ばれる
    // 引数にはflipStoreで保存した辞書が渡される
    function flipRestore(dic) {
        if (dic.files === void) { return; }
        // 画像の表示を再開
        start(dic.interval, dic.files);
    }

    // 画像の表示を開始する関数
    // 第1引数に画像を切り替える間隔、第2引数にファイル名が入った配列を渡す
    function start(interval, files) {
        this.interval = interval > 0 ? interval : 1000;
        this.files = files;
        this.startTick = System.getTickCount();
        this.currentFileIndex = void;
    }
}

// 定義したクラスをGenericFlipとして登録
GenericFlip.Entry(%[
    "class"    => ImageSwitcherGenericFlip, // GenericFlipクラス
    "type"     => "switch",                 // 登録属性
    "options"  => [ "interval" ],           // 使用する属性名
]);
@linemode mode=vn
; 環境レイヤにimage01, image02, image03を1秒ごとに繰り返し表示する例
@layer name=レイヤ switch="image01,image02,image03" interval=1000
画像を表示しています。

; レイヤを消したりfile属性などで他のファイルを読み込んだりするとGenericFlipは停止します
@layer name=レイヤ hide
画像を消去しました。

実装の詳細はコメントを参照してください。GenericFlipを作る際の一般的な注意点のみ解説します。

GenericFlipとして使うクラスは必ずGenericFlipクラスを継承してください。ここではImageSwitcherGenericFlipクラスをGenericFlipとして使います(10行目)。

クラスを定義したら、GenericFlip.Entryを使って登録する必要があります(88行目)。登録内容は辞書で渡します。classには先ほど定義したクラス、typeにはこのクラスを呼び出すのに使う属性、optionsにはそれ以外に使う属性を配列で渡します。ここでは[layer]タグでswitch属性が使われたときに呼び出されるように登録しています。switch以外にinterval属性を使うのでそれも登録します。

GenericFlipクラスとして機能させるにはいくつかの関数を実装します。ここではflipStart, flipUpdate, flipStop, flipStore, flipStore, flipRestoreを実装しています。それぞれフリップ開始時、フリップ実行中、フリップ停止時、セーブ時、ロード時に呼ばれます。

switch属性が使われるとまずImageSwitcherGenericFlipクラスのオブジェクトが作成され、その後すぐにflipStartが呼び出されます。第1引数には登録属性(ここではswitch属性)の属性値、第2引数にその他の属性が入った辞書が渡されます。

その後は停止されるまでずっとflipUpdateが呼び出され続けます。現在の時間が第1引数に渡されるのでそれに合わせてレイヤに描画すればいいです。ここで重要になるのがflipAssign()です。これはGenericFlipクラスにあらかじめ用意されている関数で、引数に渡したレイヤの内容を環境レイヤにコピーしてくれます。まずは自前のレイヤに描画してからflipAssignを使ってコピーするのが定石です。ここでもtargetLayerに画像を読み込み、それをflipAssignでコピーしています。

環境レイヤが非表示になったりするとGenericFlipは自動的に停止されます。このときに呼ばれるのがflipStopです。ここでは必ずsuper.flipStop()を呼ばなければなりません。忘れないように注意してください。

flipStore, flipRestoreはセーブ・ロードに対応させるために必要です。flipStoreで現在の状態を保存してflipRestoreで実行を再開できるようにしてください。

まとめ

GenericFlipの実装手順は以下のようになります。
・GenericFlipを継承したクラスを定義する
・定義したクラスにflipStart, flipUpdate, flipStop, flipStore, flipRestoreを実装する
・定義したクラスをGenericFlip.Entryで登録する

GenericFlipはtjsを使ってレイヤに描画できます。環境レイヤに機能を追加する形なので、rotateなどの変形やcontrastなどの色調補正、その他のコマンドと組み合わせて使えます。独自のLayerを用意するよりも、可能であればGenericFlipとして描画機能を実装する方が応用がきいて便利だと思います。

KAGEX拡張例(アクションの実行)

kag.beginActionを使うとtjsからアクションを実行できます。

レイヤを移動してみる

以下はlayerに画像を表示し、5000msかけてleft=300, top=300まで移動させる例です。

// レイヤを生成して"star.png"を表示
var layer = new Layer(kag, kag.primaryLayer);
layer.loadImages("star.png");
layer.setSizeToImageSize();
layer.visible = true;

// アクションを実行
kag.beginAction(layer, %[
    left : %[
        handler : "MoveAction",
        value : 300,
        time : 5000,
    ],
    top :  %[
        handler : "MoveAction",
        value : 300,
        time : 5000,
    ],
]);

beginActionの第一引数にはアクションの対象となるオブジェクトを指定します。第二引数にはアクション定義を渡します。アクション定義はenvinit.tjsで定義するものと同じです。今回の場合は対象のleft, topプロパティを500msかけて300まで動かすアクションになっています。

アクションについて

アクションをtjs的に言うと、特定のプロパティに対して自動的に値を代入していく機能です。以下の例ではプロパティpropを持つtestオブジェクトに対してアクションを実行しており、propには1000msかけて0から1000の値が代入されます。

// プロパティpropを持つクラスを定義
class Test {
	var _prop = 0;
	property prop {
		setter(value) {
			_prop = value;
			Debug.message("現在の値: " + value); // 代入された値を表示
		}
		getter() {
			return _prop;
		}
	}
}

// Testクラスのオブジェクトを生成
var testObj = new Test();

// testObjに対してアクションを実行
kag.beginAction(testObj, %[
    prop : %[
        handler : "MoveAction",
        start :  0,
        value : 1000,
        time : 1000,
    ]
]);

/* Debug.messageでは以下のように表示されます
    22:03:52 現在の値: +0.0
    22:03:52 現在の値: 1
    22:03:52 現在の値: 2
    22:03:52 現在の値: 2
    22:03:52 現在の値: 3
    22:03:52 現在の値: 4
    22:03:52 現在の値: 5
    22:03:52 現在の値: 6
      (中略)
    22:03:53 現在の値: 997
    22:03:53 現在の値: 998
    22:03:53 現在の値: 999
    22:03:53 現在の値: 999
    22:03:53 現在の値: 1000
    22:03:53 現在の値: 1000
*/

この例のように、Layerのleftやtopなどだけでなくプロパティであれば何でも対象にできます。上手く使えば色々と応用できると思います。

アクションのさらに詳しい使い方は次回に続きます。

KAGEX拡張例(画像の変形)

KAGEXではLayerクラスを拡張したAffineLayerクラスを利用できます。AffineLayerクラスを使うと簡単に回転や拡大縮小した画像を表示できます。

画像を表示

以下はAffineLayerに”star.png”を読み込んで表示するだけの例です。Layerと使い方は同じなので、AffineLayerの部分をLayerに変えてもそのまま動きます。

// レイヤオブジェクトを生成
var layer = new AffineLayer(kag, kag.fore.base);

// 画像を読み込む
layer.loadImages("star.png");
layer.setSizeToImageSize();

// 表示
layer.visible = true;

画像を回転

次は回転させてみる例です。

// レイヤオブジェクトを生成
var layer = new AffineLayer(kag, kag.fore.base);

// 画像を読み込む
layer.loadImages("star.png");
layer.setSizeToImageSize();

// 回転原点を画像中央に設定
layer.afx = AffineLayer.AFFINEOFFSET_CENTER;
layer.afy = AffineLayer.AFFINEOFFSET_CENTER;

// 画面中央に移動
layer.left = kag.width / 2;
layer.top = kag.height / 2;

// 90度回転させてみる
layer.rotate = 90;

// 表示
layer.visible = true;

afx, afyで回転原点を設定できます。その機能はKAGEXコマンドのafx, afyと同じです。その点を中心として回転や拡大縮小などが実行されます。afx,afyは整数で設定することもできますが、あらかじめ定義された定数も使えます。使える定数は以下の様になります。

定数名 設定される値
AffineLayer.AFFINEOFFSET_DEFAULT 初期値(0)
AffineLayer.AFFINEOFFSET_CENTER 画像中央
AffineLayer.AFFINEOFFSET_LEFT 画像左端(afxのみ)
AffineLayer.AFFINEOFFSET_RIGHT 画像右端(afxのみ)
AffineLayer.AFFINEOFFSET_TOP 画像上端(afyのみ)
AffineLayer.AFFINEOFFSET_BOTTOM 画像下端(afyのみ)

また、Layerと同じようにleft, topで画像の位置を設定できます。ただしその機能は表示原点(KAGEXコマンドのorx, ory)と同じになります。つまりleft,topに指定した座標(回転原点)に回転原点が重なるようになります。KAGEXの回転原点と表示原点について忘れた方はKAGEX講座(12) – レイヤ表示位置(afx, afy, orx, ory)を参照してください。

その他の変形プロパティ

rotate以外にも様々なプロパティが用意されています。以下の表に主なものをまとめます。これ以外にopacityやwidth, heightなどLayerクラスと同じプロパティや関数なども利用できます。

プロパティ名 初期値 説明
flipx false trueなら横方向に反転
flipy false trueなら縦方向に反転
rotate 0 回転角度
zoomx 100 横方向の拡大率
zoomy 100 縦方向の拡大率
zoom 100 zoomx, zoomyを一括指定
slantx 0 横方向の剪断
slanty 0 縦方向の剪断
afx 0 回転原点のx座標
afy 0 回転原点のy座標
orx 0 表示原点のx座標
ory 0 表示原点のy座標
raster 0 ラスター振幅量
rasterLines 100 ラスター行数
rasterCycle 1000 ラスター周期

それぞれのプロパティはKAGEXでも同じ属性名のコマンドとして用意されています。ここで改めて機能の説明はしません。

KAGEX拡張例(kag.insertTag)

前回までkag.addTagについて紹介しましたが、似たような関数にkag.insertTag()があります。

kag.addTagとkag.insertTagの違いは登録されたタグの実行される順番です。addTagでは一番最後に登録されますがinsertTagは一番最初に登録されます。

function flashMessage() {
    /* 以下のタグを登録
     * @msgoff time=500
     * @msgon time=500
     * @waittrig name=flash_message
     */
    kag.insertTag("waittrig", %[ name:"flash_message" ]);
    kag.insertTag("msgon", %[ time:500 ]);
    kag.insertTag("msgoff", %[ time:500 ]);
    
    // triggerでコンダクタの実行を再開
    kag.trigger("flash_message");
}

前回のflashMessageとほぼ同じですがaddTagの代わりにinsertTagを使っています。insertTagの場合はタグが先頭に追加されていくので後から登録したものが先に実行されます。なのでwaittrig, msgon, msgoffの順に登録しています。

タグの機能を呼び出したいだけならkag.addTagで十分です。先に登録したものが先に実行される方がわかりやすいと思います。何か複雑な機能を作る場合にはinsertTagを使うこともありそうなので一応覚えておいた方がいいかもしれません。

KAGEX拡張例(コンダクタの停止)

前回の続きです。

kag.addTagを使うとコンダクタに次のタグを登録できます。登録するだけなので[s]タグなどでコンダクタが止まっている間はタグは実行されません。

タグが実行されない例

以下の例では[msgoff][msgon]タグを登録していますが、コンダクタが停止しているため実行されることはありません。

function flashMessage() {
    /* 以下のタグを登録
     *   @msgoff time=500
     *   @msgon time=500
     */
    kag.addTag("msgoff", %[ time:500 ]);
    kag.addTag("msgon", %[ time:500 ]);
}
@linemode mode=vn

コンダクタを停止します。

@click exp="flashMessage()"
@s

[click]タグを使って左クリックされた時にflashMessage()が呼ばれるように設定しています。flashMessage()は[msgoff][msgon]タグを登録する関数です。[msgoff][msgon]を連続的に実行してメッセージレイヤを点滅させようとしていますが、これでは動作しません。

[s]タグでコンダクタの動作が停止されており、登録されたタグが実行されないためです。動作させるにはflashMessage()でタグを登録するだけでなくコンダクタの動作を開始しなければなりません。

実行されるように修正した例

以下が実際に動作するように修正したスクリプトです。クリックするたびにメッセージレイヤが点滅します。

function flashMessage() {
    /* 以下のタグを登録
     *   @msgoff time=500
     *   @msgon time=500
     *   @waittrig name=flash_message
     */
    kag.addTag("msgoff", %[ time:500 ]);
    kag.addTag("msgon", %[ time:500 ]);
    kag.addTag("waittrig", %[ name:"flash_message" ]);
    
    // triggerでコンダクタの実行を再開
    kag.trigger("flash_message");
}
@linemode mode=vn

コンダクタを停止します。

@click exp="flashMessage()"
@waittrig name=flash_message

[s]タグの代わりに[waittrig]タグを使っています。このタグもコンダクタの動作を停止しますが、name属性に指定したトリガが発動されるとコンダクタの動作を再開します。この場合は”flash_message”が発動すれば動作を再開します。

flashMessage()ではタグを登録してからkag.trigger(“flash_message”);としてトリガを発動します。これによってコンダクタの動作が再開され、登録したタグを順番に実行できます。

最初の例と異なり、[msgoff][msgon]タグだけでなく[waittrig]タグも登録していることに注意してください。[s]や[waittrig]などのタグでコンダクタを停止しない場合は当然そのまま実行し続けます。この例では登録したタグを実行し終わった後、first.ks最後の @waittrig name=flash_message の次の行から実行されます。続きを実行したくない場合はしっかり止めるようにしてください。

まとめ

kag.addTagを使うときはコンダクタの動作を意識するようにしてください。注意して使わないと思わぬバグの元になりかねません。