今回が最後の記事になります。
あみだくじアプリにたどる機能を加えるのとwebページの全体的なデザインをCSSで整えていきます。
ページの機能自体はほぼできているのであともう少しで完成です。
ここまでの制作過程は以下のリンクから確認してください。
たどる機能の追加
前回の時点でページの機能はほぼ完成していて下記の画像のようなページができています。

このままでも使えますが、いちいち自分の選択肢があたりかどうかあみだくじをたどるのが面倒です。
そこで、あみだくじを生成したのちに選択肢の名前のボタンを表示し、押すとその選択肢のルートを下までたどっていく線を表示できるようにしようと思います。
イメージとしては下記の画像のような感じです。

あみだくじと同時に選択肢の名前のボタンを生成し、そのボタンを押したら選択肢から下まであみだくじをたどっていく機能を付与します。
それでは早速作っていきます。
作成したプログラムを下記に示します。
・HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>あみだくじ</title>
<link rel="stylesheet" href="CSS/style_2.css">
</head>
<body>
<h1>あみだくじ</h1>
<div>
<label for="choices">選択肢を入力してください (改行区切り):</label><br>
<textarea id="choices" rows="8" cols="40"placeholder="例: 選択肢1
選択肢2
選択肢3"></textarea><br>
<input type="number" id="winner-count" min="1" placeholder="あたりの数" />
<button onclick="generateAmida()">あみだくじを引く</button>
</div><br>
<canvas id="canvas" style="background: #ffffff;">
</canvas>
<script src="Javascript/app_2.js"></script>
</body>
</html>
・JavaScript
let choices = [];
let numberOfWinners; // デフォルトのあたりの数
let lines = [];
let tree,treeHeight;
function generateAmida() {
// ユーザー入力から選択肢を取得
const inputText = document.getElementById('choices').value;
choices = inputText.split(/\n/).map(choice => choice.trim());
// 選択肢が空でないかチェック
if (choices.length < 2) {
alert('選択肢を2つ以上入力してください');
return;
}
// あたりの数を取得
const winnerInput = document.getElementById('winner-count').value;
numberOfWinners = parseInt(winnerInput, 10) || 1; // 1 以上の数字を設定
// あたりの数が選択肢より多い場合はエラー
if (numberOfWinners > choices.length) {
alert('あたりの数は選択肢の数より少なくなければなりません');
return;
}
tree = choices.length;
treeHeight = 15;
//canvasの設定
const canvas = document.getElementById('canvas');
if (!canvas.getContext || !canvas){
return;
}
const ctx = canvas.getContext('2d');
canvas.width = (tree + 1) * 50; // 横:線と余白
canvas.height = (treeHeight + 3) * 30; // 縦:線と余白
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.strokeStyle = 'black';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
for (let i = 0; i < choices.length; i++) {
const x = (i + 1) * 50;
ctx.fillText(choices[i], x, 25); // 30pxくらい上に表示
}
function random(){
return Math.floor(Math.random() * 2);
}
let redLine = [];//あみだを辿るときに使う座標を格納するコンテナ
let redPoint;
// 隣に横線があるか確認する関数
function hasAdjacentHorizontalLine(yIndex, x) {
const linesAtY = redLine[yIndex];
return linesAtY.some(pair => {
if (!Array.isArray(pair)) return false;
const [start, end] = pair;
return (
(start === x - 1 && end === x) || // 左隣に横線がある
(start === x && end === x + 1) // 今まさに引こうとしている場所
);
});
}
for (let y = 1; y <= treeHeight; y++){
redPoint = 0;
redLine[y - 1] = [];//ここで2重配列を作る
for (let x = 1; x <= tree; x++){
if (x === tree || random() === 0 || y === treeHeight|| hasAdjacentHorizontalLine(y - 1, x)){
redLine[y - 1][redPoint] = [0, 0];
redPoint++;
ctx.beginPath();
ctx.moveTo( 50 * x ,y * 30 );
ctx.lineTo( 50 * x ,((y + 1) * 30));
ctx.stroke();
} else {
for (let t = 0; t < 2; t++){ //同じ座標を同じ列に2つ置く(右回り用と左回り用)
redLine[y - 1][redPoint]= [x, x + 1];
redPoint++;
}
ctx.beginPath();
ctx.moveTo( 50 * x, y * 30 );
ctx.lineTo( 50 * x,((y + 1) * 30));
ctx.lineTo( 50 * (x + 1),((y + 1) * 30));
ctx.stroke();
}
}
}
const winnerIndices = [];
while (winnerIndices.length < numberOfWinners) {
const rand = Math.floor(Math.random() * choices.length);
if (!winnerIndices.includes(rand)) {
winnerIndices.push(rand);
}
}
ctx.font = '24px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = 'red';
for (let i = 0; i < winnerIndices.length; i++) {
const x = (winnerIndices[i] + 1) * 50;
const y = (treeHeight + 1) * 30 ; // 下に少し余白をつける
ctx.fillText('○', x, y);
}
const undo = ctx.getImageData(0, 0, canvas.width, canvas.height); //赤い線のない画像を保存しておく
ctx.strokeStyle = '#e65353';
ctx.lineWidth = 3;
// ボタンコンテナを一度だけ作成
const buttonContainer = document.createElement('div');
buttonContainer.id = 'button-container';
buttonContainer.style.display = 'flex';
buttonContainer.style.flexDirection = 'row';
// あみだをたどるボタンのリセット
const oldContainer = document.getElementById('button-container');
if (oldContainer) {
document.body.removeChild(oldContainer);
}
for (let x = 1; x <= tree; x++){
const inputButton = document.createElement('input');
inputButton.type = "submit";
inputButton.value = choices[x-1]
inputButton.className = 'amida-button';
// ボタンを親divに追加
buttonContainer.appendChild(inputButton);
inputButton.addEventListener('click', () => {
ctx.putImageData(undo, 0, 0); // リセット
let currentX = x;
for (let y = 1; y <= treeHeight; y++) {
let moved = false;
for (let i = 0; i < redLine[y - 1].length; i++) {
const [a, b] = redLine[y - 1][i];
if (a === currentX) {
// 右に移動
ctx.beginPath();
ctx.moveTo(50 * currentX, y * 30);
ctx.lineTo(50 * currentX, (y + 1) * 30);
ctx.lineTo(50 * (currentX + 1), (y + 1) * 30);
ctx.stroke();
currentX++;
moved = true;
break;
} else if (b === currentX) {
// 左に移動
ctx.beginPath();
ctx.moveTo(50 * currentX, y * 30);
ctx.lineTo(50 * currentX, (y + 1) * 30);
ctx.lineTo(50 * (currentX - 1), (y + 1) * 30);
ctx.stroke();
currentX--;
moved = true;
break;
}
}
if (!moved) {
// 上から下にまっすぐ
ctx.beginPath();
ctx.moveTo(50 * currentX, y * 30);
ctx.lineTo(50 * currentX, (y + 1) * 30);
ctx.stroke();
}
}
});
}
// ボタンコンテナをbodyに追加
document.body.appendChild(buttonContainer);
}
HTMLは前回と変化ないのでJavaScriptの方を説明していきます。
変更点は、116行目以降です。
それでは説明していきます。
下準備
まずは、116~129行目から説明していきます。
この部分はまだたどるための機能について書いているのではなく、その機能を付けるための下準備の部分になります。
・116,117行目
ここはたどるボタンで生成する線の色と太さを定義しています。
今回は赤く強調しようと考えたので赤っぽい色と少し太めの線にしました。
・120~123行目
ボタンコンテナの作成を行っています。
ページ上にボタンを生成するのでその定義です。
div 要素を新たに作成し、id=”button-container” と設定しました。
またボタンをCSSスタイルで横並び(flex row)のボタン配置になるようにしています。
・126~129行目
最後にボタンコンテナのリセットを行っています。
何度もあみだくじを生成しているとボタンが増えていってしまうのでそれのリセットです。
button-containerというIDを持つ以前に作られたボタンコンテナが存在すれば削除するようにしています。
これにより、ボタンコンテナが重複して追加されるのを防ぐことができるようになります。
たどるボタンの機能付与
選択肢の数分の縦線があるのでその一つ一つについて処理を行うためforループを回していきます。
・132~138行目
ボタン要素を生成しています。
表示されるラベルは choices 配列から取得するようにしました。
ここには選択肢の名前が入っているのでボタンに表示される名前を選択肢の名前にできるようになります。
CSSでスタイリングできるように className の設定もしています。
最後に138行目でそれをbuttonContainerに追加しています。
・140,141行目
ボタンがクリックされたときに機能するようにしています。
クリックされたとき、undo に保存されていた Canvas の状態に戻す(前回の線を消す)処理を行います。
これによってボタンを押すごとにその選択肢のみを下までたどるようになります。
・143~148行目
ここからの処理を行うためのforループを設定しています。
まず、x は、ボタンクリック時に選んだ 縦線の位置でcurrentX は、現在地を表します(スタートは x)。
treeHeight は、あみだくじの高さ(行数)でredLine は、横線の位置情報を階層ごとに持つ配列です。
高さ(y軸)方向のループの設定が143行目になります。
y = 1 から treeHeight まで、下に向かって処理します。
moved は、その段で「横に移動したか」を判定するフラグになります。
そして、146行目の次のループが横棒(redLine)をチェックするループです。
redLine[y – 1] は、高さ y の横線リストです。
例えば redLine[2] = [[2,3]] なら、3行目に 2本目と3本目をつなぐ横棒があるという意味になります。
各要素 [a, b] は、縦線 a と b の間に横棒があることを表します。
ここからの処理で、「今の位置 currentX に対して、どこか横線が接続しているか?」を調べます。
・a === currentX → 右に移動
・b === currentX → 左に移動
・該当しなければ → まっすぐ下に進む
詳しくは下で説明します。
・150~170行目
現在の位置 currentX が横線の左端 a にいたら、右へ移動します。
また、b にいたら、左へ移動します。
移動後は ctx.lineTo(…) を使って線を描画するようにしています。
また、それぞれの処理をした後はcurrentX++によって次の列へ移動しています。
・173~179行目
ここは横線がなかった場合の処理です。
ただ下に降りるだけなのでそのような処理をしています。
・184行目
最後に作成した button-container を DOM に追加して、ボタンを表示しています。
以上で一連の処理は終了です。
ここまでですべての機能が付与できました。
完成したページは以下になります。

