🎰

男塾名物・戦吉兆占針盤のWebアプリを作った

2023/12/23に公開

ゆるWeb勉強会@札幌 Advent Calendar 2023 23日目の記事です。

前置き

ハンバーガーメニュー ケチャップ抜きで。DE-TEIUです。

成果物

名作漫画「魁!!男塾」に登場する戦吉兆占針盤のWebアプリを作りました。
100マス中99マスが「勝利」になっているルーレットです。
ほぼ「勝利」の目しか出ないため、戦の前の景気付けなどに使えます。

Web戦吉兆占針盤

解説(超ざっくり)

以下、実装の解説など

Fabric.jsを導入

Fabric.jsとは、HTML5のcanvas要素の機能を拡張するライブラリです。
キャンバスに追加したオブジェクトを動かしたりできます。

インストール方法(npmを使う場合)

npm install fabric

TypeScriptでFabric.jsの型定義を使いたい場合は、以下も一緒にインストールしておきましょう。

npm install --save-dev @types/fabric

呼ぶ時はJSのコード内でこんな風にインポートできます。

  import { fabric } from 'fabric';

canvasにルーレット盤を描画

ルーレット盤(文字を除く土台)の描画については、JavaScript標準のCanvas APIでやっています。
実装イメージは以下。

  const CANVAS_WIDTH = 480;  //キャンバスの幅
  const CANVAS_HEIGHT = 480;  //キャンバスの高さ
  const CENTER = {  //キャンバスの中央の座標
    X: CANVAS_WIDTH / 2,
    Y: CANVAS_HEIGHT / 2
  };
  const RESULT_COUNT = 100;  //ルーレットのマス目の数
  const RESULT_ANGLE = 360 / RESULT_COUNT;  //ルーレット1マス分の円弧の角度

  const drawRouletteBoard = () => {
    //※HTML内に<canvas id="canvas"></canvas>の要素があるものとする
    const rouletteBoardCanvas = document.getElementById("canvas");
    const context = rouletteBoardCanvas.getContext('2d')!;

    [...Array(RESULT_COUNT)].map((_, i) => {  //マス目の数分ループ
      context.beginPath();  //図形を描画する領域(パス)の生成を開始
      context.moveTo(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);  //パスをキャンバスの中央に移動
      context.fillStyle = i % 2 === 0 ? '#666666' : '#999999';  //パス内を塗りつぶす色を指定
      const startAngle = (RESULT_ANGLE * (i + 1) * Math.PI) / 180;  //マスの描画開始角度
      const endAngle = (RESULT_ANGLE * i * Math.PI) / 180;  //マスの描画終了角度
      context.arc(CENTER.X, CENTER.Y, CANVAS_WIDTH / 2, startAngle, endAngle, true); //孤を描画
      context.fill(); //領域を塗りつぶす
    });
  };

針を回す

針は描画後に動かすので、fabric.jsを使って動的なオブジェクトとしてキャンバスに追加する。

  const drawArrow = () => {
    fabric.Image.fromURL('/arrow.png', (img) => { //針の画像を読み込んでfabric用のImageオブジェクトを生成
      img.selectable = false;
      img.originX = 'left';
      img.originY = 'center';
      img.left = CENTER.X;
      img.top = CENTER.Y;
      img.scale(0.5);
      img.centeredRotation = false;
      arrowImage = img;
      fabricCanvas.add(arrowImage);
      drawHead(); // ルーレット中央の黄色い円を描画
    });
  };

針を動かしてみる。

const startRotate = () => {
    if (isStarted) {
      return;
    }
    resultText = '';
    isStarted = true;
    acceleration = 0;
    const MAX_SPEED = 17;
    speed = MAX_SPEED;
    move();
  };

  const move = () => {
    const MIN_SPEED = 0.1;
    speed += acceleration; //針の回転速度UP
    arrowImage.rotate(arrowImage.angle! + speed); //針を回転させる
    fabricCanvas.renderAll(); //fabricのキャンバスを再描画

    if (speed <= MIN_SPEED) { //針の速度が一定以下になったら停止して勝利判定
      speed = 0;
      isStarted = false;
      if (isWin()) {
        resultText = '勝利';
        // party.jsで紙吹雪をまく
        party.confetti(document.body, {
          count: party.variation.range(80, 100),
          spread: 40,
          speed: party.variation.range(50, 600),
          size: party.variation.skew(1, 0.8),
          rotation: () => party.random.randomUnitVector().scale(180)
        });
      } else {
        resultText = '死';
      }
      return;
    }

    fabric.util.requestAnimFrame(move); //キャンバスが再描画できる状態になったらmoveメソッドを再実行
  };

ソースコード

GitHubに置いてあります。

参考資料

Discussion