Konva – Star rating control

Someone on Discord asked it it was possible to make a star rating widget thing with Konva – the holes in the stars sounded like an interesting challenge. Here we go…

TLDR: See the demo and code. This is a Vanilla JS solution, but @ivor adapted it for React here.

The key technique for making holes in some foreground object is to set up clipping functions. In Konva, a clipFunc defines a closed shape, or region, using native HTML5 canvas drawing commands. And, as per the name, the clipping function will cause the area defined inside the region to be drawn, and outside the region to be hidden (or clipped off). The effect is as if the clipping area is a window through which we can see whatever shapes or content is lower in the page stacking order.

Looking now specifically at the star rating metaphor – as the name suggests this is usually five stars in a line, with shading from left-to-right indicating the rating level. Our first thought would be to use a Konva Star shape to draw the stars. That would work for getting the stars onto the canvas, and you can make a clipFunc on the stars. But there’s a catch…

The clipFunc needs HTML5 canvas drawing commands, not Konva shapes. For drawing a star, the commands are a moveTo, then a lineTo for each vertex in the shape. The Konva star shape is a convenience method in the Konva library that draws a star shape for you, and there’s no prize for guessing that behind the scenes it uses HTML5 canvas drawing commands. But the issue here is that we can’t get hold of those drawing commands to feed them into the clipFunc.

The answer then is that we need to get the (x, y) points for the vertices ourselves, then we can produce the drawing commands that we need for the clipFunc.

You can look at the source for the Konva shapes in the GitHub repository. I did this and found the code for the calculation of the vertex points, shown below.

  for (let n = 1; n < numPoints * 2; n++) {
    const radius = n % 2 === 0 ? outerRadius : innerRadius,
          x = radius * Math.sin((n * Math.PI) / numPoints),
          y = -1 * radius * Math.cos((n * Math.PI) / numPoints);
  }

I modified that to make the following function to calculate the star shape vertices based on a position, number of points on the star, and the inner and outer radius of the star.

// Function to calculate points of stars - we need these for the clip function so do not use Konva Star here,
// though this is the basis of the code in the Konva source that draws stars.
function starCalc(pos, numPoints, outerRadius, innerRadius ){
  const data = [];

  data.push({x: pos.x, y: pos.y - outerRadius}); // first point
  for (let n = 1; n < numPoints * 2; n++) {
    const radius = n % 2 === 0 ? outerRadius : innerRadius,
          x = radius * Math.sin((n * Math.PI) / numPoints),
          y = -1 * radius * Math.cos((n * Math.PI) / numPoints);
    data.push({x: pos.x + x, y: pos.y + y});
  }
  data.push({x: data[0].x, y: data[0].y}) // close the shape

  return data;
}

This function returns what we need to draw the clipping region which I do below. Note that I am using a Konva.Group to put all of the components of the star rating control into one shape. Because of that I set up the clipFunc on the group rather than individual stars. Also be aware that I am drawing the 5 star shapes as individual clipping regions and that there is only _one_ ctx.beginPath() command for all five star regions.

// Now create a group to contain the stars and the background rects.
// Important! - We create the clipping function using the star points - the stars become cutouts.
const group = new Konva.Group({
  draggable: true,

   clipFunc: function (ctx) {
      // Start only one path
      ctx.beginPath();

      for (let i = 0; i < starData.length; i++){
        ctx.moveTo(starData[i][0].x, starData[i][0].y)
        for (let j = 1; j < starData[i].length; j++){
          ctx.lineTo(starData[i][j].x, starData[i][j].y)
        }
        // Closing path, but not starting a new one
        ctx.closePath();
      }  
   }
});

Elsewhere in the code I draw into the group a couple of background rectangles with colors to match the positive and neutral colors needed on the star rating control. With the star-shaped clipping regions set up, we don’t even need to draw any star shapes – the cutouts are already in the shape of stars.

Check the code for the smaller details.

Thanks for reading.

VW. June 2022

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: