# Smooth a Svg path with cubic bezier curves

## And a bit of trigonometry

Jul 26, 2017 · 5 min read

## While it is straightforward to draw straight lines in a Svg element, it requires a bit of trigonometry to smooth these lines. Let’s see how.

We have an array of tuples representing the points coordinates of a line.

`const points = [[5, 10], [10, 40], [40, 30], [60, 5], [90, 45], [120, 10], [150, 45], [200, 10]]`

And a Svg element in an HTML page:

`<svg viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" class="svg"></svg>`

We want to make a `<path>` element from the `points` array.

# Create a path from the points

The `d` attributes of `<path>` always starts with a move to command: `M x,y`, followed by several commands depending on the type of shape. The result is something like: `<path d="M 10,20 L 15,25 L 20,35">` for a straight line.

First, let’s make a generic `svgPath` function which has two parameters: the `points` array and a `command` function.

`// Render the svg <path> element // I:  - points (array): points coordinates//     - command (function)//       I:  - point (array) [x,y]: current point coordinates//           - i (integer): index of 'point' in the array 'a'//           - a (array): complete array of points coordinates//       O:  - (string) a svg path command// O:  - (string): a Svg <path> elementconst svgPath = (points, command) => {  // build the d attributes by looping over the points  const d = points.reduce((acc, point, i, a) => i === 0    // if first point    ? `M \${point[0]},\${point[1]}`    // else    : `\${acc} \${command(point, i, a)}`  , '')  return `<path d="\${d}" fill="none" stroke="grey" />`}`

Now, let’s create two commands functions:

• `lineCommand`: to draw straight lines.
• `bezierCommand`: to draw a smooth line.

# Drawing straight lines

Straight lines require the line to command, starting with the letter `L` followed by the coordinates of the end point `x,y`.

A basic `lineCommand` function to draw straight lines:

`// Svg path line command// I:  - point (array) [x, y]: coordinates// O:  - (string) 'L x,y': svg line commandconst lineCommand = point => `L \${point[0]} \${point[1]}``

Now we can use it to draw a line from the `points` array:

`const svg = document.querySelector('.svg')svg.innerHTML = svgPath(points, lineCommand)`

This gives the following result (view on Codepen):

# Drawing smooth lines

## The cubic bezier command

The cubic bezier command starts with the letter `C` followed by three pairs of coordinates `x1,y1 x2,y2 x,y`:

• `x1,y1`: coordinates of the start control point
• `x2,y2`: coordinates of the end control point
• `x,y`: coordinates of the end anchor point

A few things to notice:

• The start anchor point coordinates are given by the previous command.
• The end anchor point coordinates come from the original `points` array.
• Now we have to find the position of the two control points.

## Find the position of the control points

We join the anchor points surrounding the start and the end anchor points with a line (let’s call these the `opposed-line`s):

For the line to be smooth, the position of each control point has to be relative to its `opposed-line`:

• The control point is on a line parallel to the `opposed-line`, and tangent to the current anchor point.
• On this tangent line, the distance from the anchor point to the control point depends on the length of the `opposed-line` and an arbitrary `smoothing` ratio.
• The start control point goes in the same direction as the `opposed-line`, while the end control point goes backward.

## Code

First, a function to find the properties of the `opposed-line`:

`// Properties of a line // I:  - pointA (array) [x,y]: coordinates//     - pointB (array) [x,y]: coordinates// O:  - (object) { length: l, angle: a }: properties of the lineconst line = (pointA, pointB) => {  const lengthX = pointB[0] - pointA[0]  const lengthY = pointB[1] - pointA[1]  return {    length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),    angle: Math.atan2(lengthY, lengthX)  }}`

Then, a function to find the position of a control point:

`// Position of a control point // I:  - current (array) [x, y]: current point coordinates//     - previous (array) [x, y]: previous point coordinates//     - next (array) [x, y]: next point coordinates//     - reverse (boolean, optional): sets the direction// O:  - (array) [x,y]: a tuple of coordinatesconst controlPoint = (current, previous, next, reverse) => {  // When 'current' is the first or last point of the array  // 'previous' or 'next' don't exist.  // Replace with 'current'  const p = previous || current  const n = next || current  // The smoothing ratio  const smoothing = 0.2  // Properties of the opposed-line  const o = line(p, n)  // If is end-control-point, add PI to the angle to go backward  const angle = o.angle + (reverse ? Math.PI : 0)  const length = o.length * smoothing  // The control point position is relative to the current point  const x = current[0] + Math.cos(angle) * length  const y = current[1] + Math.sin(angle) * length  return [x, y]}`

A function to create the bezier curve `C` command:

`// Create the bezier curve command // I:  - point (array) [x,y]: current point coordinates//     - i (integer): index of 'point' in the array 'a'//     - a (array): complete array of points coordinates// O:  - (string) 'C x2,y2 x1,y1 x,y': SVG cubic bezier C commandconst bezierCommand = (point, i, a) => {  // start control point  const [cpsX, cpsY] = controlPoint(a[i - 1], a[i - 2], point)  // end control point  const [cpeX, cpeY] = controlPoint(point, a[i - 1], a[i + 1], true)  return `C \${cpsX},\${cpsY} \${cpeX},\${cpeY} \${point[0]},\${point[1]}`}`

And finally we reuse the `svgPath` function to loop over the `points` of the array and build the `<path>` element. Then we append the `<path>` to the `<svg>` element.

`const svg = document.querySelector('.svg')svg.innerHTML = svgPath(points, bezierCommand)`

And the result (view on Codepen):

Written by

Written by

## Effectively Overriding JavaScript’s toString() Method

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just \$5/month. Upgrade