Easily manage projects with monday.com

In my previous article, I've shown how to smoothly transition from one state to another using vanilla JavaScript. Make sure you check that one out first because I'll be referencing some things I explained there in a lot of detail, like demos given as examples, formulas for various timing functions or how not to reverse the timing function when going back from the final state of a transition to the initial one.

The last example showcased making the shape of a mouth to go from sad to glad by changing the `d`

attribute of the `path`

we used to draw this mouth.

Manipulating the path data can be taken to the next level to give us more interesting results, like a star morphing into a heart.

### The idea

Both are made out of five cubic Bézier curves. The interactive demo below shows the individual curves and the points where these curves are connected. Clicking any curve or point highlights it, as well as its corresponding curve/point from the other shape.

See the Pen by thebabydino (@thebabydino) on CodePen.

Note that all of these curves are created as cubic ones, even if, for some of them, the two control points coincide.

The shapes for both the star and the heart are pretty simplistic and unrealistic ones, but they'll do.

### The starting code

As seen in the face animation example, I often choose to generate such shapes with Pug, but here, since this path data we generate will also need to be manipulated with JavaScript for the transition, going all JavaScript, including computing the coordinates and putting them into the `d`

attribute seems like the best option.

This means we don't need to write much in terms of markup:

```
<svg>
<path id='shape'/>
</svg>
```

In terms of JavaScript, we start by getting the SVG element and the `path`

element - this is the shape that morphs from a star into a heart and back. We also set a `viewBox`

attribute on the SVG element such that its dimensions along the two axes are equal and the `(0,0)`

point is dead in the middle. This means the coordinates of the top left corner are `(-.5*D,-.5*D)`

, where `D`

is the value for the `viewBox`

dimensions. And last, but not least, we create an object to store info about the initial and final states of the transition and about how to go from the interpolated values to the actual attribute values we need to set on our SVG shape.

```
const _SVG = document.querySelector('svg'),
_SHAPE = document.getElementById('shape'),
D = 1000,
O = { ini: {}, fin: {}, afn: {} };
(function init() {
_SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' '));
})();
```

Now that we got this out of the way, we can move on to the more interesting part!

### The geometry of the shapes

The initial coordinates of the end points and control points are those for which we get the star and the final ones are the ones for which we get the heart. The range for each coordinate is the difference between its final value and its initial one. Here, we also rotate the shape as we morph it because we want the star to point up and we change the `fill`

to go from the golden star to the crimson heart.

Alright, but how do we get the coordinates of the end and control points in the two cases?

#### Star

In the case of the star, we start with a regular pentagram. The end points of our curves are at the intersection between the pentagram edges and we use the pentagram vertices as control points.

Getting the vertices of our regular pentagram is pretty straightforward given the radius (or diameter) of its circumcircle, which we take to be a fraction of the `viewBox `

