Star Rating — Make SVG Great Again

A walkthrough of how I built a generic, highly customizable Vue star rating component (like Google Play’s rating mechanism).

Yonatan Doron
UX Collective

--

The final star rating made by the component I built

If you just want to see the Github library you can click here.

A Web Dev Reality

During our work as web developers we occasionally get a weird requirement, one that we had never encountered before — as professionals we begin by researching and looking for a pre-built library that will satiate our needs, as the production-reality requires of us to find a good-enough solution and fast.

Research First

Assuming we find such library, we move on to our next challenge. In my case it was the quite the opposite. I indeed received a weird requirement — build a five-star mechanism for a sort of app store screen. I had never encountered this need before and like all of us — I had to adapt, move fast and deliver.

I began by prospecting various solutions to the problem, downloaded and played with a few sandboxes just to realize that none of the existing solutions is dynamic enough and allows me enough style customization to consider for this task in hand.

Improvise. Adapt, Overcome

I then thought of how I should implement the challenge — html Canvas? SVG? Something else? I wanted my solution to be scalable, dynamic and customizable that was the original motivation to go and build it my own — After some considerations I decided — SVG.

I built a very first and improvised component utilizing a prefixed values of a 5-armed star I found using a <polygon> element:

<polygon points="100,10 40,198 190,78 10,78 160,198" style="fill-rule:nonzero;"/>

This way I could focus on building all the customization logic of colors, sizes gradients and the algorithm that draws the stars according to the binded rating score in the range of 0.0–5.0.

A Star-Drawing Algorithm

As seen in the code block above, an SVG Polygon element has a points attribute that receives a string of x,y coordinates —it’s built-in drawing algorithm determines the drawing path between each x,y coordinates that eventually produce the desired polygon:

points="100,10 40,198 190,78 10,78 160,198"
A hard-coded svg polygon star

Once done with building the logic I realized it was not possible to alter the star’s width/height dynamically due to it’s hard-coded values — I had to draw the star programmatically in order to achieve a more dynamic star with width & height that are easily adjustable.

Planning an Algorithm

I started thinking and playing around with drawing triangles, circles, triangles and stars within circles — I even refreshed my memory of Sine, Cosine & Tangent from high school and played around with calculating angles and points. My goal was to find some kind of pattern before I start writing my algorithm.

Pattern 1 —Outer Radius Marks Outer Star Points

Outer Radius points to the x,y that are tangent between the circle and the outer star corners

Pattern 2 — Inner Radius Marks Inner Star Points

Inner Radius points to the x,y that are tangent between the circle and inner star corners

So if we layer up the two circles on the same star we than see all of our x,y points with the two mentioned patterns like so:

All the polygon points we need — for now still unknown

Pattern 3 — Inner-to-Outer-radius Ratio

To simplify things and start defining the unknowns I decided to determine that the outer radius is exactly two times the inner radius — I thought it would be best to just get up and running with a basic 1-by-1 ratio between the circles like so:

Outer circle is exactly double the size of the inner circle —same with radius as derivatives

Pattern 4 — A set drawing angle that accumulates in equal proportion to determine next X, Y point

First we have to understand how the first point is calculated and drawn — from the (centerX, centerY) which is the center of the circle as shown in the drawing — we use the same angle of each equal part of the circle calculated by let angle = (Math.PI / innerCirclePoints) // 5 points with it we then utilize cos & sin and a given radius, in our case it could be 50% of the starWidth/height, to get the (x,y) of the first point from which the star would be drawn from:

A simple formula to discover the x,y of a point with a circle, radius & angle

And from this point onwards we accumulate on both X,Y — to guide the polygon inner-algorithm how to draw our polygon which ends up in this behavior — (Click the Pentagram example to get a feeling of how it works)

A transitioned animation of how SVG Star polygon is drawn from the centerX centerY — Credit to Ana Tudor

Conclusion

We have 5 inner x,y points and 5 outer x,y points that would have to be mapped one after another from the first drawing point to the last alternatively — big X,Y small x,y and so on like so "X,Y x,y X,Y x,y..."

The only difference between the above way of drawing the star and our way is that in our way in each even iteration (i.e 0, 2, 4…) we draw a X,Y point of the outer circle and every odd iteration (i.e 1, 3, 5…) we draw a x,y point.

Lets get coding!

All this planning really makes me wanna start coding already! — so I began with a for loop that will iterate over the five points amount multiplied by 2–Which is 10 x,y points that compose the star. I then binded a method to the points attribute and began writing the algorithm:

<polygon :points="getStarPoints" style="fill-rule:nonzero;" />

I then stepped forth to write the first version of the algorithm — getStarPoints method which provides the relevant properties for calcStarPoints :

getStarPoints: function (){
let centerX = 0;
let centerY = 0;

let innerCirclePoints = 5; // a 5 point star

// this.style.starWidth --> this is the beam length of each
// side of the SVG square that holds the star
let innerRadius = this.style.starWidth / innerCirclePoints;
let innerOuterRadiusRatio = 2; // outter circle is x2 the innerlet outerRadius = innerRadius * innerOuterRadiusRatio;

return this.calcStarPoints(centerX, centerY, innerCirclePoints, innerRadius, outerRadius);
},

