如何在HTML5画布中绘制椭圆?

87

似乎没有原生的函数可以画出类似椭圆形状的图形,而且我也不想要一个蛋形状。是否有可能用两个贝塞尔曲线来绘制椭圆呢? 有人有经验吗?

我的目的是画一些眼睛,实际上我只是用弧线来绘制。提前感谢。

解决方案

scale() 函数可以改变所有后续图形的缩放比例。save() 函数保存设置,而 restore() 函数用于还原设置以绘制新的形状而不进行缩放。

感谢 Jani 的帮助。

ctx.save();
ctx.scale(0.75, 1);
ctx.beginPath();
ctx.arc(20, 21, 10, 0, Math.PI*2, false);
ctx.stroke();
ctx.closePath();
ctx.restore();

7
有些答案可能会说可以用贝塞尔曲线画椭圆,但实际上你无法用贝塞尔曲线准确地绘制一个椭圆。虽然你可以用多项式曲线来近似椭圆,但不能完全复制它。 - Joe
我给了一个+1,但这会扭曲线条和圆圈,这对我来说不起作用。不过信息很好。 - mwilcox
1
@mwilcox - 如果您在stroke()之前放置restore(),它将不会扭曲线条,正如Johnathan Hebert在下面的Steve Tranby的答案评论中提到的那样。此外,这是closePath()不必要和不正确的使用。这种误解的方法... https://dev59.com/Ymgv5IYBdhLWcg3wKtlB#16104699 - Brian McCutchon
请勿在问题帖中放置解决方案。请将解决方案放在答案帖中。 - starball
16个回答

121

更新:

  • 缩放方法可能会影响线条宽度的外观
  • 正确使用缩放方法可以保持线条宽度不变
  • Canvas现在支持椭圆方法
  • 在JSBin中添加了更新的测试

JSBin 测试示例(已更新以测试其他答案进行比较)

  • 贝塞尔 - 基于包含矩形的左上角和宽度/高度绘制
  • 中心贝塞尔 - 基于中心和宽度/高度绘制
  • 弧和缩放 - 基于绘制圆形并缩放
  • 二次曲线 - 用二次曲线绘制
    • 测试似乎没有完全绘制相同,可能是实现
    • 请参见oyophant's答案
  • Canvas椭圆 - 使用W3C标准ellipse()方法
    • 测试似乎没有完全绘制相同,可能是实现
    • 请参见Loktar's答案

原始:

如果你想要一个对称的椭圆,你可以创建一个半径为宽度的圆,然后将其缩放到所需的高度(编辑:请注意,这会影响线条宽度外观 - 参见acdameli的答案),但如果你想要完全控制椭圆,这里有一种使用贝塞尔曲线的方法。

<canvas id="thecanvas" width="400" height="400"></canvas>

<script>
var canvas = document.getElementById('thecanvas');

if(canvas.getContext) 
{
  var ctx = canvas.getContext('2d');
  drawEllipse(ctx, 10, 10, 100, 60);
  drawEllipseByCenter(ctx, 60,40,20,10);
}

function drawEllipseByCenter(ctx, cx, cy, w, h) {
  drawEllipse(ctx, cx - w/2.0, cy - h/2.0, w, h);
}

function drawEllipse(ctx, x, y, w, h) {
  var kappa = .5522848,
      ox = (w / 2) * kappa, // control point offset horizontal
      oy = (h / 2) * kappa, // control point offset vertical
      xe = x + w,           // x-end
      ye = y + h,           // y-end
      xm = x + w / 2,       // x-middle
      ym = y + h / 2;       // y-middle

  ctx.beginPath();
  ctx.moveTo(x, ym);
  ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  //ctx.closePath(); // not used correctly, see comments (use to close off open path)
  ctx.stroke();
}

</script>

1
这是最通用的解决方案,因为“scale”会扭曲笔画(请参见acdameli的答案)。 - Trevor Burnham
4
可以用ctx.fill()来替换ctx.stroke(),以获得一个填充的形状。 - h3xStream
15
如果您先进行缩放,然后使用圆弧或其他方式添加到路径中,并再次缩放回原始大小,然后描边现有路径,比例尺不会扭曲笔画。 - Johnathan Hebert
1
只是一个近似值4*(sqrt(2)-1)/3在许多地方都能找到,而Google的计算器给出的结果是0.55228475。快速搜索可以得到这个很好的资源:http://www.whizkidtech.redprince.net/bezier/circle/kappa/ - Steve T
1
不是一个愚蠢的问题。如果你想象一个矩形包围椭圆,那么x,y就是左上角,中心点将会在{x + w/2.0, y + h/2.0}这个位置。 - Steve T
显示剩余4条评论

48

以下是其他地方解决方案的简化版本。我绘制了一个规范的圆形,进行平移和缩放,然后描边。