size of our SVG (considered here square for simplicity, we're not going for tight packing in this case). But how do we get their intersections?

First of all, let's consider the small pentagon highlighted inside the pentagram in the illustration below. Since the pentagram is regular, the small pentagon whose vertices coincide with the edge intersections of the pentagram is also regular. It also has the same incircle as the pentagram and, therefore, the same inradius.

So if we compute the pentagram inradius, then we also have the inradius of the inner pentagon, which, together with the central angle corresponding to an edge of a regular pentagon, allows us to get the circumradius of this pentagon, which in turn allows us to compute its vertex coordinates and these are exactly the edge intersections of the pentagram and the endpoints of our cubic Bézier curves.

Our regular pentagram is represented by the Schläfli symbol `{5/2}`

, meaning that it has `5`

vertices, and, given these `5`

vertex points equally distributed on its circumcircle, `360°/5 = 72°`

apart, we start from the first, skip the next point on the circle and connect to the second one (this is the meaning of the `2`

in the symbol; `1`

would describe a pentagon as we don't skip any points, we connect to the first). And so on - we keep skipping the point right after.

In the interactive demo below, select either pentagon or pentagram to see how they get constructed.

See the Pen by thebabydino (@thebabydino) on CodePen.

This way, we get that the central angle corresponding to an edge of the regular pentagram is twice of that corresponding to the regular pentagon with the same vertices. We have `1·(360°/5) = 1·72° = 72°`

(or `1·(2·π/5)`

in radians) for the pentagon versus `2·(360°/5) = 2·72° = 144°`

(`2·(2·π/5)`

in radians) for the pentagram. In general, given a regular polygon (whether it's a convex or a star polygon doesn't matter) with the Schläfli symbol `{p,q}`

, the central angle corresponding to one of its edges is `q·(360°/p)`

(`q·(2·π/p)`

in radians).

We also know the pentagram circumradius, which we said we take as a fraction of the square `viewBox`

size. This means we can get the pentagram inradius (which is equal to that of the small pentagon) from a right triangle where we know the hypotenuse (it's the pentagram circumradius) and an acute angle (half the central angle corresponding to the pentagram edge).

The cosine of half the central angle is the inradius over the circumradius, which gives us that the inradius is the circumradius multiplied with this cosine value.

Now that we have the inradius of the small regular pentagon inside our pentagram, we can compute its circumradius from a similar right triangle having the circumradius as hypotenuse, half the central angle as one of the acute angles and the inradius as the cathetus adjacent to this acute angle.

The illustration below highlights a right triangle formed from a circumradius of a regular pentagon, its inradius and half an edge. From this triangle, we can compute the circumradius if we know the inradius and the central angle corresponding to a pentagon edge as the acute angle between these two radii is half this central angle.

Remember that, in this case, the central angle is not the same as for the pentagram, it's half of it (`360°/5 = 72°`

).

Good, now that we have this radius, we can get all the coordinates we want. They're the coordinates of points distributed at equal angles on two circles. We have `5`

points on the outer circle (the circumcircle of our pentagram) and `5`

on the inner one (the circumcircle of the small pentagon). That's `10`

points in total, with angles of `360°/10 = 36°`

in between the radial lines they're on.

We know the radii of both these circles. The radius of the outer one is the regular pentagram circumradius, which we take to be some arbitrary fraction of the `viewBox`

dimension (`.5`

or `.25`

or `.32`

or whatever value we feel would work best). The radius of the inner one is the circumradius of the small regular pentagon formed inside the pentagram, which we can compute as a function of the central angle corresponding to one of its edges and its inradius, which is equal to that of the pentagram and therefore we can compute from the pentagram circumradius and the central angle corresponding to a pentagram edge.

So, at this point, we can generate the path data that draws our star, it doesn't depend on anything that's still unknown.

So let's do that and put all of the above into code!

We start by creating a `getStarPoints(f)`

function which depends on an arbitrary factor (`f`

) that's going to help us get the pentagram circumradius from the `viewBox`

size. This function returns an array of coordinates we later use for interpolation.

Within this function, we first compute the constant stuff that won't change as we progress through it - the pentagram circumradius (radius of the outer circle), the central (base) angles corresponding to one edge of a regular pentagram and polygon, the inradius shared by the pentagram and the inner pentagon whose vertices are the points where the pentagram edges cross each other, the circumradius of this inner pentagon and, finally, the total number of distinct points whose coordinates we need to compute and the base angle for this distribution.

After that, within a loop, we compute the coordinates of the points we want and we push them into the array of coordinates.

```
const P = 5; /* number of cubic curves/ polygon vertices */
function getStarPoints(f = .5) {
const RCO = f*D /* outer (pentagram) circumradius */,
BAS = 2*(2*Math.PI/P) /* base angle for star poly */,
BAC = 2*Math.PI/P /* base angle for convex poly */,
RI = RCO*Math.cos(.5*BAS) /*pentagram/ inner pentagon inradius */,
RCI = RI/Math.cos(.5*BAC) /* inner pentagon circumradius */,
ND = 2*P /* total number of distinct points we need to get */,
BAD = 2*Math.PI/ND /* base angle for point distribution */,
PTS = [] /* array we fill with point coordinates */;
for(let i = 0; i < ND; i++) {}
return PTS;
}
```

To compute the coordinates of our points, we use the radius of the circle they're on and the angle of the radial line connecting them to the origin with respect to the horizontal axis, as illustrated by the interactive demo below (drag the point to see how its Cartesian coordinates change):

See the Pen by thebabydino (@thebabydino) on CodePen.

In our case, the current radius is the radius of the outer circle (pentagram circumradius `RCO`

) for even index points (`0`

, `2`

, ...) and the radius of the inner circle (inner pentagon circumradius `RCI`

) for odd index points (`1`

, `3`

, ...), while the angle of the radial line connecting the current point to the origin is the point index (`i`

) multiplied with the base angle for point distribution (`BAD`

, which happens to be `36°`

or `π/10`

in our particular case).

So within the loop we have:

```
for(let i = 0; i < ND; i++) {
let cr = i%2 ? RCI : RCO,
ca = i*BAD,
x = Math.round(cr*Math.cos(ca)),
y = Math.round(cr*Math.sin(ca));
}
```

Since we've chosen a pretty big value for the `viewBox`

size, we can safely round the coordinate values so that our code looks cleaner, without decimals.

As for pushing these coordinates into the points array, we do this twice when we're on the outer circle (the even indices case) because that's where we actually have two control points overlapping, but only for the star, so we'll need to move each of these overlapping points into different positions to get the heart.

```
for(let i = 0; i < ND; i++) {
/* same as before */
PTS.push([x, y]);
if(!(i%2)) PTS.push([x, y]);
}
```

Next, we put data into our object `O`

. For the path data (`d`

) attribute, we store the array of points we get when calling the above function as the initial value. We also create a function for generating the actual attribute value (the path data string in this case - inserting commands in between the pairs of coordinates, so that the browser knows what to do with those coordinates). Finally, we take every attribute we have stored data for and we set its value to the value returned by the previously mentioned function:

```
(function init() {
/* same as before */
O.d = {
ini: getStarPoints(),
afn: function(pts) {
return pts.reduce((a, c, i) => {
return a + (i%3 ? ' ' : 'C') + c
}, `M${pts[pts.length - 1]}`)
}
};
for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].ini))
})();
```

The result can be seen in the Pen below:

See the Pen by thebabydino (@thebabydino) on CodePen.

This is a promising start. However, we want the first tip of the generating pentagram to point down and the first tip of the resulting star to point up. Currently, they're both pointing right. This is because we start from `0°`

(3 o'clock). So in order to start from 6 o'clock, we add `90°`

(`π/2`

in radians) to every current angle in the `getStarPoints()`

function.

`ca = i*BAD + .5*Math.PI`

This makes the first tip of the generating pentagram and resulting star to point down. To rotate the star, we need to set its `transform`

attribute to a half circle rotation. In order to do so, we first set an initial rotation angle to `-180`

. Afterwards, we set the function that generates the actual attribute value to a function that generates a string from a function name and an argument:

```
function fnStr(fname, farg) { return `${fname}(${farg})` };
(function init() {
/* same as before */
O.transform = { ini: -180, afn: (ang) => fnStr('rotate', ang) };
/* same as before */
})();
```

We also give our star a golden `fill`

in a similar fashion. We set an RGB array to the initial value in the `fill`

case and we use a similar function to generate the actual attribute value:

```
(function init() {
/* same as before */
O.fill = { ini: [255, 215, 0], afn: (rgb) => fnStr('rgb', rgb) };
/* same as before */
})();
```

We now have a nice golden SVG star, made up of five cubic Bézier curves:

See the Pen by thebabydino (@thebabydino) on CodePen.

#### Heart

Since we have the star, let's next see how we can get the heart!

We start with two intersecting circles of equal radii, both a fraction (let's say `.25`

for the time being) of the `viewBox`

size. These circles intersect in such a way that the segment connecting their central points is on the `x` axis and the segment connecting their intersection points is on the `y` axis. We also take these two segments to be equal.

Next, we draw diameters through the upper intersection point and then tangents through the opposite points of these diameters. These tangents intersect on the `y` axis.

The upper intersection point and the diametrically opposite points make up three of the five end points we need. The other two end points split the outer half circle arcs into two equal parts, thus giving us four quarter circle arcs.

Both control points for the curve at the bottom coincide with the intersection of the the two tangents drawn previously. But what about the other four curves? How can we go from circular arcs to cubic Bézier curves?

We don't have a cubic Bézier curve equivalent for a quarter circle arc, but we can find a very good approximation, as explained in this article.

The gist of it is that we start from a quarter circle arc of radius `R`

and draw tangents to the end points of this arc (`N` and `Q`). These tangents intersect at `P`. The quadrilateral `ONPQ` has all angles equal to `90°`

(or `π/2`

), three of them by construction (`O` corresponds to a `90°`

arc and the tangent to a point of that circle is always perpendicular onto the radial line to the same point) and the final one by computation (the sum of angles in a quadrilateral is always `360°`

and the other three angles add up to `270°`

). This makes `ONPQ` a rectangle. But `ONPQ` also has two consecutive edges equal (`OQ` and `ON` are both radial lines, equal to `R`

in length), which makes it a square of edge `R`

. So the lengths of `NP` and `QP` are also equal to `R`

.

The control points of the cubic curve approximating our arc are on the tangent lines `NP` and `QP`, at `C·R`

away from the end points, where `C`

is the constant the previously linked article computes to be `.551915`

.

Given all of this, we can now start computing the coordinates of the end points and control points of the cubic curves making up our star.

Due to the way we've chosen to construct this heart, `TO _{0}SO_{1}` (see figure below) is a square since it has all edges equal (all are radii of one of our two equal circles) and its diagonals are equal by construction (we said the distance between the central points equals that between the intersection points). Here,

`O`is the intersection of the diagonals and

`OT`is half the

`ST`diagonal.

`T`and

`S`are on the

`y`axis, so their

`x`coordinate is

`0`

. Their `y`coordinate in absolute value equals the

`OT`segment, which is half the diagonal (as is the

`OS`segment).

We can split any square of edge length `l`

into two equal right isosceles triangles where the catheti coincide with the square edges and the hypotenuse coincides with a diagonal.

Using one of these right triangles, we can compute the hypotenuse (and therefore the square diagonal) using Pythagora's theorem: `d² = l² + l²`

. This gives us the square diagonal as a function of the edge `d = √(2∙l) = l∙√2`

(conversely, the edge as a function of the diagonal is `l = d/√2`

). It also means that half the diagonal is `d/2 = (l∙√2)/2 = l/√2`

.

Applying this to our `TO _{0}SO_{1}` square of edge length

`R`

, we get that the `y`coordinate of

`T`(which, in absolute value, equals half this square's diagonal) is

`-R/√2`

and the `y`coordinate of

`S`is

`R/√2`

.Similarly, the `O _{k}` points are on the

`x`axis, so their

`y`coordinates are

`0`

, while their `x`coordinates are given by the half diagonal

`OO`:

_{k}`±R/√2`

.`TO _{0}SO_{1}` being a square also means all of its angles are

`90°`

(`π/2`

in radians) angles.In the illustration above, the `TB _{k}` segments are diameter segments, meaning that the

`TB`arcs are half circle, or

_{k}`180°`

arcs and we've split them into two equal halves with the `A`points, getting two equal

_{k}`90°`

arcs - `TA`and

_{k}`A`, which correspond to two equal

_{k}B_{k}`90°`

angles, `∠TO`and

_{k}A_{k}`∠A`.

_{k}O_{k}B_{k}Given that `∠TO _{k}S` are

`90°`

angles and `∠TO`are also

_{k}A_{k}`90°`

angles by construction, it results that the `SA`segments are also diameter segments. This gives us that in the

_{k}`TA`quadrilaterals, the diagonals

_{k}B_{k}S`TB`and

_{k}`SA`are perpendicular, equal and cross each other in the middle (

_{k}`TO`,

_{k}`O`,

_{k}B_{k}`SO`and

_{k}`O`are all equal to the initial circle radius

_{k}A_{k}`R`

). This means the `TA`quadrilaterals are squares whose diagonals are

_{k}B_{k}S`2∙R`

.From here we can get that the edge length of the `TA _{k}B_{k}S` quadrilaterals is

`2∙R/√2 = R∙√2`

. Since all angles of a square are `90°`

ones and the `TS`edge coincides with the vertical axis, this means the

`TA`and

_{k}`SB`edges are horizontal, parallel to the

_{k}`x`axis and their length gives us the

`x`coordinates of the

`A`and

_{k}`B`points:

_{k}`±R∙√2`

.Since `TA _{k}` and

`SB`are horizontal segments, the

_{k}`y`coordinates of the

`A`and

_{k}`B`points equal those of the

_{k}`T`(

`-R/√2`

) and `S`(

`R/√2`

) points respectively.Another thing we get from here is that, since `TA _{k}B_{k}S` are squares,

`A`are parallel with

_{k}B_{k}`TS`, which is on the

`y`(vertical) axis, therefore the

`A`segments are vertical. Additionally, since the

_{k}B_{k}`x`axis is parallel to the

`TA`and

_{k}`SB`segments and it cuts the

_{k}`TS`, it results that it also cuts the

`A`segments in half.

_{k}B_{k}Now let's move on to the control points.

We start with the overlapping control points for the bottom curve.

The `TB _{0}CB_{1}` quadrilateral has all angles equal to

`90°`

(`∠T`since

`TO`is a square,

_{0}SO_{1}`∠B`by construction since the

_{k}`B`segments are tangent to the circle at

_{k}C`B`and therefore perpendicular onto the radial lines

_{k}`O`at that point; and finally,

_{k}B_{k}`∠C`can only be

`90°`

since the sum of angles in a quadrilateral is `360°`

and the other three angles add up to `270°`

), which makes it a rectangle. It also has two consecutive edges equal - `TB`and

_{0}`TB`are both diameters of the initial squares and therefore both equal to

_{1}`2∙R`

. All of this makes it a square of edge `2∙R`

.From here, we can get its diagonal `TC` - it's `2∙R∙√2`

. Since `C` is on the `y` axis, its `x` coordinate is `0`

. Its `y` coordinate is the length of the `OC` segment. The `OC` segment is the `TC` segment minus the `OT` segment: `2∙R∙√2 - R/√2 = 4∙R/√2 - R/√2 = 3∙R/√2`

.

So we now have the coordinates of the two coinciding control points for the bottom curve are `(0,3∙R/√2)`

.

In order to get the coordinates of the control points for the other curves, we draw tangents through their endpoints and we get the intersections of these tangents at `D _{k}` and

`E`.

_{k}In the `TO _{k}A_{k}D_{k}` quadrilaterals, we have that all angles are

`90°`

(right) angles, three of them by construction (`∠D`and

_{k}TO_{k}`∠D`are the angles between the radial and tangent lines at

_{k}A_{k}O_{k}`T`and

`A`respectively, while

_{k}`∠TO`are the angles corresponding to the quarter circle arcs

_{k}A_{k}`TA`) and the fourth by computation (the sum of angles in a quadrilateral is

_{k}`360°`

and the other three add up to `270°`

). This makes `TO`rectangles. Since they have two consecutive edges equal (

_{k}A_{k}D_{k}`O`and

_{k}T`O`are radial segments of length

_{k}A_{k}`R`

), they are also squares.This means the diagonals `TA _{k}` and

`O`are

_{k}D_{k}`R∙√2`

. We already know that `TA`are horizontal and, since the diagonals of a square are perpendicular, it results the

_{k}`O`segments are vertical. This means the

_{k}D_{k}`O`and

_{k}`D`points have the same

_{k}`x`coordinate, which we've already computed for

`O`to be

_{k}`±R/√2`

. Since we know the length of `O`, we can also get the

_{k}D_{k}`y`coordinates - they're the diagonal length (

`R∙√2`

) with minus in front.Similarly, in the `A _{k}O_{k}B_{k}E_{k}` quadrilaterals, we have that all angles are

`90°`

(right) angles, three of them by construction (`∠E`and

_{k}A_{k}O_{k}`∠E`are the angles between the radial and tangent lines at

_{k}B_{k}O_{k}`A`and

_{k}`B`respectively, while

_{k}`∠A`are the angles corresponding to the quarter circle arcs

_{k}O_{k}B_{k}`A`) and the fourth by computation (the sum of angles in a quadrilateral is

_{k}B_{k}`360°`

and the other three add up to `270°`

). This makes `A`rectangles. Since they have two consecutive edges equal (

_{k}O_{k}B_{k}E_{k}`O`and

_{k}A_{k}`O`are radial segments of length

_{k}B_{k}`R`

), they are also squares.From here, we get the diagonals `A _{k}B_{k}` and

`O`are

_{k}E_{k}`R∙√2`

. We know the `A`segments are vertical and split into half by the horizontal axis, which means the

_{k}B_{k}`O`segments are on this axis and the

_{k}E_{k}`y`coordinates of the

`E`points are

_{k}`0`

. Since the `x`coordinates of the

`O`points are

_{k}`±R/√2`

and the `O`segments are

_{k}E_{k}`R∙√2`

, we can compute those of the `E`points as well - they're

_{k}`±3∙R/√2`

.Alright, but these intersection points for the tangents are not the control points we need to get the circular arc approximations. The control points we want are on the `TD _{k}`,

`A`,

_{k}D_{k}`A`and

_{k}E_{k}`B`segments at about

_{k}E_{k}`55%`

(this value is given by the constant `C`

computed in the previously mentioned article) away from the curve end points (`T`,

`A`,

_{k}`B`). This means the segments from the endpoints to the control points are

_{k}`C∙R`

.In this situation, the coordinates of our control points are `1 - C`

of those of the end points (`T`, `A _{k}` and

`B`) plus

_{k}`C`

of those of the points where the tangents at the end points intersect (`D`and

_{k}`E`).

_{k}So let's put all of this into JavaScript code!

Just like in the star case, we start with a `getStarPoints(f)`

function which depends on an arbitrary factor (`f`

) that's going to help us get the radius of the helper circles from the `viewBox`

size. This function also returns an array of coordinates we later use for interpolation.

Inside, we compute the stuff that doesn't change throughout the function. First off, the radius of the helper circles. From that, the half diagonal of the small squares whose edge equals this helper circle radius, half diagonal which is also the circumradius of these squares. Afterwards, the coordinates of the end points of our cubic curves (the `T`, `A _{k}`,

`B`points), in absolute value for the ones along the horizontal axis. Then we move on to the coordinates of the points where the tangents through the end points intersect (the

_{k}`C`,

`D`,

_{k}`E`points). These either coincide with the control points (

_{k}`C`) or can help us get the control points (this is the case for

`D`and

_{k}`E`).

_{k}```
function getHeartPoints(f = .25) {
const R = f*D /* helper circle radius */,
RC = Math.round(R/Math.SQRT2) /* circumradius of square of edge R */,
XT = 0, YT = -RC /* coords of point T */,
XA = 2*RC, YA = -RC /* coords of A points (x in abs value) */,
XB = 2*RC, YB = RC /* coords of B points (x in abs value) */,
XC = 0, YC = 3*RC /* coords of point C */,
XD = RC, YD = -2*RC /* coords of D points (x in abs value) */,
XE = 3*RC, YE = 0 /* coords of E points (x in abs value) */;
}
```

The interactive demo below shows the coordinates of these points on click:

See the Pen by thebabydino (@thebabydino) on CodePen.

Now we can also get the control points from the end points and the points where the tangents through the end points intersect:

```
function getHeartPoints(f = .25) {
/* same as before */
const /* const for cubic curve approx of quarter circle */
C = .551915,
CC = 1 - C,
/* coords of ctrl points on TD segs */
XTD = Math.round(CC*XT + C*XD), YTD = Math.round(CC*YT + C*YD),
/* coords of ctrl points on AD segs */
XAD = Math.round(CC*XA + C*XD), YAD = Math.round(CC*YA + C*YD),
/* coords of ctrl points on AE segs */
XAE = Math.round(CC*XA + C*XE), YAE = Math.round(CC*YA + C*YE),
/* coords of ctrl points on BE segs */
XBE = Math.round(CC*XB + C*XE), YBE = Math.round(CC*YB + C*YE);
/* same as before */
}
```

Next, we need to put the relevant coordinates into an array and return this array. In the case of the star, we started with the bottom curve and then went clockwise, so we do the same here. For every curve, we push two sets of coordinates for the control points and then one set for the point where the current curve ends.

See the Pen by thebabydino (@thebabydino) on CodePen.

Note that in the case of the first (bottom) curve, the two control points coincide, so we push the same pair of coordinates twice. The code doesn't look anywhere near as nice as in the case of the star, but it will have to suffice:

```
return [
[XC, YC], [XC, YC], [-XB, YB],
[-XBE, YBE], [-XAE, YAE], [-XA, YA],
[-XAD, YAD], [-XTD, YTD], [XT, YT],
[XTD, YTD], [XAD, YAD], [XA, YA],
[XAE, YAE], [XBE, YBE], [XB, YB]
];
```

We can now take our star demo and use the `getHeartPoints()`

function for the final state, no rotation and a crimson `fill`

instead. Then, we set the current state to the final shape, just so that we can see the heart:

```
function fnStr(fname, farg) { return `${fname}(${farg})` };
(function init() {
_SVG.setAttribute('viewBox', [-.5*D, -.5*D, D, D].join(' '));
O.d = {
ini: getStarPoints(),
fin: getHeartPoints(),
afn: function(pts) {
return pts.reduce((a, c, i) => {
return a + (i%3 ? ' ' : 'C') + c
}, `M${pts[pts.length - 1]}`)
}
};
O.transform = {
ini: -180,
fin: 0,
afn: (ang) => fnStr('rotate', ang)
};
O.fill = {
ini: [255, 215, 0],
fin: [220, 20, 60],
afn: (rgb) => fnStr('rgb', rgb)
};
for(let p in O) _SHAPE.setAttribute(p, O[p].afn(O[p].fin))
})();
```

This gives us a nice looking heart:

See the Pen by thebabydino (@thebabydino) on CodePen.

### Ensuring consistent shape alignment

However, if we place the two shapes one on top of the other with no `fill`

or `transform`

, just a `stroke`

, we see the alignment looks pretty bad:

See the Pen by thebabydino (@thebabydino) on CodePen.

The easiest way to solve this issue is to shift the heart up by an amount depending on the radius of the helper circles:

`return [ /* same coords */ ].map(([x, y]) => [x, y - .09*R])`

We now have much better alignment, regardless of how we tweak the `f`

factor in either case. This is the factor that determines the pentagram circumradius relative to the `viewBox`

size in the star case (when the default is `.5`

) and the radius of the helper circles relative to the same `viewBox`

size in the heart case (when the default is `.25`

).

See the Pen by thebabydino (@thebabydino) on CodePen.

### Switching between the two shapes

We want to go from one shape to the other on click. In order to do this, we set a direction `dir`

variable which is `1`

when we go from star to heart and `-1`

when we go from heart to star. Initially, it's `-1`

, as if we've just switched from heart to star.

Then we add a `'click'`

event listener on the `_SHAPE`

element and code what happens in this situation - we change the sign of the direction (`dir`

) variable and we change the shape's attributes so that we go from a golden star to a crimson heart or the other way around:

```
let dir = -1;
(function init() {
/* same as before */
_SHAPE.addEventListener('click', e => {
dir *= -1;
for(let p in O)
_SHAPE.setAttribute(p, O[p].afn(O[p][dir > 0 ? 'fin' : 'ini']));
}, false);
})();
```

And we're now switching between the two shapes on click:

See the Pen by thebabydino (@thebabydino) on CodePen.

### Morphing from one shape to another

What we really want however is not an abrupt change from one shape to another, but a gradual one. So we use the interpolation techniques explained in the previous article to achieve this.

We first decide on a total number of frames for our transition (`NF`

) and choose the kind of timing functions we want to use - an `ease-in-out`

type of function for transitioning the `path`

shape from star to heart, a `bounce-ini-fin`

type of function for the rotation angle and an `ease-out`

one for the `fill`

. We only include these, though we could later add others in case we change our mind and want to explore other options as well.

```
/* same as before */
const NF = 50,
TFN = {
'ease-out': function(k) {
return 1 - Math.pow(1 - k, 1.675)
},
'ease-in-out': function(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1)
},
'bounce-ini-fin': function(k, s = -.65*Math.PI, e = -s) {
return (Math.sin(k*(e - s) + s) - Math.sin(s))/(Math.sin(e) - Math.sin(s))
}
};
```

We then specify which of these timing functions we use for each property we transition:

```
(function init() {
/* same as before */
O.d = {
/* same as before */
tfn: 'ease-in-out'
};
O.transform = {
/* same as before */
tfn: 'bounce-ini-fin'
};
O.fill = {
/* same as before */
tfn: 'ease-out'
};
/* same as before */
})();
```

We move on to adding request ID (`rID`

) and current frame (`cf`

) variables, an `update()`

function we first call on click, then on every refresh of the display until the transition finishes and we call a `stopAni()`

function to exit this animation loop. Within the `update()`

function, we... well, update the current frame `cf`

, compute a progress `k`

and decide whether we've reached the end of the transition and we need to exit the animation loop or we carry on.

We also add a multiplier `m`

variable which we use so that we don't reverse the timing functions when we go from the final state (heart) back to the initial one (star).

```
let rID = null, cf = 0, m;
function stopAni() {
cancelAnimationFrame(rID);
rID = null;
};
function update() {
cf += dir;
let k = cf/NF;
if(!(cf%NF)) {
stopAni();
return
}
rID = requestAnimationFrame(update)
};
```

Then we need to change what we do on click:

```
addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
m = .5*(1 - dir);
update();
}, false);
```

Within the `update()`

function, we want to set the attributes we transition to some intermediate values (depending on the progress `k`

). As seen in the previous article, it's good to have the ranges between the final and initial values precomputed at the beginning, before even setting the listener, so that's our next step: creating a function that computes the range between numbers, whether as such or in arrays, no matter how deep and then using this function to set the ranges for the properties we want to transition.

```
function range(ini, fin) {
return typeof ini == 'number' ?
fin - ini :
ini.map((c, i) => range(ini[i], fin[i]))
};
(function init() {
/* same as before */
for(let p in O) {
O[p].rng = range(O[p].ini, O[p].fin);
_SHAPE.setAttribute(p, O[p].afn(O[p].ini));
}
/* same as before */
})();
```

Now all that's left to do is the interpolation part in the update() function. Using a loop, we go through all the attributes we want to smoothly change from one end state to the other. Within this loop, we set their current value to the one we get as the result of an interpolation function which depends on the initial value(s), range(s) of the current attribute (`ini`

and `rng`

), on the timing function we use (`tfn`

) and on the progress (`k`

):

```
function update() {
/* same as before */
for(let p in O) {
let c = O[p];
_SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k)));
}
/* same as before */
};
```

The last step is to write this interpolation function. It's pretty similar to the one that gives us the range values:

```
function int(ini, rng, tfn, k) {
return typeof ini == 'number' ?
Math.round(ini + (m + dir*tfn(m + dir*k))*rng) :
ini.map((c, i) => int(ini[i], rng[i], tfn, k))
};
```

This finally gives us a shape that morphs from star to heart on click and goes back to star on a second click!

See the Pen by thebabydino (@thebabydino) on CodePen.

It's ** almost** what we wanted - there's still one tiny issue. For cyclic values like angle values, we don't want to go back by half a circle on the second click. Instead, we want to continue going in the same direction for another half circle. Adding this half circle from after the second click with the one traveled after the first click, we get a full circle so we're right back where we started.

We put this into code by adding an optional continuity property and tweaking the updating and interpolating functions a bit:

```
function int(ini, rng, tfn, k, cnt) {
return typeof ini == 'number' ?
Math.round(ini + cnt*(m + dir*tfn(m + dir*k))*rng) :
ini.map((c, i) => int(ini[i], rng[i], tfn, k, cnt))
};
function update() {
/* same as before */
for(let p in O) {
let c = O[p];
_SHAPE.setAttribute(p, c.afn(int(c.ini, c.rng, TFN[c.tfn], k, c.cnt ? dir : 1)));
}
/* same as before */
};
(function init() {
/* same as before */
O.transform = {
ini: -180,
fin: 0,
afn: (ang) => fnStr('rotate', ang),
tfn: 'bounce-ini-fin',
cnt: 1
};
/* same as before */
})();
```

We now have the result we've been after: a shape that morphs from a golden star into a crimson heart and rotates clockwise by half a circle every time it goes from one state to the other:

See the Pen by thebabydino (@thebabydino) on CodePen.

Very cool animation. Almost worth learning the geometry and performing the demonic ritual

The animation is cool… but I am most impressed with the geometric diagrams in this article. Wowza. Well done.

I was told there would be no math.… :)

