2007/04/10(火)

文字描画レイヤ このエントリをはてなブックマークに追加 

Filed under: ゲーム制作,吉里吉里 — KAY @ 23:18:30

ようやくお披露目、前からちょくちょく話に出してた文字描画レイヤです。
KAGのMessageLayerは非常によく出来ているのですが、いくつか痒い所に手が届かない部分があったので、それなら自分で作ってしまえと。
……のはずだったんですが、シナリオ側の記述で結構フレキシブルに使えるんですよねMessageLayer。^^;
例えば、リファレンスを鵜呑みにして複数文字にルビを振ろうとすればこういう感じになるんですが……

[ruby text="ひ"]向[ruby text="まわ"]日[ruby text="り"]葵

実際はこんなやり方で均等にルビを配置する事が可能なわけで。
あと、KAGはベタテキストを1文字ずつに分解してchタグとして解釈するので、袋文字を利用している場合に罫線『──』のような、2文字以上がひと続きとなる文字が現れると、2文字目の縁取り部分が前の文字に重なってしまい、文字が途切れてしまうという問題があったんですが(Fateで顕著だった気がする)、これも[ch text="──"]と書いてやる事で簡単に回避できちゃうんですよねー。

というわけで、今回紹介する文字描画レイヤのアドバンテージは、FontクラスのプロパティやdrawText()の引数が全てシナリオファイル側の記述で制御できる、という点くらいに……。
このレイヤクラスは、chなどで文字が送られてきてもすぐに描画するわけではなく、一旦partsと呼ばれるバッファに溜め込み、描画したいタイミング(クリック待ちの直前など)でprint()メソッドを呼び出して一気に描画する、という方法を取っています。
エロゲをインストールしたら即効で文字速度を最速に設定してしまう人用。^^;
まあ、KAGParserがテキストを1文字ごとに分解して吐き出すので、タグハンドラの書き方次第で1文字ごとの描画も全然いけますが。

//文字描画レイヤ
class TextLayer extends Layer {
    var padding = 12;           //パディング
    var lineSpace = 2;          //行間
    var rubySize = 13;          //ルビのサイズ
    var partsNum = 0;           //次に描画するパーツ番号
    var overParts = [];         //ページ内に収まりきらなかったパーツ   

                                //行頭禁則文字
    var noLeading = "%),:;]}。」゙゚。,、.:;゛゜ヽヾゝゞ々’”)〕]}〉》」』】°′″℃¢%‰"
        "!.?、・ァィゥェォャュョッー・?!ーぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ";
                                //行末禁則文字
    var noFollowing = "\\\\$([{「‘“(〔[{〈《「『【¥$£";
                                //分離禁止文字
    var noSeparation = /─{2,}|…{2,}|‥{2,}|\\d{2,}|[0-9]{2,}|[〇一二三四五六七八九]{2,}/;   

    var parts = [];             //フォント設定ごとの文字パーツ
    var defaultParts = %[
        text:           "",                 //描画する文字
        ruby:           "",                 //ルビ
        x:              padding,            //キャレットx座標
        y:              padding + rubySize, //キャレットy座標
        width:          0,                  //文字幅
        lineHeight:     0,                  //行の高さ
        cr:             false,              //改行するかどうか   

        valign:         "bottom",           //位置揃えの方法
        hinv:           false,              //縦中横
        image:          null,               //インライン画像   

        //fontオブジェクトプロパティ
        angle:          0,
        bold:           false,
        face:           "MS P明朝",
        height:         30,
        italic:         false,
        strikeout:      false,
        underline:      false,   

        //drawTextオプション
        color:          0x000000,
        opacity:        255,
        aa:             true,
        sdwLevel:       0,
        sdwColor:       0x000000,
        sdwWidth:       0,
        sdwOffsetX:     0,
        sdwOffsetY:     0
    ];   

    //コンストラクタ
    //横幅、高さ、デフォルトのフォント設定
    function TextLayer(owner, parent, w, h, def = %[]) {
        super.Layer(owner, parent);
        setSize(w, h);
        (Dictionary.assign incontextof defaultParts)(def, false);
        resetParts();
        assignFont(0);
    }   

    //デストラクタ
    function finalize() {
        super.finalize();
    }   

