Creating a wheel of fortune

eagleera

Dany the spacecowboy

Posted on April 27, 2021

Creating a wheel of fortune

Check out the demo here to be excited about what you can build✨✨ https://tacowheel.netlify.app/

A couple of weeks ago, I wanted to explore the magic of SVG✨ so I thought of creating a little sample of a wheel of fortune taking into account SVG collisions using GSAP since it's a big deal in the SVG community, to make it more fun, I consumed an API that returns taco recipes to help you choose what kind of tacos to eat on taco Tuesday 🌮.

So let's jump on this mini tutorial on how to build a wheel of fortune

The beginning of the project

Since I'm in love with vite, I used vite create app to start a react app with vite under the hood to make it fast! besides that, I installed gsap, recharts (which I will explain in a minute), and react-uuid (to generate unique ids)

Creating the wheel

I tried to create the wheel using handcrafted SVG, but I kinda cheat on this part, I saw this really cool repo that does kinda what I want and decided to copy its wheel functionality using recharts which is a react library for charts, so here's the code of how I created the wheel (which is a pie chart but don't tell anyone):

//we want fixed width and height to match the arrow position that we will be creating later
<PieChart width={400} height={400} style={{ margin: "0 auto" }}>
    <Pie
        /*this is an array of items with the next structure
        [{
          id: uuid(),
          name: sliceName,
          times: 1, // this is to indicate the value of the slice
          background: randomColorFunction,
        },...]
        */
        data={items}
        labelLine={false}
        label={renderCustomizedLabel}
        fill="#8884d8"
        dataKey="times" // should match the 'times' key
        isAnimationActive={false}
        redraw={false} // to not recalculate the position of the slices after spinning
    >
        {items.map((el, index) => (
        <Cell key={`cell-${index}`} fill={el.background} />
        ))}
    </Pie>
</PieChart>
Enter fullscreen mode Exit fullscreen mode

now, the label on it has a function to render the actual text that is positioned inside the slice, we calculate the x and y coordinates to position the text, so that function looks something like this:

const renderCustomizedLabel = ({
    cx,
    cy,
    midAngle,
    innerRadius,
    outerRadius,
    index,
  }) => {
    const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
    const x = cx + radius * Math.cos(-midAngle * RADIAN);
    const y = cy + radius * Math.sin(-midAngle * RADIAN);
    return (
      <text
        x={x}
        y={y}
        textAnchor="middle"
        dominantBaseline="central"
        fontSize={calcSize('fontsize')}
      >
        {name}
      </text>
    );
  };
Enter fullscreen mode Exit fullscreen mode

after creating this component you should have something like this:
wheel image

Making the wheel spin

to make the wheel spin, I use the next function which incorporates the magic of GSAP

let pos = 0;
async function rotate() {
    let x = randomNumber(1080, 1800); //between 4-6 wheel turns
    pos = pos + x;
    await gsap.to(".recharts-pie", // recharts automatically adds this class to the pie chart
     {
      duration: 1,
      rotation: pos,
      transformOrigin: "50% 50%",
      ease: " power2. ease-in-out",
    });
  }
Enter fullscreen mode Exit fullscreen mode

Adding an arrow to identify the winner

Now the fun part begins, after having the wheel spinning, we want to check which slice is the winner, to do that, I added a little triangle created on css and then I assign it an id of #arrow to it

then, to check the collision, I modified the rotate function defined before to check for the collision

const [winner, setWinner] = useState(undefined);
async function rotate() {
    let x = randomNumber(1080, 1800); //between 2-4 wheel turns
    pos = pos + x;
    await gsap.to(".recharts-pie", {
      duration: 1,
      rotation: pos,
      transformOrigin: "50% 50%",
      ease: " power2. ease-in-out",
    });
    //get all the pie slices by the classname
    let pieSectors = document.getElementsByClassName("recharts-pie-sector");
    Array.from(pieSectors).forEach((sector, index) => {
        // iterate through them to check which one is hitting the arrow
      if (Draggable.hitTest(sector, "#arrow", '50px')) {
        setWinner(items[index]);
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

One thing to notice is that the last param of the hitTest function is a threshold which you can indicate how many pixels can be overlapping between the two components to count it as a hit

Takeaways of this project

you can check the source code in here and a little demo of the app in here I added some media queries so the wheel can be playable on all the sizes and a button to call the rotate function

Also, some notes:

  • the smaller the wheel, the harder it is to check for collisions since a lot of pie slices will overlap with each other
  • if you start adding more slices, you will have the same overlapping problem
  • right now the demo has like a 95% accuracy

Sources

💖 💪 🙅 🚩
eagleera
Dany the spacecowboy

Posted on April 27, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related