17

我正在寻找一种解决方案,将由许多 auf LineTo 段组成的手绘用户绘制的 SVG 路径转换为更平滑的路径。

首选语言是 JavaScript,但欢迎提出任何建议。

4

3 回答 3

30

首先,我会推荐使用一个好的图形库,比如 raphael。它将简化实际使用 javascript 执行绘图的过程。

一个很简单的平滑方法是把所有的lineto命令用等效的curveto命令转换,并根据每条线段的角度计算一些控制点。例如,

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

变成

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

这两个都应该画一个等边三角形

下一步是计算控制点的位置。通常,您会希望平滑拐角两侧的控制点落在穿过顶点的假想线上。在等边三角形的顶点的情况下,这将是水平线。经过一些操作,你可以得到这样的东西:

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

棘手的部分是计算控制点,但这只不过是一个简单的三角问题。正如我之前提到的,这里的目标是将两个控制点放在一条平分角顶点的线上。例如,假设我们有两条线段:

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

知道切线角度,我们可以计算控制点位置

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)

最后一个问题是在实际的 curveTo 命令中放置每个控制点的位置:

CX1 Y1 X2 Y2 X3 Y3

X3 和 Y3 定义顶点位置。X1 Y1 和 X2 Y2 定义控制点。您可以将 X1 Y1 视为定义如何进入顶点的向量,将 X2 Y2 视为定义如何离开的向量。现在您有了两个控制点,您必须决定

CXcp1 Ycp1 Xcp2 Ycp2 0 0

或者

CXcp2 Ycp2 Xcp1 Ycp1 0 0

这是一个重要的决定。如果你把它们倒过来,形状会看起来像一个循环。至此,您应该能够确定应如何做出此决定...

同样,这是一个非常简单的解决方案,但它往往看起来很适合手绘路径。更好的解决方案可能会更进一步,将交点向内移动到每个线段交点的凹形部分。这更具挑战性。

于 2011-07-08T09:24:00.920 回答
5

让我们想象用户绘图是一个元组数组,我们可以这样做

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>

在这篇文章中有详细的解释。

于 2017-07-26T17:32:18.767 回答
2

我遇到了同样的问题,查看 paperjs 示例,我看到他们有一个用于路径简化的示例,隐藏在其背后的算法您可以在这里看到:https ://github.com/paperjs/paper.js/blob/主/src/path/PathFitter.js

简化路径的算法是一个名为“自动拟合数字化曲线的算法”的学术研究的 js 版本(带有优化) 。

我正在进行仅提取此算法的工作,并且可能会将其作为插件发布到svg.js

于 2013-10-11T19:26:55.687 回答