Great article! Finally an article that isn’t a jquery or an angular how-to. These days it seems so much of the focus is on abstraction and tooling that we are rarely challenged.

Absolutely! While AngularJS and jQuery are useful in some situations, it’s best to create this sort of thing with VanillaJS or no JS at all. I know I’d hate to add a call to an entire framework or library if this were the only thing I needed a script for.

Nice work, Ana Tudor!

This is really cool.

Or just use Greensock and condense it down to 2 lines…

Demo link or I’m calling shenanigans! :)

Do you have a greensock example? I’m interested.

Take a look

https://greensock.com/morphSVG

If you’d rather do this, fine, your choice, but I wrote the last couple of articles

^{(1)}for people who would rather use a brain and understand what’s going on in the back instead of making 5 HTTP requests and include over 450KB worth of JS libraries (because that’s exactly what the first example in the link above does) in order to achieve an effect they could have achieved with under 1% of that amount of code.Especially in the current landscape, where everyone is complaining about how bloated pages are.

^{(1)}Because at the end of the day, the previous article was the one that was actually about how to do the transition part, while this one in particular is primarily about how to get the geometry of the start and end shapes, which is something a morph plugin doesn’t help with. The morph plugin still needs to be fed that start and end path data.I’d just like to say that I appreciate Ana’s shorter version a great deal more than I would a Greensock example. For one, I don’t have to pay anyone to use the same method Ana did, and for two, Ana’s version is indeed shorter.

