How to smooth a freehand drawn SVG path?

后端 未结 3 886
暗喜
暗喜 2021-01-31 10:59

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 JavaScri

相关标签:
3条回答
  • 2021-01-31 11:42

    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.

    0 讨论(0)
  • 2021-01-31 11:46

    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.

    0 讨论(0)
  • 2021-01-31 11:50

    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.

    0 讨论(0)
提交回复
热议问题