Search code examples
javascripthtmlcsscanvascss-animations

How to get the color at the top in a roulette built with canvas?


I built a roulette using canvas, since I'm new at it I'm not being able to get the color I need. The roulette has an arrow at its top, I want to log the color that are below the arrow.

Here should be purple but it is yellow:

enter image description here

How can I do that?

I believe that there is a way to use coordinates to get this info, but I couldn't do it so far.

If it is necessary there is no problem adding some reference arrow at the same position of the golden one.

const canvas = document.getElementById("roulette-canvas");
const ctx = canvas.getContext("2d");
const spinButton = document.getElementById("spin-button");
const segments = 5;
const segmentSize = 360 / segments;
const colors = ["red", "yellow", "blue", "orange", "purple"];
const spinTime = 4500; // Spin time in milliseconds
const decelerationRate = 0.97; // Rate at which velocity decreases
let degree = 0;
let rotation = 0;
let velocity = 0;
let spinning = false;

function drawRoulette() {
  ctx.beginPath();
  ctx.arc(canvas.width / 2, canvas.height / 2, 353, 0, 2 * Math.PI);
  ctx.strokeStyle = "#D3A466"; // Golden color
  ctx.lineWidth = 12;
  ctx.stroke();

  // Draw the segments
  for (let i = 0; i < segments; i++) {
    ctx.fillStyle = colors[i];
    ctx.beginPath();
    ctx.moveTo(canvas.width / 2, canvas.height / 2);
    ctx.arc(
      canvas.width / 2,
      canvas.height / 2,
      350,
      (segmentSize * i - 90) * Math.PI / 180,
      (segmentSize * (i + 1) - 90) * Math.PI / 180
    );
    ctx.lineTo(canvas.width / 2, canvas.height / 2);
    ctx.fill();
  }
}

function spin() {
  if (spinning) return;

  spinning = true;

  // Random number between 1 and 360
  degree = Math.floor(Math.random() * 360) + 1;
  velocity = 10; // Initial velocity

  let start = null;
  let stopAnimation = false;

  function animation(timestamp) {
    if (!start) start = timestamp;
    let progress = timestamp - start;
    rotation += (velocity * Math.PI) / 180 / 20; // Increase rotation based on velocity
    velocity *= decelerationRate; // Decrease velocity based on deceleration rate

    // Stop the animation when velocity becomes too low
    if (velocity < 0.1) {
      velocity = 0;
      spinning = false;
    }

    if (!stopAnimation && progress >= spinTime - 1500) {
      // Start gradually slowing down the animation one second before it stops
      velocity *= decelerationRate;
      stopAnimation = true;
    }

    if (progress >= spinTime - 2000) velocity = velocity - 2;

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.translate(canvas.width / 2, canvas.height / 2);
    ctx.rotate(rotation);
    ctx.translate(-canvas.width / 2, -canvas.height / 2);
    drawRoulette();

    if (progress < spinTime) {
      requestAnimationFrame(animation);
    } else {
      // Calculate the segment that the roulette stopped at
      let stopDegree = (degree - rotation / Math.PI * 180) % 360;
      let segmentNumber = 0;

      for (let i = 0; i < segments; i++) {
        if (
          stopDegree >= segmentSize * i &&
          stopDegree < segmentSize * (i + 1)
        ) {
          segmentNumber = i + 1;
          break;
        }
      }

      console.log(
        "The roulette stopped at segment " +
          segmentNumber +
          " (color: " +
          colors[segmentNumber - 1] +
          ")"
      );
    }
  }

  requestAnimationFrame(animation);
}



// Draw the initial roulette
drawRoulette();

spinButton.addEventListener("click", spin);
.roulette-wrapper {
  position: relative;
  background: #ccc;
  height: 720px;
  width: 720px;
}

#spin-button {
  position: absolute;
}

.roulette-wrapper .top-image {
  position: absolute;
  left: 50%;
  top: -30px;
  transform: translateX(-50%);
  z-index: 777;
}