While

youmay only use two lines of codeon your endin a Greensock version, overall, the Greensock version uses far more lines of code than the VanillaJS version due to the inclusion of an entire Greensock plug-in by way of HTTP requests for a sizable chunk of additional libraries.If coding a longer and slower version (on your end) saves time for each individual viewing that version, the longer, slower VanillaJS version is absolutely worth it.

Great article (I ♥ the geometry), but please don’t use this in production. Using a library like Greensock is the correct solution, unless you have lots of time to waste.

If you are including Greensock just for this animation, you probably don’t need this animation. If your site has lots of animations you are already using Greensock (or some other library) so there will be no additional cost to using it.

Even if you enjoy all the math and have time to waste now, you should still not use it in production because at some point in the future you (or whoever is maintaining your code (chances are that they won’t like math as much as you)) will have a million things to get done and some $%#* wad from marketing will come up with the idea that they should A/B test the curvature of the shapes in this animation to see if it can drive up revenue and you will have to do the math multiple times and have it done by the end of the hour because it is “just changing a little bit”

Again great article and the concept is definitely worth learning but unless you want to ruin someone’s day in the future, use a library in production.

Careful.

I really hate comments like this, this is a place for learning, I loved the article, I don’t care if you can do it with X library in 3 characters, the great value in this article is the learning, the effort put into explaining every detail. Great article.

