How to smooth a freehand drawn SVG path?

只谈情不闲聊 提交于 2019-12-20 09:38:08

问题


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.


回答1:


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.




回答2:


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.




回答3:


i'm with the same problem, looking at paperjs examples i saw that they have one example for path simplification, lurking the algorithm behind it you can see it here: https://github.com/paperjs/paper.js/blob/master/src/path/PathFitter.js

It's the algorithm that simplify the path witch is a js version (with optimizations) of a academic study named "An algorithm for automatically fitting digitized curves".

I'm on the works of extracting this algorithm only and will probably publich it as a plugin to svg.js.



来源:https://stackoverflow.com/questions/6621518/how-to-smooth-a-freehand-drawn-svg-path

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!