La Ruleta de la Fortuna en JavaScript

Simulación de una ruleta en JavaScript, gira la ruleta hasta que pare en un número aleatorio.

ruleta-de-la-fortuna-javascript

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.

Código HTML

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>

Código JavaScript

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

Estilos CSS

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

Resultado final

A continuación puedes ver el ejemplo completo: