Build a Random Prize Competition Service: Part 2

In the previous article in this series we went through the early motions of implementing a service that would randomly generate a prize for a user.

We accomplished two of our main objectives:

  1. We created a module skeleton that (apart from being reusable) allowed us to call a service to randomly generate a number
  2. Evaluated various methods for random number generators, settling on a third-party version of the battle-tested Mersenne-Twister algorithm

In this article, we're going to implement the frontend aspect of this functionality — I want to add in a spinning wheel that picks (or, rather, pretends to) select a prize for the user. Of course, we know that the result has already been generated, so it's a case of getting a wheel to spin and then land on the desired result.

Winwheel and TweenMax

Now, we could write our own functionality for this. In fact, if you have the time and skill to do so, I'd encourage you to because it would likely be a fun and challenging project.

However, as we're here to create a proof of concept, we should look at the options available to us. I had a look around and the one I really like is Spin2Win Wheel by Chris Gannon. It looks fantastic has beautiful functionality and could well fit in nicely.

Alas, however, it is not free. The author wants $19, which is fair enough, but for the sake of this article, we're going to look for free alternatives.

After some more looking around, I found Winwheel, which certainly looks like it'll do a good job of slipping nicely into our project.

Curiously, it has a dependency for the animation. This is somewhat disappointing as the more JavaScript we add, the heavier the page gets and the more complex our application gets. Nonetheless, let's look at that too.

Winwheel depends on the GreenSock Animation Platform, specifically TweenMax.js. It's a multi-purpose JS library that provides a powerful animation API for JavaScript and HTML5, which is handy given what we're about to do.

Minified, Winwheel.js and TweenMax.js come in at 21.7KB and 112KB respectively, so we are adding a fair bit of weight. If this bothers you, you can seek out alternatives, but I think it's fine for what we want to do.

Add Winwheel

Naturally, before we can use them we need to add them to our application. Firstly, Winwheel is not AMD-compatible so we will have to shim it in like we do with other third-party JS files.

In your customizations directory, create a folder called Winwheel@2.6.0 (or whatever the current version is) and download the minified version of it into this folder. Rename it to match the folder name; ie Winwheel.js.

Create an ns.package.json file that contains:

{
  "gulp": {
    "javascript": [
      "Winwheel.js"
    ]
  }
, "jshint": "false"
}

Update your distro.json file; first add the module to the modules object at the top. Next, as it's not AMD-compatible, we need to update the amdConfig:paths object for shopping.js:

"Winwheel": "Winwheel"

Then, below that, in the shim object, add:

"Winwheel": {
    "exports": "Winwheel"
}

This combined will let the application recognize the Winwheel code, give it a recognizable name, and then include it in the application. Let's do something similar for TweenMax.

Add TweenMax

It's going to be a similar story here, except that TweenMax is AMD-compatible.

In your customizations directory, create TweenMax@1.20.2 (again, adjusting for version numbering) and put a copy of the minified JavaScript into it, renaming it to TweenMax.js.

Create ns.package.json with:

{
  "gulp": {
    "javascript": [
      "TweenMax.js"
    ]
  }
, "jshint": "false"
}

Back in distro.json, add in TweenMax as a module, and then add it to the shopping JavaScript.

Finally, before we move on, just a note that about TweenMax: their licensing is a bit different, it is not open source. However, they do have what they call a standard 'no charge' license. Be sure to check it out for yourselves but my understanding is that for what we are using the file for, we don't need to pay extra for a different license.

Prepare a Basic Template

The functionality works by looking for a canvas element. Canvases were added as part of HTML5 and allow designers and developers to create dynamic and great looking graphics and games. If you're curious, it's supported from IE9 and all modern browsers, so we don't need to worry (unless you still support IE8 users). Thus, we need to add a canvas element to our template along with a button.

Open up rng.tpl and replace its contents with:

<canvas id="canvas" width="400" height="400">Sorry but your browser does not support things functionality.</canvas>
<button onclick="myWheel.stopAnimation(); myWheel.rotationAngle=0; myWheel.draw(); myWheel.spinWheel();">Spin the Wheel</button>

