您的草图中的云朵只是沿着每个多边形边缘绘制一系列具有一定重叠的圆。
绘制填充基本云形状的简单方法是先填充多边形,然后在填充的多边形上方绘制圆形。
当您想要使用部分透明颜色填充云朵时,这种方法就会失效,因为圆与彼此和基础多边形的重叠将被涂两次。它还会错过云曲线上的小卡通式超出部分。
绘制云朵的更好方法是首先创建所有圆,然后确定每个圆与其下一个邻居的相交角度。然后,您可以创建带有圆弧段的路径,您可以对其进行填充。轮廓由具有小偏移量的独立弧组成,用于结束角度。
在您的示例中,云弧之间的距离是静态的。通过使该距离变量并强制多边形边缘可被该距离均匀整除,很容易使多边形顶点处的弧重合。
以下是JavaScript的示例实现(不包括多边形拖动)。我不熟悉C#,但我认为基本算法很清楚。该代码是一个完整的网页,您可以保存并在支持画布的浏览器中显示;我已在Firefox中进行了测试。
绘制云的函数需要一个选项对象,如半径、弧距和以度为单位的过冲。我没有测试小多边形等退化情况,但在极端情况下,算法应该只为每个多边形顶点绘制单个弧线。
多边形必须按顺时针方向定义。否则,云会更像云层中的洞。如果周围的拐角弧线没有任何伪影,那将是一个很好的特性。
编辑:我提供了一个
简单的在线测试页面来测试下面的云算法。该页面允许您玩转各种参数。它还很好地展示了算法的缺陷。(在FF和Chrome中测试过)
当起始和结束角度未正确确定时,就会出现伪影。对于非常钝角的情况,相邻拐角处的弧线之间也可能存在交点。我没有解决这个问题,但也没有太多考虑。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Cumulunimbus</title>
<script type="text/javascript">
function Point(x, y) {
this.x = x;
this.y = y;
}
function get(obj, prop, fallback) {
if (obj.hasOwnProperty(prop)) return obj[prop];
return fallback;
}
function intersect(p, q, r) {
var dx = q.x - p.x;
var dy = q.y - p.y;
var len = Math.sqrt(dx*dx + dy*dy);
var a = 0.5 * len / r;
if (a < -1) a = -1;
if (a > 1) a = 1;
var phi = Math.atan2(dy, dx);
var gamma = Math.acos(a);
return [phi - gamma, Math.PI + phi + gamma];
}
function cloud(cx, poly, opt) {
var radius = get(opt, "radius", 20);
var overlap = get(opt, "overlap", 5/6);
var stretch = get(opt, "stretch", true);
var circle = [];
var delta = 2 * radius * overlap;
var prev = poly[poly.length - 1];
for (var i = 0; i < poly.length; i++) {
var curr = poly[i];
var dx = curr.x - prev.x;
var dy = curr.y - prev.y;
var len = Math.sqrt(dx*dx + dy*dy);
dx = dx / len;
dy = dy / len;
var d = delta;
if (stretch) {
var n = (len / delta + 0.5) | 0;
if (n < 1) n = 1;
d = len / n;
}
for (var a = 0; a + 0.1 * d < len; a += d) {
circle.push({
x: prev.x + a * dx,
y: prev.y + a * dy,
});
}
prev = curr;
}
var prev = circle[circle.length - 1];
for (var i = 0; i < circle.length; i++) {
var curr = circle[i];
var angle = intersect(prev, curr, radius);
prev.end = angle[0];
curr.begin = angle[1];
prev = curr;
}
cx.save();
if (get(opt, "fill", false)) {
cx.fillStyle = opt.fill;
cx.beginPath();
for (var i = 0; i < circle.length; i++) {
var curr = circle[i];
cx.arc(curr.x, curr.y, radius, curr.begin, curr.end);
}
cx.fill();
}
if (get(opt, "outline", false)) {
cx.strokeStyle = opt.outline;
cx.lineWidth = get(opt, "width", 1.0);
var incise = Math.PI * get(opt, "incise", 15) / 180;
for (var i = 0; i < circle.length; i++) {
var curr = circle[i];
cx.beginPath();
cx.arc(curr.x, curr.y, radius,
curr.begin, curr.end + incise);
cx.stroke();
}
}
cx.restore();
}
var poly = [
new Point(250, 50),
new Point(450, 150),
new Point(350, 450),
new Point(50, 300),
];
window.onload = function() {
cv = document.getElementById("cv");
cx = cv.getContext("2d");
cloud(cx, poly, {
fill: "lightblue",
outline: "black",
incise: 15,
radius: 20,
overlap: 0.8333,
stretch: false,
});
}
</script>
</head>
<body>
<canvas width="500" height="500" id="cv"></canvas>
</body>
</html>