问题
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