VOOZH about

URL: https://dev.to/toolrapido/como-crear-una-ruleta-giratoria-con-canvas-api-en-javascript-fc4

⇱ Cómo crear una ruleta giratoria con Canvas API en JavaScript - DEV Community


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>
const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d');
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const radius = cx - 10;

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();
}

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();
 }
}

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;
}

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);
}

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);
}

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