はじめに
ただカスタマイズを行なってチーム内だけに共有だけでは"アウトプット"が足りない。
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 でカスタマイズ」
- 「PC用のJavaScriptファイル」に以下の順番で追加
- 「PC用のCSSファイル」にCSSファイルを追加
- roulette.css
コード解説
今回のkintoneルーレットカスタマイズには、参考にしたルーレットがあります。(https://jp.piliapp.com/random/wheel/)
このルーレットを参考にkintoneで実現するポイントは下記の4つです。
- ルーレットに入れる名前
- ルーレットの表示
- ルーレットの回転
- ルーレットの判定
また、ギャンブラー達の遊び心をくすぐるお楽しみ要素も追加で解説します。
- 筋肉ルーレット式
- ランダム比率
それでは解説していきます。
1. ルーレットに入れる名前
まず、ルーレットに追加するための文字列を保持する必要があります。今回は下記の理由からラジオボタンと文字列(1行)をテーブルに入れました。
- ルーレットに追加するかしないかを簡単に選択したい
- ルーレットに入れるのは人名だけでなく、タスク名、その他回したいものがある
今回のカスタマイズでは、ラジオボタンの状況を加味してテーブルにある名前を配列に代入します。(後述しますが、人名を配列にする必要があるためです。)
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;
}
})
例:
console.log(inputLabels) ///['いわくん', 'いわちゃん', 'いわさん']
2. ルーレットの表示
今回の一番重要な部分です。
ルーレットを眺めていて、
「円にデータ、しかもカラフル……円グラフじゃない??」
と思いました。
検索:JavaScript 円グラフ → Chart.js がヒットしました!
そのため、ルーレットはChart.jsの円グラフを使うことにしました。
Chart.jsを使うのは初めてだったので下記メモとして載せておきます。
- Canvasタグを指定して描写する
- ラベル、背景色、円グラフの割合は配列で入れる
//円グラフを作成する関数
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, //プラグインを使えるようにする
]
});
};
例:
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);
})
3. ルーレットの回転
ルーレットを書くことができました。
次に、回転についてです。
ルーレットの回転は、Animationオブジェクトを使用して、canvas要素を回転させることで実現しました。
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を入れたこと
-> 同期処理にしたかったため(筋肉ルーレット式で説明します)
例:
///キリが良い回転角にならないように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度でどれだけ回転したかを判定する計算を行います。
これは、剰余を使うことで可能です。
例:
console.log(3750 % 360) ///150
次に、この角度が、どの人の範囲なのか判定する必要があります。
これは、Array.reduce()を使用して、円グラフの割合と回転角の大きさを比べて計算します。
//判定関数
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度であるため
例:
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がありますが、筋肉ルーレットにあたるような動きはなかったため、
- 通常通り回す
- 止まった状態で少し焦らす
- 再び回転を開始
- 結果を出力
で実現しました。
ここで、先ほどのアニメーションをPromiseに入れたことが活きてきます。(Promiseが必要だと後からわかりました笑)
async/await で下記のようにアニメーションを繰り返します。
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番目から計算を繰り返す
関数の定義
//円の分割の配列を等分割で出力する関数(ランダムレシオのラジオボタンがオフの時には等分割)
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;
};
例:
const inputLabels = ['いわくん', 'いわちゃん', 'いわさん', 'いわきん'];
const splitRatio = randomSplitRatio(inputLabels.length);
console.log(splitRatio); ///[24, 28, 29, 19]
ソースコード
全ての解説はしませんでしたが、全体がどのような実装になっているのか気になる方は、GitHubで管理しているので、参考にしてみて下さい。
展望
- ルーレット回転音の追加
- タスク管理アプリへの反映
- 「ペリペリッ!!実はこっちでした」のアニメーション(こんなのあるのか…)
- プラグイン化
終わりに
長い記事になってしまいましたが、読んでいただきありがとうございます。
業務の効率化、自動化だけがカスタマイズではないのでは?と思っています。
どうせタスクはやるんです。
そのタスク決めが楽しみになる"ルーレットカスタマイズ"、ぜひチームでお試しいただければ嬉しいです。
やっていることは同じでも、雰囲気が楽しいだけでもいいチームワークが生まれるかもしれません。