Jak narysować owal na płótnie HTML5?


81

Wydaje się, że nie ma natywnej funkcji rysowania owalnego kształtu. Ja też nie szukam kształtu jajka.

Czy można narysować owal za pomocą 2 krzywych beziera? Ktoś to doświadczył?

Moim celem jest przyciągnięcie kilku oczu i właściwie używam tylko łuków. Z góry dziękuję.

Rozwiązanie

Zatem scale () zmienia skalowanie dla wszystkich następnych kształtów. Save () zapisuje ustawienia wcześniej, a przywracanie służy do przywracania ustawień rysowania nowych kształtów bez skalowania.

Dzięki 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
Nie możesz narysować elipsy za pomocą krzywych Beziera, w przeciwieństwie do niektórych odpowiedzi poniżej. Możesz przybliżyć elipsę krzywymi wielomianowymi, ale nie możesz jej dokładnie odtworzyć.
— Joe,

Dałem +1, ale to zniekształca linię wraz z okręgiem, który nie działa dla mnie. Dobre informacje.
— mwilcox

1
@mwilcox - jeśli wstawisz restore()wcześniej stroke(), nie zniekształci to linii, o czym Johnathan Hebert wspomniał w komentarzu do odpowiedzi Steve'a Tranby'ego poniżej. Jest to również niepotrzebne i nieprawidłowe użycie closePath(). Taka niezrozumiana metoda ... stackoverflow.com/questions/10807230/…
— Brian McCutchon

Odpowiedzi:


114

aktualizacje:

  • metoda skalowania może wpłynąć na wygląd szerokości obrysu
  • dobrze wykonana metoda skalowania może zachować niezmienioną szerokość obrysu
  • Canvas ma metodę elipsy, którą obsługuje teraz Chrome
  • dodano zaktualizowane testy do JSBin

Przykład testowania JSBin (zaktualizowany, aby przetestować odpowiedzi innych w celu porównania)

  • Bezier - rysowanie oparte na lewym górnym rogu, zawierające prostokąt i szerokość / wysokość
  • Bezier with Center - rysowanie na podstawie środka i szerokości / wysokości
  • Łuki i skalowanie - rysuj w oparciu o rysowanie okręgu i skalowanie
  • Krzywe kwadratowe - rysuj za pomocą kwadratów
    • test wydaje się nie rysować tak samo, może być implementacją
    • zobacz odpowiedź oyophant
  • Canvas Ellipse - przy użyciu standardowej metody ellipse () W3C
    • test wydaje się nie rysować tak samo, może być implementacją
    • zobacz odpowiedź Loktara

Oryginalny:

Jeśli chcesz symetrycznego owalu, zawsze możesz utworzyć okrąg o promieniu szerokości, a następnie przeskalować go do żądanej wysokości ( edycja: zauważ, że wpłynie to na wygląd szerokości obrysu - zobacz odpowiedź acdameli), ale jeśli chcesz mieć pełną kontrolę nad elipsą oto jeden ze sposobów wykorzystania krzywych Beziera.

<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
Jest to najbardziej ogólne rozwiązanie, ponieważ scalezniekształca pociągnięcia (patrz odpowiedź acdameli).
— Trevor Burnham

4
ctx.stroke () można zastąpić ctx.fill (), aby uzyskać wypełniony kształt.
— h3xStream

13
skala nie zniekształci obrysów, jeśli najpierw
— Johnathan Hebert

1
Już samo przybliżenie 4 * (sqrt (2) -1) / 3 pozwoliło znaleźć wiele miejsc, w których kalkulator Google daje 0,55228475. Szybkie wyszukiwanie daje ten wspaniały zasób: whizkidtech.redprince.net/bezier/circle/kappa
— Steve Tranby

1
To nie jest głupie pytanie. Jeśli wyobrażasz sobie prostokąt obejmujący elipsę, a następnie x, y to lewy górny róg, a środek znajdowałby się w punkcie {x + w / 2,0, y + h / 2,0}
— Steve Tranby,

45

Oto uproszczona wersja rozwiązań w innym miejscu. Rysuję okrąg kanoniczny, tłumaczę i skaluję, a następnie głaszczę.

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
To jest całkiem eleganckie. Zróbmy contextciężkie podnoszenie za pomocą scale.
— adu,

1
Zwróć uwagę na bardzo ważne przywrócenie przed pociągnięciem (w przeciwnym razie szerokość pociągnięcia zostanie zniekształcona)
— Jason S

