javascript HTML 5 画布中的一手拉圆模拟
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/18685156/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Handdrawn circle simulation in HTML 5 canvas
提问by user1477388
The following codecreates a circle in HTML 5 Canvas using jQuery:
在下面的代码创建使用jQuery在HTML 5画布的圆:
Code:
代码:
//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();
}
I am trying to simulate any of the following types of circles:
我正在尝试模拟以下任何类型的圆圈:
I have researched and found this articlebut was unable to apply it.
我已经研究并找到了这篇文章,但无法应用它。
I would like for the circle to be drawn rather than just appear.
我希望圆圈被绘制而不仅仅是出现。
Is there a better way to do this? I'm sensing there's going to be a lot of math involved :)
有一个更好的方法吗?我感觉这将涉及很多数学问题:)
P.S. I like the simplicity of PaperJs, maybe this would be the easiest approach using it's simplified paths?
PS 我喜欢PaperJs的简单性,也许这将是使用它的简化路径的最简单方法?
采纳答案by user1477388
There are already good solutions presented here. I wanted to add a variations of what is already presented - there are not many options beyond some trigonometry if one want to simulate hand drawn circles.
这里已经提供了很好的解决方案。我想添加已经呈现的内容的变体 - 如果想要模拟手绘圆圈,除了一些三角函数之外,没有太多选项。
I would first recommend to actually record a real hand drawn circle. You can record the points as well as the timeStamp
and reproduce the exact drawing at any time later. You could combine this with a line smoothing algorithm.
我首先建议实际记录一个真正的手绘圆圈。您可以timeStamp
在以后的任何时间记录点以及复制精确的绘图。您可以将其与线平滑算法结合使用。
This here solution produces circles such as these:
这里的解决方案会产生这样的圆圈:
You can change color, thickness etc. by setting the strokeStyle
, lineWidth
etc. as usual.
您可以通过设置改变颜色,厚度等strokeStyle
,lineWidth
等一切正常。
To draw a circle just call:
要画一个圆圈,只需调用:
handDrawCircle(context, x, y, radius [, rounds] [, callback]);
(callback
is provided as the animation makes the function asynchronous).
(callback
作为动画使函数异步提供)。
The code is separated into two segments:
代码分为两段:
- Generate the points
- Animate the points
- 生成点
- 动画点
Initialization:
初始化:
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
In the main loop we don't bounce around randomly but increment with a random value and then increment linearly with that value, reverse it if we are at bounds (tolerance).
在主循环中,我们不会随机反弹,而是以随机值递增,然后与该值线性递增,如果我们处于边界(容差),则将其反转。
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);
}
And in the last segment we just render what we have of points.
在最后一段中,我们只渲染我们所拥有的点。
The speed is determined by da
(delta angle) in the previous step:
速度由da
上一步中的 (delta angle)决定:
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();
}
}
}
Tip: To get a more realistic stroke you can reduce globalAlpha
to for example 0.7
.
提示:要获得更逼真的笔触,您可以减少globalAlpha
到例如0.7
。
However, for this to work properly you need to draw solid to an off-screen canvas first and then blit that off-screen canvas to main canvas (which has the globalAlpha
set) for each frame or else the strokes will overlap between each point (which does not look good).
但是,要使其正常工作,您需要首先在屏幕外画布上绘制实体,然后将该屏幕外画布 blit 到globalAlpha
每个帧的主画布(具有设置),否则笔画将在每个点之间重叠(这不好看)。
For squares you can use the same approach as with the circle but instead of using radius and angle you apply the variations to a line. Offset the deltas to make the line non-straight.
对于正方形,您可以使用与圆形相同的方法,但不是使用半径和角度,而是将变化应用于一条线。偏移增量以使线不直。
I tweaked the values a little but feel free to tweak them more to get a better result.
我稍微调整了这些值,但可以随意调整它们以获得更好的结果。
To make the circle "tilt" a little you can first rotate the canvas a little:
要使圆圈“倾斜”一点,您可以先稍微旋转画布:
rotate = Math.random() * 0.5;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-rotate);
ctx.translate(-cx, -cy);
and when the loop finishes:
当循环结束时:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
}
(included in the demo linked above).
(包含在上面链接的演示中)。
The circle will look more like this:
圆圈看起来更像这样:
Update
更新
To deal with the issues mentioned (comment fields too small :-) ): it's actually a bit more complicated to do animated lines, especially in a case like this where you a circular movement as well as a random boundary.
处理提到的问题(评论字段太小 :-) ):制作动画线条实际上有点复杂,尤其是在这样的情况下,您可以进行圆周运动以及随机边界。
Ref. comments point 1: the tolerance is closely related to radius as it defined max fluctuation. We can modify the code to adopt a tolerance (and ix/iy
as they defines how "fast" it will variate) based on radius. This is what I mean by tweaking, to find that value/sweet-spot that works well with all sizes. The smaller the circle the smaller the variations. Optionally specify these values as arguments to the function.
参考 评论点 1:公差与半径密切相关,因为它定义了最大波动。我们可以修改代码以采用ix/iy
基于半径的容差(并且因为它们定义了它变化的“速度”)。这就是我所说的调整的意思,以找到适用于所有尺寸的值/最佳位置。圆圈越小,变化越小。(可选)将这些值指定为函数的参数。
Point 2: since we're animating the circle the function becomes asynchronous. If we draw two circles right after each other they will mess up the canvas as seen as new points are added to the path from both circles which then gets stroked criss-crossed.
第 2 点:由于我们正在为圆设置动画,因此函数变得异步。如果我们一个接一个地画两个圆圈,它们会弄乱画布,因为新的点被添加到两个圆圈的路径中,然后它们被纵横交错。
We can get around this by providing a callback mechanism:
我们可以通过提供回调机制来解决这个问题:
handDrawCircle(context, x, y, radius [, rounds] [, callback]);
and then when the animation has finished:
然后当动画完成时:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
if (typeof callback === 'function')
callback(); /// call next function
}
Another issues one will run into with the code as-is (remember that the code is meant as an example not a full solution :-) ) is with thick lines:
使用原样代码会遇到的另一个问题(请记住,代码只是作为示例而不是完整的解决方案 :-) )是粗线:
When we draw segment by segment separately canvas does not know how to calculate the butt angle of the line in relation to previous segment. This is part of the path-concept. When you stroke a path with several segments canvas know at what angle the butt (end of the line) will be at. So here we to either draw the line from start to current point and do a clear in between or only small lineWidth
values.
当我们逐段单独绘制时,画布不知道如何计算与前一段相关的线的对接角度。这是路径概念的一部分。当你用几段笔画一条路径时,画布知道屁股(线的末端)将处于什么角度。所以在这里我们要么画一条从起点到当前点的线,然后在两者之间做一个清除,要么只做很小的lineWidth
值。
When we use clearRect
(which will make the line smooth and not "jaggy" as when we don't use a clear in between but just draw on top) we would need to consider implementing a top canvas to do the animation with and when animation finishes we draw the result to main canvas.
当我们使用时clearRect
(这将使线条平滑而不是“锯齿状”,因为当我们在两者之间不使用清除而只是在顶部绘制时)我们需要考虑实现一个顶部画布来执行动画以及当动画完成时我们将结果绘制到主画布上。
Now we start to see part of the "complexity" involved. This is of course because canvas is "low-level" in the sense that we need to provide all logic for everything. We are basically building systems each time we do something more with canvas than just draw simple shapes and images (but this also gives the great flexibility).
现在我们开始看到所涉及的“复杂性”的一部分。这当然是因为画布是“低级”的,因为我们需要为所有事物提供所有逻辑。每次我们用画布做更多的事情时,我们基本上都是在构建系统,而不仅仅是绘制简单的形状和图像(但这也提供了很大的灵活性)。
回答by Alex Pakka
Here are some basics I created for this answer:
以下是我为此答案创建的一些基础知识:
http://jsfiddle.net/Exceeder/TPDmn/
http://jsfiddle.net/Exceeder/TPDmn/
Basically, when you draw a circle, you need to account for hand imperfections. So, in the following code:
基本上,当你画一个圆圈时,你需要考虑手部的缺陷。因此,在以下代码中:
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) );
}
Pay attention how vertical radiusError changes when the angle (and the position) grows. You are welcome to play with this fiddle until you get a "feel" what component does what. E.g. it would make sense to introduce another component to radiusError that emulates "unsteady" hand by slowly changing it my random amounts.
注意当角度(和位置)增加时垂直radiusError 是如何变化的。欢迎您使用这个小提琴,直到您“感觉到”什么组件做了什么。例如,将另一个组件引入到 radiusError 中是有意义的,它通过慢慢地改变我的随机数量来模拟“不稳定”的手。
There are many different ways to do this. I choose trig functions for the simplicity of the simulation, as speed is not a factor here.
有许多不同的方法可以做到这一点。我选择三角函数是为了模拟的简单性,因为速度不是这里的一个因素。
Update:
更新:
This, for example, will make it less perfect:
例如,这将使它不那么完美:
var d = 2*Math.PI/360 * i;
var radiusError = +10 - i/20 + 10*Math.sin(d);
Obviously, the center of the circle is at (200,200), as the formula for drawing a circle (rather, ellipsis with vertical radius RY and horizontal radius RX) with trigonometric functions is
显然,圆的中心在(200,200),因为用三角函数绘制圆(更确切地说,垂直半径为 RY 和水平半径为 RX 的椭圆)的公式是
x = centerX + RX * cos ( angle )
y = centerY + RY * sin ( angle )
回答by B M
Being confronted with similar tasks I created a cartoon style JS drawing library for SVG or HTML5 Canvas. It works as plugin for Raphael.js
, D3.js
or SVG.js
or as lib for the Canvas
. It is called comic.js
and can be found on github. Amongst other shapes it can draw similar circles you asked for. It is based on the articleyou mention.
面对类似的任务,我为 SVG 或 HTML5 Canvas 创建了一个卡通风格的 JS 绘图库。它可以作为插件的Raphael.js
,D3.js
或SVG.js
或lib中的Canvas
。它被调用comic.js
并且可以在github上找到。在其他形状中,它可以绘制您要求的类似圆圈。它基于您提到的文章。
This what it can produce:
这是它可以产生的:
回答by markE
Your task seems to have 3 requirements:
您的任务似乎有 3 个要求:
- A hand-drawn shape.
- An “organic” rather than “ultra-precise” stroke.
- Revealing the circle incrementally instead of all-at-once.
- 手绘形状。
- 一种“有机”而不是“超精确”的笔画。
- 逐步显示圆圈而不是一次全部显示。
To get started, check out this nice on-target demo by Andrew Trice.
首先,请查看 Andrew Trice 的这个不错的目标演示。
This amazing circle is hand drawn by me (you can laugh now...!)
这个神奇的圆圈是我手绘的(现在你可以笑了......!)
Andrew's demo does steps 1 and 2 of your requirements.
Andrew 的演示完成了您要求的第 1 步和第 2 步。
It lets you hand draw a circle (or any shape) using an organic looking “brush effect” instead of the usual ultra-precise lines normally used in canvas.
它让您可以使用有机外观的“画笔效果”而不是通常在画布中使用的超精确线条来手绘圆形(或任何形状)。
It achieves the “brush effect” by by repeated drawing a brush image between hand drawn points
它通过在手绘点之间重复绘制画笔图像来实现“画笔效果”
Here's the demo:
这是演示:
http://tricedesigns.com/portfolio/sketch/brush.html#
http://tricedesigns.com/portfolio/sketch/brush.html#
And the code is available on GitHub:
代码可在 GitHub 上找到:
https://github.com/triceam/HTML5-Canvas-Brush-Sketch
https://github.com/triceam/HTML5-Canvas-Brush-Sketch
Andrew Trice's demo draws-and-forgets the lines that make up your circle.
安德鲁·特里斯 (Andrew Trice) 的演示绘制并忘记了构成圆圈的线条。
Your task would be to impliment your third requirement (remembering strokes):
你的任务是实现你的第三个要求(记住笔画):
- Hand draw a circle of your own,
- Save each line segment that makes up your circle in an array,
- “Play” those segements using Andrew's stylized brush technique.
- 手绘一个自己的圆圈,
- 将构成圆的每个线段保存在一个数组中,
- 使用 Andrew 的风格化画笔技术“播放”这些片段。
Results: A hand-drawn and stylized circle that appears incrementally instead of all at once.
结果:手绘和程式化的圆圈逐渐出现,而不是一次出现。
You have an interesting project…If you feel generous, please share your results!
你有一个有趣的项目……如果你觉得很慷慨,请分享你的结果!
回答by davidcondrey
See live demo here.Also available as a 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)