吉里吉里メモ

  1. Abstract
  2. 注意書き
  3. 吉里吉里
    1. ウィンドウに対するレイヤの位置を取得する
    2. レイヤ塗りつぶし(吉里吉里2 2.24以降)
  4. TJS
    1. 辞書配列からキー一覧、値一覧を取得する
    2. 配列をシャッフルする
    3. W3CDTFの正規表現
    4. 配列要素の挿入・削除スピード
    5. 配列ループのコスト軽減
    6. 関数内static変数(もどき)
    7. typeofとinstanceof演算子
    8. クラス内クラス
  5. KAG
    1. マクロ中の属性一括指定適用規則
    2. 属性値中の文字をエスケープ
    3. レイヤ塗りつぶし(KAG版)

1. Abstract

吉里吉里2、TJS、KAGのメモです。個人的に溜め込んでいたものです。ぼちぼち追加していく予定です。

2. 注意書き

このページにあるソースコードは自由に使用・改造してかまいません。お好きにどうぞ。

ただし、著者は、このページに書かれている内容の正しさについては一切保証しません。また、このページの内容を参考にしたことによって何が起こったとしても、著者は一切関知しませんし、一切責任を負いません。自己責任でお願いします。

3. 吉里吉里

3.1. ウィンドウに対するレイヤの位置を取得する

レイヤの親子階層をスキャンして、ウィンドウに対するレイヤの絶対位置を取得する方法です。

// レイヤの横位置を取得する
function getAbsoluteLeft(layer, val = 0)
{
  val += layer.left;
  val = getAbsoluteLeft(layer.parent, val) if (!layer.isPrimary);
  return val;
}

// レイヤの縦位置を取得する
function getAbsoluteTop(layer, val = 0)
{
  val += layer.top;
  val = getAbsoluteTop(layer.parent, val) if (!layer.isPrimary);
  return val;
}

// こんな風に使います
var absLeft = getAbsoluteLeft(objLayer);

吉里吉里2 2.24以降なら、以下のようにLayerのメソッドにしてしまう方法もあります。

// レイヤの横位置を取得する
Layer.getAbsoluteLeft = function(val = 0)
{
  val += this.left;
  val = this.parent.getAbsoluteLeft(val) if (!this.isPrimary);
  return val;
};

// レイヤの縦位置を取得する
Layer.getAbsoluteTop = function(val = 0)
{
  val += this.top;
  val = this.parent.getAbsoluteTop(val) if (!this.isPrimary);
  return val;
};

// こんな風に使います
var absLeft = objLayer.getAbsoluteLeft();

3.2. レイヤ塗りつぶし(吉里吉里2 2.24以降)

レイヤを特定の色で塗りつぶします。機能的にはどうということはないですが、意外と使うことが多かったり多くなかったり。

/*
 * レイヤのメソッドとして機能する。
 * @param value 色。デフォルトは黒。
 * @return void
 */
Layer.wipe = function(value = 0xFF000000)
{
  this.fillRect(0, 0, this.imageWidth, this.imageHeight, value);
};

4. TJS

TJS2/2.4.13での話です。

4.1. 辞書配列からキー一覧、値一覧を取得する

Dictionaryオブジェクトからキーの一覧、値の一覧を配列で取得する方法です。

// キー取得
function getKeys(dic)
{
  return getDicElementImpl(dic, 1);
}

// 値取得
function getValues(dic)
{
  return getDicElementImpl(dic, 0);
}

// 上記の内部実装
function getDicElementImpl(dic, ei = 1)
{
  var arr = new Array();
  arr.assign(dic);
  var lp = arr.count \ 2;
  for (var i = 0; i < lp; i++) {
    arr.erase(i + ei);
  }
  return arr;
}

// こんな風に使います
var keys = getKeys(objDic);
var values = getValues(objDic);

4.2. 配列をシャッフルする

配列をシャッフルして、格納された要素の順番を出鱈目にする方法です。

function shuffleArray(arr)
{
  var tmp = new global.Array();
  tmp.assign(arr);
  arr.clear();
  var cnt = tmp.count + 1;
  while (--cnt) {
    var idx = (int)(Math.random() * cnt);
    arr.add(tmp[idx]);
    tmp.erase(idx);
  }
}

// こんな風に使います
shuffleArray(objArray);

乱数発生器はMersenne Twisterじゃなきゃ嫌だ!と言う人は、適当に書き換えてください。

4.3. W3CDTFの正規表現

W3CDTFの正規表現オブジェクトは以下の通りです。正規表現部は一行で記述してください。