The canvas element contains a fallback method in case someone with an ancient browser visits the page; the buttons trigger JavaScript functions that we're going to define later — don't worry, we'll tidy up the onclick properties later.

For now, save and deploy, and then head over to your competition page. You won't see anything yet, but you will if we use one of their examples. Go to your console and type in the following:

var myWheel = new Winwheel({
  'numSegments' : 8
, 'outerRadius' : 170
, 'segments' :
  [  
    {'fillStyle' : '#eae56f', 'text' : 'Prize 1'}
  , {'fillStyle' : '#89f26e', 'text' : 'Prize 2'}
  , {'fillStyle' : '#7de6ef', 'text' : 'Prize 3'}
  , {'fillStyle' : '#e7706f', 'text' : 'Prize 4'}
  , {'fillStyle' : '#eae56f', 'text' : 'Prize 5'}
  , {'fillStyle' : '#89f26e', 'text' : 'Prize 6'}
  , {'fillStyle' : '#7de6ef', 'text' : 'Prize 7'}
  , {'fillStyle' : '#e7706f', 'text' : 'Prize 8'}
  ]
, 'animation' :
  {
    'type': 'spinToStop'
  , 'duration': 5
  , 'spins': 8
  , 'callbackAfter': 'drawTriangle()'
  }
});

myWheel.spinWheel = function ()
{
  myWheel.animation.stopAngle = 91 + Math.floor((Math.random() * 43));
  myWheel.startAnimation();
};

function drawTriangle()
{
  var ctx = myWheel.ctx;

  ctx.strokeStyle = 'navy';
  ctx.fillStyle   = 'aqua';
  ctx.lineWidth   = 2;
  ctx.beginPath();
  ctx.moveTo(170, 5);
  ctx.lineTo(230, 5);
  ctx.lineTo(200, 40);
  ctx.lineTo(171, 5);
  ctx.stroke();
  ctx.fill();
};

drawTriangle();

So there's three main bits to this:

  1. The constructor, which creates the wheel to our definitions (at the moment, just some placeholder settings)
  2. A function to fix the result to the one we want (rather than having it randomly pick a result), which in this case is the third prize
  3. A function to draw an indicator showing the user where the wheel has stopped

I think the most interesting function is the one that determines where it's going to end up. There is some math here that you should be able to work out.

As we have eight segments, we know that each one takes up 45 degrees of the wheel. Thus, if we want it to land on the third prize, we need it to have a value of anything between 90 and 134. However, both of those extremes are ambiguous because if it did land on exactly 90, then it would be confusing as to whether it was prize 2 or 3. Thus, we increase the lower bounds by 1 and lower the upper bound by 1 to give us our range (91 + 43). Now, we could just set it to any number between 91 and 134 or we can add in a little variation, which is where we bring in from the cold our old friend Math.random(). By adding a little variety to end result, it makes it seem a little less random, which is nice.

Now hit Spin the Wheel and you'll see it spin and eventually land on prize 3. Click it over and over and we'll always get the same result. Excellent!

Implement the Spinner in the View

So we can create the spinner by entering code into our console, let's move this into the code proper.

Start by adding in Winwheel and TweenMax as dependencies to RNG.View.js.

Then, create a method in the return statement that will create the wheel:

, drawWheel: function ()
  {
    var myWheel = new Winwheel({
      'numSegments': 8
    , 'outerRadius': 170
    , 'segments':
      [
        {'fillStyle': '#eae56f', 'text': 'Prize 1'}
      , {'fillStyle': '#89f26e', 'text': 'Prize 2'}
      , {'fillStyle': '#7de6ef', 'text': 'Prize 3'}
      , {'fillStyle': '#e7706f', 'text': 'Prize 4'}
      , {'fillStyle': '#eae56f', 'text': 'Prize 5'}
      , {'fillStyle': '#89f26e', 'text': 'Prize 6'}
      , {'fillStyle': '#7de6ef', 'text': 'Prize 7'}
      , {'fillStyle': '#e7706f', 'text': 'Prize 8'}
      ]
    , 'animation':
      {
        'type': 'spinToStop'
      , 'duration': 8
      , 'spins': 10
      }
    });

    myWheel.spinWheel = function ()
    {
      myWheel.stopAnimation();
      myWheel.rotationAngle = 0;
      myWheel.draw();
      myWheel.animation.stopAngle = 91 + Math.floor((Math.random() * 43));
      myWheel.startAnimation();
    };

    this.myWheel = myWheel;
  }

