Simulación de una ruleta en JavaScript, gira la ruleta hasta que pare en un número aleatorio.
Quizá alguna vez hayas visto por televisión un programa llamado La Ruleta de la Fortuna o La Ruleta de la Suerte. El programa consiste en hacer girar una rueda, similar a una ruleta, con la mano, la cual da vueltas hasta detenerse. El lugar donde se detiene indica un premio o una cantidad de dinero que el jugador gana o pierde según el reglamento. El juego puede ser divertido y emocionante, y hoy queremos simular esta ruleta en JavaScript.
Vamos a utilizar el elemento canvas, que será el espacio donde se dibujará la ruleta, los estilos CSS para maquetar los elementos de HTML y código JavaScript para añadir interactividad, así como la simulación física del giro de la ruleta.
Añadiremos elementos HTML con ids varios para luego referenciarlos desde el código JavaScript. También definimos el tamaño de la ruleta, en nuestro caso de 400px de ancho y alto. Finalmente el elemento #spin
que usaremos como botón para iniciar la simulación:
<div id="spinWheel"> <canvas id="wheel" width="400" height="400"></canvas> <div id="spin"></div></div>
Crearemos una clase JavaScript SpinWheel
para encapsular toda la funcionalidad de la ruleta. El código se explica por si mismo, por lo que sólo destacaremos lo más importante.
El argumento friction
es la fricción de la ruleta, lo que tardará en frenar, puedes probar entre varios valores: 0.995 (suave), 0.99 (medio), 0.98 (duro).
Hay una función updateFrame
que actualiza el canvas rotando el elemento <canvas>
.
class SpinWheel { constructor({ canvasSelector, buttonSelector, sectors, friction = 0.991 }) { this.sectors = sectors this.friction = friction this.canvas = document.querySelector(canvasSelector) this.context = this.canvas.getContext('2d') this.button = document.querySelector(buttonSelector) this.diameter = this.canvas.width this.radius = this.diameter / 2 this.totalSectors = sectors.length this.arcAngle = (2 * Math.PI) / this.totalSectors this.angle = 0 this.angularVelocity = 0 this.spinButtonClicked = false this.events = new EventEmitter() this.init() }
get currentIndex() { return ( Math.floor( this.totalSectors - (this.angle / (2 * Math.PI)) * this.totalSectors ) % this.totalSectors ) }
drawSector(sector, index) { const startAngle = this.arcAngle * index this.context.save()
this.context.beginPath() this.context.fillStyle = sector.color this.context.moveTo(this.radius, this.radius) this.context.arc( this.radius, this.radius, this.radius, startAngle, startAngle + this.arcAngle ) this.context.lineTo(this.radius, this.radius) this.context.fill()
this.context.translate(this.radius, this.radius) this.context.rotate(startAngle + this.arcAngle / 2) this.context.textAlign = 'right' this.context.fillStyle = sector.textColor this.context.font = "bold 30px 'Lato', sans-serif" this.context.fillText(sector.label, this.radius - 10, 10)
this.context.restore() }
rotateCanvas() { const currentSector = this.sectors[this.currentIndex] this.canvas.style.transform = `rotate(${this.angle - Math.PI / 2}rad)` }
updateFrame() { if (!this.angularVelocity && this.spinButtonClicked) { const winningSector = this.sectors[this.currentIndex] this.events.emit('finishSpinning', winningSector) this.spinButtonClicked = false return }
this.angularVelocity *= this.friction if (this.angularVelocity < 0.002) this.angularVelocity = 0
this.angle += this.angularVelocity this.angle %= 2 * Math.PI
this.rotateCanvas() }
startSimulation() { const animate = () => { this.updateFrame() requestAnimationFrame(animate) } animate() }
init() { this.sectors.forEach((sector, index) => this.drawSector(sector, index)) this.rotateCanvas() this.startSimulation()
this.button.addEventListener('click', () => { if (!this.angularVelocity) this.angularVelocity = SpinWheel.randomInRange(0.25, 0.45) this.spinButtonClicked = true }) }
static randomInRange(min, max) { return Math.random() * (max - min) + min }}
También crearemos una clase EventEmitter
para permitir suscribirse desde fuera de la clase y ser notificados cuando termine el giro.
class EventEmitter { constructor() { this.listeners = {} }
on(eventName, callback) { if (!this.listeners[eventName]) { this.listeners[eventName] = [] } this.listeners[eventName].push(callback) }
emit(eventName, ...args) { if (this.listeners[eventName]) { this.listeners[eventName].forEach(callback => callback(...args)) } }}
Finalmente la definición de los sectores de la ruleta y el inicio de la simulación:
const wheelSectors = [ { color: '#ffcd01', textColor: '#b20e12', label: 50 }, { color: '#685ca2', textColor: '#ffffff', label: 100 }, { color: '#029ede', textColor: '#ffffff', label: 150 }, { color: '#a7d02a', textColor: '#ffffff', label: 200 }, { color: '#26cda2', textColor: '#ffffff', label: 1000 }, { color: '#8f3389', textColor: '#fcce03', label: 500 }, { color: '#232026', textColor: '#ffffff', label: 550 }, { color: '#e50304', textColor: '#ffffff', label: 600 }, { color: '#eb7108', textColor: '#ffffff', label: 650 }, { color: '#ffcd01', textColor: '#b20e12', label: 700 }, { color: '#685ca4', textColor: '#ffffff', label: 10 }, { color: '#049dde', textColor: '#ffffff', label: 800 }, { color: '#a7d02a', textColor: '#ffffff', label: 2000 }, { color: '#26cda2', textColor: '#ffffff', label: 60 }, { color: '#903389', textColor: '#fcce03', label: 995 }, { color: '#232026', textColor: '#ffffff', label: 500 }, { color: '#e50403', textColor: '#ffffff', label: 10000 }, { color: '#eb7108', textColor: '#ffffff', label: 900 }]
const spinWheel = new SpinWheel({ canvasSelector: '#wheel', buttonSelector: '#spin', sectors: wheelSectors})
spinWheel.events.on('finishSpinning', sector => { console.log(`Felicidades! Ha salido el número ${sector.label}`)})
Los estilos son bastante sencillos, solamente destacar la regla CSS #spinWheel::after
que sirve como indicador del sector seleccionado de la ruleta.
* { margin: 0; padding: 0; box-sizing: border-box;}
body { height: 100vh; display: grid; place-items: center; margin: 0; height: 100vh;}
#spinWheel { display: inline-block; position: relative; overflow: hidden;}
#spinWheel::after { content: ""; position: absolute; top: 0px; left: calc(50% - 10px); width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 20px solid #feeb69; -webkit-filter: drop-shadow(1px 2px 2px rgba(0, 0, 0, .5)); filter: drop-shadow(1px 2px 2px rgba(0, 0, 0, .5));}
#wheel { display: block;}
#spin { font: 1.1em/0 "Lato", sans-serif; user-select: none; cursor: pointer; display: flex; justify-content: center; align-items: center; position: absolute; top: 60%; left: 60%; width: 10%; height: 10%; margin: -15%; background: #333; color: #fff; box-shadow: 0 0 0 4px currentColor, 0 0px 10px 5px rgba(0, 0, 0, 0.6); border-radius: 50%; transition: 0.8s;}
.row { display: flex; flex-direction: row; flex-wrap: wrap; width: 100%;}
.column { display: flex; flex-direction: column; flex-basis: 100%; flex: 1;}
A continuación puedes ver el ejemplo completo:
<!doctype html><html lang="en">
<head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Ruleta de la fortuna</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; }
body { height: 100vh; display: grid; place-items: center; margin: 0; height: 100vh; }
#spinWheel { display: inline-block; position: relative; overflow: hidden; }
#spinWheel::after { content: ""; position: absolute; top: 0px; left: calc(50% - 10px); width: 0; height: 0; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 20px solid #feeb69; -webkit-filter: drop-shadow(1px 2px 2px rgba(0, 0, 0, .5)); filter: drop-shadow(1px 2px 2px rgba(0, 0, 0, .5)); }
#wheel { display: block; }
#spin { font: 1.1em/0 "Lato", sans-serif; user-select: none; cursor: pointer; display: flex; justify-content: center; align-items: center; position: absolute; top: 60%; left: 60%; width: 10%; height: 10%; margin: -15%; background: #333; color: #fff; box-shadow: 0 0 0 4px currentColor, 0 0px 10px 5px rgba(0, 0, 0, 0.6); border-radius: 50%; transition: 0.8s; }
.row { display: flex; flex-direction: row; flex-wrap: wrap; width: 100%; }
.column { display: flex; flex-direction: column; flex-basis: 100%; flex: 1; } </style></head>
<body> <div id="spinWheel"> <canvas id="wheel" width="400" height="400"></canvas> <div id="spin"></div> </div> <script> class SpinWheel { constructor({ canvasSelector, buttonSelector, sectors, friction = 0.991 }) { this.sectors = sectors; this.friction = friction; this.canvas = document.querySelector(canvasSelector); this.context = this.canvas.getContext("2d"); this.button = document.querySelector(buttonSelector); this.diameter = this.canvas.width; this.radius = this.diameter / 2; this.totalSectors = sectors.length; this.arcAngle = (2 * Math.PI) / this.totalSectors; this.angle = 0; this.angularVelocity = 0; this.spinButtonClicked = false; this.events = new EventEmitter(); this.init(); }
get currentIndex() { return Math.floor(this.totalSectors - (this.angle / (2 * Math.PI)) * this.totalSectors) % this.totalSectors; }
drawSector(sector, index) { const startAngle = this.arcAngle * index; this.context.save();
// Draw sector this.context.beginPath(); this.context.fillStyle = sector.color; this.context.moveTo(this.radius, this.radius); this.context.arc(this.radius, this.radius, this.radius, startAngle, startAngle + this.arcAngle); this.context.lineTo(this.radius, this.radius); this.context.fill();
// Add label this.context.translate(this.radius, this.radius); this.context.rotate(startAngle + this.arcAngle / 2); this.context.textAlign = "right"; this.context.fillStyle = sector.textColor; this.context.font = "bold 30px 'Lato', sans-serif"; this.context.fillText(sector.label, this.radius - 10, 10);
this.context.restore(); }
rotateCanvas() { const currentSector = this.sectors[this.currentIndex]; this.canvas.style.transform = `rotate(${this.angle - Math.PI / 2}rad)`; }
updateFrame() { // Stop spinning and emit event if (!this.angularVelocity && this.spinButtonClicked) { const winningSector = this.sectors[this.currentIndex]; this.events.emit("finishSpinning", winningSector); this.spinButtonClicked = false; return; }
// Apply friction to reduce angular velocity this.angularVelocity *= this.friction; if (this.angularVelocity < 0.002) this.angularVelocity = 0; // Stop completely
// Update angle this.angle += this.angularVelocity; this.angle %= 2 * Math.PI; // Normalize angle
this.rotateCanvas(); }
startSimulation() { const animate = () => { this.updateFrame(); requestAnimationFrame(animate); }; animate(); }
init() { this.sectors.forEach((sector, index) => this.drawSector(sector, index)); this.rotateCanvas(); this.startSimulation();
this.button.addEventListener("click", () => { if (!this.angularVelocity) this.angularVelocity = SpinWheel.randomInRange(0.25, 0.45); this.spinButtonClicked = true; }); }
static randomInRange(min, max) { return Math.random() * (max - min) + min; } }
class EventEmitter { constructor() { this.listeners = {}; }
on(eventName, callback) { if (!this.listeners[eventName]) { this.listeners[eventName] = []; } this.listeners[eventName].push(callback); }
emit(eventName, ...args) { if (this.listeners[eventName]) { this.listeners[eventName].forEach(callback => callback(...args)); } } }
// Define sectors const wheelSectors = [ { color: "#ffcd01", textColor: "#b20e12", label: 50 }, { color: "#685ca2", textColor: "#ffffff", label: 100 }, { color: "#029ede", textColor: "#ffffff", label: 150 }, { color: "#a7d02a", textColor: "#ffffff", label: 200 }, { color: "#26cda2", textColor: "#ffffff", label: 1000 }, { color: "#8f3389", textColor: "#fcce03", label: 500 }, { color: "#232026", textColor: "#ffffff", label: 550 }, { color: "#e50304", textColor: "#ffffff", label: 600 }, { color: "#eb7108", textColor: "#ffffff", label: 650 }, { color: "#ffcd01", textColor: "#b20e12", label: 700 }, { color: "#685ca4", textColor: "#ffffff", label: 10 }, { color: "#049dde", textColor: "#ffffff", label: 800 }, { color: "#a7d02a", textColor: "#ffffff", label: 2000 }, { color: "#26cda2", textColor: "#ffffff", label: 60 }, { color: "#903389", textColor: "#fcce03", label: 995 }, { color: "#232026", textColor: "#ffffff", label: 500 }, { color: "#e50403", textColor: "#ffffff", label: 10000 }, { color: "#eb7108", textColor: "#ffffff", label: 900 }, ];
// Initialize the wheel const spinWheel = new SpinWheel({ canvasSelector: "#wheel", buttonSelector: "#spin", sectors: wheelSectors, });
// Listen for spin end event spinWheel.events.on("finishSpinning", (sector) => { console.log(`Felicidades! Número ${sector.label}`); });
</script></body>
</html>