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();
Tammo
źródło
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>
Steve Tranby
źródło
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();
}
Deven Kalra
źródło
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

Loktar
źródło
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

Andrew Staroscik
źródło
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();


}
Alankar Misra
źródło
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ę.

Jani Hartikainen
źródło
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();
}
Girardi
źródło
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.

jaredwilli
źródło
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?).

acdameli
źródło
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

cuixiping
źródło
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>
aspYou
źródło
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/


źródło
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.

oyophant
źródło
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);
}
Dotacja
źródło
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.

StackSlave
źródło