这里已经有很好的解决方案了。我想要补充一些已经提出的变化 - 除了一些三角函数之外,如果想模拟手绘圆形,没有太多选择。
我首先建议实际记录一个真正的手绘圆。您可以记录点以及timeStamp
,并在以后的任何时间重新生成精确的图纸。您可以将其与线平滑算法相结合。
这里的解决方案会产生这样的圆:
![Snapshot](https://istack.dev59.com/fUDxD.webp)
您可以通过设置strokeStyle
,lineWidth
等来更改颜色、线条粗细等。
要绘制一个圆,请调用:
handDrawCircle(context, x, y, radius [, rounds] [, callback]);
(callback
作为动画函数的提供者,使其成为异步函数)。
代码分为两个部分:
- 生成点
- 动画点
初始化:
function handDrawCircle(ctx, cx, cy, r, rounds, callback) {
/// rounds is optional, defaults to 3 rounds
rounds = rounds ? rounds : 3;
var x, y, /// the calced point
tol = Math.random() * (r * 0.03) + (r * 0.025), ///tolerance / fluctation
dx = Math.random() * tol * 0.75, /// "bouncer" values
dy = Math.random() * tol * 0.75,
ix = (Math.random() - 1) * (r * 0.0044), /// speed /incremental
iy = (Math.random() - 1) * (r * 0.0033),
rx = r + Math.random() * tol, /// radius X
ry = (r + Math.random() * tol) * 0.8, /// radius Y
a = 0, /// angle
ad = 3, /// angle delta (resolution)
i = 0, /// counter
start = Math.random() + 50, /// random delta start
tot = 360 * rounds + Math.random() * 50 - 100, /// end angle
points = [], /// the points array
deg2rad = Math.PI / 180; /// degrees to radians
在主循环中,我们不会随机地跳来跳去,而是用一个随机值递增,然后用该值线性递增,如果到达边界(容差),则将其反转。
for (; i < tot; i += ad) {
dx += ix;
dy += iy;
if (dx < -tol || dx > tol) ix = -ix;
if (dy < -tol || dy > tol) iy = -iy;
x = cx + (rx + dx * 2) * Math.cos(i * deg2rad + start);
y = cy + (ry + dy * 2) * Math.sin(i * deg2rad + start);
points.push(x, y);
}
在最后一段中,我们只是渲染我们拥有的点。
速度由上一步中的
da
(角度差)确定:
i = 2;
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
draw();
function draw() {
ctx.lineTo(points[i], points[i + 1]);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(points[i], points[i + 1]);
i += 2;
if (i < points.length) {
requestAnimationFrame(draw);
} else {
if (typeof callback === 'function')
callback();
}
}
}
提示:为了获得更真实的笔画,您可以将globalAlpha
减少到例如0.7
。
但是,为了使其正常工作,您需要首先绘制一个实心的离屏画布,然后将该离屏画布复制到主画布上(该画布已设置globalAlpha
),以便每帧都能够进行绘制,否则各个点之间的笔画会重叠在一起(这看起来不好)。
对于正方形,您可以使用与圆相同的方法,但是不是使用半径和角度,而是将变化应用于线条。偏移增量以使线条非直线。
我稍微调整了一下值,但是请随意调整它们以获得更好的结果。
为了使圆形略微倾斜,您可以先轻微旋转画布:
rotate = Math.random() * 0.5;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-rotate);
ctx.translate(-cx, -cy);
而当循环结束时:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
}
(包含在上面链接的演示中)。
圆形将会更像这样:
![Snapshot tilted](https://istack.dev59.com/P2FEn.webp)
更新
为了解决提到的问题(评论字段太小:-)):在这种情况下,做动画线条实际上要复杂一些,因为你需要处理圆形运动以及随机边界。
参考评论点1:公差与半径密切相关,因为它定义了最大波动。我们可以修改代码,根据半径采用公差(和ix/iy
,因为它们定义了变化的“速度”)来调整。这就是我所说的微调,找到适用于所有尺寸的值/最佳位置。圆越小,变化越小。可以选择将这些值作为函数参数指定。
点2:由于我们正在对圆进行动画处理,所以该函数变成了异步的。如果我们在彼此之后立即绘制两个圆,它们会使画布混乱,因为从两个圆中添加新点到路径中,然后交叉描绘。
我们可以通过提供回调机制来解决这个问题:
handDrawCircle(context, x, y, radius [, rounds] [, callback]);
然后当动画完成时:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
if (typeof callback === 'function')
callback();
}
使用现有代码(请记住,该代码仅作为示例而非完整解决方案)时,您可能会遇到其他问题,其中之一是粗线的问题:
当我们逐个绘制线段时,画布不知道如何计算与前一个线段相对的线端角度。这是路径概念的一部分。当您用多个线段描边路径时,画布知道线端(线的末端)的角度在哪里。因此,在这里,我们要么从起点到当前点绘制线条并在两者之间进行清除,要么只使用小的lineWidth
值。
当我们使用clearRect
(它将使线条光滑,而不是像没有在两者之间清除时那样“锯齿状”地绘制)时,我们需要考虑实现一个顶部画布来进行动画处理,当动画完成后,我们将结果绘制到主画布上。
现在我们开始看到涉及的“复杂性”的一部分。这当然是因为画布在某种意义上是“低级”的,我们需要为所有内容提供所有逻辑。每次我们使用画布进行比简单形状和图像更多的操作时,我们基本上都在构建系统(但这也提供了极大的灵活性)。
ix,iy,rx,ry
等目的以及您如何打造此解决方案的东西。 - user1477388