这里已经有很好的解决方案。我想添加已经呈现的内容的变体 - 如果想要模拟手绘圆圈,除了一些三角函数之外没有太多选择。
我首先建议实际记录一个真实的手绘圆圈。您可以timeStamp
在以后的任何时间记录点以及复制精确的绘图。您可以将其与线平滑算法结合使用。
这里的解决方案会产生如下圆圈:
您可以像往常一样通过设置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;
/// start line
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
/// call loop
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
.
但是,要使其正常工作,您需要先将实体绘制到屏幕外画布,然后将该屏幕外画布blit到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();
}
(包含在上面链接的演示中)。
圆圈看起来更像这样:
更新
要处理提到的问题(评论字段太小:-)):实际上制作动画线条有点复杂,尤其是在这样的情况下,您需要进行圆周运动以及随机边界。
参考。评论点 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(); /// call next function
}
代码原样会遇到的另一个问题(请记住,代码仅作为示例而不是完整的解决方案:-))是粗线:
当我们逐段单独绘制时,画布不知道如何计算线相对于前一段的对接角度。这是路径概念的一部分。当您用几段画布描边路径时,知道对接(线的末端)将处于什么角度。所以在这里我们要么从开始点到当前点画线,要么在两者之间或只做一个小lineWidth
值。
当我们使用时clearRect
(这将使线条平滑而不是“锯齿状”,就像我们之间不使用清晰而只是在顶部绘制时一样)我们需要考虑实现一个顶部画布来执行动画以及动画何时完成我们将结果绘制到主画布上。
现在我们开始看到所涉及的“复杂性”的一部分。这当然是因为画布是“低级”的,因为我们需要为所有内容提供所有逻辑。每次我们用画布做更多的事情时,我们基本上都是在构建系统,而不仅仅是绘制简单的形状和图像(但这也提供了很大的灵活性)。