HTML 5画布中手绘圆形模拟

18

代码:

下面的代码使用jQuery在HTML 5画布中创建了一个圆形:

//get a reference to the canvas
var ctx = $('#canvas')[0].getContext("2d");

DrawCircle(75, 75, 20);

//draw a circle
function DrawCircle(x, y, radius)
{
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI*2, true); 
    ctx.fillStyle = 'transparent';
    ctx.lineWidth = 2;
    ctx.strokeStyle = '#003300';
    ctx.stroke();
    ctx.closePath();
    ctx.fill();
}

我试图模拟以下任意一种类型的圆形:

examples

我已经进行了研究并找到了这篇文章,但是无法应用它。

我希望绘制圆形而不仅仅是出现。

是否有更好的方法? 我感觉需要涉及很多数学 :)

P.S. 我喜欢PaperJs的简单性,也许使用其简化路径会是最容易的方法?

4个回答

17

这里已经有很好的解决方案了。我想要补充一些已经提出的变化 - 除了一些三角函数之外,如果想模拟手绘圆形,没有太多选择。

我首先建议实际记录一个真正的手绘圆。您可以记录点以及timeStamp,并在以后的任何时间重新生成精确的图纸。您可以将其与线平滑算法相结合。

这里的解决方案会产生这样的圆:

Snapshot

您可以通过设置strokeStylelineWidth等来更改颜色、线条粗细等。

要绘制一个圆,请调用:

handDrawCircle(context, x, y, radius [, rounds] [, callback]);

callback 作为动画函数的提供者,使其成为异步函数)。

代码分为两个部分:

  1. 生成点
  2. 动画点

初始化

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

但是,为了使其正常工作,您需要首先绘制一个实心的离屏画布,然后将该离屏画布复制到主画布上(该画布已设置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

更新

为了解决提到的问题(评论字段太小:-)):在这种情况下,做动画线条实际上要复杂一些,因为你需要处理圆形运动以及随机边界。

参考评论点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(它将使线条光滑,而不是像没有在两者之间清除时那样“锯齿状”地绘制)时,我们需要考虑实现一个顶部画布来进行动画处理,当动画完成后,我们将结果绘制到主画布上。

现在我们开始看到涉及的“复杂性”的一部分。这当然是因为画布在某种意义上是“低级”的,我们需要为所有内容提供所有逻辑。每次我们使用画布进行比简单形状和图像更多的操作时,我们基本上都在构建系统(但这也提供了极大的灵活性)。


