【kintoneカスタマイズ】ルーレットアプリを作ってみた

## はじめに

ただカスタマイズを行なってチーム内だけに共有だけでは"アウトプット"が足りない。
Hazime styleのように、今後はアウトプットを意識していきます。

## 何を作ったのか
今回、kintoneカスタマイズで「タスク決めルーレットアプリ」を作成しました。

作成の背景として、現在所属しているチームでのタスク決めは、この神アプリ"[webルーレット](https://jp.piliapp.com/random/wheel/)"を使って、ギャンブルチックに決定していました。
毎タスクでルーレットがハラハラドキドキを与えてくれます。我々のチームでは「今日もアレで決めますか！」となるほどルーレットに執着しています。

ある日のタスク決めの後に、
「いわくん、これkintoneでやってみてくれない？」

これがこのルーレットカスタマイズのきっかけでした。

と、話すと長くなるので、早速デモをお見せしましょう。

## デモ
![デモ動画](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/5453b925-55e1-c410-8217-43786036396e.gif)


## 開発に必要なもの
- kintone
  - ルーレットアプリ
- jsファイル
- cssファイル
- ライブラリのCDN URL(今回は Chart.js と Chart.jsのプラグイン)
  
## 解説
### kintone
#### アプリ構成
- Table1

  | フィールド名 | フィールド形式 | フィールドコード | 備考 |
  | :---: | :---: | :---: | :--- |
  | タイトル | 文字列(1行) | タイトル | ex. 次のいもキャンMCは君だぁぁぁぁ！！！！ |
  | - | スペース | circle | ルーレット表示用 |
  | - | スペース | start_button | スタートボタン配置用 |
  | お楽しみボタン | ラジオボタン | お楽しみボタン | 選択肢：オン/オフ <br> お楽しみ要素を使用するかしないか |
  | 筋肉ルーレット式 | ラジオボタン | 筋肉ルーレット | ルーレットが一度決定したかと思わせて、再度回転する |
  | ランダム比率 | ラジオボタン | ランダムレシオ | ルーレットの分割をランダムで決定する |
  | テーブル | テーブル | テーブル | (Table2 を参照) |
  | - | グループ | 結果 | (Table3 を参照) |

- Table2

  | フィールド名 | フィールド形式 | フィールドコード | 備考 |
  | :---: | :---: | :---: | :--- |
  | ルーレットに追加 | ラジオボタン | ラジオボタン_追加 | 選択肢：オン/オフ <br> ルーレットに反映させるかどうか |
  | 名前 | 文字列(1行) | 名前 | ルーレットに追加する名前 |

- Table3
  
  | フィールド名 | フィールド形式 | フィールドコード | 備考 |
  | :---: | :---: | :---: | :--- |
  | 決定者 | 文字列(1行) | 決定者 | ルーレット終了後のデータ保持用 |
  | 割合 | 文字列(1行) | 割合 | ルーレット終了後のデータ保持用 |
  | 回転数 | 文字列(1行) | 回転数 | ルーレット終了後のデータ保持用 |

#### js,cssファイルの配置
- (スペース名) > (アプリ名) > アプリの設定
- 設定タブの「JavaScript / CSS でカスタマイズ」
- 「PC用のJavaScriptファイル」に以下の順番で追加  
  1. https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.1.0/chartjs-plugin-datalabels.min.js
  3. roulette.js
- 「PC用のCSSファイル」にCSSファイルを追加
  1. roulette.css

![JavaScript / CSS でカスタマイズのキャプチャ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/6b10d84d-fd77-0164-c59f-c1d78fb5b3be.png)