    //現在のパーツに文字を追加していく。
    //戻り値:ページ内に入りきらなかったパーツがあるかどうか
    function pushText(ch, ww = true) {
        var p = parts[-1];
        var p2 = parts[-2];
        var chWidth = getTextWidth(p, ch);
        var chHeight = getTextHeight(p, ch);   

        if(!p.text.length) {
            if((chWidth + padding * 2 > width) || (chHeight + padding * 2 > height)) {
                throw new Exception("大きすぎてレイヤに描画出来ません。");
                return;
            }   

            //高さのチェック
            if(p.y + chHeight + padding > height) {
                p.text = ch;
                while(parts[-1] !== void && !parts[-1].cr) {
                    overParts.insert(0, %[]);
                    (Dictionary.assign incontextof overParts[0])(parts[-1]);
                    parts.erase(-1);
                }
                return 1;
            }
        }   

        //幅は大丈夫?
        if(p.x + p.width + chWidth + padding > width) {
            cr();
            return pushText((ww? wordWrap(ch): ch), false);
        }   

        p.text += ch;
        p.width += chWidth;
        p.lineHeight = chHeight if(!p.lineHeight);
        evenLineHeight(chHeight) if(chHeight > p.lineHeight);   

    }   

    //画像パーツを追加する
    function pushImage(img) {
        var p = parts[-1];
        var p2 = parts[-2];
        var imgWidth = *(&this.width incontextof img);
        var imgHeight = *(&this.height incontextof img);   

        if((imgWidth + padding * 2 > width) || (imgHeight + padding * 2 > height)) {
            throw new Exception("大きすぎてレイヤに描画出来ません。");
            return;
        }   

        if(p.y + imgHeight + padding > height) {
            p.image = img;
            while(parts[-1] !== void && !parts[-1].cr) {
                overParts.insert(0, %[]);
                (Dictionary.assign incontextof overParts[0])(parts[-1]);
                parts.erase(-1);
            }
            return 1;
        }   

        if(p.x + imgWidth + padding > width) {
            cr();
            return pushImage(img);
        }   

        p.image = img;
        p.width = imgWidth;
        p.lineHeight = imgHeight if(!p.lineHeight);
        evenLineHeight(imgHeight) if(imgHeight > p.lineHeight);
    }   

    //改行
    function cr() {
        partsAdd();
        if(parts[-2] === void) { return; }   

        var p = parts[-1];
        var p2 = parts[-2];   

        p2.cr = true;
        p.x = padding;
        p.y = p2.y + p2.lineHeight + lineSpace + rubySize;
        p.lineHeight = 0;
    }   

    //禁則処理
    //tail...送り込み先の文字
    function wordWrap(tail) {
        var p2 = parts[-2];
        var bk = tail;   

        while(
            noLeading.indexOf(tail.substring(0, 1)) != -1 ||
            noFollowing.indexOf(p2.text.reverse().substring(0, 1)) != -1 ||
            noSeparation.test(p2.text.reverse().substring(0, 1) + tail.substring(0, 1))
        )
        {
            //禁則処理に引っ掛かったので1文字送り込む
            if(!p2.text.length) {
                p2.text = tail.substring(0, tail.length - bk.length);
                tail = bk;
                break;
            }
            var last = p2.text.length - 1;
            tail = p2.text.substring(last) + tail;
            p2.text = p2.text.substring(0, last);
        }   

        return tail;
    }   

    //文字の描画
    function print() {
        var n = parts.count;
        for(var i = partsNum; i < n; i++) {
            var p = parts[i];   

            assignFont(i);   

            //位置揃え
            var textHeight = (p.image instanceof "Layer")? *(&this.height incontextof p.image): getTextHeight(p, p.text);
            var y = p.y + (p.hinv? textHeight: 0);
            switch(p.valign) {
                case "top":
                    break;
                case "center":
                    y += (p.lineHeight - textHeight) \\ 2;
                    break;
                case "bottom":
                default:
                    y += p.lineHeight - textHeight;
            }   

            if(p.image instanceof "Layer") {
                if(this instanceof "VerticalTextLayer") {
                    p.image.setPos(height - y - p.image.width, p.x);
                } else {
                    p.image.setPos(p.x, y);
                }
                p.image.visible = true;
            } else {
                drawText(p.x + adjustX(i), y + adjustY(i), p.text, p.color, p.opacity, p.aa,
                    p.sdwLevel, p.sdwColor, p.sdwWidth, p.sdwOffsetX, p.sdwOffsetY);
            }   

            //ルビの描画
            if(p.ruby.length) {
                font.angle = 0;
                font.height = rubySize;
                font.bold = font.italic = font.strikeout = font.underline = false;   

                var rubyWidth = font.getTextWidth(p.ruby);
                var rubyY = p.y - font.height;
                if(rubyWidth < p.width) {
                    var l = p.ruby.length;
                    var w = p.width / l;
                    for(var i = 0; i < l; i++) {
                        drawText(p.x + Math.round((w * i) + (w - font.getTextWidth(p.ruby[i])) / 2), rubyY, p.ruby[i], p.color, p.opacity, p.aa,
                            p.sdwLevel, p.sdwColor, p.sdwWidth, p.sdwOffsetX, p.sdwOffsetY);
                    }
                } else {
                    var x = p.x - (rubyWidth - p.width) \\ 2;
                    if(x < padding) {
                        x = padding;
                    } else if(x + rubyWidth + padding > width) {
                        x = width - padding - rubyWidth;
                    }
                    drawText(x, rubyY, p.ruby, p.color,
                        p.opacity, p.aa, p.sdwLevel, p.sdwColor, p.sdwWidth, p.sdwOffsetX, p.sdwOffsetY);
                }
            }
        }
        partsNum = n;
        partsAdd();
    }   