Having a ton of the most whiz-bang, high-end, plug-in animations might make for a gorgeous website, but if people are annoyed that the site is taking too long to load, chances are they will just close their browser tab and make a mental note to never go back to that site.

Animations are great, but like anything, they should be used in moderation. If you can do a simple animation using HTML/CSS/VanillaJS and it loads blazing fast, and it serves a purpose, make that animation. Better yet, do it with HTML/CSS only. The less JS you use, the easier it is to avoid issues with conflicting scripts.

I’m not saying Greensock should never be used, but I honestly don’t see a point in using it purely to avoid writing long code (which can be commented) and math (which should be easy enough for anyone to tweak if it’s documented out separately, like Ana’s post).

I see Greensock as something that should be used for apps or sites that are set up purely for the sake of animation as content or possibly for sites like CodePen, where large numbers of users may need Greensock functionality for the snippets they post. But there’s a whole lot out there that does not fit either situation.

Personally, I pride myself on sticking to an absolute minimum of HTTP requests, streamlined and minified code, and using as few languages as possible to create minimal designs that load in 10ms or less, when possible. My users seem to appreciate my extra work put into making sites load as fast as possible, and I absolutely appreciate people like Ana who can put together a thorough tutorial on how to do things without relying on bulky libraries and frameworks for every little piece of functionality, which, in turn, makes fast load times possible.