哇!太棒了,这正是我所需要的。也许你可以解决两个问题:1)我注意到当我画一个较小的圆时,它更容易出错。2)当我尝试调用函数两次(以绘制 2 个或更多圆)时,脚本会变得混乱。http://jsfiddle.net/8w2GZ/4/ - user1477388
1
@user1477388 你好,谢谢!我更新了我的答案,并提供了可能的解决方案(评论栏太小了 :-))。 - user1693593
谢谢更新。这比我构建的大多数应用程序更数学化(我构建的是更逻辑而非数学的商业系统)。我想两者都做得好,那么你有什么参考资料推荐吗?例如,一些可以阐明ix,iy,rx,ry等目的以及您如何打造此解决方案的东西。 - user1477388
1
@user1477388 我在处理 dx/dy(ix/iy 分别增加它们)时使用它们。所以当 dx/dy 达到公差时,它们会被取反以朝另一个方向移动。至于制作它:我知道它至少必须模拟“离心力”(这里是一个强词 :-)),但它需要平滑,因此采用了小的增量值。最初,我想使用物理学来模拟围绕圆形路径的阻力,但我发现这有点过度,所以改用了一个简单的妥协。我大多数时间都用于商业应用程序,但在 80/90 年代的 CBM Amiga 上进行了很多有趣的数学和低级别的东西。 - user1693593
1
@user1477388 rx和ry分别是x轴和y轴的半径,因为我们绘制的是椭圆而不是圆形,所以它们必须分开。 - user1693593
1
出色的工作!我冒昧添加了一些线宽的变化(http://jsfiddle.net/0397mkf4/)。这可以通过lineWidth()参数进行调整。由于函数调用已经相当庞大,我没有将其上升到更高层次。一个库解决方案可能会使用一个选项对象。 - Joseph Thomas-Kerr

8

以下是我为这个答案创建的一些基础知识:

http://jsfiddle.net/Exceeder/TPDmn/

基本上,在绘制圆形时,需要考虑手部不完美因素。因此,在下面的代码中:

var img = new Image();
img.src="data:image/png;base64,...";

var ctx = $('#sketch')[0].getContext('2d');
function draw(x,y) {
  ctx.drawImage(img, x, y);
}

for (var i=0; i<500; i++) {
    var radiusError = +10 - i/20;
    var d = 2*Math.PI/360 * i;
    draw(200 + 100*Math.cos(d), 200 + (radiusError+80)*Math.sin(d) );
}

请注意,当角度(和位置)增加时,垂直半径误差如何变化。欢迎您尝试这个“小工具”,直到您对组件的功能有了“感觉”。例如,引入另一个组件来模拟“不稳定”的手,通过缓慢地随机改变它来改变半径误差是有意义的。
有很多不同的方法可以实现这个目标。我选择三角函数来进行简单的模拟,因为速度在这里并不重要。
更新:
例如,以下内容将使其变得不那么完美:
var d = 2*Math.PI/360 * i;
var radiusError = +10 - i/20 + 10*Math.sin(d);

显然,圆心在(200,200)处,因为用三角函数绘制圆(实际上是具有垂直半径RY和水平半径RX的椭圆)的公式如下:
x = centerX + RX * cos ( angle )
y = centerY + RY * sin ( angle )

+1 利用半径“误差”巧妙地为圆形引入变化的创意运用! - markE
这是一个很好的答案。PNG部分是如何工作的?我在哪里可以查看它?我想改变笔画的大小(也许是颜色)。另外,我最初想要动画绘制圆圈(而不仅仅是在画布上出现)。再次感谢您对这个问题的出色回答! - user1477388
2
@user1477388 我已创建了一个动画版本(可能不是最清晰的):http://jsfiddle.net/TPDmn/2/ - ComFreek

3

您的任务似乎有三个要求:

  1. 手绘形状。
  2. “有机”的笔画而不是“超精确”的笔画。
  3. 逐步显示圆形而不是一次性全部显示。

要开始,请查看Andrew Trice的这个漂亮的示例。

这个惊人的圆是我手绘的(您现在可以笑了...!)

My amazing circle created with Andrew's technique

安德鲁的演示符合您需求中的步骤1和2。

它允许您使用有机的“画笔效果”手绘圆形(或任何形状),而不是通常在画布上使用的超精确线条。

通过在手绘点之间重复绘制画笔图像,它实现了“画笔效果”。

这是演示:

http://tricedesigns.com/portfolio/sketch/brush.html#

代码可在GitHub上获取:

https://github.com/triceam/HTML5-Canvas-Brush-Sketch

Andrew Trice的演示程序绘制并忘记了组成您的圆的线条。

您的任务是实现第三个要求(记住笔画):

  • 手绘一个自己的圆形,
  • 将组成您的圆的每个线段保存在数组中,
  • 使用Andrew的风格化刷技术“播放”这些线段。

结果:手绘和风格化的圆逐步出现,而不是一下子全部出现。

您有一个有趣的项目...如果您感到慷慨,请分享您的成果!


1

在此处查看实时演示。 还可作为gist使用。

<div id="container">
    <svg width="100%" height="100%" viewBox='-1.5 -1.5 3 3'></svg>
</div>

#container {
  width:500px;
  height:300px;
}
path.ln {
  stroke-width: 3px;
  stroke: #666;
  fill: none;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1000;
  stroke-dashoffset: 1000;
  -webkit-animation: dash 5s ease-in forwards;
  -moz-animation:dash 5s ease-in forwards;
  -o-animation:dash 5s ease-in forwards;
  animation:dash 5s ease-in forwards;
}

@keyframes dash {
  to { stroke-dashoffset: 0; }
}

function path(δr_min,δr_max, el0_min, el0_max, δel_min,δel_max) {

    var c = 0.551915024494;
    var atan = Math.atan(c)
    var d = Math.sqrt( c * c + 1 * 1 ), r = 1;
    var el = (el0_min + Math.random() * (el0_max - el0_min)) * Math.PI / 180;
    var path = 'M';

    path += [r * Math.sin(el), r * Math.cos(el)];
    path += ' C' + [d * r * Math.sin(el + atan), d * r * Math.cos(el + atan)];

    for (var i = 0; i < 4; i++) {
        el += Math.PI / 2 * (1 + δel_min + Math.random() * (δel_max - δel_min));
        r *= (1 + δr_min + Math.random()*(δr_max - δr_min));
        path += ' ' + (i?'S':'') + [d * r * Math.sin(el - atan), d * r * Math.cos(el - atan)];
        path += ' ' + [r * Math.sin(el), r * Math.cos(el)];
    }

    return path;
}

function cX(λ_min, λ_max, el_min, el_max) {
    var el = (el_min + Math.random()*(el_max - el_min));
    return 'rotate(' + el + ') ' + 'scale(1, ' + (λ_min + Math.random()*(λ_max - λ_min)) + ')'+ 'rotate(' + (-el) + ')';
}

function canvasArea() {
    var width = Math.floor((Math.random() * 500) + 450);
  var height = Math.floor((Math.random() * 300) + 250);
    $('#container').width(width).height(height);
}
d3.selectAll( 'svg' ).append( 'path' ).classed( 'ln', true) .attr( 'd', path(-0.1,0, 0,360, 0,0.2 )).attr( 'transform', cX( 0.6, 0.8, 0, 360 ));

setTimeout(function() { location = '' } ,5000)

1
干得好!感谢分享。从编程角度来看,我通常不喜欢在代码中看到像λ和δ这样的特殊字符:( - user1477388
1
谢谢。大部分功劳归功于Patrick Surry,我进行了分支。我只是在他们的代码基础上进行了改进。你说得对,我一定会记下来调整(可能今晚或这周的某个时间)。感谢您的查看。 - davidcondrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接