¿Cómo dibujar un óvalo en un lienzo html5?

81

No parece haber una función nativa para dibujar una forma ovalada. Además, no estoy buscando la forma de huevo.

¿Es posible dibujar un óvalo con 2 curvas Bézier? ¿Alguien experimentó con eso?

Mi propósito es dibujar algunos ojos y en realidad solo estoy usando arcos. Gracias por adelantado.

Solución

Entonces scale () cambia la escala para todas las siguientes formas. Guardar () guarda la configuración antes y restaurar se usa para restaurar la configuración para dibujar nuevas formas sin escalar.

Gracias a 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();
Tammo
fuente
7
No puede dibujar una elipse usando curvas de Bezier, a diferencia de lo que dicen algunas de las respuestas a continuación. Puede aproximar una elipse con curvas polinomiales, pero no puede reproducirla exactamente.
Joe
Di un +1, pero esto distorsiona la línea junto con el círculo, lo que no me funciona. Sin embargo, buena información.
mwilcox
1
@mwilcox: si lo pones restore()antes stroke(), no distorsionará la línea, como mencionó Johnathan Hebert en un comentario a la respuesta de Steve Tranby a continuación. Además, este es un uso innecesario e incorrecto de closePath(). Un método tan incomprendido ... stackoverflow.com/questions/10807230/…
Brian McCutchon

Respuestas:

113

actualizaciones:

  • El método de escala puede afectar la apariencia del ancho del trazo.
  • El método de escalado bien hecho puede mantener intacto el ancho del trazo
  • Canvas tiene un método de elipse que Chrome ahora admite
  • se agregaron pruebas actualizadas a JSBin

Ejemplo de prueba JSBin (actualizado para probar las respuestas de otros para compararlas)

  • Bézier: dibujo basado en la parte superior izquierda que contiene rect y ancho / alto
  • Bézier con centro: dibuja según el centro y el ancho / alto
  • Arcos y escala: dibujar según el círculo de dibujo y la escala
  • Curvas cuadráticas: dibujar con cuadráticas
    • la prueba parece no dibujar exactamente lo mismo, puede ser la implementación
    • ver la respuesta de oyophant
  • Canvas Ellipse: utilizando el método elipse () estándar de W3C
    • la prueba parece no dibujar exactamente lo mismo, puede ser la implementación
    • ver la respuesta de Loktar

Original:

Si desea un óvalo simétrico, siempre puede crear un círculo de ancho de radio y luego escalarlo a la altura que desee ( editar: observe que esto afectará la apariencia del ancho del trazo; consulte la respuesta de acdameli), pero si desea un control total de la elipse aquí hay una forma de usar curvas Bézier.

<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>
Steve Tranby
fuente
1
Esta es la solución más general, ya scaleque distorsionará los trazos (consulte la respuesta de acdameli).
Trevor Burnham
4
ctx.stroke () se puede reemplazar por ctx.fill () para tener una forma rellena.
h3xStream
13
la escala no distorsionará los trazos si solo escala primero, luego agrega a la ruta usando un arco o lo que sea, luego escala de nuevo al original, luego traza la ruta existente
Johnathan Hebert
1
Solo una aproximación de 4 * (sqrt (2) -1) / 3 encontró muchos lugares, donde el cálculo de Google da 0.55228475. Una búsqueda rápida produce este excelente recurso: whizkidtech.redprince.net/bezier/circle/kappa
Steve Tranby
1
No es una pregunta estúpida. Si imagina un rectángulo que encierra la elipse, entonces x, y es la esquina superior izquierda, y el centro estaría en el punto {x + w / 2.0, y + h / 2.0}
Steve Tranby
45

