LoginSignup
9
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-10-26

はじめに

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

何を作ったのか

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

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

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

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

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

デモ

デモ動画

開発に必要なもの

  • kintone
    • ルーレットアプリ
  • jsファイル
  • cssファイル
  • ライブラリのCDN URL(今回は Chart.js と Chart.jsのプラグイン)

解説

kintone

アプリ構成

  • Table1

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

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

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

js,cssファイルの配置

JavaScript / CSS でカスタマイズのキャプチャ

コード解説

今回の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)   ///['いわくん', 'いわちゃん', 'いわさん']

テーブルに入力した際の写真

2. ルーレットの表示

今回の一番重要な部分です。

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

検索:JavaScript 円グラフ → Chart.js がヒットしました!

そのため、ルーレットは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のスペースにルーレットを表示させたとき

3. ルーレットの回転

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

ルーレットの回転は、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,   ///1つのアニメーションにかかる時間(ミリ秒)
          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);

ルーレットの様子

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がありますが、筋肉ルーレットにあたるような動きはなかったため、

  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秒で回す

筋肉ルーレット式

最終的に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で管理しているので、参考にしてみて下さい。

展望

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

終わりに

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

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

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

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

9
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2