Dokładnie to, czego szukałem, dziękuję! Wiedziałem, że jest to możliwe inaczej niż sposób beziera, ale nie mogłem sam zmusić wagi do pracy.

@JasonS Ciekawe, dlaczego szerokość obrysu uległaby zniekształceniu, jeśli przywrócisz je po pociągnięciu?
— OneMoreQuestion

17

Istnieje teraz natywna funkcja elipsy dla płótna, bardzo podobna do funkcji łuku, chociaż teraz mamy dwie wartości promienia i obrót, który jest niesamowity.

elipsa (x, y, radiusX, radiusY, rotacja, startAngle, endAngle, przeciwnie do ruchu wskazówek zegara)

Live Demo

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

Wydaje się, że obecnie działa tylko w Chrome


2
Tak, implementacja tego jest powolna.

@Epistemex Zgoda. Wyobrażam sobie / mam nadzieję, że z czasem będzie szybciej.
— Loktar

Według developer.mozilla.org/en-US/docs/Web/API/… , jest on teraz obsługiwany również w Operze
— jazzpi

16

Podejście krzywej Beziera doskonale nadaje się do prostych owali. Aby uzyskać większą kontrolę, możesz użyć pętli do narysowania elipsy z różnymi wartościami promienia xiy (promienie, promienie?).

Dodanie parametru RotationAngle umożliwia obracanie owalu wokół jego środka o dowolny kąt. Częściowe owale można narysować, zmieniając zakres (var i), w którym przebiega pętla.

Renderowanie owalu w ten sposób pozwala określić dokładne położenie x, y wszystkich punktów na prostej. Jest to przydatne, jeśli położenie innych obiektów zależy od lokalizacji i orientacji owalu.

Oto przykład kodu:

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

Zobacz interaktywny przykład tutaj: http://www.scienceprimer.com/draw-oval-html5-canvas


Ta metoda tworzy najbardziej poprawną matematycznie elipsę. Zastanawiam się, jak wypada w porównaniu z innymi pod względem kosztów obliczeniowych.
— Eric

@Eric Tuż po mojej głowie wygląda na to, że różnica jest między (Ta metoda: 12 mnożeń, 4 dodania i 8 funkcji trygonometrycznych) a (4 sześcienne beziers: 24 mnożenia i 24 dodawania). Zakładając, że płótno używa iteracyjnego podejścia De Casteljau, nie jestem pewien, jak są one faktycznie zaimplementowane w różnych płótnach przeglądarek.
— DerekR,

próbowałem profilować te 2 podejścia w Chrome: 4 sześcienne Beziers: około 0,3 ms Ta metoda: około 1,5 ms z krokiem = 0,1 wygląda na to samo 0,3 ms
— sol0mka

Najwyraźniej CanvasRenderingContext2D.ellipse () nie jest obsługiwany we wszystkich przeglądarkach. Ta procedura działa świetnie, doskonała dokładność, wystarczająco dobra dla szablonów warsztatowych. Nawet działa w Internet Explorerze, westchnij.
— zipzit

12

Potrzebujesz 4 krzywych Beziera (i magicznej liczby), aby wiarygodnie odtworzyć elipsę. Spójrz tutaj:

www.tinaja.com/glib/ellipse4.pdf

Dwa beziery nie odtwarzają dokładnie elipsy. Aby to udowodnić, wypróbuj niektóre z dwóch powyższych rozwiązań beziera o równej wysokości i szerokości - powinny idealnie przybliżać okrąg, ale nie będą. Nadal będą wyglądać owalnie, co dowodzi, że nie robią tego, co do nich należy.

Oto coś, co powinno działać:

http://jsfiddle.net/BsPsj/

Oto kod:

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


}

To rozwiązanie działa najlepiej dla mnie. Cztery parametry. Szybki. Łatwy.
— Oliver

Witam, podobało mi się również to rozwiązanie, ale kiedy bawiłem się twoim kodem i zauważyłem, że podczas dodawania zera po lewej stronie wartości parametru kółko jest przekrzywione. Jest to podkreślane, gdy koło powiększa się, czy wiesz, dlaczego tak się może stać? jsfiddle.net/BsPsj/187
— alexcons

1
@alexcons poprzedzające 0 sprawia, że ​​jest to ósemkowy literał liczby całkowitej.
— Alankar Misra

11