The calcStarPoints method that will calculate each x,y point in a single-star according to the total x,y points(10 in our case) — and will generate the polygon from point to point

calcStarPoints(centerX, centerY, innerCirclePoints, innerRadius, outerRadius) {
const angle = (Math.PI / innerCirclePoints);
const angleOffsetToCenterStar = 0;

const totalPoints = innerCirclePoints * 2; // 10 in a 5-points star
let points = '';
for (let i = 0; i < totalPoints; i++) {
let isEvenIndex = i % 2 == 0;
let r = isEvenIndex ? outerRadius : innerRadius;
let currX = centerX + Math.cos(i * angle + angleOffsetToCenterStar ) * r;
let currY = centerY + Math.sin(i * angle + angleOffsetToCenterStar) * r;
points += currX + ',' + currY + ' ';
}
return points;
},

First Drawing Impressions

And the result can be seen — although its not perfect we achieved greatness!

5 stars rendered woohoo! — but what in god name had happend?!

It is clear that our star is being drawn from the default x,y properties of an SVG element — 0,0 a simple way to make the star render in the middle of the svg would be to always give it 50% of it’s height as y property & 50% of it’s width as x property — If we want it to support dynamic width/height binded from the parent component utilizing this component the solution would be to change just these two lines to the following

getStarPoints: function() {
let centerX = this.style.starWidth / 2; // e.g 100
let centerY = this.style.starHeight / 2; // e.g 100
...
}

And the result reveals itself — great success!

These are some chubby stars!(better get in shape) — but at least they are positioned centrally in the SVG Element

Chubby stars slim stars — And everything between

Some of us software developers have a tendency to eat unhealthy foods(myself included), sweets, drinks and minimize the amount of sport we are doing with all kinds of excuses — well, it could take time until every developer in the world would adopt a more healthy approach — but hey! in a flick of a the ratio property from x2 to x2.5 to the inner circle we can now make our friendly star a bit more in shape ;)

getStarPoints: function() {
let innerOuterRadiusRatio = 2.5; // set star sharpness/chubyness
let outerRadius = innerRadius * innerOuterRadiusRatio;
...
}

Ahaa that’s much better now:

A bit tiltted/drunk — but succedded in losing a few pounds

Rotation Headaches

The next challenge I faced was to straighten up the star so it would be positioned vertically relative to the screen. My instinct told me hey! that’s not a problem just transform rotate it and all your problems would be solved. And so I went on and tested it —

.star-svg {
transform: rotate(55deg);
}
Holy smokes! the starts are vertically aligned — but… gradient is broken

Holy smokes! the starts are vertically aligned — but the gradient seesm to be broken… So I though well lets try and rotate the gradient itself <linearGradient gradientTransform="rotate(55)"> which lead to even more troubles due to the fact that the gradient sets its center according to the original origin the SVG was drawn in (the transform rotate on the .star-svg did not change the original origin)—

Eeeeek! that’s deffinetly not the way to do it

Aha Moment! — Mid-Calculation Rotating

After these few test I realized that I need to change my approach — the tests did make me better understand how <linearGradient> is working and i realized that If I cannot change the origin after rendering, I must change it during the drawing/creation of each SVG element — during calculations.

So inside my calcStarPoints() method I declared a certain angle offset and had my cos and sin trigo functions consider this offset with each calculation of a X or Y of a certain draw point in the star like so —

calcStarPoints(...) {
...
let angleOffsetToCenterStar = 60;
...
for (let i = 0; i < totalPoints; i++) {
let currX = centerX + Math.cos(i * angle + angleOffsetToCenterStar) * r;
let currY = centerY + Math.sin(i * angle + angleOffsetToCenterStar) * r;
...
}
}

And Voilà! now the star is drawn in a perfectly vertical way and the linear gradient’s origin is the exact left-to-right origin of the SVG —

Voila! both star svg and gradient are vertically aligned — 4.6 rating

Summary

I would appreciate feedback, claps, shares & a github star for the five-star rating library :) you could of course find all code, demo & a live sandbox to play around with as well as organized API docs for convenience of use.

More Recommended Posts by me about Product Design, UX & Frontend:

More open-source Vue Components:

I am Jonathan Doron, a Web Developer with great passion for User Centric Frontend, and modular client architecture.

What thrills me these days is exploring the ocean of Interaction Design more specifically of Microinteractions and their impact on our lives. I do it by recreating existing interactions as well as designing my own interactions along my quest to deepen my knowledge in the field.

You are welcome to follow, tweet or message me freely with any questions, feedback or suggestions! — Twitter

Reviewers

Thanks a whole lot for the help of these great people who helped review and feedback the article drafts, Your amazing! ;)— Dvir Hazout Ofir Ovadia Matan Yosef Elad Shechter

--

--

Frontend Craftsman & Consultant at ClientSide.org, UX Engineer, Community Leader at Hodash Dev Community, Stealth Startup Cofounder, open source contributor