I've been trying to implement Roco C. Buljan's JS Spinning Wheel in React and I'm completely stuck at this particular point. The intended behavior is that the wheel spins once and after stopping displays the pointed sector.
Here's what my code looks like:
import React, { useEffect, useRef } from "react";
import "./SpinWheel.css";
function SpinWheel() {
const sectors = [
{ color: "#f82", label: "Stack" },
{ color: "#0bf", label: "10" },
{ color: "#fb0", label: "200" },
{ color: "#0fb", label: "50" },
{ color: "#b0f", label: "100" },
{ color: "#f0b", label: "5" },
];
// Generate random float in range min-max:
const rand = (m, M) => Math.random() * (M - m) + m;
const tot = sectors.length;
const wheel = useRef(null);
const spin = useRef(null);
useEffect(() => {
const ctx = wheel.current.getContext("2d");
const elSpin = spin.current;
spinWheel(ctx, elSpin);
}, []);
function spinWheel(ctx, elSpin) {
const dia = ctx.canvas.width;
const rad = dia / 2;
const PI = Math.PI;
const TAU = 2 * PI;
const arc = TAU / sectors.length;
const friction = 0.991; // 0.995=soft, 0.99=mid, 0.98=hard
const angVelMin = 0.002; // Below that number will be treated as a stop
let angVelMax = 0; // Random ang.vel. to acceletare to
let angVel = 0; // Current angular velocity
let ang = 0; // Angle rotation in radians
let isSpinning = false;
let isAccelerating = false;
//* Get index of current sector */
const getIndex = () => Math.floor(tot - (ang / TAU) * tot) % tot;
//* Draw sectors and prizes texts to canvas */
const drawSector = (sector, i) => {
const ang = arc * i;
ctx.save();
// COLOR
ctx.beginPath();
ctx.fillStyle = sector.color;
ctx.moveTo(rad, rad);
ctx.arc(rad, rad, rad, ang, ang + arc);
ctx.lineTo(rad, rad);
ctx.fill();
// TEXT
ctx.translate(rad, rad);
ctx.rotate(ang + arc / 2);
ctx.textAlign = "right";
ctx.fillStyle = "#fff";
ctx.font = "bold 30px sans-serif";
ctx.fillText(sector.label, rad - 10, 10);
//
ctx.restore();
};
//* CSS rotate CANVAS Element */
const rotate = () => {
const sector = sectors[getIndex()];
ctx.canvas.style.transform = `rotate(${ang - PI / 2}rad)`;
elSpin.textContent = !angVel ? sector.label : "SPIN";
elSpin.style.background = sector.color;
};
const frame = () => {
if (!isSpinning) return;
if (angVel >= angVelMax) isAccelerating = false;
// Accelerate
if (isAccelerating) {
angVel ||= angVelMin; // Initial velocity kick
angVel *= 1.06; // Accelerate
}
// Decelerate
else {
isAccelerating = false;
angVel *= friction; // Decelerate by friction
// SPIN END:
if (angVel < angVelMin) {
isSpinning = false;
angVel = 0;
}
}
ang += angVel; // Update angle
ang %= TAU; // Normalize angle
rotate(); // CSS rotate!
};
const engine = () => {
frame();
requestAnimationFrame(engine);
};
elSpin.addEventListener("click", () => {
if (isSpinning) return;
isSpinning = true;
isAccelerating = true;
angVelMax = rand(0.25, 0.4);
});
// INIT!
sectors.forEach(drawSector);
rotate(); // Initial rotation
engine(); // Start engine!
}
return (
<div id="wheelOfFortune">
<canvas id="wheel" ref={wheel} width="300" height="300"></canvas>
<div id="spin" ref={spin}>
SPIN asd asd asd as dasd as dasd asd asd as d
</div>
</div>
);
}
export default SpinWheel;
And here's the sandbox link
The issue is that if I console.log the useEffect block, I can see that it executes twice at the start which is not what I intend. I tried putting an empty array as the dependency array but it still doesn't work for me. The second issue is that the wheel supposedly stops and then spins a small amount yet again, resulting in two final sector outputs. Again, not what I intend. I can only guess that it is rendering more than once but I'm not sure. Any ideas what I'm doing wrong?