WEBアプリの作り方 ~ルーレット~
WEBブラウザの進化により、ブラウザ上で動かせるアプリが増えてきました。ここでは、そのようなWEBアプリはどうやって作ればいいのか?について解説していきます。どんなものが作れる?完成したものはルーレット WEBツールにて公開しています。最新
2020/1/162020/1/19サービス, サイト
ここからはapp.js内に記述していきます。
ルーレットはどのような流れで動作するのでしょうか。考えてみると、
- スタートボタンが押されるまで待機
- スタートボタンが押されて加速
- 最大速度となり定速に
- ストップボタンが押されて減速
- 停止・結果表示 となります。ストップボタンは加速中でも押せるようにしてもいいかもしれません。 以上5つの状態を列挙型(enum)で定義しておきます。また、現在の状態を保持する変数も設けます。状態の初期値はwaitingとしておきます。
var Mode = { waiting: 0, acceleration: 1, constant: 2, deceleration: 3, result: 4 }; var mode = Mode.waiting; ルーレットを回している最中に、ルーレットの設定が変更されるかもしれません。それに備えて、フォームの内容を変数に保存しておきます。 1つの項目につき
- 項目名
- 確率
- 色
を持っておけばいいので、それぞれを保持する空の配列を作成します。
var nameList = ; var probabilityList = ; var colorList = ; とりあえず必要になりそうな変数はこのようになります。 それでは、ロジックを書いていきましょう。
基本設定
まず、描画領域(canvas)のサイズを設定します。テンプレートの中に createCanvas(600,300) という記述がありますが、少し高さが足りないので、300を400に変更します。
テンプレート内のdraw関数は、毎フレーム呼ばれます。以下、この中に描画処理を書いていきます。
毎フレーム、背景を白色にしたいので、fillで塗りつぶし色を指定して、rectで実際に描画します。width(幅)やheight(高さ)変数には、自動的にcanvasのサイズが入ります。今回はwidthには600、heightには400が自動的に代入されます。
次にルーレットの中心を設定します。今回は単純にcanvasの中心とするのでx座標はwidth/2、y座標はheight/2だけ移動します。
function draw(){ fill(255,255,255); rect(0,0,width,height); translate(width/2, height/2); //… } このコード以降、原点が(width/2, height/2)に移動します。つまり、例えば(10, 20)に点を打つというコードを書くと、自動的に(width/2 + 10, height/2 + 20)という座標に点が打たれるということです。
フォームデータの取得
先ほど用意した、フォームデータ保存用配列に対して代入処理を行っていきます。
その前に、入力された値が妥当かどうか検証するvalidation関数を作っておきましょう。 検証内容としては、「項目名が空欄ではない」、「割合が0より大きい」の2つとします。
function validation(){ var badflag = false; $(’.name’).each(function(){ if($(this).val()==""){ badflag = true; } }); $(’.ratio’).each(function(){ if(!($(this).val()>0)){ badflag = true; } }); if(badflag){ alert(‘項目名と割合を正しく設定してください。’); return 1; } return 0; } validationが書けたので、次は実際にフォーム内容を取得していきます。 まず、各項目の確率計算はフォーム作成の部分で既に作成しています。 同じような処理なので説明は割愛します。 それに加えて、項目名も同様に取得していきます。
function dataFetch(){ var ratioSum = 0.0; $(’.item’).each(function(){ var ratio = $(this).find(’.ratio’).val()-0; ratioSum += ratio; }); nameList = ; probabilityList = ; $(’.item’).each(function(){ var name = $(this).find(’.name’).val(); var ratio = $(this).find(’.ratio’).val()-0; nameList.push(name); probabilityList.push(ratio/ratioSum); }); //… } 難しいのはここからです。各項目に対してルーレットで割り当てられる色を設定するのですが、見やすくするにはどのような配色を行えば良いか考えてみます。
基本的に人間の目は、同じような色が隣り合うと見にくいと思います。そのため、できるだけ色相環で遠い色が隣に来るようにしていきます。つまり補色に近い色が隣にあると見やすいというわけです。 色相を扱うためRGBではなく、ここではHSL色空間を使用して、Hue(色相)、Saturation(彩度)、Lightness(輝度)をうまく設定していくこととします。
彩度と輝度はとりあえず置いておいて、色相の値を並べていきます。 方針としては、0~255の色相を項目数でn等分します。そして、その中から 0番目、n/2番目、1番目、n/2+1番目、… と順番に値を取得し、並べ替えていきます。
n=6の場合、
0: 0
1: 127
2: 42
3: 170
4: 85
5: 212
という値となります。各項目の色相がうまく離れていることがわかります。
nが奇数の場合もうまい具合にプログラムしておきます。 COLOR_ADJという定数も追加します。
//datafetch関数の外 const COLOR_ADJ = 0.4; //datafetch関数の中 //… var colors = ; len = nameList.length; for(var i=0;i<len;i++){ colors.push(Math.floor(255/len*i)); } colorList = ; if(len%2==0){ for(var i=0;i<len;i+=2){ colorListi = colorsMath.floor(i/2); } for(var i=1;i<len;i+=2){ colorListi = colorsMath.floor(i/2 + len/2); } }else{ for(var i=0;i<len;i+=2){ colorListi = colorsMath.floor(i/2); } for(var i=1;i<len;i+=2){ colorListi = colorsMath.floor(i/2)+Math.floor(len/2)+1; } } cssColorSet();//後述 ここで、フォームの色表示部分に色を反映させたいと思います。 色の計算式を決めないといけないので、以下のように決めました。
H=上記の色相S=255-とある定数*上記の色相L=128
Lは128で純色となります。Sは255で純色、0で灰色になります。Sをなぜこのような式にしたかについては、やってみるとわかりますが、赤色と紫色の区別がつきにくかったためです。というのも、色相が255までいくと、色相環を一周してしまい、ほぼ同じ色となってしまいます。そこで、このように設定して差をつけました。
ここまで設計した上で、実際にCSSに色を適用していきます。CSSに対しては、なじみ深い(?)RGBで設定していきます。
function cssColorSet(){ var counter = 0; $(’.color-indicator’).each(function(){ push(); colorMode(HSL, 255); var c = color(colorListcounter,255-COLOR_ADJ*colorListcounter,128); pop(); $(this).css(‘background-color’, “rgb("+c._getRed()+”,"+c._getGreen()+","+c._getBlue()+")"); counter++; }); } cssColorSet関数を呼び出すと、色の配列から自動的にフォーム内の色表示部分に色が表示されるようになりました。
ルーレットの描画
求めた確率に従って、各項目の面積割合を設定していきます。 全項目合わせて1周(2π)するようにします。 arc関数で円弧を描画できます。 始点・終点の角度をうまく計算してやる必要があります。 n番目の円弧を描くときは、0~n-1番目の円弧の角度の和を始点として、2π*(n番目の項目の当選確率)だけ進んだところを終点とすれば良いでしょう。 半径は定数で設定しておきます。 ここで、push関数、pop関数を使用しています。push関数で現在の描画に関する情報をいろいろ保存してくれます。pop関数を呼び出すと、それらを復元してくれます。カラーモード(colorMode)だったり、塗りつぶし色(fill)だったりを保存・復元するために使用しています。
const RADIUS = 100; function drawRoulette(){ var angleSum = 0.0; push(); colorMode(HSL, 255); for(var i=0;i<len;i++){ fill(colorListi,255-COLOR_ADJ*colorListi,128); arc(0,0,RADIUS*2,RADIUS*2,angleSum,angleSum+2*PI*probabilityListi); angleSum += probabilityListi*2*PI; } pop(); } ルーレットの回転はここでは考えていません。以下の場合分けの時に座標を丸ごと回転させた後にルーレットを描画することで、ルーレットが回転しているように見せるようにします。
状態での場合分け
switch文により、現在の状態で場合分けを行います。
function draw(){ fill(255,255,255); rect(0,0,width,height); translate(width/2, height/2); switch(mode){ case Mode.waiting: break; case Mode.acceleration: break; case Mode.constant: break; case Mode.deceleration: break; case Mode.result: break; } } それぞれどのような処理を行えばいいか考えていきます。
・waiting(待ち状態) ルーレットを回転させずそのまま描画
・acceleration(加速中) ルーレットを、ある加速度で加速させる ルーレットを描画 ある速さ以上になったら、定速状態へ移行
・constant(定速) 毎フレーム同じだけルーレットの角度が増加する ルーレットを描画
・deceleration(減速) ルーレットを、ある加速度で減速させる ルーレットを描画 速さが0になったら結果表示状態へ移行
・result(結果) ルーレットを停止 ルーレットを描画 結果を取得してHTML側に表示させる
どの状態でもルーレットを描画するのは変わらないので、ルーレット描画関数はswitch文の外に出しても良いことになります。mode=Mode.resultの場合の記述の後にdrawRoulette関数を置くことにしましょう。
次に、必要になる定数を指定したり、現在の様々な状態を持っておく変数を用意しておきます。
//draw関数外 const ACCEL = 0.01; //加速時の加速度 const DECEL = 0.01; //減速時の加速度 const MAX_SPEED = 1.0; //最大速度 const DECEL_RAND_LEVEL = 10; //減速の乱数の幅を設定 const DECEL_RAND_MAGNITUDE = 0.001; //減速の乱数の影響力を設定 var speed = 0.0; var theta = 0.0; var len = 0; var resultDisplayed = false; それでは実装していきます。
・mode = Mode.accelerationのとき
rotate関数により、ルーレットをthetaradianだけ回転させます。 物理的な等角加速度運動を考えて、speedとthetaを変化させていきます。 thetaが2πを超えたら、0<=theta<2π となるようにthetaの値をいじります。
case Mode.acceleration: if(speed<MAX_SPEED){ speed += ACCEL; }else{ mode = Mode.constant; speed = MAX_SPEED; } theta += speed; theta-=(Math.floor(theta/2/PI))*2*PI; rotate(theta); break; ・mode = Mode.constantのとき
speedは一定です。
case Mode.constant: theta += speed; theta-=(Math.floor(theta/2/PI))*2*PI; rotate(theta); break; ・mode = Mode.decelerationのとき
accelerationの逆です。 ただし、ここで一工夫できます。毎回同じ加速度で減速すると、スタートボタンとストップボタンを同じように押せばルーレットが毎回同じところで停止することになってしまいます。ランダム性をもたせるため、値をランダムに変動させましょう。ランダムな整数を取得する関数は既にテンプレートに入っているので、これを利用します。
case Mode.deceleration: if(speed>DECEL){ speed -= DECEL+getRandomInt(-DECEL_RAND_LEVEL,DECEL_RAND_LEVEL)*DECEL_RAND_MAGNITUDE; }else{ speed = 0.0; mode = Mode.result; } theta += speed; theta-=(Math.floor(theta/2/PI))*2*PI; rotate(theta); break; ・mode = Mode.resultのとき
ルーレットはthetaだけ回転した状態で止まっています。 モードがここに入った最初のフレームだけ、結果を取得してHTML側に反映させることにします。 結果の取得は少し難しい式です。 まず前提として、ルーレットの矢印はルーレットの真上にあるものとします。 p5.jsでは、角度が0というのは水平右向き(→)、角度の正方向は時計回り(⤵)となっているので、真上(↑)は270°、つまり3π/2radianとなります。 n番目の項目の領域が真上にあるかどうかを1つ1つ判定していきます。つまり、n番目の領域が3π/2をまたいでいるかどうかを判定します。 ここで注意が必要なのは、ルーレットに乗った座標系(rotate後)での0°が、静止系(rotate前)での270°~360°の中に入っている場合です。この場合はn番目の領域が3π/2 + 2π = 7π/2をまたいでいるかどうか判定しなければなりません。 以上に注意して判定していきます。 項目名はnameListに入っているので、result番目の項目が当選したとわかった場合、nameListresultで項目名を取り出すことができます。 その値をjQueryでHTML側に表示させます。
case Mode.result: rotate(theta); if(!resultDisplayed){ resultDisplayed = true; var angleSum = theta; var beforeAngleSum = theta; var result = 0; for(var i=0;i<len;i++){ angleSum += probabilityListi*2*PI; if((angleSum>3/2*PI&&beforeAngleSum<3/2*PI) || (angleSum>7/2*PI&&beforeAngleSum<7/2*PI)){ result = i; break; } beforeAngleSum = angleSum; } $(’#result’).html(nameListresult); } break; }//switchがここで終わる drawRoulette();//ルーレット描画関数はswitchの外に出す
ルーレットの矢印の描画
矢印はルーレットと一緒に回らないように、rotate関数を呼ぶ前に描画します。 triangle関数で赤色の三角形を描画します。 ルーレットの中心から、ルーレットの半径+マージンだけ上に配置します。 三角形のサイズとマージンは定数で設定します。
const TRIANGLE_SIZE = 10; const MARGIN = 10; function draw(){ fill(255,255,255); rect(0,0,width,height); translate(width/2, height/2); fill(255,0,0); push(); translate(0, -RADIUS-MARGIN); triangle(0, 0, -TRIANGLE_SIZE/2, -TRIANGLE_SIZE, TRIANGLE_SIZE/2, -TRIANGLE_SIZE); pop(); switch(mode){
リセット・スタート・ストップボタンの実装
スタートボタンはmodeがwaitingの時に押されると、スタートボタンを消してストップボタンを出現させたり、フォーム情報を取得したり、modeをaccelerationに変更したりします。
function start(){ if(mode==Mode.waiting){ if(validation()==1){ return; } $(’#stop’).css(‘display’, ‘inline-block’); $(’#start’).css(‘display’, ’none’); dataFetch(); mode = Mode.acceleration; } } ストップボタンはmodeがconstantの時に押されると、ストップボタンを消してmodeをdecelerationに変更します。
function stop(){ if(//mode==Mode.acceleration || //加速中でもストップボタンを効かせるにはコメントアウトを解除 mode==Mode.constant){ $(’#start’).css(‘display’, ’none’); $(’#stop’).css(‘display’, ’none’); mode = Mode.deceleration; } } リセットボタンはスピードや角度を0にしたり、結果表示を初期化したり、スタートボタンを表示させたりします。
function reset(){ $(’#start’).css(‘display’, ‘inline-block’); $(’#stop’).css(‘display’, ’none’); theta = 0.0; speed = 0.0; mode = Mode.waiting; if(validation()==0){ dataFetch(); } $(’#result’).html(’????’); resultDisplayed = false; } それぞれ、フォームの入力内容の検証が必要な部分には、validation関数を入れて、その返り値が0かどうかを確認します。
ページが初めてロードされたときも、フォームの初期入力が自動的にルーレットに入るようにしておきます。同時に、パーセンテージも計算させます。 ルーレットの初期化にはp5.jsが関わってくるので、HTML側に初期化処理を書くのではなく、p5.jsが準備完了となったのを保証されたタイミングで初期化させます。そのためには、app.js内のsetup関数内に処理を記述します。
function setup(){ var canvas = createCanvas(600,400); canvas.parent(‘canvas’); textSize(20); stroke(0,0,0); fill(0,0,0); background(255,255,255); recalculate(); dataFetch(); }
HTMLファイルの修正
項目を追加・削除・変更したとき、かつmodeがwaitingのとき、フォームの入力内容を即時ルーレット側に反映させるようにします。 以下のコードを、$(’.add’).click、function rmItem(e)、$(’#table’).on それぞれのrecalculate()の後に入れます。
if(mode==Mode.waiting){ dataFetch(); }
動かしてみる
エディタ上でエラーが出ていないことを確認して、ブラウザ上で実際に動かしてみます。index.htmlを右クリック、プログラムから開く、Google Chromeを選択します。
このような画面が出れば成功です! 実際に回してみると…
このように結果が表示されました。
このバージョンではHTMLは特に装飾を施していません。ご自身でCSSをいじってみるのも良いかもしれません。
スポンサーリンク
最後に
最近増えてきている「WEBアプリ」というものの作り方が少しでも伝わったようでしたら幸いです。p5.jsを使用することで、JavaScriptで生のCanvasを操作することなく簡単にアプリケーションを作成できますので、ルーレット以外のアプリづくりに応用していただけますと嬉しいです。
ここで作成したルーレットは、ルーレット WEBツール こちらにて実際にWEBアプリとして公開しています。 Bootstrapによる装飾も加えておりますので、ぜひご参考にしてください。
また、最新のソースコードはGitHubにて公開しています。更新があった場合はGitHubにコミットされます。
解説動画はYouTubeにて公開しています。
最後までご覧いただきありがとうございました。
参考文献
p5.jsのリファレンス
この記事への感想を教えてください- 内容が十分 ()
- 内容が足りなかったが役立った ()
- 内容が足りず役立たなかった ()
- 求めている記事ではなかった ()
PHPのPDOで同時に複数の操作を行い整合性を保つ「トランザクション」 @MySQL
はじめに PHPのPDOを触っていると、2つのテーブルを同時に更新したいシチュエーションが発生します。 例として、商品購入情報を持っ…
Herokuでデータベースをresetしたい時
Herokuのデータベースをresetしたいときは、Herokuのコンソールから rails db:reset を打ってしまいた…
常時SSL対応しました。
本日、このサイトは常時SSL対応をしました。うちのサーバーはXREAというところの、無料サーバーです。 ちなみに、なぜこのサーバーを利…
PHPでstr_replaceが動かない
PHPを書いていると頻繁にstr_replaceを使用します。ただ、全角文字などが混ざっていると、うまく動いてくれないことがあります。そのよ…
Rails5でmaterializeを使用したcheckboxを表示する方法
Railsを使いながらmaterializeを適用してcheckboxを表示しようとすると、checkboxが消えてしまうことがあります。 …
Rails5で更新時または新規作成時のみValidationをかける方法
普通、ユーザー登録後にユーザー情報を変更するための画面を作ると思います。登録時は当然パスワードを設定してもらうわけですが、ユーザー情報変更画…
WEBアイコンフォント「Font Awesome」を自由自在に触る
WEBサイトを運営されている方であれば、こんな雰囲気のアイコンを目にしたことがあることがあると思います。これは「Font Awesome」と…
同じソースのはずなのにレイアウトが違う時
1つのソースから複数ページに移植することがありますが、極稀に同じソースでもレイアウトが異なってしまいます。その場合、まずはDOCTYPE宣言…
レンタルサーバーでPhantomJSを動かす方法
レンタルサーバーでは、PhantomJSを標準ではサポートしていない場合があります。その場合は自分で実行ファイルを持ってくる必要があります。…
Railsでidが不要なアクション・ページを追加する方法
Railsを使っていると、idが要らないにも関わらず、 Couldn’t find User with ‘id’〜〜〜 のようなエラーが…
WEBルーレットは、ルーレットの選択肢を入力することで、WEBサイト上でルーレットのができる無料ツールです。
このサイトでは関連する記事のみを収集しています。オリジナルを表示するには、以下のリンクをコピーして開いてください。WEBアプリの作り方 ~ルーレット~