Great article. Thanks for sharing all the details through your wonderful drawings.

Thank you!

Well done, I agree, the geometry in the diagrams are amazing. I might start learning maths again.

I don’t really know what you mean by this, but changing the shape to something different would be the job of someone with some artistic sense, not the job of a tech.

Changing a the shape a little bit is also not something Greensock does. In all the examples you’ve seen, you feed Greensock a shape to morph from and a shape to morph to. Greensock doesn’t generate those shapes. The shapes are created by someone with enough of an artistic sense.

Same thing goes for the plain JS technique I’ve described

in the previous article. In this article I’ve just explained a demo where I used it. I had an idea of how to geometrically generate a star and heart and I acted on it.However, the morphing technique I explained works the same way for any other shapes, geometrically generated or not. In this demo for example, I just took two SVG shapes from Wikipedia, tweaked them a bit and used them as the initial and final shapes. No geometry computation was required. All I did was introduce some zero length lines so that I’d have the same curve to curve, line to line sequence in the path data. If somebody wants a different curvature for one of those curves, fine, give me the new path data with the new curvature, I’ll just paste it in place of the previous one. It doesn’t change my JS at all.

If you had an animation that just took two plain SVGs and morphed them (most likely with a library like Greensock) than you would be correct. Creating the SVGs would probably be the job of a designer.