You'll note a number of things:

  1. We removed the call to draw a triangle. The way that the Winwheel functionality is written is that this is handled by the TweenMax code and not in the current context. Thus, the only reliable calls we can make here are those in the global namespace, of which the current view is not a member. If you wanted to use this functionality you could change the code so it, say, adds it to the window object instead — but, honestly, this is messy. We'll find an alternative.
  2. We moved the spinWheel() method inside this function and included all of the calls we had tacked onto the button in the template.
  3. We add the final wheel object to the view object so we can reference it later.

Next we need to think about how we're going to call this code. We know that we have to wait for the template to be processed because the this functionality targets the canvas element. For this, we intercept the showContent function and tell it to draw the wheel when it's done.

Add another function:

, showContent: function ()
  {
    var self = this;
    this.options.application.getLayout().showContent(this).done(
      function () {
        self.drawWheel();
      }
    );
  }

Now, we need to think about how we're going to call for a prize to be calculated. As you'll remember, we bound this directly to the button in the template and we need to change that.

Starting with the view, add the following function:

, spinWheel: function ()
  {
    this.myWheel.spinWheel();
  }

Then we bind a call to that function using events. Add:

, events: {
    'click [data-action="spin-to-win"]': 'spinWheel'
  }

As you can see, we've set up a click event. We now need somewhere for this event to be generated.

Head over to rng.tpl and replace the button element code with:

<button class="rng-spin" data-action="spin-to-win">Spin the Wheel</button>

Save the files and then start your local server. After heading over to the local version of the competition page, you should find that the button works and the whole thing spins (and lands on prize 3). Nice!

Connect the Spinner to the Backend RNG

The next logical step is to use the functionality we wrote in part one so that the frontend spinner lands on the number the RNG has picked. For that, we need to look closer at the code of the spinner.

Preparing the Spinner Segments

Remember: the way the spinner knows which prize to stop is determined by telling it what angle to stop at. Thus we need to create the segments for our prizes that fit into this schema.

We have three prizes with different probability, and it would certainly look silly if we simply had three segments where we had one very large, one small and one very small. Instead, we should find out what size the smallest segment needs to be and then create additional segments for the more popular prizes.

In our scenario, the big prize ($50) has a 5% of being picked; as 20/0.05 = 100, we will need 20 segments as follows:

  • 1 x $50
  • 2 x $25
  • 17 x $10

First, change the numSegments value to 20, and then replace the segments object with:

   {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'silver', 'text': '$25'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'gold', 'text': '$50'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'silver', 'text': '$25'}
, {'fillStyle': 'lightpink', 'text': '$10'}
, {'fillStyle': 'lightgoldenrodyellow', 'text': '$10'}
, {'fillStyle': 'lightpink', 'text': '$10'}

If you refresh your page you'll see a neat looking wheel ready for you to spin.

Calculating Where to Land

Now let's think about how we're figure out how to land on a correct segment: after all, the way we generate a random number is in large blocks, so we need to think about how we're going to assign a prize.

You'll remember in the first part of this article we had a getPrize function that took a random number and then returned a prize based on an analysis of that random number. Let's reintroduce that concept.

In SuiteScript > RNG.Model.js, add:

