Canvas学习:绘制圆和圆弧
826875339
8年前
<p>圆和圆弧是图形中基本图形之一,今天我们来了解在Canvas中怎么绘制圆和圆弧。在Canvas中绘制圆和圆弧其实和绘制线段和矩形一样的简单。在Canvas中, CanvasRenderingContext2D 对象提供了两个方法( arc() 和 arcTo() )来绘制圆和圆弧。</p> <h2>与圆和圆弧相关的基础知识</h2> <p>在学习如何绘制圆和圆弧之前,有一些相关的基础知识有必要先进行了解。</p> <ul> <li>角度旋转</li> <li>角度和弧度</li> <li>正切</li> </ul> <h3>角度旋转</h3> <p>在坐标系中,旋转分为顺时针和逆时针两个方向旋转:</p> <p><img src="https://simg.open-open.com/show/8cd0266021d9cbfc546fb4e1fe00d314.png"></p> <h3>角度和弧度</h3> <p>在CSS中,做旋转常用到的都是角度( deg )。但在Canvas中绘制圆或圆弧时用到的是弧度( rad )。 <a href="/misc/goto?guid=4959742172467831736" rel="nofollow,noindex">维基百科</a> 中是这样描述弧度的:</p> <p>弧度又称弪度,是平面角的单位,也是国际单位制导出单位。单位弧度定义为圆弧长度等于半径时的圆心角。角度以弧度给出时,通常不写弧度单位,或有时记为 rad 。</p> <p><img src="https://simg.open-open.com/show/3b1c18d189863a574a9a3cc4ea6f7b13.jpg"></p> <p>一个完整的圆的弧度是 2π ,所以 2π rad = 360° , 1 π rad = 180° , 1°=π/180 rad , 1 rad = 180°/π (约 57.29577951° )。以度数表示的角度,把数字乘以 π/180 便转换成弧度;以弧度表示的角度,乘以 180/π 便转换成度数。</p> <pre> <code class="language-javascript">rad = (π / 180) * deg</code></pre> <p>同样的:</p> <pre> <code class="language-javascript">deg = (rad * 180) / π</code></pre> <p>平时我们常看到的各种弧度如下:</p> <p><img src="https://simg.open-open.com/show/d119b114af74e6387f8de45f4009ac8b.jpg"></p> <p>JavaScript中弧度角度换算</p> <p>仅难了解角度和弧度之间的关系是不够的,我们还需要知道怎么使用JavaScript来实现角度和弧度之间的换算。一个 π 大约是 3.141592653589793 ,在JavaScript中对应的是 Math.PI 。那么角度和弧度之间的换算:</p> <pre> <code class="language-javascript">rad = (Math.PI * deg) / 180</code></pre> <p>同样的:</p> <pre> <code class="language-javascript">deg = (rad * 180) / Math.PI</code></pre> <p>为了方便计算和使用,可以将其封装成JavaScript函数:</p> <pre> <code class="language-javascript">function getRads (degrees) { return (Math.PI * degrees) / 180; } function getDegrees (rads) { return (rads * 180) / Math.PI; }</code></pre> <p>比如我们要将 30deg 转换成 rad ,可以直接使用:</p> <pre> <code class="language-javascript">getRads(30); // 0.5235987755982988rad getDegrees(0.7853981633974483); // 45deg</code></pre> <p>下图展示了常见的角度和弧度之间的换算:</p> <p><img src="https://simg.open-open.com/show/ac4c114fc675083e662c74592ecce008.jpg"></p> <h3>正切</h3> <p><a href="/misc/goto?guid=4959742172555874146" rel="nofollow,noindex">正切</a> (Tangent,tan,也作tg)是 <a href="/misc/goto?guid=4959649625619299554" rel="nofollow,noindex">三角函数</a> 的一种。它是周期函数,其最小正周期为 π ( Math.PI )。正切函数是 <a href="/misc/goto?guid=4959742172676656979" rel="nofollow,noindex">奇函数</a> 。</p> <p>在Canvas中常常需要和 <a href="/misc/goto?guid=4959649625619299554" rel="nofollow,noindex">三角函数</a> 打交道,这也说明了数学是多么的重要,真后悔当初没有认真学。有关于Canvas中三角函数的运用,后面我们将会花很大的篇幅来介绍。</p> <p>为什么在画圆要提到正切呢?那是因为我们后面在介绍 artTo() 时会涉及到正切相关的知识。下图可以说明,正切和圆以及圆弧之间的关系,看上去一点复杂,但不用急于求成,后面会慢慢懂的:</p> <p> </p> <p>有了这些基础,我们就可以开始学习在Canvas中怎么画圆和圆弧了。这也是这篇文章真正的主题,如果你等不及了,那继续往后阅读。</p> <h2>arc()方法</h2> <p>先来看 arc() 方法怎么绘制圆和圆弧。Canvas中的 arc() 方法接受六个参数:</p> <pre> <code class="language-javascript">arc(x, y, radius, startRad, endRad, [anticlockwise]);</code></pre> <p>在Canvas画布上绘制以坐标点 (x,y) 为圆心、半么为 radius 的圆上的一段弧线。这段弧线的起始弧度是 startRad ,结束弧度是 endRad 。这里的弧度是以 x 轴正方向为基准、进行顺时针旋转的角度来计算。其中 anticlockwise 表示 arc() 绘制圆或圆弧是以顺时针还是逆时针方向开始绘制。如果其值为 true 表示逆时针,如果是 false 表示为顺时针。该参数是一个可选参数,如果没有显式设置,其值是 false (也是 anticlockwise 的默认值)。</p> <p><img src="https://simg.open-open.com/show/721f630cff89aa809235120aab7aa4a0.jpg"></p> <p>记得当初我们学数时,圆的周长与半径的关系是: C = πd 或 C = 2πr 。具备这些基础,我们就可以使用 arc() 绘制弧线或圆了。</p> <h3>绘制弧线</h3> <p>先来看 arc() 绘制弧线,根据上面介绍的内容,传对应参数给他:</p> <pre> <code class="language-javascript">function drawScreen () { // x,y => 圆心坐标点 // r => 圆弧半径 var arc = { x: myCanvas.width / 2, y: myCanvas.height / 2, r: 100 }, w = myCanvas.width, h = myCanvas.height; ctx.save(); ctx.lineWidth = 10; ctx.strokeStyle = '#e3f'; // startRad => getRads(-45) // endRad => getRads(45) // 顺时针旋转 ctx.beginPath(); ctx.arc(arc.x, arc.y, arc.r,getRads(-45),getRads(45)); ctx.stroke(); // startRad => getRads(-135) // endRad => getRads(135) // 逆时针旋转 ctx.beginPath(); ctx.strokeStyle = "#f36"; ctx.arc(arc.x, arc.y, arc.r,getRads(-135),getRads(135),true); ctx.stroke(); ctx.restore(); }</code></pre> <p><img src="https://simg.open-open.com/show/52ae5704f78a43d3fa84b2d5009a6104.png"></p> <p>当我们把上面的 stroke() 换 fill() ,上面的效果就不是一个弧线了:</p> <p><img src="https://simg.open-open.com/show/2b72b4eec64e4259fec99e2907b91c86.png"></p> <p>另外在 stroke() 之前调用 closePath() ,那么弧线的起始点和终止点将会以一条直接连接在一起。比如上面的示例,加上之后的效果:</p> <pre> <code class="language-javascript">ctx.beginPath(); ctx.arc(arc.x, arc.y, arc.r,getRads(-45),getRads(45)); ctx.closePath(); ctx.stroke();</code></pre> <p><img src="https://simg.open-open.com/show/c27df749538440dc163e2f519b72d38f.png"></p> <p>arc() 绘制弧线是不是很简单,在实际中,借助一些条件循环,我们可以做一些有意思的效果。比如下面的这个示例,使用 arc() 绘制一个声波波率放大图:</p> <pre> <code class="language-javascript">function drawScreen () { var arc = { x: myCanvas.width / 2, y: myCanvas.height / 2, r: 10 }, w = myCanvas.width, h = myCanvas.height; ctx.save(); ctx.lineWidth = 1; ctx.strokeStyle = '#e3f'; for(var i = 0;i < 10; i++){ ctx.beginPath(); ctx.arc(arc.x, arc.y, arc.r * i,getRads(-45),getRads(45)); ctx.stroke(); ctx.beginPath(); ctx.arc(arc.x, arc.y, arc.r * i,getRads(-135),getRads(135),true); ctx.stroke(); } }</code></pre> <p><img src="https://simg.open-open.com/show/95d6ea95374061ce270d026df0fd33b9.png"></p> <p>特别注意:</p> <ul> <li>使用 arc() 绘制图形时,如果没有设置 moveTo() 那么会从圆弧的开始的点( startRad 处)作为起始点。如果设置了 moveTo() ,那么该点会连线到圆弧起始点。</li> <li>如果使用 stroke() 方法,那么会从开始连线到圆弧的起始位置。 如果是 fill 方法, 会自动闭合路径填充</li> </ul> <p><img src="https://simg.open-open.com/show/a269eaf9f546f2ef5dafc4fdbe3d9b21.png"></p> <h3>绘制制圆</h3> <p>使用 arc 绘制圆和绘制圆弧是一样的,只不过绘制圆的时候 startRad 和 endRad 是相同的。比如:</p> <pre> <code class="language-javascript">function drawScreen () { var arc = { x: myCanvas.width / 2, y: myCanvas.height / 2, r: 50 }, w = myCanvas.width, h = myCanvas.height; ctx.save(); ctx.lineWidth = 2; ctx.strokeStyle = '#fff'; ctx.fillStyle = '#000'; // 绘制一个边框圆 ctx.beginPath(); ctx.arc(arc.x / 2, arc.y, arc.r, getRads(0), getRads(360), false); ctx.stroke(); // 绘制一个闭合边框圆 ctx.beginPath(); ctx.arc(arc.x, arc.y, arc.r, getRads(0), getRads(360), false); ctx.closePath(); ctx.stroke(); // 绘制一个填充圆 ctx.beginPath(); ctx.arc(arc.x * 1.5, arc.y, arc.r,getRads(0), getRads(360), true); ctx.fill(); //绘制一个带边框填充的圆 ctx.beginPath(); ctx.arc(arc.x, arc.y, arc.r / 2,getRads(0), getRads(360), false); ctx.stroke(); ctx.fill(); ctx.restore(); }</code></pre> <p><img src="https://simg.open-open.com/show/8cb478310e54fb5a5d63ecf61adf96a2.png"></p> <p>来做个小练习,使用 arc() 绘制一个太极图:</p> <ul> <li>绘制一个白色和黑色大半圆,拼成一个圆形</li> <li>绘制制一个小的白色半圆和另一个黑色小半圆</li> <li>绘制一个白色和黑色小圆点</li> </ul> <p>这几个组合起来,就是我们想要的太极图:</p> <pre> <code class="language-javascript">function drawScreen () { var arc = { x: myCanvas.width / 2, y: myCanvas.height / 2, r: 100 }, w = myCanvas.width, h = myCanvas.height; ctx.save(); ctx.lineWidth = 1; // 绘制白色大圆 ctx.beginPath(); ctx.fillStyle = '#fff'; ctx.arc(arc.x, arc.y, arc.r, getRads(-90), getRads(90), false); ctx.fill(); // 绘制黑色大圆 ctx.beginPath(); ctx.fillStyle = '#000'; ctx.arc(arc.x, arc.y, arc.r, getRads(-90), getRads(90), true); ctx.fill(); // 绘制白色小圆 ctx.beginPath(); ctx.fillStyle = '#fff'; ctx.arc(arc.x, arc.y - arc.r/2, arc.r / 2,getRads(-90), getRads(90), true); ctx.fill(); // 绘制黑色小圆 ctx.beginPath(); ctx.fillStyle = '#000'; ctx.arc(arc.x, arc.y + arc.r/2, arc.r / 2,getRads(-90), getRads(90), false); ctx.fill(); // 绘制小黑点 ctx.beginPath(); ctx.fillStyle = '#000'; ctx.arc(arc.x, arc.y - arc.r/2, arc.r / 10,getRads(0), getRads(360), false); ctx.fill(); // 绘制小白点 ctx.beginPath(); ctx.fillStyle = '#fff'; ctx.arc(arc.x, arc.y + arc.r/2, arc.r / 10,getRads(0), getRads(360), false); ctx.fill(); }</code></pre> <p><img src="https://simg.open-open.com/show/7427e6d422ae55efcb2474b245487423.png"></p> <h3>绘制扇形</h3> <p>使用 arc() 除了可以绘制弧线和圆之外,还可以绘制扇形。绘制扇形关键点是通过 moveTo() 把起始点位置设置为圆心处,然后通过 closePath() 闭合路径。</p> <pre> <code class="language-javascript">function drawScreen () { var arc = { x: myCanvas.width / 2, y: myCanvas.height / 2, r: 100 }, w = myCanvas.width, h = myCanvas.height; ctx.save(); ctx.lineWidth = 1; ctx.strokeStyle = '#e3f'; ctx.fillStyle = '#e3f'; ctx.beginPath(); // 起始点设置在圆心处 ctx.moveTo(arc.x, arc.y); ctx.arc(arc.x, arc.y, arc.r,getRads(-45),getRads(45)); // 闭合路径 ctx.closePath(); ctx.stroke(); ctx.beginPath(); // 起始点设置在圆心处 ctx.moveTo(arc.x, arc.y); ctx.arc(arc.x, arc.y, arc.r,getRads(-135),getRads(135),true); // 闭合路径 ctx.closePath(); ctx.fill(); ctx.restore(); }</code></pre> <p><img src="https://simg.open-open.com/show/8d0b0e84a316e78158b2a69d6eba4a8e.png"></p> <p>利用这个原理,可以很轻松的实现一个饼图效果。</p> <p>特别声明: arc() 方法中的起始弧度参数 startRad 和结束弧度参数 endRad 都是以弧度为单位,即使你填入一个数字,例如 360 ,仍然会被看作是 360 弧度。</p> <h2>arcTo()方法</h2> <p>前面学习了 arc() 方法如何绘制弧线、圆或扇形等。在Canvas中 CanvasRenderingContext2D 还提供了另一个方法 arcTo() 用来绘制弧线,但 arcTo() 绘制不出圆。为什么呢?接下来,咱们就来了解 arcTo() 的使用方法。</p> <p>arcTo() 接受五个参数:</p> <pre> <code class="language-javascript">arcTo(x1, y1, x2, y2, radius)</code></pre> <p>arcTo() 方法将利用当前端点、端点一 (x1, y1) 和端点二 (x2, y2) 这三点所形成的夹角,然后绘制一段与夹角的两边相切并且半径为 radius 的圆上的弧线。弧线的起点就是当前端点所在边与圆的切点,弧线的终点就是商端点二 (x2,y2) 所在边与圆的切点,并且绘制的弧线是两个切点之间长度最短的那个圆弧。此外,如果当前端点不是弧线起点, arcTo() 方法还将添加一条当前端点到弧线起点的直线线段。</p> <p>上面理解起来有点吃力。事实上, arcTo() 尽管是通过两点和半径绘制弧线或圆,但事实上是有三个点参与。也就是说,不管是否调用 arcTo() ,其实就有一个点已经存在 (x0,y0) 。这样就 (x0,y0) 和 (x1,y1) 构成一线,然后 (x1,y1) 和 (x2,y2) 构成一线,这两条线交叉点就是 (x1,y1) 。然后以 radius 绘制的圆弧或圆都会与这两条线相切。这也就是在文章开头提正切的基础。</p> <p><img src="https://simg.open-open.com/show/3c37ef2ed01945fff15d7502c4c8c0de.png"></p> <p>或者换下图,你能更好的理解。</p> <p><img src="https://simg.open-open.com/show/89d8d6181071d970328c181ca7448629.png"></p> <p>在 arcTo() 函数中,虽然参数只涉及到 P1 也就是参数中的 (x1,y1) 和 P2 也就是参数中的 (x2,y2) 两个点,实际上还有一个隐含的点,就是画布上的当前点( P0 )也就是前面所说的 (x0,y0) 。当 P0 , P1 , P2 不重叠也不在一条直线的时候,这 3 个点可以构成一个三角形。想象一下,从 P0 开始,向 P1 画一条线段,从 P1 开始到 P2 再画一条线段,这两条线段形成一个夹角,然后以 r 画一个圆,移动这个圆将这个圆与线段 P0P1 和线段 P1P2 相切(也可能切点是在 P0P1 或者 P1P2 的延长线上),然后保留朝向 P1 这个点的弧线,就是 arcTo() 在弧线这部分做的事情。</p> <p>实际绘制是这样的:</p> <ul> <li>moveTo() 给出 P0 点坐标</li> <li>arcTo() 函数中的参数给出了 P1 点和 P2 点的坐标,以及圆形的半径 r</li> <li>计算以 r 为半径的圆和直线 P0P1 以及 P1P2 的切点,记为 S 点和 E 点,对应图上 Start 和 End 两个点</li> <li>从 P0 向 S 点画出一条线段</li> <li>从 S 点到 E 点,画出一段圆弧,半径为 r</li> <li>此时,画布当前点为 E 点</li> <li>然后从 E 点又画了一条线段到 P2 点,至此 arcTo 的工作已经完成,接下来你可以 stroke() 或者 fill() 了</li> </ul> <p>来看一个绘制过程:</p> <pre> <code class="language-javascript">function drawScreen () { ctx.lineWidth = 1; ctx.strokeStyle = '#f36'; ctx.fillStyle = 'red'; // 一个起始点 ( 100, 50 ), 那么绘制其点. 颜色设置为红色 ctx.fillRect( 100 - 4, 50 - 4, 8, 8 ); // 两个参考点分别为 ( 100, 200 ) 和 ( 300, 200 ), 绘制出该点 ctx.fillRect( 100 - 4, 200 - 4, 8, 8 ); ctx.fillRect( 300 - 4, 200 - 4, 8, 8 ); // 连接两个参考点 ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.moveTo( 100, 200 ); ctx.lineTo( 300, 200 ); ctx.stroke(); // 调用 arcTo 方法绘制圆弧. 记得将起始点设置为 ( 100, 50 ) ctx.beginPath(); ctx.strokeStyle = 'blue'; ctx.moveTo( 100, 50 ); ctx.arcTo( 100, 200, 300, 200, 80); ctx.stroke(); }</code></pre> <p><img src="https://simg.open-open.com/show/ce2cc2adab29f2ccdaed09a27da37955.gif"></p> <h3>绘制带圆角矩形</h3> <p>在学习Canvas中绘制矩形一节时,我们提到通过 lineJoin 改变线段端点形状来模拟一个圆角矩形。通过这样的方法绘制带圆角的矩形,局限性还是非常大的。不过值得庆達的是, acrTo() 可以轻易实现两线内切圆弧,言外之意,使用 arcTo() 来绘制一个圆角矩形是非常的方便。</p> <p>圆角矩形是由四段线条和四个 1/4 圆弧组成,拆解如下。</p> <p><img src="https://simg.open-open.com/show/ed76ce480c7fa0ce098c7140298bc14c.jpg"></p> <p>如此一来,我们就可以封装一个函数,用来绘制圆角矩形。根据上图,我们可以给这个函数,比如 drawRoundedRect() 函数传递对应的参数:</p> <ul> <li>ctx :Canvas画布绘图环境</li> <li>x,y :左上角</li> <li>width :矩形宽度</li> <li>height :矩形高度</li> <li>r :矩形圆角半径</li> <li>fill : 绘制一个填充的矩形</li> <li>stroke :绘制一个边框矩形</li> </ul> <p>开始封装函数:</p> <pre> <code class="language-javascript">function drawRoundedRect(ctx, x, y, width, height, fill, stroke) { ctx.save(); ctx.beginPath(); // draw top and top right corner ctx.moveTo(x + r, y); ctx.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner ctx.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner ctx.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner ctx.arcTo(x, y, x + r, y, r); if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } ctx.restore(); }</code></pre> <p>函数封装好之后,只需要调用,就可以轻易绘制出带圆角的矩形,而且简单易用:</p> <pre> <code class="language-javascript">function drawScreen () { ctx.strokeStyle = 'rgb(150,0,0)'; ctx.fillStyle = 'rgb(0,150,0)'; ctx.lineWidth = 7; drawRoundedRect(ctx, 30, 50, 200, 220, 20, true, true); ctx.strokeStyle = 'rgb(150,0,150)'; ctx.fillStyle = 'rgba(0,0,150,0.6)'; ctx.lineWidth = 7; drawRoundedRect(ctx, 300, 100, 250, 150, 8, true, false); }</code></pre> <p><img src="https://simg.open-open.com/show/33a6b5eb116acfd53079b8b655fefc1b.png"></p> <p>是不是很简单。可以来个复杂点的练习。比如,当初火热的2048游戏。</p> <p><img src="https://simg.open-open.com/show/ad01c994e66faeddf4911b984624d5b8.png"></p> <p>使用 drawRoundedRect() 函数就可以很轻易的绘制出来,有兴趣的同学不仿一试。</p> <h2>绘制月亮</h2> <p>这节主要学习了 arc() 和 arcTo() 两个方法,学习了怎么使用这两个方法在Canvas中如何绘制圆弧或圆。那么我们来看一个小示例,结合两者来绘制一月亮。</p> <p>用一张图来阐述绘制月亮的原理:</p> <p><img src="https://simg.open-open.com/show/e5793ca0838ce309a91504c99f07daa8.jpg"></p> <p>此图已经说明一切,直接上代码吧:</p> <pre> <code class="language-javascript">function drawMoon(cxt, d, x, y, R, rot){ cxt.save(); cxt.translate(x, y); cxt.scale(R, R); cxt.rotate(Math.PI / 180 * rot); pathMoon(cxt, d); cxt.fillStyle = 'hsl' + randomColor(); cxt.fill(); cxt.restore(); } //画路径 function pathMoon(cxt, d){ //D表示控制点的横坐标; cxt.beginPath(); cxt.arc(0, 0, 1, Math.PI * 0.5, Math.PI * 1.5, true); cxt.moveTo(0, -1); cxt.arcTo(d, 0, 0, 1, dis(0, -1, d, 0) * 1 / d); cxt.closePath(); } function dis(x1, y1, x2, y2){ return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } function drawScreen () { drawMoon(ctx,2,myCanvas.width / 2,myCanvas.height / 2,100,15); }</code></pre> <h2>总结</h2> <p>在Canvas中, CanvasRenderingContext2D 对象提供了两个方法( arc() 和 arcTo() )来绘制圆和圆弧。其中 arc() 即可绘制弧线,圆,也可以绘制扇形,但 arcTo() 仅能绘制出弧线。但 arcTo() 可以更轻易的帮助我们实现带圆角的矩形。</p> <p> </p> <p>来自:http://www.w3cplus.com/canvas/drawing-arc-and-circle.html</p> <p></p> <p> </p>