I made a spinning wheel using chart.js pie chart and it works flawlessly on pc – basically am drawing a pie chart with datalabels for each option, and then spinning the chart accordingly. The problem comes when i try it on my mobile phone – the animation is very choppy.
"use client";
import { useEffect, useRef, useState } from "react";
import { Chart, registerables } from "chart.js";
import ChartDataLabels from "chartjs-plugin-datalabels";
Chart.register(...registerables, ChartDataLabels);
const WheelChartMobile = ({ segments, segColors, winningSegment, onFinished }) => {
const wheel = useRef(null);
const spinBtn = useRef(null);
const [chart, setChart] = useState(null);
const rotationValues = segments.map((_, index) => {
return {
minDegree: (index * 360) / segments.length,
maxDegree: ((index + 1) * 360) / segments.length,
value: segments[index],
};
});
console.table(rotationValues);
const data = new Array(rotationValues.length).fill(1);
const pieColors = segColors;
useEffect(() => {
if (!wheel.current) return;
const ctx = wheel.current.getContext("2d");
const chartInstance = new Chart(ctx, {
plugins: [
ChartDataLabels,
{
id: "customImageLabels",
afterDraw: (chart) => {
const {
ctx,
chartArea: { left, right, top, bottom, width, height },
} = chart;
const centerX = (left + right) / 2;
const centerY = (top + bottom) / 2;
const radius = Math.min(width, height) / 2;
chart.data.labels.forEach((label, index) => {
const allowed = ["bones", "zrpy", "xrp"];
if (!allowed.includes(label.split(" ")[1])) return;
const angle =
(chart.getDatasetMeta(0).data[index].startAngle +
chart.getDatasetMeta(0).data[index].endAngle) /
2;
const img = new Image();
if (label.endsWith("bones")) {
const imageX = centerX + Math.cos(angle) * radius * 0.35;
const imageY = centerY + Math.sin(angle) * radius * 0.35;
img.src = "/images/wheel/bone.png";
img.onload = () => {
ctx.drawImage(img, imageX - 10, imageY - 10, 20, 20);
};
ctx.drawImage(img, imageX - 10, imageY - 10, 20, 20); //image isnt drawn while wheel is spinning, thats why i draw the image a second time even after using onload
} else if (label.endsWith("zrpy")) {
const imageX = centerX + Math.cos(angle) * radius * 0.37;
const imageY = centerY + Math.sin(angle) * radius * 0.37;
img.src = "/images/zerpaay.png";
img.onload = () => {
ctx.drawImage(img, imageX - 11, imageY - 10, 23, 23);
};
ctx.drawImage(img, imageX - 11, imageY - 10, 23, 23);
} else if (label.endsWith("xrp")) {
img.src = "/images/xrp.png";
const imageX = centerX + Math.cos(angle) * radius * 0.38;
const imageY = centerY + Math.sin(angle) * radius * 0.38;
img.onload = () => {
ctx.drawImage(img, imageX - 15, imageY - 15, 30, 30);
};
ctx.drawImage(img, imageX - 15, imageY - 15, 30, 30);
}
});
},
},
{
id: "customCanvasBackgroundImage",
afterDraw: (chart) => {
const rimImage = new Image();
rimImage.src = "/images/wheel/rim.png";
const ctx = chart.ctx;
ctx.save();
ctx.drawImage(rimImage, -25, -30, 343, 343);
rimImage.onload = () => {
ctx.drawImage(rimImage, -25, -30, 343, 343);
};
ctx.restore();
},
},
],
type: "pie",
data: {
labels: segments,
datasets: [
{
backgroundColor: pieColors,
data: data,
},
],
},
options: {
responsive: true,
animation: { duration: 0 },
rotation: -90,
plugins: {
tooltip: false,
legend: { display: false },
datalabels: {
color: "#ffffff",
formatter: (value, context) => {
if (
context.chart.data.labels[context.dataIndex].endsWith("bones")
) {
return context.chart.data.labels[context.dataIndex].replace(
"bones",
" "
);
} else if (
context.chart.data.labels[context.dataIndex].endsWith(
"liscense"
)
) {
return context.chart.data.labels[context.dataIndex].replace(
"liscense",
"liscense "
);
} else if (
context.chart.data.labels[context.dataIndex].endsWith("zrpy")
) {
return context.chart.data.labels[context.dataIndex].replace(
"zrpy",
" "
);
} else if (
context.chart.data.labels[context.dataIndex].endsWith("xrp")
) {
return context.chart.data.labels[context.dataIndex].replace(
"xrp",
" "
);
} else {
return context.chart.data.labels[context.dataIndex];
}
},
font: (context) => {
const index = context.dataIndex;
const width = context.chart.width + 10;
const size = Math.round(width / 25);
if (segments[index].endsWith("bones")) {
return {
size: size + 10,
family: "Bowlby One SC",
};
} else if (segments[index].endsWith("zrpy")) {
return {
size: size + 10,
family: "Bowlby One SC",
};
} else if (segments[index].endsWith("xrp")) {
return {
size: size + 10,
family: "Bowlby One SC",
};
} else {
return {
size: size,
family: "Bowlby One SC",
};
}
},
align: "center",
rotation: (context) => {
const wheelRotation = context.chart.options.rotation; // Get current rotation of the wheel
const segmentIndex = context.dataIndex;
const segmentAngle = 360 / segments.length;
const rotation =
wheelRotation + segmentIndex * segmentAngle + 110;
return rotation;
},
// clip: true,
},
},
borderColor: "#000",
},
});
setChart(chartInstance);
return () => {
chartInstance.destroy();
setChart(null);
};
}, [wheel]);
let count = 0;
let resultValue = 101;
const spin = () => {
spinBtn.current.disabled = true;
wheel.current.classList.add("wheel-blur");
let rotationInterval = setInterval(() => {
if (chart) {
chart.options.rotation += resultValue;
chart.update();
let currentWinner = null;
const angle = 180 / Math.PI + 2;
chart.getDatasetMeta(0).data.forEach((datapoint, index) => {
const netStartAngle = (datapoint.startAngle * angle) % 360;
const netEndAngle = (datapoint.endAngle * angle) % 360;
if (280 > netStartAngle && 280 < netEndAngle) {
currentWinner = chart.data.labels[index];
}
});
if (chart.options.rotation >= 360) {
count += 1;
resultValue -= 5;
chart.options.rotation = 0;
}
if (count >= 19 && currentWinner === winningSegment) {
clearInterval(rotationInterval);
count = 0;
resultValue = 101;
if (currentWinner) {
onFinished(currentWinner);
}
wheel.current.classList.remove("wheel-blur");
}
//also spin the button with the wheel
spinBtn.current.style.transform = `translate(-50%, -50%) rotate(${chart.options.rotation}deg)`;
chart.update();
}
}, 10);
};
useEffect(() => {
//find the middle of chart and place the button there
if (chart) {
const {
chartArea: { left, right, top, bottom, width, height },
} = chart;
const centerX = (left + right) / 2;
const centerY = (top + bottom) / 2;
spinBtn.current.style.left = `${centerX}px`;
spinBtn.current.style.top = `${centerY}px`;
}
}, [chart]);
return (
<div className="wrapper">
<div className="container font-bowlbyUp">
<canvas id="wheel" ref={wheel} className="mt-[10%]"/>
<button id="spin-btn" ref={spinBtn} onClick={spin} />
</div>
</div>
);
};
export default WheelChartMobile;
I tried using requestAnimationFrame
to optimize it for mobile – but the problem persists, is there any better way to create a spinning wheel with dynamic options? I also tried reducing the setInterval
delay from 10 to 20/30/40 ms – but it still doesnt work smoothly, maybe the excessive number of images is a problem?