Home

電脳麻将UI 〜 面子

様々な面子

電脳麻将 では牌を画像として表示していますが、副露メンツ内の横向きとなっている牌はどのように実現しているのでしょうか*1

例えば m3-m1m2 の場合、HTMLは以下となっています。


<span class="mianzi">
    <span class="rotate">
        <img class="pai" data-pai="m3" src="img/m3.png">
    </span>
    <img class="pai" data-pai="m1" src="img/m1.png">
    <img class="pai" data-pai="m2" src="img/m2.png">
</span>

メンツ全体を class="mianzi" の要素で囲い、その中に表示順序にしたがい牌の画像を並べます。 ただし、横向きにする牌は class="rotate" の要素で囲んでいます。

これを以下のようなCSSで必要な部分を回転させます。

.mianzi .pai {
    display: inline-block;
    height: 48px;
    width:  36px;
}
.mianzi .rotate {
    display: inline-block;
    white-space: nowrap;
    text-align: left;
    width: 48px;
    transform-origin: 0% 0%;
    transform: rotate(270deg) translate(- 48px, 0px);
}

回転させる部分の幅を牌の高さ(width: 48px)に合わせ、起点を左上(transform-origin: 0 0)として反時計回りに90°回転(transform: rotate(270deg))させて、回転後の座標軸で左へ牌の高さ(transform: translate(- 48px, 0px))だけ移動(つまり下に移動)させています。

電脳麻将では、StylusMixin を使用してCSSを部品化しています。 面子に関しては下記の部品があります。

pai-width($height)
    $height / 4 * 3

pai-size($height)
    display: inline-block
    height: $height
    width: pai-width($height)

mianzi-size($height)
    display: inline-block
    .pai
        pai-size: $height
    .rotate
        display: inline-block
        white-space: nowrap
        text-align: left
        width: $height
        transform-origin: 0% 0%
        transform: rotate(270deg) translate(- $height, 0px)
pai-width
$height に対する牌の幅を返す関数。 牌画像を変更したときはここでアスペクト比を調整することを想定している。
pai-size
$height で指定された高さで牌に関するCSSをまとめて設定するMixin。
mianzi-size
$height で指定された高さで面子に関するCSSをまとめて設定するMixin。

MixinはあたかもCSSのプロパティのように動作します。 mianzi-size: 48px とすると先に挙げたCSSがすべて設定されます。

面子を表すDOMノード群を生成するのが @kobalab/majiang-ui の内部関数 mianzi です。

/*
 *  Majiang.UI.mianzi
 */
"use strict";

const $ = require('jquery');
const label = require('./label')('mianzi');

module.exports = function(pai) {

    function get_label(p) {
        return pai(p).attr('alt')
                    ? [ 'alt',        pai(p).attr('alt')        ]
                    : [ 'aria-label', pai(p).attr('aria-label') ];
    }

    return function(m) {
        let mianzi = $('<span>').addClass('mianzi');
        let s = m[0];
        if (m.replace(/0/g,'5').match(/^[mpsz](\d)\1\1\1$/)) {
                                                        // 暗槓
            let nn = m.match(/\d/g);
            mianzi.attr('aria-label', label.angang)
                  .append(pai('_').attr(...get_label(s+nn[0])))
                  .append(pai(s+nn[2]))
                  .append(pai(s+nn[3]))
                  .append(pai('_').attr(...get_label(s+nn[1])));
        }
        else if (m.replace(/0/g,'5').match(/^[mpsz](\d)\1\1/)) {
                                                        // 刻子・大明槓・加槓
            let gang  = m.match(/[\+\=\-]\d$/);
            let d     = m.match(/[\+\=\-]/);
            let nn    = m.match(/\d/g);
            let pai_s = pai(s+nn[0]);
            let pai_e = (! gang && nn.length == 4)
                                         ? nn.slice(1, 3).map(n=>pai(s+n))
                                         : nn.slice(1, 2).map(n=>pai(s+n));
            let pai_r = $('<span>').addClass('rotate')
                            .append(gang ? nn.slice(-2).map(n=>pai(s+n))
                                         : nn.slice(-1).map(n=>pai(s+n)));
            mianzi.attr('aria-label', label[d]
                            + (  gang           ? label.gang
                               : nn.length == 4 ? label.minggang
                               :                  label.peng ));
            if (d == '+') mianzi.append(pai_s).append(pai_e).append(pai_r);
            if (d == '=') mianzi.append(pai_s).append(pai_r).append(pai_e);
            if (d == '-') mianzi.append(pai_r).append(pai_s).append(pai_e);
        }
        else {                                          // 順子
            let nn = m.match(/\d(?=\-)/).concat(m.match(/\d(?!\-)/g));
            mianzi.attr('aria-label', label.chi)
                  .append($('<span>').addClass('rotate').append(pai(s+nn[0])))
                  .append(pai(s+nn[1]))
                  .append(pai(s+nn[2]));
        }
        return mianzi;
    }
}

mianzi() は関数を返す関数として実装しています。 電脳麻将UI 〜 牌 で紹介した「牌を生成する関数」を引数に mianzi() を呼び出すと「面子を生成する関数」を返します。

返された関数は、引数で指定された 面子 を表すDOMノードを以下の要領で生成しています。

暗槓の場合
裏向きの牌2枚にカンした牌の2枚を挟みます。 このとき赤牌をできるだけ見せるようにします。 p5550_p5p0_ と表示します。
刻子・大明槓・加槓の場合
刻子と大明槓の場合は、鳴いた牌を適切な位置で回転させて表示します。 s222+s2s2s2-m8888-m8-m8m8m8 とします。 加槓の場合はポンした牌とカンした牌を並べて回転させて表示します。 s550=5s5s5=s0-s5 となります。
順子の場合
鳴いた牌を右に移動して回転させます。 m12-3m2-m1m3 とします。

以下で実際の表示を確認できます。

  1. ^ 実は11年前の開発最初期にも 説明 したことがあります