真实世界是不完美的,当我们需要模拟真实世界时,经常需要引入不完美/不规则的形状。比如陨石、雨滴、行星、树叶、绵延的海岸线、云朵等。
本文介绍如何基于Canvas生成随机大小的不完美圆形,所用方法适用于很多场合,一个典型的用例如逼真的下雨场景。
首先我们要了解,在几何学上圆形可以通过增加等边对称多边形边数来无限逼近。
那么所谓不完美圆形,实际可以通过一个不等边不对称的多边形来实现。
要实现不等边不对称,一个简单的方法是使多边形各个顶点距离中心点的距离(即半径)为一个随机值就好。
为避免半径落差过大,我们可以给其设定一个最大和最小值,然后在这个区间进行随机,代码如下:
function drawCircle(centerX, centerY, minRad, maxRad) {
var points = 512; //多边形边的总数目
var rad, theta;
var twoPi = 2 * Math.PI;
var x0, y0;
context.strokeStyle = "#aa6699";
context.lineWidth = 1.01;
context.fillStyle = "#6633aa";
context.beginPath();
theta = 0;
x0 = centerX + rad * Math.cos(theta);
y0 = centerY + rad * Math.sin(theta);
context.lineTo(x0, y0);
for (var i = 0; i < points; i++) {
theta += twoPi / points;
rad = minRad + Math.random() * (maxRad - minRad); //随机半径
x0 = centerX + rad * Math.cos(theta);
y0 = centerY + rad * Math.sin(theta);
context.lineTo(x0, y0);
}
context.stroke();
context.fill();
}
上面的代码实现效果如下:
上面这样的图形可以用来模拟松果、毛线球、刺猬等物体。
但如果想模拟海岸线、雨滴、云朵等线条比较柔和的物体,则显然不能满足要求。
我们需要边沿有一个平滑的过渡,而分形算法刚好可以用来完成这个任务。
我们假设半径的长度为1,我们来对这个区间进行细分,第1步在线段中间添加一个节点把区间分成2段,随机一个y坐标,第2步在左右半区间内重复类似操作,如此反复,直到达到预先设定的细分粒度。
为了避免线条的起伏过大,我们在给新增中间节点设定y坐标时,使其和所在细分线段的长度正相关,这样随着细分粒度的提高,局部区域的波动就越小,就形成了一个平滑过渡的效果,代码如下:
function setLinePoints(iterations) {
var pointList = {};
pointList.first = {
x: 0,
y: 1
};
var lastPoint = {
x: 1,
y: 1
}
var minY = 1;
var maxY = 1;
var point;
var nextPoint;
var dx, newX, newY;
pointList.first.next = lastPoint;
for (var i = 0; i < iterations; i++) {
point = pointList.first;
while (point.next != null) {
nextPoint = point.next;
dx = nextPoint.x - point.x;
newX = 0.5 * (point.x + nextPoint.x);
newY = 0.5 * (point.y + nextPoint.y);
newY += dx * (Math.random() * 2 - 1);
var newPoint = {
x: newX,
y: newY
};
//min, max
if (newY < minY) {
minY = newY;
} else if (newY > maxY) {
maxY = newY;
}
//put between points
newPoint.next = nextPoint;
point.next = newPoint;
point = nextPoint;
}
}
var normalizeRate = 1 / (maxY - minY);
point = pointList.first;
while (point != null) {
point.y = normalizeRate * (point.y - minY);
point = point.next;
}
return pointList;
}
function drawCircle(centerX, centerY, minRad, maxRad, phase) {
var point;
var rad, theta;
var twoPi = 2 * Math.PI;
var x0, y0;
//生成分形细分顶点链表,用来获取随机半径, 9次迭代将返回512个顶点。
var pointList = setLinePoints(9);
context.strokeStyle = "#aa6699";
context.lineWidth = 1.01;
context.fillStyle = "#6633aa";
context.beginPath();
point = pointList.first;
theta = phase;
rad = minRad + point.y * (maxRad - minRad);
x0 = centerX + rad * Math.cos(theta);
y0 = centerY + rad * Math.sin(theta);
context.lineTo(x0, y0);
while (point.next != null) {
point = point.next;
theta = twoPi * point.x + phase;
rad = minRad + point.y * (maxRad - minRad);
x0 = centerX + rad * Math.cos(theta);
y0 = centerY + rad * Math.sin(theta);
context.lineTo(x0, y0);
}
context.stroke();
context.fill();
}
上面的代码实现效果如下:
我们实现了一个"完美"的不完美的圆形。原文来自踏得网博客。