, getPrize: function ()
  {
    var draw = this.genRandom()
    , prize = ''
    // As there are 20 segments (360/20 = 18 deg each)
    , segment = Math.floor((360*draw)/18)+1
    // As each segment is 18 deg each, the midpoint is the segment number times 18 minus 9 degrees
    , stopAngle = (segment*18) - 9;

    if (draw < 0.05) {prize = '$10'}
    else if (draw >= 0.05 && draw < 0.1) {prize = '$10'}
    else if (draw >= 0.1 && draw < 0.15) {prize = '$10'}
    else if (draw >= 0.15 && draw < 0.2) {prize = '$25'}
    else if (draw >= 0.2 && draw < 0.25) {prize = '$10'}
    else if (draw >= 0.25 && draw < 0.3) {prize = '$10'}
    else if (draw >= 0.3 && draw < 0.35) {prize = '$10'}
    else if (draw >= 0.35 && draw < 0.4) {prize = '$10'}
    else if (draw >= 0.4 && draw < 0.45) {prize = '$10'}
    else if (draw >= 0.45 && draw < 0.5) {prize = '$50'}
    else if (draw >= 0.5 && draw < 0.55) {prize = '$10'}
    else if (draw >= 0.55 && draw < 0.6) {prize = '$10'}
    else if (draw >= 0.6 && draw < 0.65) {prize = '$10'}
    else if (draw >= 0.65 && draw < 0.7) {prize = '$10'}
    else if (draw >= 0.7 && draw < 0.75) {prize = '$10'}
    else if (draw >= 0.75 && draw < 0.8) {prize = '$25'}
    else if (draw >= 0.8 && draw < 0.85) {prize = '$10'}
    else if (draw >= 0.85 && draw < 0.9) {prize = '$10'}
    else if (draw >= 0.9 && draw < 0.95) {prize = '$10'}
    else if (draw >= 0.95) {prize = '$10'}
    else throw new Error ('Prize calculation failed. Received: ' + draw)

    return {
      'draw': draw
    , 'segment': segment
    , 'stopAngle': stopAngle
    , 'prize': prize
    }
  }

So here's what we're doing:

  1. Generate a random number.
  2. Figure out what segment it falls into by multiplying 360 (degrees) by the random number and then dividing that number by 18 (degrees in each segment); we use Math.floor to get an integer. In other words, if the number returned is 12.324, then we know it's in the 12th segment. We then add 1 to the result, as this algorithm would return results between 0 and 19 (we want 1-20 as we're humans).
  3. Calculate the stop angle by taking the segment number, multiplying it by 18 (degrees in each segment) and then adding 9 (degrees, which is half way through each segment).
  4. Calculate the prize by running it through up to 20 conditional statements until it falls into a range.
  5. Return all variables as an object.

As we now have this function, we need a way to call it: and for that we're currently using the get. Thus, underneath, replace this function with:

, get: function ()
  {
    var prizeData = {};

    prizeData.isLoggedIn = CommerceAPI.session.isLoggedIn2();
    prizeData.spin = this.getPrize();

    return prizeData
  }

Great! Now let's plug it into the view.

In the drawWheel function, add this to the top:

var self = this;

Then, in myWheel.spinWheel, replace myWheel.animation.stopAngle with:

myWheel.animation.stopAngle = self.model.get('spin').stopAngle;

Save, deploy and refresh.

Now, before you spin, take a look in the Network tab of your browser's dev tools and check out the server response to RNG.Service.ss, you should see something like this:

Hit the spin button. The final spin should align with the response the data. If you spin again (without refreshing), it should at the same place. Neat!

You can read the official docs on setting the prize before spinning if you need more information.

Provide Feedback to the User

OK, so we did all that and we still haven't got an arrow or any sort of messaging that actually shows the user what they've won. As we're not going to draw a pointer using the the in-built functionality, we can look at alternatives. Then, we can think about how we're going to present information to the user about what they've won.

Add a Pointer

Helpfully, they have documentation on prize pointers and wheel backgrounds which we can refer to. They specify four different ways, of varying complexity, to add a pointer:

  1. With a background image behind the canvas (easy)
  2. In a image positioned over the top of the canvas (easyish)
  3. Use canvas lineTo() functions to draw a triangle on the canvas (more complex)
  4. Use canvas drawImage() function to place an image on the canvas (complex)

Looking at their docs, I like the look of the second one: positioning an image over it. We need to be careful with this, though, as we're all aware of quirks in browsers that can cause incorrect alignments.

For the purposes of this tutorial, we're going to use basic pointer that they provide:

To implement this, we start by heading into the template, rng.tpl. We need to wrap the whole thing in a container, so that we can position the arrow, as well as add the arrow itself.

