Tutorial hero
Lesson icon

Using HTML5 Canvas to Create a Rating Component in Ionic

Originally published April 12, 2017 Time 10 mins

I’ve seen this concept around a few times, where rather than having a typical slider or star rating system, an application will use a smiley face. The smiley face can be adjusted to represent how the user feels about whatever it is they are rating. I think it’s genius, being able to connect with the user at an emotional level is a great goal and I think this metaphor achieves that.

Often around the streets where I live there are signs installed that will detect your speed and display a smiley face if you are within the speed limit, and a frowny face if you are going over the speed limit. I haven’t actually ready any studies that comment on the effectiveness of this approach, but it makes sense to me that this message would connect more with the driver rather than just being told that they are traveling 65km/ph when the speed limit is 60.

I decided to create a component that implemented this concept in Ionic, and this is what I came up with:

Ionic Rate Component

It uses an HTML5 canvas and bezier curves to draw the smiley face, so this tutorial will double as an example of how you can use an HTML5 canvas in your Ionic applications.

Before We Get Started

Last updated for Ionic 3.0.1

Before you go through this tutorial, you should have at least a basic understanding of Ionic concepts. You must also already have Ionic set up on your machine.

If you’re not familiar with Ionic already, I’d recommend reading my Ionic Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic, then take a look at Building Mobile Apps with Ionic.

IMPORTANT: This tutorial is using lazy loading for pages, you will need to make sure that you lazy load the home page for this tutorial. If you are unsure what this means, I would recommend watching this.

1. Generate a new Ionic Application

We are going to be focusing specifically on creating a custom rating component, we aren’t going to worry about integrating it into some overall rating process. So, we are just going to use a blank template with a single page for this application.

Run the following command to generate the application:

ionic start ionic-smiley-rate blank --v2

Once the project has finished generating, make it your working directory:

cd ionic-smiley-rate

We will also be generating a custom component to implement the functionality for this tutorial.

Run the following command to generate a new component

ionic g component SmileRate

In order to be able to use the SmileRate component, we will need to import it into the module file for the HomePage.

Modify src/pages/home/home.module.ts to reflect the following:

import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { HomePage } from './home';
import { SmileRateModule } from '../../components/smile-rate/smile-rate.module';

@NgModule({
  declarations: [HomePage],
  imports: [SmileRateModule, IonicPageModule.forChild(HomePage)],
  exports: [HomePage],
})
export class HomePageModule {}

2. Implement the Smile Rate Component

We’re going to jump right into the interesting stuff for this tutorial by implementing the functionality for our custom component. The basic idea is that our custom component will contain an HTML5 canvas and a simple label. The canvas will display the smiley face, and as we pan on the canvas the smiley face will be redrawn to change the amount of the smile or frown, and the label will just display a simple number rating from 0-100.

You will be able to follow this tutorial without an intimate knowledge of how the canvas works, but this tutorial isn’t mean to be an introduction to the <canvas> element – you may want to look up another resource for that. In short, a canvas is an area that we can draw on using a Javascript API.

We’re going to set up the entire component now, and then talk through how each section works.

Modify src/components/smile-rate/smile-rate.html to reflect the following:

<canvas width="250" height="250" #smileCanvas></canvas>
<div id="rating">{{rating}}</div>

The template for this component is quite simple, all we have is our canvas and our label. We attach a template variable of #smileCanvas to the canvas so that we can easily grab a reference to it in our .ts file.

Modify src/components/smile-rate/smile-rate.ts to reflect the following:

import { Component, ViewChild } from '@angular/core';

@Component({
  selector: 'smile-rate',
  templateUrl: 'smile-rate.html',
})
export class SmileRate {
  @ViewChild('smileCanvas') smileCanvas;
  smileHeight: number = 250;
  rating: number = Math.round(100 - (250 - this.smileHeight) / 2);

  constructor() {}

  ngAfterViewInit() {
    let hammer = new window['Hammer'](this.smileCanvas.nativeElement);
    hammer.get('pan').set({ direction: window['Hammer'].DIRECTION_ALL });

    hammer.on('pan', (ev) => {
      this.handlePan(ev);
    });

    this.drawEyes();
    this.drawSmile();
  }