var reW3CDTF = /(?:^\d{4}(?:-(?:0[1-9]|1[012]))?$)|(?:^(?:(?:(?:[02468][048]|[13579][26])00-(?:(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))|(?:02-(?:0[1-9]|[12]\d))|(?:(?:0[13579]|1[02])-(?:0[1-9]|[12]\d|3[01]))))|(?:\d\d00-(?:(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))|(?:02-(?:0[1-9]|[12][0-8]))|(?:(?:0[13579]|1[02])-(?:0[1-9]|[12]\d|3[01]))))|(?:\d\d(?:[2468][048]|0[48]|[13579][26])-(?:(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))|(?:02-(?:0[1-9]|[12]\d))|(?:(?:0[13579]|1[02])-(?:0[1-9]|[12]\d|3[01]))))|(?:\d{4}-(?:(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))|(?:02-(?:0[1-9]|[12][0-8]))|(?:(?:0[13579]|1[02])-(?:0[1-9]|[12]\d|3[01])))))(?:T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(\.\d+)?)?(?:Z|[-+](?:[01]\d|2[0-3]):[0-5]\d))?$)/;

特徴は下記の通りです。

  • 閏年判定に対応
  • 2月29日判定にも対応
  • タイムゾーン表記に対応

西暦0000年が通ってしまうという欠点もありますが、その辺はご愛敬ということで勘弁してください。ちなみに、Perlにも流用できるはずです。

一応、作成過程も書いておきます。Perlです。

$year1 = '(?:[02468][048]|[13579][26])00'; # mod 400
$year2 = '\d\d00';  # mod 100
$year3 = '\d\d(?:[2468][048]|0[48]|[13579][26])'; # mod 4
$year4 = '\d{4}'; # else
$month1 = '(?:0[469]|11)'; # 4,6,9,11
$month2 = '02'; # 2
$month3 = '(?:0[13579]|1[02])'; # 1,3,5,7,8,10,12
$month0 = '(?:0[1-9]|1[012])'; # 01-12
$day1  = '(?:0[1-9]|[12]\d|30)'; # 4,6,9,11
$day2a = '(?:0[1-9]|[12][0-8])'; # 2 
$day2b = '(?:0[1-9]|[12]\d)'; # 2 (leap year)
$day3  = '(?:0[1-9]|[12]\d|3[01])'; # 1,3,5,7,8,10,12
$month_day1 = "(?:(?:${month1}-${day1})|(?:${month2}-${day2a})|(?:${month3}-${day3}))"; # MD
$month_day2 = "(?:(?:${month1}-${day1})|(?:${month2}-${day2b})|(?:${month3}-${day3}))"; # YMD (leap year)
$ym  = "${year4}(?:-${month0})?"; # yyyy | yyyy-mm
$ymd = "(?:(?:${year1}-${month_day2})|(?:${year2}-${month_day1})|(?:${year3}-${month_day2})|(?:${year4}-${month_day1}))"; # yyyy-mm-dd
$hhmm = '(?:[01]\d|2[0-3]):[0-5]\d'; # hh:mm
$ss_s = '[0-5]\d(\.\d+)?'; # ss | ss.s+
$tzd  = "(?:Z|[-+]${hhmm})"; # Z | +hh:mm | -hh:mm
# 以上より
$reW3CDTF = "(?:^${ym}\$)|(?:^${ymd}(?:T${hhmm}(?::${ss_s})?${tzd})?\$)"; # result

4.4. 配列要素の挿入・削除スピード

配列から要素を削除する場合、前の方(インデックスの若い方)から削除するよりも、後ろから削除した方が速いです。挿入に関しても同じことが言えます。これは、TJSにおける配列の実装がC++のベクタ(std::vector)であることに起因します。速度的にシビアなケースでは、このことを念頭に置いてコーディングした方がいいかもしれません。特に、大きな配列から大量の要素を削除する場合、大きな差が出ます。

極端な例ですが、下記のように、要素数20000の配列から、全ての要素をeraseメソッドで削除する場合ですと、頭から削除するよりも、後ろから削除する方が100倍以上速いという結果が出ます。

// 配列の要素全てをeraseメソッドで削除する

// 配列作成
function getLargeArray(size)
{
  var r = new Array();
  for (var i = 0; i < size; i++) {
    r.add(i);
  }
  return r;
}

// 前から消す
function deleteFromHead(arr)
{
  var cnt = arr.count;
  with (arr) {
    while (cnt--) {
      .erase(0);
    }
  }
}

// 後ろから消す
function deleteFromTail(arr)
{
  var cnt = arr.count;
  with (arr) {
    while (cnt--) {
      .erase(cnt);
    }
  }
}

const ARRAY_SIZE = 20000;

