view source

JavaScript

document.title = 'ダーツ得点計算記録用ツール';

// canvas要素を作成して #demo に追加
const canvas = document.createElement('canvas');
canvas.width = 440;
canvas.height = 440;
document.getElementById('demo').appendChild(canvas);
const ctx = canvas.getContext('2d');

let count = 0;
let round = 1;
let roundScore = 0;
let totalScore = 501;
let leftScore = totalScore;

const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const hits = [];
// スコア順
const numbers = [20, 1, 18, 4, 13, 6, 10, 15, 2, 17,
                 3, 19, 7, 16, 8, 11, 14, 9, 12, 5];

// セクターの角度
const sectorAngle = 2 * Math.PI / 20;

// 各リングの半径
const outerRadius = 220;
const doubleOuter = 180;
const doubleInner = 170;
const tripleOuter = 110;
const tripleInner = 100;
const bullOuter = 10;
const bullInner = 5;

// 角度補正(20を真上に)
const offsetAngle = -Math.PI / 2 - (sectorAngle / 2);


let isPortrait = window.innerHeight > window.innerWidth;//縦向
if(isPortrait){
  document.body.classList.add('isPortrait');
}

function drawBoard(){

  // 外周黒円を描く
  ctx.beginPath();
  ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2);
  ctx.fillStyle = 'black';
  ctx.fill();
  ctx.closePath();

  // ダブルリングの描画 (赤黒交互)
  for (let i = 0; i < 20; i++) {
      const start = sectorAngle * i + offsetAngle;
      const end = start + sectorAngle;
      
      ctx.beginPath();
      ctx.arc(centerX, centerY, doubleOuter, start, end);
      ctx.arc(centerX, centerY, doubleInner, end, start, true);
      ctx.closePath();
      ctx.fillStyle = (i % 2 === 0) ? 'red' : 'blue';
      ctx.fill();
  }

  // ダブル~トリプルの間 (白黒交互)
  for (let i = 0; i < 20; i++) {
      const start = sectorAngle * i + offsetAngle;
      const end = start + sectorAngle;

      ctx.beginPath();
      ctx.arc(centerX, centerY, doubleInner, start, end);
      ctx.arc(centerX, centerY, tripleOuter, end, start, true);
      ctx.closePath();
      ctx.fillStyle = (i % 2 === 0) ? 'black' : 'white';
      ctx.fill();
  }

  // トリプルリングの描画 (赤黒交互)
  for (let i = 0; i < 20; i++) {
      const start = sectorAngle * i + offsetAngle;
      const end = start + sectorAngle;
      
      ctx.beginPath();
      ctx.arc(centerX, centerY, tripleOuter, start, end);
      ctx.arc(centerX, centerY, tripleInner, end, start, true);
      ctx.closePath();
      ctx.fillStyle = (i % 2 === 0) ? 'red' : 'blue';
      ctx.fill();
  }

  // トリプル~ブルの間 (白黒交互)
  for (let i = 0; i < 20; i++) {
      const start = sectorAngle * i + offsetAngle;
      const end = start + sectorAngle;

      ctx.beginPath();
      ctx.arc(centerX, centerY, tripleInner, start, end);
      ctx.arc(centerX, centerY, bullOuter, end, start, true);
      ctx.closePath();
      ctx.fillStyle = (i % 2 === 0) ? 'black' : 'white';
      ctx.fill();
  }

  // ブルズアイ外側(赤)
  ctx.beginPath();
  ctx.arc(centerX, centerY, bullOuter, 0, 2 * Math.PI);
  ctx.fillStyle = 'red';
  ctx.fill();
  ctx.closePath();

  // ブルズアイ内側(緑)
  ctx.beginPath();
  ctx.arc(centerX, centerY, bullInner, 0, 2 * Math.PI);
  ctx.fillStyle = 'green';
  ctx.fill();
  ctx.closePath();

  // スコア数字を描画 (白字)
  ctx.fillStyle = 'white';
  ctx.font = 'bold 18px Arial';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';

  for (let i = 0; i < 20; i++) {
      const angle = sectorAngle * i + offsetAngle + (sectorAngle / 2);
      const radius = 195;  // 数字を置く半径
      const x = centerX + radius * Math.cos(angle);
      const y = centerY + radius * Math.sin(angle);
      ctx.fillText(numbers[i], x, y);
  }
}


function getCorrectedCoords(e) {
  // CSSで適用されているtransform(scaleなど)を考慮
  const rect = canvas.getBoundingClientRect();//DOM挿入が先
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  let clientX, clientY;
  // マウスイベントまたはタッチイベントに対応
  if (e.touches) {
    clientX = e.touches[0].clientX;
    clientY = e.touches[0].clientY;
  } else {
    clientX = e.clientX;
    clientY = e.clientY;
  }
  // スケーリング補正された座標を返す
  return {
    x: (clientX - rect.left) * scaleX,
    y: (clientY - rect.top) * scaleY
  };
}

class DartHit {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.angle = 0;
    this.scale = 1.5;
    this.life = 0;
    this.maxLife = 120; // アニメーションフレーム数
    this.alive = true;
  }

  update() {
    this.angle += 0.2;
    this.scale *= 0.92;
    if(this.life > 40){
      this.scale = 0.4;
      this.angle = Math.PI / 4;
    }
    this.life++;
    if (this.life >= this.maxLife) this.alive = false;
  }

  draw() {
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(this.angle);
    ctx.scale(this.scale, this.scale);
    ctx.strokeStyle = 'yellow';
    ctx.lineWidth = 4;
    
    // 十字線
    ctx.beginPath();
    ctx.moveTo(-20, 0);
    ctx.lineTo(20, 0);
    ctx.moveTo(0, -20);
    ctx.lineTo(0, 20);
    ctx.stroke();

    ctx.beginPath();
    ctx.arc(0, 0, 4, 0, 2 * Math.PI);
    ctx.fillStyle = 'black';
    ctx.fill();
    ctx.closePath();

    ctx.restore();

  }
}