  drawEyes() {
    let ctx = this.smileCanvas.nativeElement.getContext('2d');

    ctx.beginPath();
    ctx.arc(50, 20, 15, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'white';
    ctx.fill();

    ctx.beginPath();
    ctx.arc(200, 20, 15, 0, 2 * Math.PI, false);
    ctx.fillStyle = 'white';
    ctx.fill();
  }

  drawSmile() {
    let ctx = this.smileCanvas.nativeElement.getContext('2d');
    ctx.beginPath();
    ctx.moveTo(20, 150);
    ctx.bezierCurveTo(20, this.smileHeight, 230, this.smileHeight, 230, 150);
    ctx.lineWidth = 6;
    ctx.strokeStyle = 'white';
    ctx.stroke();
  }

  redraw() {
    let ctx = this.smileCanvas.nativeElement.getContext('2d');

    ctx.clearRect(
      0,
      0,
      this.smileCanvas.nativeElement.width,
      this.smileCanvas.nativeElement.height
    );
    this.drawEyes();
    this.drawSmile();
  }

  handlePan(ev) {
    this.smileHeight = ev.center.y - ev.target.offsetHeight;

    if (this.smileHeight > 250) {
      this.smileHeight = 250;
    } else if (this.smileHeight < 50) {
      this.smileHeight = 50;
    }

    this.rating = Math.round(100 - (250 - this.smileHeight) / 2);

    this.redraw();
  }
}

We’ve got quite a bit of code here, but let’s focus on one section at a time. We set up our member variables first, where we get a reference to the <canvas> element by using @ViewChild – if you’re not familiar with the use of @ViewChild you should watch this. We also set up a variable to keep track of the “smile height” – this is a number between 50 and 250 that indicates where the center point of the smile is, as this number increases the smile will become happier, and as it decreases it will become sadder (you can think of basically just grabbing the middle of the smile and pulling it up and down). We also have a variable for the “rating” number.

In the ngAfterViewInit function, we set up a pan listener. We use the Hammer.js API directly for this (which Ionic uses behind the scenes for gestures) since vertical panning is not enabled by default. With the pan listener set up on the canvas, we trigger the handlePan event every time the user pans up or down (a pan is when the user taps, holds, and drags without their finger leaving the screen). Then we call two separate functions drawEyes and drawSmile.

The drawEyes function is relatively straightforward. We just draw two circles using the arc method and fill it with a white colour. The drawSmile function is a bit more complicated. Our goal is to start a line at some point on the left of the canvas, and then create a smooth curve to the “smile height” point, and then another smooth curve to the other side of the screen. Then by adjusting the “smile height” it will change whether the face is smiling or frowning.

We start the smile at (20, 50) and then we start drawing the curve, which is defined by this:

ctx.bezierCurveTo(20, this.smileHeight, 230, this.smileHeight, 230, 150);

This can be a bit hard to follow, but what we are doing is supplying three points here (in pairs of coordinates). The first two points specify the “control” points that will determine the curve, and the last point determines the end point of the curve.

The best diagram I have seen to illustrate how this works can be found here under the Definition & Usage section.

Once we have our curve defined, we “stroke” it with a white line 6 pixels wide to form the smile. The important part is that this changes based on the smileHeight so we need to clear and redraw that smile every time we receive a pan event.

NOTE: We are using white for the drawing here, so you won’t be able to see it against the white background. You can change the colour from white to something else now if you prefer, but we will be changing the background colour of the home page shortly so that you can see the drawing.

That handles the functionality for the component, but we also need to add a bit of styling to the component.

Modify src/components/smile-rate/smile-rate.scss to reflect the following:

smile-rate {
  width: 250px;
  height: 300px;

  #rating {
    text-align: center;
    font-size: 3em;
    font-weight: bold;
    color: #fff;
  }
}

3. Use the Smile Rate Component

Now that we have our component created, we can make use of it in our HomePage.

Modify src/pages/home/home.html to reflect the following:

<ion-content no-bounce>
  <h4>How does this make you feel?</h4>

  <smile-rate></smile-rate>

  <button ion-button color="light">Let us know!</button>
</ion-content>

We’ve just designed a basic page here that incorporates the Smile Rate component. It is also important to put the no-bounce attribute on the <ion-content> if you plan to use this on a mobile device – as you attempt to pan the smile, if you do not have the web view bounce turned off it is very awkward to use.

Finally, we just need to style that page a little.

Modify src/pages/home/home.scss to reflect the following:

.ios,
.md {
  page-home {
    .scroll-content {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      background-color: #1abc9c;
    }

    h4 {
      text-align: center;
      font-size: 1.2em;
      color: #fff;
    }

    smile-rate {
      margin-top: 50px;
      margin-bottom: 50px;
    }
  }
}

I’ve used a flex layout to align all of the items on the page nicely in the center of the screen, and then we just have a few extra styles to control the colour and sizing.

You should now have something that looks like this:

Ionic Rate Component

Summary

This concept was reasonably easy to implement, and it turns what would likely otherwise be a pretty boring feedback screen into something fun and engaging.

Learn to build modern Angular apps with my course