はじめに
地元のお祭りのお囃子の練習を有志でやろうという話になったが、太鼓を持ってくるのが大変。
なら、何とかすればいいじゃない!ということでジャンクで購入したプレステ2用のタタコンを使って太鼓をたたけるようにしました。
実際にできたものは↓これ
https://taka-hama.sakura.ne.jp/lab/taiko/
ついでに、今更感はありますが、ChatGPT(無課金)でどの程度プログラムが作れるのか試しました
ChatGPTとコミュニケーションする
ファーストインプレッション
最初にこんな感じでお願いしてみました。
WebブラウザでUSBにつながったコントローラーを使ってボタンを押すと音を出すプログラムを作ってください。
コントローラーは複数接続されます。
コントローラーの数に応じて音の位置を変えたいです。例えば1つだけならセンター、2つなら1つ目が左、2つ目が右といった感じです。
コントローラーは最大4つを想定しています。
保守性と可動性を考慮してください。
で、出てきたのがコレ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ゲームパッド音再生</title>
</head>
<body>
<h1>ゲームパッド音再生</h1>
<p>コントローラーのボタンを押すと音が鳴ります</p>
<script>
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const gamepads = new Map(); // index -> {id, buttons, panNode, buffer}
const maxPads = 4;
// 左右パン値の割り当て [-1:左, 0:中央, 1:右]
const panValues = [-1, -0.5, 0.5, 1];
// 任意のボタンに対応(例:ボタン0)
const targetButtonIndex = 0;
// 音源読み込み
async function loadSound(url) {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return await audioCtx.decodeAudioData(arrayBuffer);
}
// 音再生
function playSound(buffer, panValue) {
const source = audioCtx.createBufferSource();
const panner = audioCtx.createStereoPanner();
panner.pan.value = panValue;
source.buffer = buffer;
source.connect(panner).connect(audioCtx.destination);
source.start();
}
// ゲームパッドの状態を更新して音を鳴らす
function updateGamepads() {
const pads = navigator.getGamepads ? navigator.getGamepads() : [];
gamepads.forEach((data, index) => {
const pad = pads[index];
if (!pad) return;
const isPressed = pad.buttons[targetButtonIndex].pressed;
if (isPressed && !data.buttons[targetButtonIndex]) {
playSound(data.buffer, data.panValue);
}
data.buttons = pad.buttons.map(b => b.pressed);
});
requestAnimationFrame(updateGamepads);
}
// ゲームパッド接続時
async function onGamepadConnected(e) {
const pad = e.gamepad;
if (gamepads.size >= maxPads || gamepads.has(pad.index)) return;
const buffer = await loadSound("sound.mp3"); // 任意の音ファイルに変更
const panValue = panValues[gamepads.size] || 0;
gamepads.set(pad.index, {
id: pad.id,
buttons: pad.buttons.map(b => b.pressed),
buffer: buffer,
panValue: panValue
});
console.log(`Gamepad connected: ${pad.id} (index ${pad.index})`);
}
// ゲームパッド切断時
function onGamepadDisconnected(e) {
gamepads.delete(e.gamepad.index);
console.log(`Gamepad disconnected: ${e.gamepad.id}`);
}
window.addEventListener("gamepadconnected", onGamepadConnected);
window.addEventListener("gamepaddisconnected", onGamepadDisconnected);
window.addEventListener("click", () => audioCtx.resume()); // 初回クリックでAudioContextを起動
requestAnimationFrame(updateGamepads);
</script>
</body>
</html>
これだとタタコンに対応できてない
タタコンに対応させる
タタコンに対応させたいので聞いてみる
タタコンを接続する想定に変更してください。
そのため音は太鼓の面を叩くケースと縁を叩くケースを想定。
コントローラーを接続してない場合は画面をタップして音を出したいので、それを考慮したUIにしてください。
太鼓は円で描き、たたいたら色が変化するようにしてください。
で以下のソースが出てくる
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タタコン音再生</title>
<style>
body {
text-align: center;
font-family: sans-serif;
background-color: #eee;
}
canvas {
margin-top: 30px;
border: 1px solid #aaa;
background-color: #fff;
}
</style>
</head>
<body>
<h1>タタコン音再生</h1>
<p>太鼓の面 or 縁を叩くと音が鳴ります</p>
<canvas id="taiko" width="300" height="300"></canvas>
<script>
const canvas = document.getElementById('taiko');
const ctx = canvas.getContext('2d');
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let soundDon, soundKa;
const pads = new Map(); // 接続されたタタコン
const TARGET_BUTTONS = {
don: [0, 1], // ボタン0・1が面
ka: [2, 3] // ボタン2・3が縁
};
let hitEffect = null; // { type: 'don' | 'ka', timestamp }
async function loadSound(url) {
const res = await fetch(url);
const arrayBuffer = await res.arrayBuffer();
return await audioCtx.decodeAudioData(arrayBuffer);
}
function playSound(buffer) {
const src = audioCtx.createBufferSource();
src.buffer = buffer;
src.connect(audioCtx.destination);
src.start();
}
function drawTaiko() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// エフェクト色
let colorDon = '#cc0000';
let colorKa = '#0000cc';
if (hitEffect && Date.now() - hitEffect.timestamp < 100) {
if (hitEffect.type === 'don') colorDon = '#ff6666';
if (hitEffect.type === 'ka') colorKa = '#6666ff';
}
// 太鼓の縁(大きな円)
ctx.beginPath();
ctx.arc(150, 150, 120, 0, Math.PI * 2);
ctx.fillStyle = colorKa;
ctx.fill();
// 太鼓の面(小さい円)
ctx.beginPath();
ctx.arc(150, 150, 80, 0, Math.PI * 2);
ctx.fillStyle = colorDon;
ctx.fill();
}
function handleHit(type) {
if (type === 'don') playSound(soundDon);
if (type === 'ka') playSound(soundKa);
hitEffect = { type, timestamp: Date.now() };
}
function updateGamepads() {
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
pads.forEach((padData, index) => {
const pad = gamepads[index];
if (!pad) return;
const pressed = pad.buttons.map(b => b.pressed);
TARGET_BUTTONS.don.forEach(i => {
if (pressed[i] && !padData.prev[i]) {
handleHit('don');
}
});
TARGET_BUTTONS.ka.forEach(i => {
if (pressed[i] && !padData.prev[i]) {
handleHit('ka');
}
});
padData.prev = pressed;
});
drawTaiko();
requestAnimationFrame(updateGamepads);
}
async function init() {
soundDon = await loadSound("don.mp3");
soundKa = await loadSound("ka.mp3");
drawTaiko();
requestAnimationFrame(updateGamepads);
}
// ゲームパッド接続
window.addEventListener("gamepadconnected", (e) => {
const pad = e.gamepad;
pads.set(pad.index, {
id: pad.id,
prev: pad.buttons.map(b => false)
});
console.log("Gamepad connected:", pad.id);
});
// ゲームパッド切断
window.addEventListener("gamepaddisconnected", (e) => {
pads.delete(e.gamepad.index);
console.log("Gamepad disconnected:", e.gamepad.id);
});
// ユーザーの初回操作でAudioContextをresume
window.addEventListener("click", () => audioCtx.resume());
// Canvasクリック(タタコンなしの時用)
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - 150;
const dy = y - 150;
const dist = Math.sqrt(dx*dx + dy*dy);
if (dist <= 80) {
handleHit('don');
} else if (dist <= 120) {
handleHit('ka');
}
});
init();
</script>
</body>
</html>
パンの設定どこ行った?
まぁ、あとで何とかすればいいかということで、タタコンのボタンを調べて直そうとした・・・が、WindowsだとなぜかAXIS9が反応する。Androidは全部ボタンに割り当てられる。
ボタンの割り当ては以下のサイトで確認
https://jp.hexgaming.com/pages/gamepad-tester
AXIS9の対応方法を聞く
そこでこんな質問
タタコンの入力のひとつがAXIS9なのですがどうしたらいいですか?
でupdateGamepads 内に追加するコードを提示してくれる。
const AXIS_SENSITIVITY = 0.5; // このしきい値を超えたら叩いたと判断
const AXIS_INDEX = 9;
pads.forEach((padData, index) => {
const pad = connected[index];
if (!pad) return;
const nowButtons = pad.buttons.map(b => b.pressed);
const nowAxes = pad.axes;
// ---- 以下略(通常のボタン判定) ----
// 追加: AXIS9の判定(縁の入力など)
const prevValue = padData.prevAxis9 || 0;
const currValue = nowAxes[AXIS_INDEX] || 0;
if (Math.abs(currValue) > AXIS_SENSITIVITY && Math.abs(prevValue) <= AXIS_SENSITIVITY) {
// 軸の値が急激に動いたときに「叩いた」と判定
handleHit('ka');
}
padData.prevAxis9 = currValue;
padData.prevButtons = nowButtons;
});
動かしてみると・・・なんかおかしい・・・
叩くと、値がマイナス方向から+方向に動くのでこの処理だとだめだった。
ここだけ自分で治す。
スマホで問題
スマホだと太鼓の描画が小さいので対応方法を聞く
スマホだと太鼓が小さいおお効くしてほしい
で、スタイルシートとJavaScriptの差分のコードが返ってくる
canvas サイズの指定・初期化・リサイズイベントの追加・修正のコード
<style>
body {
text-align: center;
font-family: sans-serif;
background-color: #f0f0f0;
margin: 0;
}
canvas {
display: block;
margin: 0 auto;
touch-action: manipulation;
}
</style>
const canvas = document.getElementById('taiko');
const ctx = canvas.getContext('2d');
let canvasSize = 300; // 実際のサイズ(正方形)
function resizeCanvas() {
const minSize = Math.min(window.innerWidth, window.innerHeight * 0.8);
canvasSize = Math.floor(minSize * 0.8);
canvas.width = canvas.height = canvasSize;
drawTaiko(); // リサイズ後再描画
}
drawTaiko() を canvasSize を使うよう変更
function drawTaiko() {
ctx.clearRect(0, 0, canvasSize, canvasSize);
const center = canvasSize / 2;
const innerRadius = canvasSize * 0.27;
const outerRadius = canvasSize * 0.42;
let colorDon = '#cc0000';
let colorKa = '#0000cc';
if (hitEffect && Date.now() - hitEffect.timestamp < 100) {
if (hitEffect.type === 'don') colorDon = '#ff6666';
if (hitEffect.type === 'ka') colorKa = '#6666ff';
}
// 外円
ctx.beginPath();
ctx.arc(center, center, outerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorKa;
ctx.fill();
// 内円
ctx.beginPath();
ctx.arc(center, center, innerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorDon;
ctx.fill();
}
タップ判定も canvasSize ベースに変更
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= canvasSize * 0.27) {
handleHit('don');
} else if (dist <= canvasSize * 0.42) {
handleHit('ka');
}
});
初期化時とウィンドウリサイズ時に呼び出し
window.addEventListener("resize", resizeCanvas);
async function init() {
soundDon = await loadSound("don.mp3");
soundKa = await loadSound("ka.mp3");
setupCanvasInput();
resizeCanvas(); // 初期化時にリサイズ実行
requestAnimationFrame(updateGamepads);
}
タップのコードの修正は提案してくれたが、クリックのコードだったので、お願いしてみる
タップのソースの修正とされているところはクリックのソースです。canvas.addEventListener("touchstart", (e) => {のコードも直してください
canvas.addEventListener("touchstart", (e) => {
e.preventDefault(); // スクロール防止
const rect = canvas.getBoundingClientRect();
const touch = e.touches[0];
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= canvasSize * 0.27) {
handleHit('don');
} else if (dist <= canvasSize * 0.42) {
handleHit('ka');
}
}, { passive: false });
スマホの縦表示は良くなった。
横表示を確認すると、サイズがでかくてはみ出る・・・
なので、提案してもらう
スマホで横にすると太鼓がはみ出ます。縦横に対応するようにしてください。
resizeCanvas() の修正案を呈してくれる
function resizeCanvas() {
// 画面サイズに応じて canvas のサイズを調整
const margin = 20; // 余白を少し持たせる
const width = window.innerWidth;
const height = window.innerHeight;
// 横幅と高さの 80% を上限にして、正方形のキャンバスを作る
const size = Math.min(width, height) - margin * 2;
canvasSize = Math.max(100, size); // 小さすぎる場合の最低サイズ
canvas.width = canvas.height = canvasSize;
drawTaiko(); // リサイズ後に再描画
}
さらに回転時の対応も併せて提案してくれた。
window.addEventListener("resize", resizeCanvas);
・・・が、回転してもおかしいので
画面が回転されたらサイズを再計算してほしい
と聞くと、以下の提案をしてくれる
window.addEventListener("resize", resizeCanvas); // ウィンドウサイズ変更時
window.addEventListener("orientationchange", () => {
// 少し遅延してから再計算(回転が完了するまで wait)
setTimeout(resizeCanvas, 200);
});
レイアウトを直してもらう
横方向の中心に表示されているが、垂直方向は中央ではない。なので
垂直方向に中心に表示したい
CSSの修正を提案される。
内容に納得
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background-color: #f0f0f0;
}
body {
display: flex;
justify-content: center; /* 横方向中央 */
align-items: center; /* 縦方向中央 */
font-family: sans-serif;
}
canvas {
touch-action: manipulation;
}
</style>
表示は良くなった
タタコンで遅延する
タタコンで叩くとどうも音が遅延する。
requestAnimationFrame()メソッドで処理しているので当然と言えば当然なので、お願いしてみる
タタコンで叩いたときにタップ操作に比べて音が遅延します。プログラム的な問題を含んでいませんか?
で改善案を示される
改善案①
setInterval(() => {
pollGamepads(); // navigator.getGamepads() など
}, 8); // 8ms ≒ 120Hz
これはダメだと思う
改善案②
const context = new AudioContext();
function playSound(buffer) {
const source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start(0); // 遅延なしで即時再生
}
まぁ、わからんでもないけど、これもダメだともう。
改善案③
これだけコードを示してくれず、
pollGamepads() の中で handleHit() を即呼び出し、音を鳴らす
まぁ、そうだよね。
入力されたらすぐに再生だよね。
ここまで聞いた内容に対応したソースコードを要求したが・・・
提案された内容と、手で修正した内容に差異があるか確認したかったので聞いてみる
ここまで対応したソースコードを提示してください
で、出てきたコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タタコン 太鼓アプリ</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background-color: #f0f0f0;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
canvas {
touch-action: manipulation;
background-color: #ffffff;
border-radius: 12px;
}
</style>
</head>
<body>
<canvas id="taikoCanvas"></canvas>
<script>
const canvas = document.getElementById("taikoCanvas");
const ctx = canvas.getContext("2d");
let canvasSize = 300; // 初期値
let audioContext;
const sounds = {
don: null,
ka: null
};
const taiko = {
color: "#d94d4d",
ringColor: "#a52a2a",
hitColor: "#ff9999",
ringHitColor: "#ff6666",
isDon: false,
isKa: false
};
function resizeCanvas() {
const margin = 20;
const width = window.innerWidth;
const height = window.innerHeight;
const size = Math.min(width, height) - margin * 2;
canvasSize = Math.max(100, size);
canvas.width = canvasSize;
canvas.height = canvasSize;
drawTaiko();
}
window.addEventListener("resize", resizeCanvas);
window.addEventListener("orientationchange", () => {
setTimeout(resizeCanvas, 200);
});
function drawTaiko() {
const center = canvasSize / 2;
const radius = canvasSize * 0.45;
const ringWidth = canvasSize * 0.1;
ctx.clearRect(0, 0, canvasSize, canvasSize);
// Ring
ctx.beginPath();
ctx.arc(center, center, radius, 0, Math.PI * 2);
ctx.fillStyle = taiko.isKa ? taiko.ringHitColor : taiko.ringColor;
ctx.fill();
// Face
ctx.beginPath();
ctx.arc(center, center, radius - ringWidth, 0, Math.PI * 2);
ctx.fillStyle = taiko.isDon ? taiko.hitColor : taiko.color;
ctx.fill();
}
async function initAudio() {
if (!audioContext) {
audioContext = new AudioContext();
}
const loadSound = async (url) => {
const res = await fetch(url);
const arrayBuffer = await res.arrayBuffer();
return await audioContext.decodeAudioData(arrayBuffer);
};
sounds.don = await loadSound("don.mp3"); // 面
sounds.ka = await loadSound("ka.mp3"); // 縁
}
function playSound(buffer) {
if (!buffer || !audioContext) return;
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(0);
}
function handleHit(type) {
if (type === "don") {
taiko.isDon = true;
playSound(sounds.don);
setTimeout(() => {
taiko.isDon = false;
drawTaiko();
}, 100);
} else if (type === "ka") {
taiko.isKa = true;
playSound(sounds.ka);
setTimeout(() => {
taiko.isKa = false;
drawTaiko();
}, 100);
}
drawTaiko();
}
// タップ(画面タッチ or クリック)イベント
canvas.addEventListener("pointerdown", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
const innerRadius = canvasSize * 0.35;
const outerRadius = canvasSize * 0.45;
if (dist <= innerRadius) {
handleHit("don");
} else if (dist <= outerRadius) {
handleHit("ka");
}
});
// Gamepad入力処理
let prevButtons = [];
function pollGamepads() {
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
for (let i = 0; i < gamepads.length; i++) {
const gp = gamepads[i];
if (!gp) continue;
// 事前に初期化
if (!prevButtons[i]) prevButtons[i] = [];
const buttons = gp.buttons;
const axes = gp.axes;
// 押されたボタンや軸をチェック(必要に応じて番号調整)
const buttonDon = buttons[0]?.pressed || false; // Aボタンなど
const buttonKa = buttons[1]?.pressed || false; // Bボタンなど
const axis9 = axes[9] !== undefined ? axes[9] : 0;
// edge検出で判定
if (buttonDon && !prevButtons[i][0]) handleHit("don");
if (buttonKa && !prevButtons[i][1]) handleHit("ka");
if (axis9 < -0.5 && prevButtons[i][9] >= -0.5) handleHit("ka"); // AXIS9 が左に倒されたら
// 状態保存
prevButtons[i][0] = buttonDon;
prevButtons[i][1] = buttonKa;
prevButtons[i][9] = axis9;
}
}
// 高速ポーリング
setInterval(pollGamepads, 8);
// 起動処理
resizeCanvas();
initAudio();
</script>
</body>
</html>
修正案出してきたコードからいろいろ変えすぎ!
すかさずここまで直してきたコードのもととなるタタコンに対応させるときに生成されたコードを指定して再度お願いする。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タタコン音再生</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background-color: #f0f0f0;
}
body {
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
}
canvas {
touch-action: manipulation;
}
</style>
</head>
<body>
<canvas id="taiko" width="300" height="300"></canvas>
<script>
const canvas = document.getElementById('taiko');
const ctx = canvas.getContext('2d');
let canvasSize = 300;
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let soundDon, soundKa;
const pads = new Map();
const TARGET_BUTTONS = {
don: [2, 3, 1, 14],
ka: [4, 5]
};
let hitEffect = null;
const AXIS_INDEX = 9;
const AXIS_THRESHOLD = 0.8;
async function loadSound(url) {
const res = await fetch(url);
const buffer = await res.arrayBuffer();
return await audioCtx.decodeAudioData(buffer);
}
function playSound(buffer) {
const src = audioCtx.createBufferSource();
src.buffer = buffer;
src.connect(audioCtx.destination);
src.start(0);
}
function resizeCanvas() {
const margin = 20;
const width = window.innerWidth;
const height = window.innerHeight;
const size = Math.min(width, height) - margin * 2;
canvasSize = Math.max(100, size);
canvas.width = canvas.height = canvasSize;
drawTaiko();
}
function drawTaiko() {
ctx.clearRect(0, 0, canvasSize, canvasSize);
const center = canvasSize / 2;
const innerRadius = canvasSize * 0.27;
const outerRadius = canvasSize * 0.42;
let colorDon = '#cc0000';
let colorKa = '#0000cc';
if (hitEffect && Date.now() - hitEffect.timestamp < 100) {
if (hitEffect.type === 'don') colorDon = '#ff6666';
if (hitEffect.type === 'ka') colorKa = '#6666ff';
}
ctx.beginPath();
ctx.arc(center, center, outerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorKa;
ctx.fill();
ctx.beginPath();
ctx.arc(center, center, innerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorDon;
ctx.fill();
}
function handleHit(type) {
if (type === 'don') playSound(soundDon);
if (type === 'ka') playSound(soundKa);
hitEffect = { type, timestamp: Date.now() };
}
function updateGamepads() {
const connected = navigator.getGamepads();
pads.forEach((padData, index) => {
const pad = connected[index];
if (!pad) return;
const nowButtons = pad.buttons.map(b => b.pressed);
const nowAxes = pad.axes;
TARGET_BUTTONS.don.forEach(i => {
if (nowButtons[i] && !padData.prevButtons[i]) {
handleHit('don');
}
});
TARGET_BUTTONS.ka.forEach(i => {
if (nowButtons[i] && !padData.prevButtons[i]) {
handleHit('ka');
}
});
const axisVal = nowAxes[AXIS_INDEX] || 0;
const prevVal = padData.prevAxis9 || 0;
if (Math.abs(axisVal) > AXIS_THRESHOLD && Math.abs(prevVal) <= AXIS_THRESHOLD) {
if (axisVal < 0) {
handleHit('don'); // 上または左
} else {
handleHit('ka'); // 下または右
}
}
padData.prevAxis9 = axisVal;
padData.prevButtons = nowButtons;
});
drawTaiko();
requestAnimationFrame(updateGamepads);
}
function setupCanvasInput() {
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= canvasSize * 0.27) {
handleHit('don');
} else if (dist <= canvasSize * 0.42) {
handleHit('ka');
}
});
canvas.addEventListener("touchstart", (e) => {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= canvasSize * 0.27) {
handleHit('don');
} else if (dist <= canvasSize * 0.42) {
handleHit('ka');
}
e.preventDefault();
});
}
window.addEventListener("gamepadconnected", (e) => {
pads.set(e.gamepad.index, {
prevButtons: e.gamepad.buttons.map(b => b.pressed),
prevAxis9: 0
});
console.log("Gamepad connected:", e.gamepad.id);
});
window.addEventListener("gamepaddisconnected", (e) => {
pads.delete(e.gamepad.index);
console.log("Gamepad disconnected:", e.gamepad.id);
});
window.addEventListener("click", () => {
audioCtx.resume();
});
async function init() {
soundDon = await loadSound("don.mp3");
soundKa = await loadSound("ka.mp3");
setupCanvasInput();
resizeCanvas();
requestAnimationFrame(updateGamepads);
}
window.addEventListener("resize", resizeCanvas);
window.addEventListener("orientationchange", () => {
setTimeout(resizeCanvas, 200);
});
init();
</script>
</body>
</html>
必要なところは直して実行するといい感じ。
コントローラーの接続状況を描画する
修正したコードを示しつつ、お願いする。
このコードの状態が正しい動作をします。
このコードにコントローラーの接続状態がわかる表示を追加してください。
可能であればキャンパスの左上に表示してください。
表示はコントローラーの1~4までが接続されているかいないかがわかるように丸の中に数字を表示し、状態がわかるようにしてください
※ながいのでQiita上ではコードは省略していますが、ここに実際に動作するコードを記載している
で。出てきたコード
function drawTaiko() {
ctx.clearRect(0, 0, canvasSize, canvasSize);
const center = canvasSize / 2;
const innerRadius = canvasSize * 0.27;
const outerRadius = canvasSize * 0.42;
let colorDon = '#d94d4d';
let colorKa = '#a52a2a';
if (hitEffect && Date.now() - hitEffect.timestamp < 100) {
if (hitEffect.type === 'don') colorDon = '#ff9999';
if (hitEffect.type === 'ka') colorKa = '#ff6666';
}
// 外円
ctx.beginPath();
ctx.arc(center, center, outerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorKa;
ctx.fill();
// 内円
ctx.beginPath();
ctx.arc(center, center, innerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorDon;
ctx.fill();
// --- コントローラー接続状態の表示 ---
const radius = canvasSize * 0.05;
const margin = canvasSize * 0.015;
for (let i = 0; i < 4; i++) {
const x = margin + radius;
const y = margin + radius + i * (radius * 2 + margin);
const isConnected = navigator.getGamepads()[i];
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = isConnected ? "#4caf50" : "#cccccc";
ctx.fill();
ctx.fillStyle = "#ffffff";
ctx.font = `${radius}px sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText((i + 1).toString(), x, y);
}
}
お、いい感じ
ただ、コントローラーの番号の表示が太鼓に重なるので、手作業で調整する。
最終的なコード
音のパンの割り当ては時分で実装してしまった。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>お囃子練習用バーチャル太鼓</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background-color: #f0f0f0;
}
body {
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
}
canvas {
touch-action: manipulation;
background-color: #ffffff;
//border-radius: 12px;
}
</style>
</head>
<body>
<canvas id="taiko" width="300" height="300"></canvas>
<script>
const canvas = document.getElementById('taiko');
const ctx = canvas.getContext('2d');
let canvasSize = 300;
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let soundDon, soundKa;
const pan = new Array(
[0],
[0],
[-0.5, 0.5],
[-0.5, 0, 0.5],
[-1, -0.5, 0.5, 1],
);
const pads = new Map();
// タタコンの各ボタン番号に対応(機種によって異なる場合あり)
const TARGET_BUTTONS = {
don: [2, 3, 1, 14], // 通常「面」 Win 2,3 Android 1,14
ka: [4, 5] // 通常「縁」
};
let hitEffect = null;
const AXIS_SENSITIVITY = 1.5; // このしきい値を超えたら叩いたと判断
const AXIS_INDEX = 9;
async function loadSound(url) {
const res = await fetch(url);
const buffer = await res.arrayBuffer();
return await audioCtx.decodeAudioData(buffer);
}
function playSound(buffer, index) {
panValue = pan[pads.size][index];
if(panValue == undefined){
panValue = 0;
}
const src = audioCtx.createBufferSource();
src.buffer = buffer;
const panner = audioCtx.createStereoPanner();
panner.pan.value = panValue;
src.connect(panner).connect(audioCtx.destination);
src.start(0);
}
function resizeCanvas() {
// 画面サイズに応じて canvas のサイズを調整
const margin = 20;
const width = window.innerWidth;
const height = window.innerHeight;
// 横幅と高さの 80% を上限にして、正方形のキャンバスを作る
const size = Math.min(width, height) - margin * 2;
canvasSize = Math.max(100, size);
canvas.width = canvas.height = canvasSize;
drawTaiko();
}
function drawTaiko() {
ctx.clearRect(0, 0, canvasSize, canvasSize);
const center = canvasSize / 2;
const innerRadius = canvasSize * 0.27;
const outerRadius = canvasSize * 0.42;
let colorDon = '#d94d4d';
let colorKa = '#a52a2a';
if (hitEffect && Date.now() - hitEffect.timestamp < 100) {
if (hitEffect.type === 'don') colorDon = '#ff9999';
if (hitEffect.type === 'ka') colorKa = '#ff6666';
}
// 外円
ctx.beginPath();
ctx.arc(center, center, outerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorKa;
ctx.fill();
// 内円
ctx.beginPath();
ctx.arc(center, center, innerRadius, 0, Math.PI * 2);
ctx.fillStyle = colorDon;
ctx.fill();
// --- コントローラー接続状態の表示 ---
const radius = canvasSize * 0.03;
const margin = canvasSize * 0.015;
for (let i = 0; i < 4; i++) {
const x = margin + radius;
const y = margin + radius + i * (radius * 2 + margin);
const isConnected = navigator.getGamepads()[i];
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = isConnected ? "#4caf50" : "#cccccc";
ctx.fill();
ctx.fillStyle = "#ffffff";
ctx.font = `${radius}px sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText((i + 1).toString(), x, y);
}
}
function handleHit(index, type) {
if (type === 'don') playSound(soundDon, index);
if (type === 'ka') playSound(soundKa, index);
hitEffect = { type, timestamp: Date.now() };
}
function updateGamepads() {
const connected = navigator.getGamepads();
pads.forEach((padData, index) => {
const pad = connected[index];
if (!pad) return;
const nowButtons = pad.buttons.map(b => b.pressed);
const nowAxes = pad.axes;
TARGET_BUTTONS.don.forEach(i => {
if (nowButtons[i] && !padData.prevButtons[i]) {
handleHit(index, 'don');
}
});
TARGET_BUTTONS.ka.forEach(i => {
if (nowButtons[i] && !padData.prevButtons[i]) {
handleHit(index, 'ka');
}
});
const prevValue = padData.prevAxis9 || 0;
const currValue = nowAxes[AXIS_INDEX] || 0;
if (Math.abs(prevValue - currValue) > AXIS_SENSITIVITY) {
// 軸の値が急激に動いたときに「叩いた」と判定
handleHit(index, 'don');
}
//padData.prevAxis9 = axisVal;
padData.prevAxis9 = currValue;
padData.prevButtons = nowButtons;
});
drawTaiko();
requestAnimationFrame(updateGamepads);
}
function setupCanvasInput() {
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= canvasSize * 0.27) {
handleHit(0, 'don');
} else if (dist <= canvasSize * 0.42) {
handleHit(0, 'ka');
}
});
// スマホタッチ対応
canvas.addEventListener("touchstart", (e) => {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
const dx = x - canvasSize / 2;
const dy = y - canvasSize / 2;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= canvasSize * 0.27) {
handleHit(0, 'don');
} else if (dist <= canvasSize * 0.42) {
handleHit(0, 'ka');
}
e.preventDefault();
});
}
window.addEventListener("gamepadconnected", (e) => {
pads.set(e.gamepad.index, {
prevButtons: e.gamepad.buttons.map(b => b.pressed),
prevAxis9: 0
});
console.log("Gamepad connected:", e.gamepad.id);
});
window.addEventListener("gamepaddisconnected", (e) => {
pads.delete(e.gamepad.index);
console.log("Gamepad disconnected:", e.gamepad.id);
});
window.addEventListener("click", () => {
// ユーザー操作でAudioContext解禁(ブラウザ制限対応)
audioCtx.resume();
});
async function init() {
soundDon = await loadSound("don.mp3");
soundKa = await loadSound("ka.mp3");
setupCanvasInput();
resizeCanvas();
requestAnimationFrame(updateGamepads);
}
window.addEventListener("resize", resizeCanvas);
window.addEventListener("orientationchange", () => {
// 少し遅延してから再計算(回転が完了するまで wait)
setTimeout(resizeCanvas, 200);
});
init();
</script>
</body>
</html>
最後に
今回のように割と簡単なプログラムならChatGPTでもかなりいい感じのコードを出してくれる。
ただ、完璧ではないということを改めて認識。
プログラムを理解せず、動作だけ確認をして指示を出して修正も可能だと思うけど、修正の段階で処理が抜けたりする可能性もある。
なので、プログラムを理解せず作るのは結構大変だよねといった感想。
プログラムが書ける人なら結構時短もできそうではある。
あとは、どれだけプロンプトで的確に指示を出せるかも重要だね。
とはいえ今回のようにわりとふんわりとお願いしてもそこそこできるのはやっぱりすごい。
ただ、自分のコードではないので、なか読みづらい感じがある。
軸9で処理している太鼓の判定がまだちょっとおかしいので直さないと・・