function ellipse(context, cx, cy, rx, ry){
        context.save(); // save state
        context.beginPath();

        context.translate(cx-rx, cy-ry);
        context.scale(rx, ry);
        context.arc(1, 1, 1, 0, 2 * Math.PI, false);

        context.restore(); // restore to original state
        context.stroke();
}

2
这非常优雅。让 contextscale 来处理繁重的工作。 - adu
2
请注意,在描边之前非常重要的还原操作(否则描边宽度会变形)。 - Jason S
正是我所需要的,谢谢!我知道除了贝塞尔曲线之外还有其他方法,但我自己无法使比例尺起作用。 - user4051844
@JasonS 只是好奇,如果你在描边后进行还原,为什么笔画宽度会变形? - OneMoreQuestion

21
现在Canvas有一个本地的ellipse函数,与arc函数非常相似,不过现在我们有两个半径值和一个旋转角度,这很棒。

ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)

演示实例

ctx.ellipse(100, 100, 10, 15, 0, 0, Math.PI*2);
ctx.fill();

目前仅在Chrome中运行正常


2
是的,这个实现比较慢。 - user1693593
@Epistemex 同意。我想/希望它会随着时间的推移变得更快。 - Loktar
1
根据 https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.ellipse,现在Opera也支持它了。 - jazzpi

16

对于简单的椭圆形,贝塞尔曲线方法非常好用。如果需要更多控制,可以使用循环以不同的x和y半径值(radiuses, radii?)来绘制椭圆。

添加旋转角度参数(rotationAngle parameter)可以使椭圆沿中心旋转任意角度。通过改变循环运行的范围(var i),可以绘制部分椭圆形。

使用这种方式渲染椭圆可以确定线上所有点的精确xy位置。如果其他对象的位置和方向依赖于椭圆的位置和方向,则此方法非常有用。

这里是代码示例:

for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) {
    xPos = centerX - (radiusX * Math.sin(i)) * Math.sin(rotationAngle * Math.PI) + (radiusY * Math.cos(i)) * Math.cos(rotationAngle * Math.PI);
    yPos = centerY + (radiusY * Math.cos(i)) * Math.sin(rotationAngle * Math.PI) + (radiusX * Math.sin(i)) * Math.cos(rotationAngle * Math.PI);

    if (i == 0) {
        cxt.moveTo(xPos, yPos);
    } else {
        cxt.lineTo(xPos, yPos);
    }
}

在此处查看交互式示例:http://www.scienceprimer.com/draw-oval-html5-canvas


这种方法产生的椭圆在数学上是最正确的。我想知道它在计算成本方面与其他方法相比如何。 - Eric Rini
@Eric 我脑海中的第一印象是,它们之间的区别在于(这种方法:12次乘法、4次加法和8个三角函数)与(4个立方贝塞尔曲线:24次乘法和24次加法)。假设画布使用De Casteljau迭代方法,我不确定它们在各种浏览器画布中实际上是如何实现的。 - DerekR
尝试在Chrome中对这两种方法进行分析: 4个立方贝塞尔曲线:约0.3毫秒 该方法:约1.5毫秒 当步长= 0.1时,看起来大约相同为0.3毫秒。 - sol0mka
显然,CanvasRenderingContext2D.ellipse()不支持所有浏览器。这个例程非常好用,精度完美,足以满足工作坊模板的需求。甚至在Internet Explorer上也可以使用,唉。 - zipzit

13

你可以尝试使用非均匀缩放。你可以提供X和Y缩放,只需将X或Y缩放设置得比另一个大,并绘制一个圆,你就有了一个椭圆。


12
您需要使用4个贝塞尔曲线(和一个魔数)才能可靠地重现椭圆形。请参见这里:www.tinaja.com/glib/ellipse4.pdf
两条贝塞尔曲线无法准确重现一个椭圆形。要证明这一点,请尝试使用上面的某些2条贝塞尔曲线方案,其高度和宽度相等-它们理想上应该近似于一个圆形,但事实并非如此。它们仍然看起来是椭圆形,这证明它们没有按预期执行。
这是应该有效的代码:http://jsfiddle.net/BsPsj/.
function ellipse(cx, cy, w, h){
    var ctx = canvas.getContext('2d');
    ctx.beginPath();
    var lx = cx - w/2,
        rx = cx + w/2,
        ty = cy - h/2,
        by = cy + h/2;
    var magic = 0.551784;
    var xmagic = magic*w/2;
    var ymagic = h*magic/2;
    ctx.moveTo(cx,ty);
    ctx.bezierCurveTo(cx+xmagic,ty,rx,cy-ymagic,rx,cy);
    ctx.bezierCurveTo(rx,cy+ymagic,cx+xmagic,by,cx,by);
    ctx.bezierCurveTo(cx-xmagic,by,lx,cy+ymagic,lx,cy);
    ctx.bezierCurveTo(lx,cy-ymagic,cx-xmagic,ty,cx,ty);
    ctx.stroke();


}