I don’t know any non-technical designers who would know how to change the shape of the star with the code in this article.

I was really just using “changing the shape” as something that might need to be modified in the future. The math in this article is

outrageouslyfun, but not something most people use every day. Which means at some point in the future (if this is used in production) this animation will need to be modified, and the person modifying it (even if it is the person who created it) will probably have to learn this math all over again.Moral of this story:

Allcode becomes legacy eventually, and for most software projects the developers’ time is the most expensive cost.I don’t know any non-technical designers who would be able to change the shape of the star given the code in this article.

My point was just that the math in this article is

outrageouslyfun but not something most people use every day, and sinceallcode becomes legacy eventually someone will have to relearn the math to modify it.No non-technical person would have to touch the code. They’d just have to edit the shape/ create another shape in a graphical editor, dragging handles and stuff and then pass the new SVG result on.

Whatever SVG is used doesn’t influence the morphing code (which, again, is the topic of the previous article, not this one). The fact that I chose to generate the shape with JS here is absolutely irrelevant to the morphing part (and again, morphing is what Greensock does, not drawing the shape).

I can well take all the generating code out and just put the path data for the star in an

`ini`

attribute the path data for the heart in a`fin`

attribute on the path element, tweak the JS so that it takes the coordinates from those attributes instead of from the generating functions and that’s it.Which is exactly what I did in the demo linked above. The note and the nine are not geometrically generated, I just took them from Wikipedia.