    //高さを揃える
    function evenLineHeight(height) {
        var i = -1;
        while(parts[i] !== void && !parts[i].cr) {
            parts[i--].lineHeight = height;
        }
    }   

    //ルビの分割
    //2分割までしか考慮しない。前のパーツにルビがあって、現在のパーツにはルビが無いという期待の元に実行される
    function splitRuby() {
        var p = parts[-1];
        var p2 = parts[-2];
        var length = Math.round(p2.ruby.length / (p2.text.length + p.text.length) * p2.text.length);
        p.ruby = p2.ruby.substring(length);
        p2.ruby = p2.ruby.substring(0, length);
    }   

    //パーツを追加する
    function partsAdd(replace = %[]) {
        var prev = parts[-1];   

        if(prev.text.length || (prev.image instanceof "Layer")) {
            //現在のパーツで、描画する文字があった場合のみ新規パーツを生成する
            parts.add(%[]);
            //とりあえず前の設定を引き継ぐ
            (Dictionary.assign incontextof parts[-1])(prev);
            //引き継げない分は初期化
            parts[-1].ruby = "";
            parts[-1].hinv = false;
            parts[-1].angle = 0;
            parts[-1].image = null;
        }   

        var p = parts[-1];
        delete replace.x;
        delete replace.y;
        delete replace.lineHeight;
        delete replace.width;
        (Dictionary.assign incontextof p)(replace, false);   

        p.x = prev.x + prev.width;
        p.y = prev.y;
        p.lineHeight = prev.lineHeight;
        p.width = 0;
        p.cr = false;
        p.text = "";        //引数でtextが指定してあっても無効   

        assignFont(-1);
    }   

    //初期化
    function resetParts() {
        partsNum = 0;
        fillRect(0, 0, width, height, 0);
        while(parts.count) {
            invalidate parts[0].image if(parts[0].image instanceof "Layer");
            parts.erase(0);
        }
        parts[0] = %[];
        (Dictionary.assign incontextof parts[0])(defaultParts);
    }   

    //idxで指定されたパーツの設定をfontオブジェクトに反映
    function assignFont(idx) {
        font.angle = parts[idx].angle;
        font.bold = parts[idx].bold;
        font.face = parts[idx].face;
        font.height = parts[idx].height;
        font.italic = parts[idx].italic;
        font.strikeout = parts[idx].strikeout;
        font.underline = parts[idx].underline;
    }   

    //拡張getTextWidth
    function getTextWidth(p, text) {
        if(p.hinv) {
            return font.getTextHeight(text);
        } else if(p.angle) {
            return Math.ceil(Math.abs(font.getEscWidthX(text)) + Math.abs(font.getEscHeightX(text)));
        } else {
            return font.getTextWidth(text);
        }
    }   

    //拡張getTextHeight
    function getTextHeight(p, text) {
        if(p.hinv) {
            return font.getTextWidth(text);
        } else if(p.angle) {
            return Math.ceil(Math.abs(font.getEscWidthY(text)) + Math.abs(font.getEscHeightY(text)));
        } else {
            return font.getTextHeight(text);
        }
    }   