.roulette-wrapper .center-image {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

.roulette-wrapper canvas {
  position: absolute;
  background-color: white;
  border-radius: 50%;
}
<div class="roulette-wrapper">
    <button id="spin-button">Spin</button>
    <img class="top-image" src="https://sonhodospes.vteximg.com.br/arquivos/top-arrow.svg" alt="top-image"/>
  <canvas id="roulette-canvas" width="720" height="720"></canvas>
  <img class="center-image" src="https://sonhodospes.vteximg.com.br/arquivos/coupon-spin-center-full.svg" alt="center-image"/>

</div>


Solution

  • There are these issues:

    • The rotation you apply to the canvas is cumulative. I'm not referring to rotation +=, but to ctx.rotate: that adds the given rotation argument value to the current transformation of the canvas. So here you lose track of the absolute rotation that is in effect explaining why you get the wrong color notification at the end. To avoid this accumulation, reset the transformation before you call ctx.rotation. When you do this, you'll notice you'll need to increase the initial velocity to compensate for this correction.

    • The variable degree is only used in determining the final color, which makes no sense as it is a random number. So remove this variable.

    • The initial velocity is always the same. It would make more sense to make it a random value, like between 20 and 100.

    • When you add to the rotation, you divide by some "magic" number 20. Don't do this. Instead choose a more appropriate velocity.

    • The if block that tests progress >= spinTime - 1500 will only execute once, since it sets stopAnimation to true. So the effect of this block is negligible. Remove it.

    • The assignment velocity = velocity - 2; will also be executed when the velocity is already tiny, making it negative. This is not right: a negative velocity would signify a backwards movement. You'll want to always have a non-negative velocity. So remove this.

    • You have several stop conditions, like spinning, spinTime, stopAnimation, ... Just use spinning only.

    • The stopDegree calculation could give a negative value, because % in JavaScript is a remainder operator, not a modulo operator. So subtract from 360 to make sure the result remains non-negative.

    Here is your snippet with the above changes. Comments in Capitals indicate where changes were made:

      const canvas = document.getElementById("roulette-canvas");
      const ctx = canvas.getContext("2d");
      const spinButton = document.getElementById("spin-button");
      const segments = 5;
      const segmentSize = 360 / segments;
      const colors = ["red", "yellow", "blue", "orange", "purple"];
      const spinTime = 4500; // Spin time in milliseconds
      const decelerationRate = 0.97; // Rate at which velocity decreases
      let degree = 0;
      let rotation = 0;
      let velocity = 0;
      let spinning = false;
    
      function drawRoulette() {
        ctx.beginPath();
        ctx.arc(canvas.width / 2, canvas.height / 2, 353, 0, 2 * Math.PI);
        ctx.strokeStyle = "#D3A466"; // Golden color
        ctx.lineWidth = 12;
        ctx.stroke();
    
        // Draw the segments
        for (let i = 0; i < segments; i++) {
          ctx.fillStyle = colors[i];
          ctx.beginPath();
          ctx.moveTo(canvas.width / 2, canvas.height / 2);
          ctx.arc(
            canvas.width / 2,
            canvas.height / 2,
            350,
            (segmentSize * i - 90) * Math.PI / 180,
            (segmentSize * (i + 1) - 90) * Math.PI / 180
          );
          ctx.lineTo(canvas.width / 2, canvas.height / 2);
          ctx.fill();
        }
      }
    
      function spin() {
        if (spinning) return;
    
        spinning = true;
    
        // Random number between 1 and 360
        // NOT USED IN MEANINGFUL WAY: degree = Math.floor(Math.random() * 360) + 1;
        // NOT RANDOM: velocity = 10; // Initial velocity
        velocity = Math.floor(Math.random() * 80) + 20; // Initial velocity (20-100)
    
        let start = null;
        let stopAnimation = false;
    
        function animation(timestamp) {
          if (!start) start = timestamp;
          let progress = timestamp - start;
          // NO MAGIC NUMBERS (20): rotation += (velocity * Math.PI) / 180 / 20;
          rotation += (velocity * Math.PI) / 180; // Increase rotation based on velocity
          velocity *= decelerationRate; // Decrease velocity based on deceleration rate
    
          // Stop the animation when velocity becomes too low
          if (velocity < 0.1) {
            velocity = 0;
            spinning = false;
          }
    
    /*
          // NOT ONE EXTRA DECELERATION
          if (!stopAnimation && progress >= spinTime - 1500) {
            // Start gradually slowing down the animation one second before it stops
            velocity *= decelerationRate;
            stopAnimation = true;
          }
    
          // NO NEGATIVE VELOCITY
          if (progress >= spinTime - 2000) velocity = velocity - 2;
    */
          // RESET ROTATION:
          ctx.resetTransform(); 
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          ctx.translate(canvas.width / 2, canvas.height / 2);
          ctx.rotate(rotation);
          ctx.translate(-canvas.width / 2, -canvas.height / 2);
          drawRoulette();
    
    
          // ONLY USE spinning FOR ENDING: if (progress < spinTime) {
          if (spinning) {
            requestAnimationFrame(animation);
          } else {
            // Calculate the segment that the roulette stopped at
            // DON'T USE degree: let stopDegree = (degree - rotation / Math.PI * 180) % 360;
            let stopDegree = 360 - (rotation / Math.PI * 180) % 360;
            let segmentNumber = 0;
    
            for (let i = 0; i < segments; i++) {
              if (
                stopDegree >= segmentSize * i &&
                stopDegree < segmentSize * (i + 1)
              ) {
                segmentNumber = i + 1;
                break;
              }
            }
    
            console.log(
              "The roulette stopped at segment " +
                segmentNumber +
                " (color: " +
                colors[segmentNumber - 1] +
                ")"
            );
          }
        }
    
        requestAnimationFrame(animation);
      }
    
      // Draw the initial roulette
      drawRoulette();
    
      spinButton.addEventListener("click", spin);
    .roulette-wrapper {
      position: relative;
      background: #ccc;
      height: 720px;
      width: 720px;
    }
    
    #spin-button {
      position: absolute;
    }
    
    .roulette-wrapper .top-image {
      position: absolute;
      left: 50%;
      top: -30px;
      transform: translateX(-50%);
      z-index: 777;
    }
    
    .roulette-wrapper .center-image {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    }
    
    .roulette-wrapper canvas {
      position: absolute;
      background-color: white;
      border-radius: 50%;
    }
    <div class="roulette-wrapper">
        <button id="spin-button">Spin</button>
        <img class="top-image" src="https://sonhodospes.vteximg.com.br/arquivos/top-arrow.svg" alt="top-image"/>
      <canvas id="roulette-canvas" width="720" height="720"></canvas>
      <img class="center-image" src="https://sonhodospes.vteximg.com.br/arquivos/coupon-spin-center-full.svg" alt="center-image"/>
    
    </div>