这个解决方案对我来说最好。四个参数。快速。简单。 - Oliver
你好,我也喜欢这个解决方案,但当我尝试运行你的代码时,发现在参数值左侧添加零时,圆形会变形。随着圆形尺寸的增加,这种变形更加明显,你知道这是为什么吗?http://jsfiddle.net/BsPsj/187/ - alexcons
1
@alexcons 在数字前加0可以将其转换为八进制整数字面量。 - Alankar Misra

9
我对Andrew Staroscik的这段代码进行了一些改编,适用于那些不想要一个如此普通的椭圆并且只有椭圆的大半轴和离心率数据(例如天文学中用于绘制轨道的javascript玩具)的人们。
在这里,我提供了这些步骤,可以根据需要调整以获得更高的绘图精度:
/* draw ellipse
 * x0,y0 = center of the ellipse
 * a = greater semi-axis
 * exc = ellipse excentricity (exc = 0 for circle, 0 < exc < 1 for ellipse, exc > 1 for hyperbole)
 */
function drawEllipse(ctx, x0, y0, a, exc, lineWidth, color)
{
    x0 += a * exc;
    var r = a * (1 - exc*exc)/(1 + exc),
        x = x0 + r,
        y = y0;
    ctx.beginPath();
    ctx.moveTo(x, y);
    var i = 0.01 * Math.PI;
    var twoPi = 2 * Math.PI;
    while (i < twoPi) {
        r = a * (1 - exc*exc)/(1 + exc * Math.cos(i));
        x = x0 + r * Math.cos(i);
        y = y0 + r * Math.sin(i);
        ctx.lineTo(x, y);
        i += 0.01;
    }
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = color;
    ctx.closePath();
    ctx.stroke();
}

5

我的解决方案与所有这些解决方案有些不同。我认为最接近的是上面得票最多的答案,但我认为这种方法更清洁、更易理解。

http://jsfiddle.net/jaredwilli/CZeEG/4/

    function bezierCurve(centerX, centerY, width, height) {
    con.beginPath();
    con.moveTo(centerX, centerY - height / 2);

    con.bezierCurveTo(
        centerX + width / 2, centerY - height / 2,
        centerX + width / 2, centerY + height / 2,
        centerX, centerY + height / 2
    );
    con.bezierCurveTo(
        centerX - width / 2, centerY + height / 2,
        centerX - width / 2, centerY - height / 2,
        centerX, centerY - height / 2
    );

    con.fillStyle = 'white';
    con.fill();
    con.closePath();
}

然后像这样使用:

bezierCurve(x + 60, y + 75, 80, 130);

在fiddle中有几个使用示例,以及一个使用quadraticCurveTo的失败尝试。

代码看起来很不错,但是如果你使用它传递相同的高度和宽度值来绘制一个圆圈,你会得到一个形状类似于拥有非常圆润的角落的正方形。在这种情况下,有没有办法获得一个真正的圆形呢? - Drew Noakes
我认为在这种情况下最好的选择是使用arc()方法,你可以定义弧线的起始和结束角度位置,就像这里得到的最高票答案一样。我认为这也是最好的答案。 - jaredwilli

4
我喜欢上面的贝塞尔曲线解决方案。我注意到比例也会影响线条宽度,所以如果你试图画一个比高更宽的椭圆形,你的顶部和底部“边缘”将比左右“边缘”更细...
一个很好的例子是:
ctx.lineWidth = 4;
ctx.scale(1, 0.5);
ctx.beginPath();
ctx.arc(20, 20, 10, 0, Math.PI * 2, false);
ctx.stroke();

您需要注意的是,椭圆峰谷处线条的宽度只有左右两端顶点的一半。


5
如果您在描边操作之前反转比例,就可以避免描边缩放... 因此将相反的缩放作为倒数第二行添加:ctx.scale(0.5, 1); - Johnathan Hebert

4

Chrome和Opera支持canvas 2d context的ellipse方法,但IE、Edge、Firefox和Safari不支持。

我们可以通过JS实现ellipse方法或使用第三方polyfill。

ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)

使用示例:

ctx.ellipse(20, 21, 10, 10, 0, 0, Math.PI*2, true);

您可以使用canvas-5-polyfill提供椭圆方法。
或者只需粘贴一些js代码即可提供椭圆方法:
if (CanvasRenderingContext2D.prototype.ellipse == undefined) {
  CanvasRenderingContext2D.prototype.ellipse = function(x, y, radiusX, radiusY,
        rotation, startAngle, endAngle, antiClockwise) {
    this.save();
    this.translate(x, y);
    this.rotate(rotation);
    this.scale(radiusX, radiusY);
    this.arc(0, 0, 1, startAngle, endAngle, antiClockwise);
    this.restore();
  }
}

ellipse 1

ellipse 2

ellipse 3

ellipse 4


1
今天最佳答案。由于原生的Eclipse功能仍处于实验阶段,拥有备用方案绝对是非常棒的。谢谢! - walv

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