CSS
最後にCSSで体裁を整えました。
CSSは以下のようになります。
・CSS
html {
font-size: 100%;
}
body {
font-family: Arial, sans-serif;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #bfe797e3;
padding: 20px;
}
textarea {
width: 80%; /* 幅を80%に設定 */
max-width: 500px; /* 最大幅を500pxに制限 */
height: 140px; /* 高さ */
margin: 1em 0; /* 外側の余白 */
padding: 1em; /* 内側の余白 */
font-size: 1em; /* 文字サイズ */
border: solid 2px #e1e3e8; /* ボーダー線 */
resize: none; /* リサイズを無効 */
}
#columns {
display: flex;
justify-content: center; /* カラムを中央揃え */
gap: 20px; /* カラム間の間隔 */
margin-top: 0px;
position: relative; /* 縦線や横線を重ねるために相対位置 */
height: 100%; /* 全体の高さを確保 */
width: auto;
}
.amida-button {
width: 80px;
height: 30px;
margin: 5px;
font-size: 14px;
}
また、完成したページは以下のようになります。

コメントで書いてある通りの処理しかしていません。
気になる部分があったら数値を変えてどのような変化があるか見たらどんな処理をしているかわかると思います。
まとめ
以上が今回作成したあみだくじアプリの作成過程でした。
全3回と長くなりましたが読んでいただいた方ありがとうございました。
説明が足りない部分は多々あると思いますので、定期的に修正しようと思います。
あみだくじアプリはベタですけど意外と難しかったです。
これからも精進します。
お疲れ様でした。完成したアプリも見てみてください!
コメント