### コード解説
今回のkintoneルーレットカスタマイズには、参考にしたルーレットがあります。(https://jp.piliapp.com/random/wheel/)

このルーレットを参考にkintoneで実現するポイントは下記の4つです。
- ルーレットに入れる名前
- ルーレットの表示
- ルーレットの回転
- ルーレットの判定

また、ギャンブラー達の遊び心をくすぐるお楽しみ要素も追加で解説します。
- 筋肉ルーレット式
- ランダム比率

それでは解説していきます。

#### 1. ルーレットに入れる名前
まず、ルーレットに追加するための文字列を保持する必要があります。今回は下記の理由からラジオボタンと文字列(1行)をテーブルに入れました。
- ルーレットに追加するかしないかを簡単に選択したい
- ルーレットに入れるのは人名だけでなく、タスク名、その他回したいものがある

今回のカスタマイズでは、ラジオボタンの状況を加味してテーブルにある名前を配列に代入します。(後述しますが、人名を配列にする必要があるためです。)

```roulette.js
const inputLables = [];

//テーブルの配列を取得
const tblDataArray = event.record.テーブル.value;  ///event: kintoneイベントによるeventオブジェクト
//ラジオボタンでフィールド変更不可, 名前の配列を取得
Object.keys(tblDataArray).forEach((key) => {
  if (tblDataArray[key].value.ラジオボタン_追加.value === '追加'){
    tblDataArray[key].value.名前.disabled = false;
    if (tblDataArray[key].value.名前.value) {
      inputLabels.push(tblDataArray[key].value.名前.value);   //名前を配列に追加
  } else {
    tblDataArray[key].value.名前.disabled = true;
  }
})
```

例：
```roulette.js
console.log(inputLabels)   ///['いわくん', 'いわちゃん', 'いわさん']
```
![テーブルに入力した際の写真](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/5316bd23-1802-875a-1180-4309dba1797e.png)

#### 2. ルーレットの表示
今回の一番重要な部分です。

ルーレットを眺めていて、
「円にデータ、しかもカラフル……円グラフじゃない？？」
と思いました。

検索：JavaScript 円グラフ　→ [Chart.js](https://www.chartjs.org/) がヒットしました！

そのため、ルーレットはChart.jsの円グラフを使うことにしました。

Chart.jsを使うのは初めてだったので下記メモとして載せておきます。
- Canvasタグを指定して描写する
- ラベル、背景色、円グラフの割合は配列で入れる

```roulette.js
//円グラフを作成する関数
const showChart = (element, labels, splitRatio, color) => {
  const myChart = new Chart(element, {
    type: 'pie',
    data: {
      labels: labels,   ///ラベル
      datasets: [{
        backgroundColor: color,   ///背景色
        data: splitRatio,   ///円グラフの割合
        borderColor: '#fff',
        borderWidth: 1
      }]
    },
    options: {
      plugins: {
        tooltip: {
          enabled: false
        },
        //凡例を消す
        legend: {
          labels: {
            fontColor: 'black'
          },
          display: false   ///falseで非表示、デフォでtrue
        },
        datalabels: {
          color: 'black',
          anchor: 'center',
          font: {
            weight: "bold",
            size: 35,
          },
          formatter: (value, element) => {
            let label = element.chart.data.labels[element.dataIndex];
            return label;
          },
        },
      },
      animation: false,   //グラフを表示させたときのアニメーションを消す
    },
    plugins: [
      ChartDataLabels,   //プラグインを使えるようにする
    ]
  });
};
```

例：
```roulette.js
kintone.events.on('app.record.create.show', (event) => {
  ///canvasタグを生成
  const circleElement = document.createElement('canvas');
  circleElement.id = 'roulette';

  ///関数の引数を定義（今回はデモ用に変数を定義）
  const inputLabels = ['いわくん', 'いわちゃん', 'いわさん'];
  const backgroundColor = ['red', 'orange', 'yellow'];
  const splitRatio = [33.3, 33.3, 33.3];

  ///関数の呼び出し
  showChart(circleElement, inputLabels, splitRatio, backgroundColor);

  ///kintoneへ表示
  kintone.app.record.getSpaceElement('circle').appendChild(circleElement);
})
```
結果
![kitnoneのスペースにルーレットを表示させたとき](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/9bcb44d1-e5d2-ec27-1a02-c5ac82c20a36.png)

#### 3. ルーレットの回転
ルーレットを書くことができました。
次に、回転についてです。

ルーレットの回転は、[Animationオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/Animation/Animation)を使用して、canvas要素を回転させることで実現しました。

```roulette.js
const startRotating = (initialDegree, finalDegree, duration) => {
  return new Promise((resolve) => {
    const keyFrames = new KeyframeEffect(
        document.getElementById('roulette'),   ///作成したルーレットのid
        [
        {transform: `rotate(${initialDegree}deg)`},   ///回転する前の角度（初期値）
        {transform: `rotate(${finalDegree}deg)`}   //回転した後の角度（終値）
        ],
        {
          duration: duration,   ///１つのアニメーションにかかる時間(ミリ秒)
          easing: 'ease-in-out',   ///始めゆっくりで、その後定回転で、終わりゆっくり
          iterations: 1,   ///指定した動作の繰り返し数 ⇄ infinite でずっと
          fill: 'forwards',   ///終わったときにどういう状態で止めるか　(forwardsで終わったときの状態で止める)
        }
      );
    const playAnimation = new Animation(keyFrames);
    playAnimation.play();
    playAnimation.onfinish = () => {resolve()};   ///アニメーションが終わったらresolveを返す
  })
};
```

KeyframeEffectオブジェクトを生成し、これをもとにAnimationオブジェクトを生成しています。  
そもそもキーフレームとはアニメーションの動きを作る機能で、KeyframeEffect()は、動作の中身を生成すること、と私は解釈しています。  
KeyframeEffectでは、  
`new KeyframeEffect(動かしたい要素, 動き, オプション)`  
のように指定して使用します。  
ここでのポイントとしては、下記の内容です。
- 回転角を引数として、ランダムに生成した回転角を代入している  
  -> 回転数や、止まる場所はルーレットという点から、ランダムである必要があるため
- アニメーションオブジェクトは `.play()`で再生しないと動作しない
- Promiseオブジェクトの中にAnimationを入れたこと
  -> 同期処理にしたかったため(筋肉ルーレット式で説明します)

例：
```roulette.js
///キリが良い回転角にならないように7倍をかけて、10回転 + 0~7回転のランダムな回転角を生成
const revolution = 3600 + 7 * Math.floor( Math.random() * 361);

///上で生成したランダムの回転数でルーレットを回転
startRotating(0, revolution, 8000);
```

![ルーレットの様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/51f7db81-52ff-5c67-badf-f762222cafbe.gif)


#### 4. ルーレットの判定
今回最も頭を使った部分です。

ルーレットでは、0度(時計でいう12時の部分)から0~360度でどのくらい回転したか、で判定しました。
実際に、円グラフが12時の部分から増えていくため、絶対にずれない初期値になるためです。

生成されるランダムな回転数は3600度以上なので、回転が止まった場所が初期値から0~360度でどれだけ回転したかを判定する計算を行います。  
これは、剰余を使うことで可能です。  
例：
```roulette.js
console.log(3750 % 360)   ///150
```

次に、この角度が、どの人の範囲なのか判定する必要があります。
これは、Array.reduce()を使用して、円グラフの割合と回転角の大きさを比べて計算します。
```roulette.js
//判定関数
const judgeSelectedLabel = (labelLength, revolution, splitRatioArray) => {
  const remainder = revolution % 360;
  let selectedLabel = 0;
  const splitDegreeArray = [];
  splitRatioArray.forEach((key) => {
      splitDegreeArray.push(key * 360 /100);
  });
  const reversedSplitDegree = splitDegreeArray.reverse();
  reversedSplitDegree.reduce((previousValue, currentValue, currentIndex) => {
      if (previousValue <= remainder && remainder < previousValue + currentValue) {
        selectedLabel = (labelLength - 1) - currentIndex;
      }
      return previousValue + currentValue;
  }, 0);
  return selectedLabel;
};
```

ここのポイントとしては、
- 時計周りの回転だとinputLabelsに入っている順番とは逆順で判定しないといけない
  -> Array.reverse()で逆順にした
- 角度と100分率をごちゃごちゃにしない
  -> グラフは100分率を想定しているが、回転角は360度であるため

例：
```roulette.js
const inputLabels = ['いわくん', 'いわちゃん', 'いわさん', 'いわきん'];
const labelLength = inputLabels.length;   ///今回は4
const revolution = 3601;
const splitRatio = [25, 25, 25, 25]

const result = judgeSelectedLabel(labelLength, revolution, splitRatio);
console.log(result);   ///3
console.log(inputLabels[result]);   ///いわきん
```

これで、ルーレットの基本的な動きはOKです！

#### お楽しみ要素（筋肉ルーレット式）
次は、ギャンブラー達の心をくすぐる機能、「筋肉ルーレット」について解説します。

筋肉ルーレットは、ルーレットが止まって決定したかと思わせて、再度ルーレットが回転する動作です。

キーフレームオブジェクトのオプションには、複数の[easing](https://developer.mozilla.org/ja/docs/Web/API/KeyframeEffect/KeyframeEffect)がありますが、筋肉ルーレットにあたるような動きはなかったため、
1. 通常通り回す
2. 止まった状態で少し焦らす
3. 再び回転を開始
4. 結果を出力
で実現しました。

ここで、先ほどのアニメーションをPromiseに入れたことが活きてきます。（Promiseが必要だと後からわかりました笑）
async/await で下記のようにアニメーションを繰り返します。

```roulette.js
  await startRotating(0, revolution/2, 4000);   /// (revolution÷2) まで4秒で回す
  await startRotating(revolution/2, revolution/2, 500);   ///止まった状態で0.5秒間停止
  await startRotating(revolution/2, revolution, 4000);   /// (revolution÷2) -> revolution まで4秒で回す
```

![筋肉ルーレット式](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2949201/7c68d82d-5b45-5b08-d2e6-1ff5e215f3b7.gif)


最終的にrevolutionの角度まで回ったアニメーションにしないと、見えている結果と判定結果が異なる場合があるため、トータルでrevolutionだけ回るように設定しました。

#### お楽しみ要素（ランダム比率）
次は、またもやギャンブラー達の心をくすぐる機能、「ランダム比率」について解説します。
この機能は、円グラフの割合をランダムで変えてしまい、当たる確率さえランダムにしてしまおうという悪魔的な機能です。

変化率は、人数で等分割した際の角度を基準に、±50％の範囲で変化します。下記にランダム比率の詳細を解説します。
- 360°を100%, N = 人数, (-0.5 <= σ <= 0.5) と仮定した際に、ルーレットを (100 / N) * N の等分　もしくは、 {(100 / N) * σ} * N で分割する
- [N-1]番目まで (-0.5 <= σ <= 0.5) の範囲で乱数を発生させ、(100 / N) * σ で割合を決定する
- N番目では、N番目までの割合の合計値を算出し、-0.5 * (100 / N) <= 100 - SUM(1 ~ (N-1)) <= 0.5 * (100 / N) に収まれば N番目の割合は　100 - SUM(1 ~ (N-1))　それ以外だった場合は、範囲に収まるまで繰り返し1番目から計算を繰り返す

関数の定義
```roulette.js
//円の分割の配列を等分割で出力する関数(ランダムレシオのラジオボタンがオフの時には等分割)
const normalSplitRatio = (labelLength) => {
  const ratioArray = [];
  for (let count = 0; count < labelLength; count++) {
      ratioArray.push(100 / labelLength);
  }
  return ratioArray;
};

//円の分割の配列をランダムで出力する関数(100/labelLength の値の ±50% の変動率)
const randomSplitRatio = (labelLength) => {
  const ratioArray = [];
  const calculateRatio = () => {
      for (let count = 0; count < labelLength - 1; count++) {
          const random = Math.floor(Math.random() * 101) -50;
          const variation = (100 + random) / 100;
          const ratio = Math.floor(100 * variation / labelLength);
          ratioArray.push(ratio);
      }
      const sum = ratioArray.reduce((previousValue, currentValue) => previousValue + currentValue, 0)
      ratioArray.push(100 - sum);
  }
  calculateRatio();
  while (ratioArray[labelLength - 1 ] < 0.5 * 100 / labelLength || 1.5 * 100 / labelLength < ratioArray[labelLength - 1]) {
      ratioArray.splice(0);
      calculateRatio();
  }
  return ratioArray;
};
```

例：
```roulette.js
const inputLabels = ['いわくん', 'いわちゃん', 'いわさん', 'いわきん'];

const splitRatio = randomSplitRatio(inputLabels.length);
console.log(splitRatio);   ///[24, 28, 29, 19]
```

## ソースコード
全ての解説はしませんでしたが、全体がどのような実装になっているのか気になる方は、[GitHub](https://github.com/s-iwakawa/roulette/tree/feature-funmode)で管理しているので、参考にしてみて下さい。

## 展望
- [ ] ルーレット回転音の追加
- [ ] タスク管理アプリへの反映
- [ ] 「ペリペリッ！！実はこっちでした」のアニメーション(こんなのあるのか…)
- [ ] プラグイン化

## 終わりに
長い記事になってしまいましたが、読んでいただきありがとうございます。

業務の効率化、自動化だけがカスタマイズではないのでは？と思っています。  

どうせタスクはやるんです。  

そのタスク決めが楽しみになる"ルーレットカスタマイズ"、ぜひチームでお試しいただければ嬉しいです。
やっていることは同じでも、雰囲気が楽しいだけでもいいチームワークが生まれるかもしれません。