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 +
requestAnimationFrameson suficientes para una animación fluida - El truco está en calcular el
targetAnglecorrectamente para garantizar el ganador -
easeOutcon exponente 4 da una desaceleración natural muy convincente - La Web Audio API permite sonido sin archivos externos
Top comments (0)