// 頭から消す場合
{
  var startTime = System.getTickCount();
  var arr = getLargeArray(ARRAY_SIZE);
  deleteFromHead(arr);
  var endTime = System.getTickCount();
  System.inform(endTime - startTime);
}

// 後ろから消す場合
{
  var startTime = System.getTickCount();
  var arr = getLargeArray(ARRAY_SIZE);
  deleteFromTail(arr);
  var endTime = System.getTickCount();
  System.inform(endTime - startTime);
}

なお、インデックスによる参照(ランダムアクセス)と要素の追加は高速で、配列のサイズや参照位置によらず、処理時間は一定です。

4.5. 配列ループのコスト軽減

TJSで、配列の各要素に対して処理を行う、あるいは配列の要素数と同じ回数だけループさせる場合、forループを使用することが多いと思います。ここでは、以下の条件下でループのコストを軽減させる方法を考えてみます。

  • 配列の要素数と同じ回数だけループさせる
  • ループ内の処理では、配列の要素数が変化しない

普通にJavaやC/C++の感覚で考えると、下記のようなコードになるでしょう。

for (var i = 0; i < array.count; i++) {
  // 処理
}

この方法ですと、ループ一回ごとにarray.count を評価することになります。array.count が評価される過程は以下のようになります。

  1. 現在のスコープから外側のスコープ(globalオブジェクト)に向かってarrayという名前のオブジェクトを検索する
  2. array を評価する
  3. array オブジェクトからcountという名前のメンバ(オブジェクト)を検索する
  4. count を評価する

array.count の評価コストはさほど高いものではありませんが、評価回数が多ければ、それなりにコストは上がります。特にループ内の処理が軽い場合は、ループ全体のコストに占める、ループ自身のコストの割合が高くなるので、array.count の評価コストがループの処理速度に大きく影響します。そこで、array.count の評価回数を一回に減らし、ループの処理を高速化してみます。

var cnt = array.count;
while (cnt--) {
  // 処理
}

あくまでループ中でarray.count が変化しないのが前提ですが、たったこれだけでもループ自身にかかるコストは半分以下に軽減されます。このテクニックは、ループ回数が多く、かつループ内の処理が比較的軽い場合に効果を発揮するでしょう。

4.6. 関数内static変数(もどき)

TJSの関数には、C/C++のstatic変数に相当する機能はありません(staticという予約語はありますが、実装されていません)。が、それっぽいことはできます。

TJSでは、オブジェクトはメンバを持つことができます。関数もまたオブジェクトですので、関数はメンバを持つことができる、ということになります。このメンバを、static変数の代わりにします。

// 関数
function foo()
{
  return hoge;
}

// static変数もどき
foo.hoge = 1;

// 関数fooを、fooコンテキストで実行する。
var result = (foo incontextof foo)();

この方法だと、実行するときに毎回コンテキストを指定しなければならないので、面倒です。こういうときは、実行する前にコンテキストを変更しておきます。

// 関数
function foo()
{
  return hoge;
}

// static変数もどき
foo.hoge = 1;

// コンテキストを変更する。
foo = foo incontextof foo;

// 関数fooを実行する。
var result = foo();

4.7. typeofとinstanceof演算子

typeof演算子を使用すると、対象の型を判別できます。TJSではクラスインスタンスのほか、関数、クラス、プロパティ、nullがオブジェクトとして扱われます。typeof演算子はこれらに対して'Object'を返します。

instanceof演算子は、左辺値に指定したオブジェクトが右辺値のインスタンスに当たる場合、trueを返します。通常は左辺値にクラスインスタンスを、右辺値にクラス名を指定します。左辺値に関数、クラス、プロパティを指定した場合は、右辺値にそれぞれ'Function', 'Class', 'Property'を指定するとtrueが返ります。左辺値が整数、実数、void、未定義オブジェクト以外の場合は、右辺値に'Object'を指定するとtrueが返ります。

typeof演算子とinstanceof演算子
対象 typeofの結果文字列 instanceofがtrueを返す文字列 その他の判別方法
関数 Object Function または Object
クラス Object Class または Object
プロパティ Object Property または Object
クラスインスタンス Object クラス名 または Object
null Object Object target === null
整数 Integer (判別不可)
実数 Real (判別不可)
文字列 String String または Object
オクテット列 Octet Octet または Object
void void (判別不可) target === void
未定義のオブジェクト undefined (例外発生)

4.8. クラス内クラス

TJSでは、クラスの宣言はグローバルコンテキストで行います。クラス宣言中で(Javaのような)内部クラス宣言を行ったり、class Hoge.Foo { ... }のように宣言することはできません。