Aquí hay una versión simplificada de soluciones en otros lugares. Dibujo un círculo canónico, traduzco y escalo y luego trazo.

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();
}
Deven Kalra
fuente
2
Esto es bastante elegante. Deja contexthacer el trabajo pesado con scale.
adu
1
Tenga en cuenta la restauración muy importante antes del trazo (de lo contrario, el ancho del trazo se distorsiona)
Jason S
Exactamente lo que estaba buscando, ¡gracias! Sabía que era posible de otra manera que no fuera de la forma bezier, pero no podía hacer que la escala funcionara.
@JasonS Solo tengo curiosidad, ¿por qué se distorsionaría el ancho del trazo si restauras después del trazo?
OneMoreQuestion
17

Ahora hay una función de elipse nativa para lienzo, muy similar a la función de arco, aunque ahora tenemos dos valores de radio y una rotación que es increíble.

elipse (x, y, radiusX, radiusY, rotación, startAngle, endAngle, antihorario)

Demo en vivo

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

Parece que solo funciona en Chrome actualmente

Loktar
fuente
2
Sí, la implementación de este es lenta.
@Epistemex De acuerdo. Me imagino / espero que sea más rápido con el tiempo.
Loktar
De acuerdo con developer.mozilla.org/en-US/docs/Web/API/… , también es compatible con Opera ahora
jazzpi
16

El enfoque de la curva de Bézier es ideal para óvalos simples. Para mayor control, puede usar un bucle para dibujar una elipse con diferentes valores para los radios xey (radios, radios?).

Agregar un parámetro de ángulo de rotación permite que el óvalo se gire alrededor de su centro en cualquier ángulo. Se pueden dibujar óvalos parciales cambiando el rango (var i) sobre el que se ejecuta el bucle.

Representar el óvalo de esta manera le permite determinar la ubicación exacta x, y de todos los puntos en la línea. Esto es útil si la posición de otros objetos depende de la ubicación y orientación del óvalo.

A continuación, se muestra un ejemplo del código:

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);
    }
}

Vea un ejemplo interactivo aquí: http://www.scienceprimer.com/draw-oval-html5-canvas

Andrew Staroscik
fuente
Este método produce la elipse más matemáticamente correcta. Me pregunto cómo se compara con los demás en términos de costo computacional.
Eric
@Eric En la parte superior de mi cabeza parece que la diferencia está entre (Este método: 12 multiplicaciones, 4 sumas y 8 funciones trigonométricas) vs (4 Cubic Beziers: 24 multiplicaciones y 24 sumas). Y eso es asumiendo que el lienzo usa un enfoque iterativo de De Casteljau, no estoy seguro de cómo se implementan realmente en varios lienzos de navegadores.
DerekR
Intenté perfilar estos 2 enfoques en Chrome: 4 Cubic Beziers: aproximadamente 0,3 ms Este método: aproximadamente 1,5 ms con paso = 0,1 parece aproximadamente el mismo 0,3 ms
sol0mka
Aparentemente, CanvasRenderingContext2D.ellipse () no es compatible con todos los navegadores. Esta rutina funciona muy bien, con una precisión perfecta, lo suficientemente buena para las plantillas de taller. Incluso funciona en Internet Explorer, suspiro.
zipzit
12

Necesita 4 curvas Bézier (y un número mágico) para reproducir una elipse de manera confiable. Mira aquí:

www.tinaja.com/glib/ellipse4.pdf

Dos béziers no reproducen con precisión una elipse. Para probar esto, pruebe algunas de las 2 soluciones Bézier anteriores con la misma altura y ancho; idealmente deberían aproximarse a un círculo pero no lo harán. Todavía se verán ovalados, lo que demuestra que no están haciendo lo que se supone que deben hacer.

Aquí hay algo que debería funcionar:

http://jsfiddle.net/BsPsj/

Aquí está el código:

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();


}
Alankar Misra
fuente
Esta solución funciona mejor para mí. Cuatro parámetros. Rápido. Fácil.
Oliver
Hola, también me gustó esta solución, pero cuando estaba jugando con su código y noté que al agregar un cero a la izquierda del valor del parámetro, el círculo está sesgado. Esto se acentúa a medida que el círculo aumenta de tamaño, ¿sabe por qué puede suceder esto? jsfiddle.net/BsPsj/187
alexcons
1
@alexcons anteponiendo un 0 lo convierte en un literal entero octal.
Alankar Misra
11