Replace the contents of the template with:

<div class="rng-canvas-container">
    <img class="rng-canvas-pointer" src="{{pointerImage}}">
    <canvas class="rng-canvas-canvas" id="canvas" width="400" height="400">Sorry but your browser does not support things functionality.</canvas>
</div>
<button class="rng-spin" data-action="spin-to-win">Spin the Wheel</button>

You'll see that we reference an expression for the pointer image, which we haven't added yet. Swing back to RNG.View.js and add the following to the getContext function:

, pointerImage: _.getAbsoluteUrl('img/basic_pointer.png')

Now we know this image isn't uploaded or ready yet, so let's do that.

In the RNG directory, create a directory called Images and then put the pointer image in there.

Then open ns.package.json and add an entry so that its included in the distro files:

, "images": [
    "Images/*"
  ]

Finally, we need to add some styling so that it is positioned correctly.

In Sass, create _rng.scss and add in:

.rng-canvas-container {
    position: relative;
    width: 400px;
}

.rng-canvas-canvas {
    z-index: 1;
}

.rng-canvas-pointer {
    position: absolute;
    left: 175px;
    top: 0;
    z-index: 999;
}

Save it all and deploy. If you're running your local server, you'll need to stop and restart it as you've added new files. When it reloads, you should see a neat pointer pointing at the correct result.

Add Feedback

We need to alert the user of what their spins mean: an arrow pointed at "$10" doesn't mean anything. We need to think about messaging.

I don't want to dwell too much on this as it's incidental to this tutorial: how you do it (if you do) will be subject to your site's own designs and requirements.

Append the following to rng.tpl:

<p id ="rng-message" class="rng-message">Congratulations, you've won a voucher that you can use for {{prize}} off your order. Use promo code <strong>({{promoCode}})</strong> during checkout.</p>

We don't have a property in the context for the prize or promo code yet; we're not going to address the latter for a while, but in the meantime we can add something for the prize.

Update the getContext function to include:

, prize: this.model.get('spin').prize

Next, we know that we don't want the message to show until the wheel reveals the prize; it would be pretty useless for the user to visit the page and see their prize before they've had the fun of seeing the luck of the draw.

In _rng.scss, add:

.rng-message {
    visibility: hidden;
}

This will keep its position on the page in place, while making it invisible.

Finally, we just need to toggle this when the draw is made. As we already have an event set up in the view for starting the draw and we know how long the wheel spins for, we can do something like the following.

In RNG.View.js, replace the spinWheel function with:

, spinWheel: function ()
  {
    this.myWheel.spinWheel();
    setTimeout(function ()
    {
      document.getElementById('rng-message').style.visibility = 'visible'
    }, 8500);
  }

Using setTimeout feels very 2005, but in this situation it actually works: we've set up the animation on the wheel so that it spins for exactly 8 seconds, so we just set our JavaScript to wait 8.5 seconds before toggling the styling on the element.

Save and refresh your local server page. Spin the wheel, you should get something cool like this:

Final Thoughts

OK, in this article we built on the fact that we had a backend service that sends us a randomly generated number. We wanted something fun, like a game or other joyful interaction, that went beyond merely showing some "here's what you get". This is why we went with a spinner.

There are loads of ways you can do a spinner. If you have the time and skill I would highly recommend building your own, otherwise there are some excellent paid-for ones available. We chose somewhere in the middle, one that's a free third-party file that relies on a JavaScript animation library. This is kinda sub-optimal as it adds weight to the build, but it's good enough for going forward.

The important thing was that during construction of the wheel, we can set a pre-determined angle for the wheel to stop at. We did some math and calculated what those angles needed to be so that they would stop in the middle of each segment.

Finally, we clarified things by adding a little pointer graphic so that it was obvious which segment had been selected, and then followed it up with a message saying explicitly what was returned.

After all of that, we now need to think about some of the behind-the-scenes aspects of this, such as how we're going to get the promo codes to the users and how we're going to ensure that it's only them who use them. We will also need to think about where this functionality might live, as the shopping application may not be the most suitable place for it.

For a copy of my files at this point, see RNGpart2.zip.