YIXUNFE / blog

文章区
151 stars 25 forks source link

如何将 N 个点用光滑的曲线连接 #52

Open ajccom opened 8 years ago

ajccom commented 8 years ago

如何将 N 个点用光滑的曲线连接

在思考如何对线段进行噪化的时候,我设想了一种方案 —— 在直线上获取的 N 个点并用光滑的曲线连接点与点,使之变成一条不规则的波浪线。


对比效果展示

l1l2

线段噪化前后对比


下面我就来讲述如何才能实现这个效果。


实现方案

我设计了一种基于二次贝塞尔曲线的解决方案,即通过二次贝塞尔曲线去连接分割直线后的 N 个散列点,所以我需要完成如下几个步骤才能得到最终效果:


获取散列点

为了计算简单,我设计了一种对线段进行等分的散列点获取函数,它会返回被分割后线段所包含的所有的点。

/**
 * _getNoisePath 将路径噪化处理
 * @param {Number} dist 路径长度
 * @param {Number} interval 噪化点之间的间隙
 * @param {Number} f 幅度
 * @return {Array} 噪化路径的点集合
 *
 * 处理前需要将画布原点移动到路径起始点位置且旋转画布使x轴与线段重合
 */
function _getNoisePath (dist, interval, f) {
  var i = 1, l = Math.floor(dist / interval), d, result = [[0, 0]]
  for (i; i < l; i++) {
    result.push([interval * i, f * Math.random() * (Math.random() > 0.5 ? -1 : 1)])
  }
  result.push([dist, 0])
  return result
}

这个函数有个前提设定,即设定我们的线段是一条从原点开始并与 X 轴重合的水平线。这是为了方便处理数据,而后期在绘制的时候,我们需要先对坐标轴进行变换,将坐标轴的 X 轴与线段重合并且原点 (0, 0) 处与线段起点重合。

另外,函数中的 f 参数表示振幅,即该点可能偏离原线段路径的可能偏移值。


获取每条曲线的特定控制点

为了能够使曲线与曲线之间可以光滑的连接在一起,我们需要找到特定的二次贝塞尔曲线的控制点。而这个控制点,就是上一条曲线的控制点相对该曲线的终点的对称点。

c

当前曲线的控制点与前一个曲线的终点和控制点的关系
/**
 * _getSymmetryPoint 获取对称控制点
 * @param {Array} controlPoint 当前路径控制点
 * @param {Array} endPoint 当前路径终点
 * @return {Array} 对称点
 */
function _getSymmetryPoint (controlPoint, endPoint) {
  var x, y, x1 = controlPoint[0], y1 = controlPoint[1], x2 = endPoint[0], y2 = endPoint[1]
  x = 2 * x2 - x1
  y = 2 * y2 - y1
  return [x, y]
}


绘制

有了上面两个函数的铺垫,绘制出我们需要的最终效果就易如反掌了。

var ctx = canvas.getContext('2d'),
  width = canvas.width,
  height = canvas.height

//假设线段是由点 (100, 100) 和 点 (400, 400) 组成
//注意这里的 prevCtrl 变量,赋值的值是第一个曲线的控制点
var path = _getNoisePath(Math.sqrt(90000 + 90000), 100, 40),
  prevCtrl = [(path[1][0] - path[0][0]) / 2, 40]

ctx.beginPath()
ctx.translate(100, 100)
ctx.rotate(Math.atan(300/300))
ctx.moveTo(0, 0) // 坐标轴变换的好处多多
path.shift()
path.map(function (p, i) {
  ctx.quadraticCurveTo(prevCtrl[0], prevCtrl[1], p[0], p[1])
  prevCtrl = _getSymmetryPoint(prevCtrl, p) //获取下一条曲线的控制点
})
ctx.stroke()
ctx.closePath()

查看 DEMO


扩展阅读 - 坐标系变换

在日常生活中对“斜”着的线做计算总是很复杂,需要 X 轴与 Y 轴共同参与计算,而且两点之间的距离公式(a^2 + b^2 = c^2)也比较复杂。

而通过坐标系变换后,平移画布至线段起点,使画布原点与线段起点重合。再旋转画布,使 X 轴与线段重合,这样在计算线段本身长度(仅需要 X 轴参与计算)或垂直于该线段的线段的长度( Y 轴的值)时,取值都很简单方便。

当然了,你需要多做的就是平移和旋转坐标系。


Thanks