Możesz także spróbować użyć niejednolitego skalowania. Możesz zapewnić skalowanie X i Y, więc po prostu ustaw skalowanie X lub Y na większe niż inne i narysuj okrąg, a otrzymasz elipsę.


9

Zrobiłem małą adaptację tego kodu (częściowo przedstawionego przez Andrew Staroscika) dla ludzi, którzy nie chcą tak ogólnej elipsy i którzy mają tylko większą półosi i dane o ekscentryczności elipsy (dobre dla astronomicznych zabawek javascript do kreślenia orbit , na przykład).

Proszę bardzo, pamiętając, że można dostosować kroki, iaby uzyskać większą precyzję w rysunku:

/* 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

Moje rozwiązanie jest trochę inne niż wszystkie te. Myślę, że najbliższa odpowiedź to najczęściej głosowana powyżej odpowiedź, ale myślę, że ta metoda jest nieco czystsza i łatwiejsza do zrozumienia.

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

A potem użyj tego w ten sposób:

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

W skrzypcach jest kilka przykładów użycia, a także nieudana próba wykonania jednego przy użyciu quadraticCurveTo.


Kod wygląda ładnie, ale jeśli użyjesz tego do narysowania koła, przekazując te same wartości wysokości i szerokości, otrzymasz kształt przypominający kwadrat z bardzo zaokrąglonymi narożnikami. Czy w takim przypadku można uzyskać prawdziwy krąg?
— Drew Noakes

Myślę, że najlepszą opcją w takim przypadku byłoby użycie metody arc (), w której można następnie zdefiniować położenie kąta początkowego i końcowego łuku, podobnie jak w przypadku wykonywania tutaj najwyższej głosowanej odpowiedzi. Myślę, że to też najlepsza odpowiedź.
— jaredwilli,

4

Podoba mi się powyższe rozwiązanie krzywych Beziera. Zauważyłem, że skala wpływa również na szerokość linii, więc jeśli próbujesz narysować elipsę, która jest szersza niż wysoka, Twoje górne i dolne „boki” będą wyglądały na cieńsze niż lewe i prawe „boki” ...

dobrym przykładem byłoby:

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

należy zauważyć, że szerokość linii na szczycie i dolinie elipsy jest o połowę mniejsza niż na lewym i prawym wierzchołku (wierzchołkach?).


5
Możesz uniknąć skalowania pociągnięć, jeśli po prostu odwrócisz skalę tuż przed operacją pociągnięcia ... więc dodaj odwrotną skalę jako przedostatnią linięctx.scale(0.5, 1);
— Johnathan Hebert


3

Chrome i Opera obsługują elipsę metodę w kontekście kanwy 2D, ale IE, Edge, Firefox i Safari jej nie obsługują.

Metodę elipsy możemy zaimplementować w JS lub skorzystać z polyfillu innej firmy.

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

Przykład użycia:

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

Możesz użyć płótna-5-polyfill aby zapewnić metodę elipsy.

Lub po prostu wklej kod js, aby zapewnić metodę elipsy:

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

elipsa 1

elipsa 2

elipsa 3

elipsa 4


najlepsza odpowiedź na dziś. Ponieważ natywna funkcja zaćmienia jest nadal eksperymentalna, korzystanie z funkcji zastępczej jest absolutnie niesamowite. dzięki!
— walv

2

Jest to inny sposób tworzenia kształtu podobnego do elipsy, chociaż wykorzystuje funkcję „fillRect ()”, której można użyć do zmiany argumentów w funkcji 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>

2

Dzięki temu możesz nawet narysować segmenty elipsy:

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/


2

Ponieważ nikt nie wymyślił podejścia używającego prostszego quadraticCurveTo, dodaję rozwiązanie. Wystarczy wymienić bezierCurveTopołączenia w @ Steve'a odpowiedzi z tego:

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

Możesz również usunąć closePath. Jednak owal wygląda nieco inaczej.


0

Oto funkcja, którą napisałem, która używa tych samych wartości, co łuk elipsy w SVG. X1 i Y1 to ostatnie współrzędne, X2 i Y2 to końcowe współrzędne, promień to wartość liczbowa, a zgodnie z ruchem wskazówek zegara to wartość logiczna. Zakłada się również, że kontekst kanwy został już zdefiniowany.

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

0

Jeśli chcesz, aby elipsa w pełni mieściła się w prostokącie, to naprawdę wygląda tak:

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

Upewnij się, że nie jesteś canvasContext.fillStyle = 'rgba(0,0,0,0)';wypełniony tym projektem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.