どっちが多い?

どの図形が一番多い?

view source

JavaScript

document.title = 'どっちが多い?';
//どの図形が一番多い?

var canvas = document.createElement('canvas');
var w = document.body.clientWidth;
var h = document.body.clientHeight;
var min = Math.min(w,h);

canvas.setAttribute('width',w);
canvas.setAttribute('height',h);
let demo = document.getElementById('demo');
demo.appendChild(canvas);

var ctx = canvas.getContext('2d');

var buttonContainer = document.createElement('div');
buttonContainer.id = 'buttonContainer';
demo.appendChild(buttonContainer);

document.body.style.overflow = 'hidden';

const textCanvas = document.createElement('canvas');
textCanvas.width = w;
textCanvas.height = h;
textCanvas.style.position = 'absolute';
textCanvas.style.left = '0';
textCanvas.style.top = '0';
textCanvas.style.pointerEvents = 'none'; // ボタン操作の邪魔をしない
demo.appendChild(textCanvas);

const textCtx = textCanvas.getContext('2d');

const allShapeTypes = [
  { type: 'circle', symbol: '●' },
  { type: 'square', symbol: '■' },
  { type: 'triangle', symbol: '▲' },
//  { type: 'diamond', symbol: '◆' }
];

const bottomMargin = 60;
let textTimeoutId = null; //

let stage = 1;
let shapes = [];

let shapeColorMap = {};

function assignColors(shapeTypes) {
  const numShapes = shapeTypes.length;
  const startHue = Math.floor(Math.random() * 360); // ランダム起点
  const interval = 360 / numShapes;

  shapeColorMap = {};
  shapeTypes.forEach((shape, i) => {
    const hue = (startHue + i * interval) % 360;
    const color = `hsl(${hue}, 70%, 50%)`; // 鮮やかで中明度
    shapeColorMap[shape.type] = color;
  });
}

function drawTextOnCanvas(text, callback = null) {
  if (textTimeoutId !== null) {
    clearTimeout(textTimeoutId);
    textTimeoutId = null;
  }

  const size = min / 12;
  textCtx.clearRect(0, 0, textCanvas.width, textCanvas.height); // ← テキストだけ消す
  textCtx.fillStyle = 'black';
  textCtx.font = 'bold ' + size + 'px sans-serif';
  textCtx.textAlign = 'center';
  textCtx.fillText(text, textCanvas.width / 2, textCanvas.height - 120);

  textTimeoutId = setTimeout(() => {
    textCtx.clearRect(0, 0, textCanvas.width, textCanvas.height); // ← テキスト消去のみ
    if (callback) callback();
  }, 3000);
}



function drawTriangle(x, y, size, color) {
  ctx.beginPath();
  ctx.moveTo(x, y - size);
  ctx.lineTo(x - size, y + size);
  ctx.lineTo(x + size, y + size);
  ctx.closePath();
  ctx.fillStyle = color;
  ctx.fill();
}

function drawDiamond(x, y, size, color) {
  ctx.beginPath();
  ctx.moveTo(x, y - size);
  ctx.lineTo(x + size, y);
  ctx.lineTo(x, y + size);
  ctx.lineTo(x - size, y);
  ctx.closePath();
  ctx.fillStyle = color;
  ctx.fill();
}

function drawShape(s) {
  ctx.beginPath();
  switch (s.type) {
    case 'circle':
      ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
      ctx.fillStyle = s.color;
      ctx.fill();
      break;
    case 'square':
      ctx.fillStyle = s.color;
      ctx.fillRect(s.x - s.size, s.y - s.size, s.size * 2, s.size * 2);
      break;
    case 'triangle':
      drawTriangle(s.x, s.y, s.size, s.color);
      break;
    case 'diamond':
      drawDiamond(s.x, s.y, s.size, s.color);
      break;
  }
}

function isOverlapping(x, y, size) {
  for (let s of shapes) {
    let dx = s.x - x;
    let dy = s.y - y;
    let dist = Math.sqrt(dx * dx + dy * dy);
    if (dist < size * 1.2) return true;
  }
  return false;
}