También puede intentar usar una escala no uniforme. Puede proporcionar escalas X e Y, así que simplemente configure la escala X o Y más grande que la otra, y dibuje un círculo, y tendrá una elipse.

Jani Hartikainen
fuente
9

Hice una pequeña adaptación de este código (presentado parcialmente por Andrew Staroscik) para personas que no quieren una elipse tan general y que solo tienen el semieje mayor y los datos de excentricidad de la elipse (bueno para los juguetes astronómicos de JavaScript para trazar órbitas , por ejemplo).

Aquí tienes, recordando que se pueden adaptar los pasos ipara tener una mayor precisión en el dibujo:

/* 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();
}
Girardi
fuente
5

Mi solución es un poco diferente a todas estas. Sin embargo, creo que la respuesta más cercana es la más votada, pero creo que de esta manera es un poco más limpia y más fácil de comprender.

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();
}

Y luego úsalo así:

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

Hay un par de ejemplos de uso en el violín, junto con un intento fallido de hacer uno usando quadraticCurveTo.

jaredwilli
fuente
El código se ve bien, pero si usa esto para dibujar un círculo pasando los mismos valores de alto y ancho, obtiene una forma que se asemeja a un cuadrado con esquinas muy redondeadas. ¿Hay alguna forma de crear un círculo genuino en tal caso?
Drew Noakes
Creo que la mejor opción en ese caso sería usar el método arc (), que luego puede definir una posición de ángulo inicial y final para el arco, similar a cómo se hace la respuesta más votada aquí. Creo que también es la mejor respuesta en general.
jaredwilli
4

Me gusta la solución de curvas de Bezier anterior. Noté que la escala también afecta el ancho de la línea, por lo que si está tratando de dibujar una elipse que sea más ancha que alta, sus "lados" superior e inferior aparecerán más delgados que sus "lados" izquierdo y derecho ...

un buen ejemplo sería:

ctx.lineWidth = 4;
ctx.scale(1, 0.5);
ctx.beginPath();
ctx.arc(20, 20, 10, 0, Math.PI * 2, false);
ctx.stroke();

debe notar que el ancho de la línea en el pico y el valle de la elipse es la mitad de ancho que en los ápices izquierdo y derecho (¿ápices?).

Acdameli
fuente
5
Puede evitar la escala de trazo si simplemente invierte la escala justo antes de la operación de trazo ... así que agregue la escala opuesta como la penúltima líneactx.scale(0.5, 1);
Johnathan Hebert
3

Chrome y Opera admiten el método de elipse para el contexto de lienzo 2d, pero IE, Edge, Firefox y Safari no lo admiten.

Podemos implementar el método elipse por JS o usar un polyfill de terceros.

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

Ejemplo de uso:

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

Puede utilizar un canvas-5-polyfill para proporcionar el método de elipse.

O simplemente pegue un poco de código js para proporcionar el método de elipse:

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();
  }
}

elipse 1

elipse 2

elipse 3

elipse 4

cuixiping
fuente
la mejor respuesta para hoy. Como la función nativa de eclipse todavía es experimental, es absolutamente increíble tener una alternativa. ¡Gracias!
walv
2

Esta es otra forma de crear una forma de elipse, aunque usa la función "fillRect ()", esta se puede usar para cambiar los argumentos en la función fillRect ().

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Sine and cosine functions</title>
</head>
<body>
<canvas id="trigCan" width="400" height="400"></canvas>

<script type="text/javascript">
var canvas = document.getElementById("trigCan"), ctx = canvas.getContext('2d');
for (var i = 0; i < 360; i++) {
    var x = Math.sin(i), y = Math.cos(i);
    ctx.stroke();
    ctx.fillRect(50 * 2 * x * 2 / 5 + 200, 40 * 2 * y / 4 + 200, 10, 10, true);
}
</script>
</body>
</html>
aspYou
fuente
2

Con esto incluso puedes dibujar segmentos de una elipse:

function ellipse(color, lineWidth, x, y, stretchX, stretchY, startAngle, endAngle) {
    for (var angle = startAngle; angle < endAngle; angle += Math.PI / 180) {
        ctx.beginPath()
        ctx.moveTo(x, y)
        ctx.lineTo(x + Math.cos(angle) * stretchX, y + Math.sin(angle) * stretchY)
        ctx.lineWidth = lineWidth
        ctx.strokeStyle = color
        ctx.stroke()
        ctx.closePath()
    }
}

http://jsfiddle.net/FazAe/1/


fuente
2

Como a nadie se le ocurrió un enfoque que utilice el más simple quadraticCurveTo, estoy agregando una solución para eso. Simplemente reemplace las bezierCurveTollamadas en la respuesta de @ Steve con esto:

  ctx.quadraticCurveTo(x,y,xm,y);
  ctx.quadraticCurveTo(xe,y,xe,ym);
  ctx.quadraticCurveTo(xe,ye,xm,ye);
  ctx.quadraticCurveTo(x,ye,x,ym);

También puede eliminar el closePath. Sin embargo, el óvalo se ve ligeramente diferente.

oyophant
fuente
0

Aquí hay una función que escribí que usa los mismos valores que el arco de elipse en SVG. X1 e Y1 son las últimas coordenadas, X2 e Y2 son las coordenadas finales, el radio es un valor numérico y el sentido de las agujas del reloj es un valor booleano. También asume que su contexto de lienzo ya ha sido definido.

function ellipse(x1, y1, x2, y2, radius, clockwise) {

var cBx = (x1 + x2) / 2;    //get point between xy1 and xy2
var cBy = (y1 + y2) / 2;
var aB = Math.atan2(y1 - y2, x1 - x2);  //get angle to bulge point in radians
if (clockwise) { aB += (90 * (Math.PI / 180)); }
else { aB -= (90 * (Math.PI / 180)); }
var op_side = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) / 2;
var adj_side = Math.sqrt(Math.pow(radius, 2) - Math.pow(op_side, 2));

if (isNaN(adj_side)) {
    adj_side = Math.sqrt(Math.pow(op_side, 2) - Math.pow(radius, 2));
}

var Cx = cBx + (adj_side * Math.cos(aB));            
var Cy = cBy + (adj_side * Math.sin(aB));
var startA = Math.atan2(y1 - Cy, x1 - Cx);       //get start/end angles in radians
var endA = Math.atan2(y2 - Cy, x2 - Cx);
var mid = (startA + endA) / 2;
var Mx = Cx + (radius * Math.cos(mid));
var My = Cy + (radius * Math.sin(mid));
context.arc(Cx, Cy, radius, startA, endA, clockwise);
}
Conceder
fuente
0

Si desea que la elipse quepa completamente dentro de un rectángulo, es realmente así:

function ellipse(canvasContext, x, y, width, height){
  var z = canvasContext, X = Math.round(x), Y = Math.round(y), wd = Math.round(width), ht = Math.round(height), h6 = Math.round(ht/6);
  var y2 = Math.round(Y+ht/2), xw = X+wd, ym = Y-h6, yp = Y+ht+h6, cs = cards, c = this.card;
  z.beginPath(); z.moveTo(X, y2); z.bezierCurveTo(X, ym, xw, ym, xw, y2); z.bezierCurveTo(xw, yp, X, yp, X, y2); z.fill(); z.stroke();
  return z;
}

Asegúrese canvasContext.fillStyle = 'rgba(0,0,0,0)';de no llenar con este diseño.

StackSlave
fuente