const textarea = document.createElement('textarea');
document.getElementById('demo').appendChild(textarea);

// クリックイベントでスコア計算
canvas.addEventListener('click', function(e) {
    const {x,y} = getCorrectedCoords(e);
    
    const clickX = x;
    const clickY = y;


    // DartHit インスタンスを生成
    hits.push(new DartHit(x, y));

    // 中心からの距離
    const dx = clickX - centerX;
    const dy = clickY - centerY;
    const distance = Math.sqrt(dx * dx + dy * dy);
    
    // クリック角度を計算
    let angle = Math.atan2(dy, dx); // -PI~PI
    angle -= offsetAngle;           // オフセット補正
    if (angle < 0) angle += 2 * Math.PI; // 正の値に直す
    
    // どのセクター番号か(0~19)
    const sectorIndex = Math.floor(angle / sectorAngle) % 20;
    const score = numbers[sectorIndex];
    
    let finalScore = 0;

    // どのリングに当たったか判定
    if (distance <= bullInner) {
        finalScore = 50; // インブル (50点)
    } else if (distance <= bullOuter) {
        finalScore = 25; // アウターブル (25点)
    } else if (distance >= doubleInner && distance <= doubleOuter) {
        finalScore = score * 2; // ダブル
    } else if (distance >= tripleInner && distance <= tripleOuter) {
        finalScore = score * 3; // トリプル
    } else if (distance <= doubleInner) {
        finalScore = score; // シングル (内側)
    } else {
        finalScore = 0; // 外周の黒いエリア(ミス)
    }
    
    console.log(`得点: ${finalScore} 点(${score} 点セクター)`);
    //alert(`得点: ${finalScore} 点!`);
    count++;
    roundScore += finalScore;

    let text = '';
    // 得点の一時保持
    let tempTotal = totalScore - finalScore;
    text = count + ' : ' + pad3(finalScore);
    leftScore -= finalScore;

    if(input501.checked){
      if (leftScore === 0 && isDoubleOrInnerBull(distance)) {
        text += `🎯`;
        gameOver = true;
      } else if (leftScore < 0 || leftScore === 1) {
        // バースト(無効)
        leftScore += finalScore;
        text += `❌ : ${leftScore}`;
      } else {
        text +=  ' 残: ' + pad3(leftScore);
        roundScore += finalScore;
      }
    }

    textarea.value += text + '\n';

    //textarea.value += count +' : '+ finalScore + '\n';
    if(count%3===0){
      textarea.value += '----------------'+ '\n';
      textarea.value += 'round '+ round+ ' : ' +roundScore+ '\n';
      round++;
      count=0;
      textarea.value += '================'+ '\n';
    }

    textarea.scrollTop = textarea.scrollHeight;
});

function pad3(num) {
  return String(num).padStart(3, ' ');
}
function isDoubleOrInnerBull(distance) {
  return (distance <= bullInner) || (distance >= doubleInner && distance <= doubleOuter);
}

function deleteLastLine(n=1) {
  let lines = textarea.value.split('\n');
  lines.pop();
  for(i=0;i<n;i++){
    if (lines.length > 0) {
      lines.pop();
    }
  }
  textarea.value = lines.join('\n') + '\n';
}


function loop() {

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBoard();

  for (let i = hits.length - 1; i >= 0; i--) {
    const hit = hits[i];
    hit.update();
    hit.draw();
    if (!hit.alive) hits.splice(i, 1);
  }
  requestAnimationFrame(loop);
}

loop();

const input501 = document.createElement('input');
const label501 = document.createElement('label');
label501.textContent = '501 rule';
label501.appendChild(input501);
input501.type = 'checkbox';
document.getElementById('demo').appendChild(label501);

const button = document.createElement('button');
button.textContent = 'Redo';
document.getElementById('demo').appendChild(button);
button.addEventListener('click', function(e) {
  if(count === 0){
    deleteLastLine(4);
    if(round>1){
      round--;
    }
    if(count<0){
      count=2;
    }
  } else {
    deleteLastLine();
    
  }
});

CSS

body{
  overflow:hidden;
  background-color:#ffffff;
  font-family:monospace;
}

#demo{
  text-align:center;
  position:relative;
}

canvas{
  display: block;
  margin: auto;
  width: 100vmin;
  aspect-ratio: 1 / 1;
  cursor:crosshair;
}

textarea{
  position:fixed;
  top:0;
  right:0;
  width:180px;
  height:100%;
  font-size:18px;
  padding-bottom:80px;
  box-sizing: border-box;
}
.isPortrait textarea{
  top:70%;
  height:30%;
}

button{
  position:fixed;
  bottom:0;
  right:0;
  width:160px;
  height:32px;
  z-index:3;
  font-size:18px;
}
label{
  position:fixed;
  bottom:32px;
  right:0;
  width:160px;
  height:32px;
  z-index:3;
  font-size:18px;
}

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-js/js-darts/

ABOUT

hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

仕様変更、API廃止等、実験途中で動かないページもあります。