Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to smooth a freehand drawn SVG path?

I am looking for a solution to convert a freehand, user drawn SVG path, consisting of lots auf LineTo segments, into a smoother one.

Preferred language would be JavaScript, but any advice is welcome.

like image 977
florianguenther Avatar asked Jul 08 '11 07:07

florianguenther


People also ask

Is it possible to draw any path in SVG?

The element in SVG is the ultimate drawing element. It can draw anything! I've heard that under the hood all the other drawing elements ultimately use path anyway. The path element takes a single attribute to describe what it draws: the d attribute.

How do you draw a curved line in SVG?

The other type of curved line that can be created using SVG is the arc, called with the A command. Arcs are sections of circles or ellipses. For a given x-radius and y-radius, there are two ellipses that can connect any two points (as long as they're within the radius of the circle).


2 Answers

first of all, I would recommend using a good graphics library, such as raphael. It will simplify the process of actually using javascript to perform the drawing.

A very simple method of smoothing is to convert all lineto commands with equivalent curveto commands and calculate some control points based on the angles of each line segment. For example,

<svg width="1000" height="1000" version="1.1"
xmlns="http://www.w3.org/2000/svg">

<path d="
M250 150 
L150 350
L350 350
L250 150
" />

</svg> 

becomes

<svg width="1000" height="1000" version="1.1"
xmlns="http://www.w3.org/2000/svg">

<path d="
M250 150 
C250 150 150 350 150 350
C150 350 350 350 350 350
C350 350 250 150 250 150
" />

</svg> 

Both of these should draw an equilateral triangle

The next step would be to calculate the position of the control points. Generally, you will want the control points on either side of a smooth corner to fall on an imaginary line that passes through the vertex. In the case of the top point of the equilateral triangle, this would be horizontal line. After some manipulation, you can get something like this:

<svg width="1000" height="1000" version="1.1"
xmlns="http://www.w3.org/2000/svg">

<path d="
M250 150 
C230 150 140 333 150 350
C160 367 340 367 350 350
C360 333 270 150 250 150
" />

</svg> 

The tricky part is calculating the control points, but that turns into not much more than a simple trig problem. As I mentioned previously, the goal here is to put the two control points on a line that bisects the corner vertex. For example, suppose we have two line segments:

A. (0,0) to (3,2)
B. (0,0) to (1,-4)

the absolute angle of A is arctan(2/3) = 33.69 deg
the absolute angle of B is arctan(-4/1) = -75.96 deg
the bisection angle of AB is (33.69 + -75.96)/2 = -21.135
the tangent angle is AB is (-21.135 + 90) = 68.865

knowing the tangent angle, we can calculate the control point positions

smoothness = radius = r
tangent angle = T
Vertex X = Xv
Vertex Y = Yv

Control Point 1:
Xcp1 = cos(T)*r
Ycp1 = sin(T)*r

Control Point 2:
Xcp2 = cos(T)*(-r)
Ycp2 = sin(T)*(-r)

The last problem is where to put each control point in the actual curveTo command:

CX1 Y1 X2 Y2 X3 Y3

X3 and Y3 define the vertex location. X1 Y1 and X2 Y2 define the control points. You can think of X1 Y1 as defining the vector of how to enter the vertex and X2 Y2 as defining the vector of how to leave. Now that you have the two control points you must decide on

CXcp1 Ycp1 Xcp2 Ycp2 0 0

or

CXcp2 Ycp2 Xcp1 Ycp1 0 0

this is an important decision. If you get them backwards, the shape will look like a loop. By this point you should be able to determine how this decision should be made...

Again, this is a very simple solution, but it tends to look good for hand drawn paths. A better solution might take it a step further and move the intersection point inwards towards the concave section of each line segment intersection. This is quite a bit more challenging.

like image 111
jordancpaul Avatar answered Sep 16 '22 17:09

jordancpaul


Let's imagine the user drawing is an array of tuples, we could do something like

const points = [[100, 50], [50, 15], [5, 60], [10, 20], [20, 10], [30, 190], [40, 10], [50, 60], [60, 120], [70, 10], [80, 50], [90, 50], [120, 10], [150, 80], [160, 10] ]

const lineProperties = (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)
  }
}

const controlPointCalc = (current, previous, next, reverse) => {
  const c = current
  const p = previous ? previous : c
  const n = next ? next : c
  const smoothing = 0.2
  const o = lineProperties(p, n)
  const rev = reverse ? Math.PI : 0

  const x = c[0] + Math.cos(o.angle + rev) * o.length * smoothing
  const y = c[1] + Math.sin(o.angle + rev) * o.length * smoothing

  return [x, y]
}

const svgPathRender = points => {      
  const d = points.reduce((acc, e, i, a) => {
      if (i > 0) {
        const cs = controlPointCalc(a[i - 1], a[i - 2], e)
        const ce = controlPointCalc(e, a[i - 1], a[i + 1], true)
        return `${acc} C ${cs[0]},${cs[1]} ${ce[0]},${ce[1]} ${e[0]},${e[1]}`
      } else {
        return `${acc} M ${e[0]},${e[1]}`
      }
    },'')

  return `<path d="${d}" fill="none" stroke="black" />`
}

const svg = document.querySelector('.svg')

svg.innerHTML = svgPathRender(points)
<svg viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" class="svg">
</svg>

Detailed explanations in this article.

like image 38
François Romain Avatar answered Sep 18 '22 17:09

François Romain