function generateUniqueCounts(types, baseCount) {
  let countValues = [];

  // 基準値±1をランダムにシャッフル
  const possible = [baseCount - 1, baseCount, baseCount + 1];
  while (countValues.length < types.length) {
    let val = possible[Math.floor(Math.random() * possible.length)];
    if (!countValues.includes(val)) {
      countValues.push(val);
    }
  }

  const counts = {};
  types.forEach((shape, i) => {
    counts[shape.type] = countValues[i];
  });

  return counts;
}

function generateShapes(shapeTypes) {
  shapes = [];
  ctx.clearRect(0, 0, canvas.width, canvas.height);


  const sizeBase = min * 0.1;
  const size = Math.max(min * 0.03, sizeBase - stage * (min * 0.01));

  const usableHeight = canvas.height - bottomMargin;

  const cellSize = size * 2.4; // セル間隔 = 図形サイズの約3倍
  const cols = Math.floor(canvas.width / (cellSize));
  const rows = Math.floor(usableHeight / cellSize);

  const totalCells = cols * rows;

  let baseCount = 4 + stage - shapeTypes.length;
  const maxBase = Math.floor(totalCells / shapeTypes.length);  // 1形状あたりの最大数
  baseCount = Math.min(baseCount, maxBase, 9); // さらに9個上限も維持
  const counts = generateUniqueCounts(shapeTypes, baseCount);
  // グリッド座標の一覧を作ってシャッフル
  let gridPositions = [];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      let x = c * cellSize + cellSize / 2;
      let y = r * cellSize + cellSize / 2;
      gridPositions.push({x, y});
    }
  }
  gridPositions.sort(() => Math.random() - 0.5); // ランダム配置

  let gridIndex = 0;

  shapeTypes.forEach(shape => {
    const count = counts[shape.type];
    const color = shapeColorMap[shape.type];
    for (let i = 0; i < count; i++) {
      if (gridIndex >= gridPositions.length) break; // セルが足りないときは打ち切り
      let base = gridPositions[gridIndex++];
      let jitter = cellSize * 0.3; // ゆらぎの大きさ

      let x = base.x + (Math.random() - 0.5) * jitter;
      let y = base.y + (Math.random() - 0.5) * jitter;

      shapes.push({ x, y, type: shape.type, color: color, size });
    }
  });

  shapes.forEach(drawShape);
  if(stage < 3){
    drawTextOnCanvas(document.title);
  } else {
    drawTextOnCanvas(stage);
  }
  return counts;
}


function getMostCommon(counts) {
  let max = -1;
  let top = null;
  for (let type in counts) {
    if (counts[type] > max) {
      max = counts[type];
      top = type;
    }
  }
  return top;
}

function createButtons(shapeTypes, correctType) {
  buttonContainer.innerHTML = '';

  let buttons = [];
  shapeTypes.forEach(shape => {
    const btn = document.createElement('button');
    btn.textContent = shape.symbol;
    const color = shapeColorMap[shape.type];
    btn.style.color = color;
    btn.onclick = () => {
      buttons.forEach(b => b.disabled = true);
      if (shape.type === correctType) {
        drawTextOnCanvas('正解!', () => {
          stage++;
          startStage();
        });
      } else {
        drawTextOnCanvas('不正解…', () => {
          if(stage>4){
            stage--;
          }
          startStage();
        });
      }
    };
    buttons.push(btn);
    buttonContainer.appendChild(btn);
  });
}

function startStage() {
  let addShapes;

  if (stage <= 3) {
    addShapes = 2;
  } else if (stage <= 6) {
    addShapes = 3;
  } else {
    // stage 7以降は交互にする(7→2つ, 8→3つ, 9→2つ, ...)
    addShapes = stage % 2 === 1 ? 2 : 3;
  }

  const currentShapes = allShapeTypes.slice(0, Math.min(allShapeTypes.length, addShapes));
  assignColors(currentShapes);
  const counts = generateShapes(currentShapes);
  const correct = getMostCommon(counts);
  createButtons(currentShapes, correct);
}

startStage();

CSS

#buttonContainer {
  position: absolute;
  bottom: 0;
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 0;
}
#buttonContainer button {
  flex: 1;
  font-size: 2em;
  padding: 12px 0;
  border: 1px solid #999;
  background: rgba(255,255,255, 0.6);
  cursor: pointer;
  border-radius: 0;
}

#buttonContainer button:hover{
  background: #aaaaaa;
}

HTML

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

view-source:https://hi0a.com/game/most-common-shape/

ABOUT

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

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

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

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

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

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

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