HTML+JavaScript(Matter.js)で簡易ガラポン

弁護士の笠間です。所属している団体では毎週抽選を行っているのですが、ランダムな抽選結果だけを表示するちょっと地味なものだったので、動きのある抽選システムを作ってみました。 
![shot1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192970/5f77357b-4dad-cc75-60ef-38f781840ec4.png)
![shot2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192970/60367b53-6aa4-13c1-88f4-34168fb15236.png)
![shot3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192970/182d54f7-08f6-32fa-ed44-b104247cc1ed.png)
画像のとおり、右側の`textarea`に`名前,票数`のCSVな文字列を入力して「抽選開始」をクリックすると、票数に応じたボールが上から降ってきます。 
降ってきたボールは物理モデリングで跳ね回って、窪みに溜まる。
で、好きなボール（通常は凹の底を想定）をクリックすると、そのボールが誰のボールかが表示される。 

物理モデリングには [Matter.js](https://brm.io/matter-js/)を使っています。 
# シャッフルされた配列を作る 
```javascript:配列作り
// CSV風の文字列を行ごとに分割。
var lines = document.getElementById("list").value.split("\n");
var names = [];
for(var i in lines){ //行=人に対応することから、
    //その人の色を設定
    var color = [
        "hsl(", //色を考えるのがめんどくさいので、HSL色空間で色相を一定に回す。
        (360/lines.length * i).toString(), //色相。360度を人数=lines.lengthで割る。
        ",100%", //彩度
        ",50%)" //輝度
    ].join(""); 
    // => "hsl(90,100%,50%)"みたいな文字列ができる。

    //textareaの値から、人数分の {name, color} を作る。
    var name = lines[i].split(",")[0]; 
    var num = lines[i].split(",")[1]; //numは票数
    for(j=0; j < num; j++){
        names.push({"name":name, "color":color}); //票数だけ、namesに{name, color}が入る。
    }
}
// できた[{name, color},...,{name, color}]をシャッフルする。
names = names.sort(()=>Math.random()-0.5);
```
まぁこの辺は、文字列から配列を生成しているだけですが。
配列内の各オブジェクトが、同じ名前なら同じ色を持っている、という仕組みです。今思うと色はnameをkeyとする辞書にすればよかったですね。

# 物理モデリングのあれこれ 
## 枠組みとかを作る。 
```javascript:Matter.jsをあらかじめ読み込んだ上で… 
//  HTMLにはこれ=> <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
//------
function main(){ 
    //お手本からコピペ
    const Events = Matter.Events; //ボールをクリックする部分を作るため。
    const engine = Matter.Engine.create();
    const runner = Matter.Runner.create();
    Matter.Runner.run(runner, engine);
    
    const canvas = document.getElementById("canvas");
    const render = Matter.Render.create({
        canvas, engine,
        options:{
            width: canvas.width,
            height:canvas.height,
            wireframes: false,
            background: "rgba(255,255,255,0)",
//          background: "transparent", //背景透過にすればcanvasにCSS設定でいい感じにできるかなという邪念(うまく動かず)
//          wireframeBackground  : "transparent", // 透過させるには背景だけでなくwireframeBackgroundも要設定らしい。
        }
    });
    Matter.Render.run(render);
    
    // ボールが動く外枠作り。外枠は重力で動かないので、 {isStatic: true} がポイント。
    const ground = Matter.Bodies.rectangle(canvas.width/2, canvas.height, canvas.width*1, 10, {isStatic: true});
    const top = Matter.Bodies.rectangle(canvas.width/2, 0, canvas.width*1, 10, {isStatic: true});
    const frameL = Matter.Bodies.rectangle( 0, canvas.height/2, 10, canvas.height, {isStatic:true});
    const frameR = Matter.Bodies.rectangle(canvas.width, canvas.height/2, 10, canvas.height, {isStatic:true});
    const slopeL = Matter.Bodies.rectangle(canvas.width/5, canvas.height/3*2, canvas.width*0.7, 10, {angle: Math.PI/5, isStatic:true});
    const slopeR = Matter.Bodies.rectangle(canvas.width/5*4, canvas.height/3*2, canvas.width*0.7, 10, {angle: -Math.PI/5, isStatic:true});
    // ど真ん中に▲を置いて、まっすぐ下に落ちないようにする。
    const jama = Matter.Bodies.polygon(canvas.width/2, canvas.height/2, 3, 30, {angle: Math.PI+Math.random(), isStatic:true});
    Matter.Composite.add(engine.world, [ground, top, frameL, frameR, slopeL, slopeR, jama]);
    //なんか関数上手く使えばもうちょい綺麗なコードになる気がする…。
// コードは続く……
```
`main()`の中で、Matter.jsの初期化を行います。`main()`は、製作中はwindowの`DOMContentLoaded`時に呼び出されますが、本番はHTML内のボタンクリックで発火させます。 
ここの固定の外枠作りがわくわく感 ~~（枠だけに）~~ のメインになるはずですが、ピンボールのように上手く作るのは後回し。


## ボールを横位置ランダムに発生させる 
```javascript:main()の中身の続き
   // ballの生成
    var balls = [];
    for(var i in names){ //先ほど作った[{name, color}]配列の一つ一つに。
        var name = names[i];
        var ball = Matter.Bodies.circle(
            BALL_SIZE+Math.random()*(canvas.width-BALL_SIZE), //X座標は、左右にボール１個分残してランダム。重なっても良いみたいだ。
            BALL_SIZE+3, // Y座標は上端からちょっとだけ下。
            BALL_SIZE, // これがボールの大きさ。そういえば、BALL_SIZEはスクリプトの上の方で指定してある。
            { // その他のオプション
                label: name.name, // {name,color}をvar nameと名付けたのは間違いだった感。ラベルはマウスイベントで呼べる。
                restitution:1.1, //弾力
                frictionStatic: 1, //動摩擦らしい。どのくらい効果あるのかわからないけど…。
                render: { fillStyle: name.color } //{name, color}で持ってきた色文字列。
            }
        );
        balls.push(ball); 
    }
    // 画面にballを追加           
    Matter.Composite.add(engine.world, balls );
```
だいたいコメントに書いたとおり。無計画なコーディングの成れの果て。 

## マウスクリックを拾うようにする。 
```javascript:main()の中身の続き
    // マウスイベント。コピペなので正直なにもわかってない。
    var mouseConstraint = Matter.MouseConstraint.create(engine,{
        element: canvas,
        constraint: {
            render: {
                visible: false
            },
            stiffness: 0.8
        }
    });
    Matter.World.add(engine.world, mouseConstraint);
    // mousedownに対してイベントリスナーを設定。
    Matter.Events.on(mouseConstraint, "mousedown", function(event){
        alert(mouseConstraint.body.label);
    }) //ただ、これだとalertが出た後はずっとマウスカーソルにボールがくっついてきてしまう。まぁ、抽選して終わりだし！
} //main()の終わり
```
…記事を書くために、コピペしてからコメントもりもりしたけど、元のソースにコメント入れればよかったのでは。 

# 参考文献 
https://mathogen.com/2020/10/24/tracking-mouseup-and-mousedown-in-matter-js/