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?
CodePudding user response:
There are a couple things you need to do to get this working in a react context:
The
spinWheel
method should return start/stop methods for both starting the spin and stopping. The stop method should also clean up any artifacts such as event handlers and whatnot.const startSpin = () => { if (isSpinning) return; isSpinning = true; isAccelerating = true; angVelMax = rand(0.25, 0.4); }; const stopSpin = () => { isSpinning = false; isAccelerating = false; } elSpin.addEventListener("click", startSpin); const cleanup = () => { elSpin.removeEventListener("click", startSpin); }; ... return { startSpin, stopSpin, cleanup };
As mentioned in a comment, you need to make sure your effect has a "cleanup" function which stops the wheel (see this link). This is so that the setup → cleanup → setup sequence works in development as described on that page.
useEffect(() => { const ctx = wheel.current.getContext("2d"); const elSpin = spin.current; const { startSpin, stopSpin, cleanup } = spinWheel(ctx, elSpin); startSpin(); return () => { stopSpin(); cleanup(); } }, []);
Here's a link to a working sandbox: https://codesandbox.io/s/spin-wheel-forked-tb42zf?file=/src/components/SpinWheel/SpinWheel.js:546-826