DEV Community

ToolRapido
ToolRapido

Posted on

Cómo crear una ruleta giratoria con Canvas API en JavaScript

Una ruleta giratoria parece compleja pero se puede construir con Canvas 2D puro en menos de 200 líneas. En este tutorial explico cada parte del algoritmo.

La estructura básica

<canvas id="wheel" width="400" height="400"></canvas>
<button id="spin">Girar</button>
Enter fullscreen mode Exit fullscreen mode
const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d');
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const radius = cx - 10;
Enter fullscreen mode Exit fullscreen mode

Dibujar los segmentos

Cada segmento es un arco de círculo. Con N elementos, cada segmento ocupa 2π/N radianes.

function drawWheel(items, colors, currentAngle) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const n = items.length;
  const segAngle = (2 * Math.PI) / n;

  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(currentAngle);

  for (let i = 0; i < n; i++) {
    const start = -Math.PI / 2 + i * segAngle;
    const end = start + segAngle;

    // Segmento
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.arc(0, 0, radius, start, end);
    ctx.closePath();
    ctx.fillStyle = colors[i];
    ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.7)';
    ctx.lineWidth = 2;
    ctx.stroke();

    // Texto
    ctx.save();
    ctx.rotate(start + segAngle / 2);
    ctx.textAlign = 'right';
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText(items[i], radius - 12, 0);
    ctx.restore();
  }

  ctx.restore();
}
Enter fullscreen mode Exit fullscreen mode

El algoritmo de giro con easing

La física del giro tiene tres fases: aceleración, velocidad máxima y desaceleración. La función easeOut simula la fricción.

const SPIN_DURATION = 5500; // ms
const MIN_SPINS = 6;

let animStart = null;
let animStartAngle = 0;
let targetAngle = 0;
let currentAngle = 0;

function easeOut(t) {
  return 1 - Math.pow(1 - t, 4);
}

function animate(timestamp) {
  if (!animStart) animStart = timestamp;

  const elapsed = timestamp - animStart;
  const progress = Math.min(elapsed / SPIN_DURATION, 1);

  currentAngle = animStartAngle + (targetAngle - animStartAngle) * easeOut(progress);
  drawWheel(items, colors, currentAngle);

  if (progress < 1) {
    requestAnimationFrame(animate);
  } else {
    onSpinEnd();
  }
}
Enter fullscreen mode Exit fullscreen mode

Calcular el ganador

El puntero está fijo arriba (en -π/2). Para saber qué segmento apunta arriba al terminar el giro:

function getWinnerIndex(angle, n) {
  const segAngle = (2 * Math.PI) / n;
  const normalized = (((-angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI));
  return Math.floor(normalized / segAngle) % n;
}
Enter fullscreen mode Exit fullscreen mode

Calcular el ángulo objetivo

Para garantizar que la ruleta se detiene en un ganador concreto (o aleatorio):

function startSpin(items) {
  const n = items.length;
  const segAngle = (2 * Math.PI) / n;
  const winnerIndex = Math.floor(Math.random() * n);

  // Ángulo donde debe quedar el centro del segmento ganador apuntando arriba
  const f = 0.2 + Math.random() * 0.6; // posición aleatoria dentro del segmento
  const targetMod = 2 * Math.PI - (winnerIndex + f) * segAngle;
  const currentMod = ((currentAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
  const step = (targetMod - currentMod + 2 * Math.PI) % (2 * Math.PI);
  const extraSpins = (MIN_SPINS + Math.floor(Math.random() * 5)) * 2 * Math.PI;

  animStartAngle = currentAngle;
  targetAngle = currentAngle + extraSpins + step;
  animStart = null;

  requestAnimationFrame(animate);
}
Enter fullscreen mode Exit fullscreen mode

El sonido de tick

Usando la Web Audio API puedes añadir el sonido de tick cuando la ruleta pasa por cada segmento, sin archivos de audio externos:

function playTick() {
  const ac = new AudioContext();
  const osc = ac.createOscillator();
  const gain = ac.createGain();

  osc.connect(gain);
  gain.connect(ac.destination);
  osc.frequency.value = 680;
  osc.type = 'triangle';
  gain.gain.setValueAtTime(0.05, ac.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.07);
  osc.start(ac.currentTime);
  osc.stop(ac.currentTime + 0.07);
}
Enter fullscreen mode Exit fullscreen mode

Ver el resultado final

Si quieres ver una implementación completa y funcional con presets, modo eliminar ganador e historial, puedes probar esta ruleta aleatoria online, construida exactamente con las técnicas de este artículo.

Conclusión

  • Canvas 2D + requestAnimationFrame son suficientes para una animación fluida
  • El truco está en calcular el targetAngle correctamente para garantizar el ganador
  • easeOut con exponente 4 da una desaceleración natural muy convincente
  • La Web Audio API permite sonido sin archivos externos

Top comments (0)