TJS標準のMath.RandomGeneratorのように、クラスをオブジェクトのメンバとする場合には、下記のようにします。

class Hoge {}	// 適当なクラス(コンテナ)
class Tmp {}	// 適当なクラス(実体)
Hoge.Foo = global.Tmp;	// HogeのメンバとしてFooを作成し、Tmpの実体を参照
delete global.Tmp;	// グローバル側の参照を削除

TJSではクラスもオブジェクトの一種なので、こういうことができます。インスタンスを作成する場合はvar foo = new Hoge.Foo();とします。

ただし、欠点もあります。上記の例だと、Hoge.Fooは完全な静的メンバではないため、HogeクラスのインスタンスにFooが含まれてしまいます。また、foo = new Hoge.Foo();で作成したインスタンスは、Tmpクラスのインスタンスと看做されます。Hoge.Fooは、Tmpという名前で宣言されたクラス(オブジェクト)への参照に過ぎないので、クラスの実体は依然としてTmpという名前のクラスです。つまり、foo instanceof 'Tmp'は真を返しますが、foo instanceof 'Foo'foo instanceof 'Hoge.Foo'は偽を返します。

5. KAG

KAG3/3.24での話です。

5.1. マクロ中の属性一括指定適用規則

マクロ中のタグの属性に*を指定すると、マクロに渡された属性をすべて渡すことができます。この記号を使用すると、この記号の前に指定された属性は無効とされ、この記号とその後に書かれたものだけが生き残ります。

; マクロ定義
[macro name=foo]
[bar *]
[endmacro]

; マクロ呼び出し
[foo attr='ぴよ']

; [bar] には attr='ぴよ' が渡されます。
; マクロ定義
[macro name=foo]
[bar preaster='ほげ' * postaster='ふが']
[endmacro]

; マクロ呼び出し
[foo attr='ぴよ']

; [bar] には attr='ぴよ' と postaster='ふが' が渡されます。
; * の前に書かれた preaster='ほげ' は無効となります。

5.2. 属性値中の文字をエスケープ

属性値として指定された文字列中の文字は、バッククォート(` )でエスケープできます。つまり、バッククォートの次の文字(改行を除く)は、そのままの文字として解釈されるということです。文字としてのバッククォートを表現するときは、バッククォートを二つ続けて記述します。exp属性などで、シングルクォート/ダブルクォートが足りなくなった時に役に立つかもしれません。

[link exp="System.inform(`"バッククォート '``' でエスケープ`")"]サンプル[endlink]

5.3. レイヤ塗りつぶし(KAG版)

KAGで扱うレイヤを特定の色で塗りつぶします。機能的にはどうということはないですが、意外と使うことが多かったり多くなかったり。

予め、global.kag に以下のようなメソッドを持たせます。AfterInit.tjs辺りに記述すればよいでしょう。

/*
 * 指定レイヤをべた塗りします。
 * 子レイヤ(ラインレイヤ、リンクレイヤ等)には適用されません。
 * @param type 対象レイヤ。メッセージレイヤなら1、前景レイヤなら2、背景レイヤなら3を指定します。
 * @param page 表画面なら0、裏なら1を指定します。
 *      デフォルトは、メッセージレイヤの場合はカレント、それ以外では表画面です。
 * @param n レイヤ番号。背景レイヤの場合は無視されます。
 *      デフォルトは、メッセージレイヤの場合はカレント番号、前景レイヤの場合は0です。
 * @param value 色を指定します。デフォルトは黒です。
 * @return void
 */
global.kag.wipeLayer = function(type, page, n, value = 0xFF000000)
{
  if (page === void) { page = (type == 1) ? this.currentPage : 0; }
  if (n    === void) { n    = (type == 1) ? this.currentNum  : 0; }

  var target = (page == 0) ? this.fore : this.back;

  with (target) { target = [ null, .messages[n], .layers[n], .base ][(int)type]; }
  with (target) { .fillRect(0, 0, .imageWidth, .imageHeight, value); }
} incontextof global.kag;

あとはマクロにするとか。

; メッセージレイヤ
; ※子レイヤ(ラインレイヤ、リンクレイヤ等)には適用されません。
@macro name="wipe"
@eval exp="global.kag.wipeLayer(1, mp['page'], mp['n'], mp['color'])"
@endmacro

; 前景レイヤ
@macro name="wipechar"
@eval exp="global.kag.wipeLayer(2, mp['page'], mp['n'], mp['color'])"
@endmacro

; 背景レイヤ
@macro name="wipebase"
@eval exp="global.kag.wipeLayer(3, mp['page'], mp['n'], mp['color'])"
@endmacro