    //X軸補正
    function adjustX(idx) {
        var p = parts[idx];
        if(p.angle > 900 && p.angle <= 1800) {
            return Math.ceil(Math.abs(font.getEscWidthX(p.text)));
        } else if(p.angle > 1800 && p.angle <= 2700) {
            return Math.ceil(Math.abs(font.getEscWidthX(p.text)) + Math.abs(font.getEscHeightX(p.text)));
        } else if(p.angle > 2700) {
            return Math.ceil(Math.abs(font.getEscHeightX(p.text)));
        }
        return 0;
    }   

    //Y軸補正
    function adjustY(idx) {
        var p = parts[idx];
        if(p.angle > 0 && p.angle <= 900) {
            return Math.ceil(Math.abs(font.getEscWidthY(p.text)));
        } else if(p.angle > 900 && p.angle <= 1800) {
            return Math.ceil(Math.abs(font.getEscWidthY(p.text)) + Math.abs(font.getEscHeightY(p.text)));
        } else if(p.angle > 1800 && p.angle <= 2700) {
            return Math.ceil(Math.abs(font.getEscHeightY(p.text)));
        }
        return 0;
    }
}   

//縦書き用文字描画レイヤ
class VerticalTextLayer extends TextLayer {
    var fontAngle;          //font.angleプロパティ
    var fontFace;           //font.faceプロパティ   

    //コンストラクタ
    function VerticalTextLayer(owner, parent, w, h, def = %[]) {
        super.TextLayer(...);   

        font.angle += 2700;
        fontAngle = &font.angle;
        &font.angle = &myFontAngle;   

        font.face = "@" + font.face;
        fontFace = &font.face;
        &font.face = &myFontFace;   

        font.getEscWidthX <-> font.getEscWidthY;
        font.getEscHeightX <-> font.getEscHeightY;   

        if(def.valign === void) {
            //縦書きの場合はcenterがデフォルト
            defaultParts.valign = parts[0].valign = "center";
        }
    }   

    //デストラクタ
    function finalize() {
        super.finalize();
    }   

    function drawText(x, y, *) {
        super.drawText(height - y, x, *);
    }   

    function fillRect(l, t, w, h, color) {
        super.fillRect(l, t, h, w, color);
    }   

    //assignFontオーバーライド
    function assignFont(idx) {
        super.assignFont(idx);   

        //縦中横
        if(parts[idx].hinv) {
            fontFace = font.face;
            fontAngle = font.angle;
        }
    }   

    property width {
        setter(v) {
            super.height = v;
        }
        getter {
            return super.height;
        }
    }   

    property height {
        setter(v) {
            super.width = v;
        }
        getter {
            return super.width;
        }
    }   

    property myFontAngle {
        setter(v) {
            fontAngle = v + 2700;   //3600以上を指定すると自動的に丸められるみたいです。
        }
        getter {
            return fontAngle + 900;
        }
    }   

    property myFontFace {
        setter(v) {
            fontFace = "@" + v;
        }
        getter {
            return fontFace.substring(1);
        }
    }
}

あらゆる面でKAGのMessgeLayerに劣ってる感が漂ってますが、一応、基本的な機能は一通り揃えたつもりです。
縦中横や縦方向の位置揃え(top, center, bottom)、禁則処理、インライン画像、ルビなどは全て実装してます。
バグとかは、一応、気が付いた部分は全部取れてると思うんですが……。このクラスに限らず、今までここで公開したスクリプトで、バグを見つけた人がいれば教えて欲しいなあとか思いつつ、それ以前にこんな辺境で吉里吉里系のコンテンツやってるという事を、ほとんどの人が知らないだろな。^^;

ちなみに、このレイヤクラスはKAGシステムとは一切互換性はありません。というか、KAYがここで公開してるスクリプトは基本的にKAGとの互換性は考慮してないです。
ただ、汎用的に使えるようには気を付けてるつもりなので、上手く使えばKAG上で運用する事も出来るかと。
まあそんな訳で、この文字描画レイヤを使うにはこれ専用のタグハンドラを書かなければいけないわけですが……一応、自前のシステム用のタグハンドラは既にあるんですが、これを公開したところでなあ……KAGのMainWindowクラスもConductorクラスも使ってないんで、タグハンドラ部分だけ見せてもあんまし意味ないかなと。
じゃあ自前のWindowクラスとかも公開しろよという話ですが、ここら辺はまだまだ書き掛けの部分なので。つか、Windowクラスなんかは多分一番最後までいじってると思う。

コメント(0)

コメントはまだありません。

トラックバック(0)

この記事へのトラックバックはありません。

この投稿へのコメントの RSS フィード。 TrackBack URL

コメントする