Як намалювати овал на полотні html5?


81

Здається, немає власної функції для малювання овальної форми. Також я не шукаю яєчну форму.

Чи можна намалювати овал з 2 кривими Безьє? Хтось описував це?

Моя мета - намалювати деякі очі, а насправді я просто використовую дуги. Заздалегідь спасибі.

Рішення

Тож масштаб () змінює масштаб для всіх наступних фігур. Зберегти () зберігає налаштування до, а відновлення використовується для відновлення налаштувань для малювання нових фігур без масштабування.

Дякую Джані

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
Ви не можете намалювати еліпс, використовуючи криві Безьє, на відміну від деяких відповідей нижче. Ви можете наблизити еліпс за допомогою поліноміальних кривих, але не можете його точно відтворити.
Джо

Я дав +1, але це спотворює лінію разом із колом, яке для мене не працює. Хороша інформація.
mwilcox

1
@mwilcox - якщо ви ставите restore()раніше, stroke()це не спотворить лінію, як згадав Джонатан Хеберт у коментарі до відповіді Стіва Транбі нижче. Крім того, це непотрібне і неправильне використання closePath(). Такий метод незрозумілим ... stackoverflow.com/questions/10807230 / ...
Брайан McCutchon

Відповіді:


114

оновлення:

  • метод масштабування може вплинути на зовнішній вигляд ширини штриху
  • Правильний метод масштабування може зберегти незмінною ширину штриху
  • canvas має метод еліпса, який зараз підтримує Chrome
  • додав оновлені тести до JSBin

Приклад тестування JSBin (оновлений, щоб перевірити відповіді інших для порівняння)

  • Без'є - малюнок на основі лівого верхнього кута, що містить прямокутник та ширину / висоту
  • Без'є з центром - малюйте на основі центру та ширини / висоти
  • Дуги та масштабування - малюйте на основі малювання кола та масштабування
  • Квадратичні криві - малюйте квадратиками
    • тест здається не зовсім однаковим, може бути реалізацією
    • див . відповідь ойофанта
  • Canvas Ellipse - із використанням стандартного методу W3C ellipse ()
    • тест здається не зовсім однаковим, може бути реалізацією
    • див . відповідь Локтара

Оригінал:

Якщо вам потрібен симетричний овал, ви завжди можете створити коло з радіусною шириною, а потім масштабувати його до потрібної висоти ( редагувати: зверніть увагу, це вплине на зовнішній вигляд ширини штриху - див. Відповідь 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спотворює штрихи (див. Відповідь акдамелі).
Тревор Бернем

4
ctx.stroke () можна замінити на ctx.fill (), щоб мати заповнену форму.
h3xStream

13
масштаб не спотворюватиме штрихи, якщо ви просто зробите масштаб спочатку, потім додасте до контуру за допомогою дуги або що завгодно, потім зменшите масштаб до початкового, а потім обведете існуючий шлях
Джонатан Хеберт

1
Лише наближення 4 * (sqrt (2) -1) / 3 знайшло багато місць, де розрахунок Google дає 0,55228475. Швидкий пошук дає цей чудовий ресурс: whizkidtech.redprince.net/bezier/circle/kappa
Стів Транбі

1
Не дурне запитання. Якщо ви уявляєте собі прямокутник, що охоплює еліпс, тоді x, y - верхній лівий кут, а центр буде в точці {x + w / 2.0, y + h / 2.0}
Стів Транбі

45

Ось спрощена версія рішень в іншому місці. Я малюю канонічне коло, перекладаю та масштабую, а потім обводжу.

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
Це досить елегантно. Нехай contextзайміться важким підйомом scale.
adu

1
Зверніть увагу на дуже важливе відновлення перед штрихом (інакше ширина штриху спотворюється)
Джейсон С

Саме те, що я шукав, дякую! Я знав, що це можливо інакше, ніж безезіє, але сам не міг отримати масштаб для роботи.

@JasonS Просто цікаво, чому ширина штриха спотворюється, якщо ви відновите після обведення?
OneMoreQuestion

17

Зараз для полотна існує вбудована функція еліпса , дуже подібна до функції дуги, хоча зараз у нас є два значення радіуса та обертання, що є приголомшливим.

еліпс (x, y, радіусX, радіусY, обертання, startAngle, endAngle, проти годинникової стрілки)

Демо в прямому ефірі

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

Здається, зараз працює лише в Chrome


2
Так, впровадження цього відбувається повільно.

@Epistemex Погодився. Я уявляю / сподіваюся, що з часом це стане швидшим.
Локтар,

За повідомленням developer.mozilla.org/en-US/docs/Web/API/… , він підтримується і в Opera зараз
jazzpi

16

Підхід кривої Безьє чудово підходить для простих овалів. Для більшого контролю ви можете використовувати цикл, щоб намалювати еліпс з різними значеннями радіусів x та y (радіуси, радіуси?).

Додавання параметра rotaAngle дозволяє повертати овал навколо свого центру на будь-який кут. Часткові овали можна намалювати, змінивши діапазон (var i), по якому проходить цикл.

Візуалізація овалу таким чином дозволяє визначити точне розташування x, y всіх точок на прямій. Це корисно, якщо розташування інших об’єктів залежить від розташування та орієнтації овалу.

Ось приклад коду:

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 Просто біля моєї голови здається, різниця між (Цей метод: 12 множень, 4 додавання та 8 функцій тригера) проти (4 кубічних Безьє: 24 множення та 24 додавання). І це припускаючи, що полотно використовує ітераційний підхід Де Кастеляу, я не впевнений, як вони насправді реалізовані в різних полотнах браузерів.
DerekR

спробував профіль 2-х підходів у Chrome: 4 кубічних Безьє: близько 0,3 мс Цей метод: близько 1,5 мс з кроком = 0,1 здається приблизно однаковими 0,3 мс
sol0mka

Очевидно, CanvasRenderingContext2D.ellipse () не підтримується у всіх браузерах. Ця процедура чудово працює, ідеальна точність, достатня для шаблонів семінарів. Навіть працює в Internet Explorer, зітхайте.
zipzit

12

Вам потрібно 4 криві Безьє (і магічне число), щоб надійно відтворити еліпс. Дивіться тут:

www.tinaja.com/glib/ellipse4.pdf

Два безьє не відтворюють точно еліпс. Щоб довести це, спробуйте кілька з вище розглянутих рішень Безьє з однаковою висотою та шириною - вони в ідеалі повинні наближати коло, але цього не робитимуть. Вони все ще виглядатимуть овальними, що свідчить про те, що вони не роблять того, що повинні.

Ось щось, що має працювати:

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


}

Це рішення найкраще працює для мене. Чотири параметри. Швидко. Легко.
Олівер,

Привіт, мені сподобалось і це рішення, але коли я погрався з вашим кодом і помітив, що при додаванні нуля зліва від значення параметра коло перекошене. Це підкреслюється, коли коло збільшується в розмірі, чи знаєте ви, чому це може статися? jsfiddle.net/BsPsj/187
alexcons

1
@alexcons, що додає 0, робить його восьмеричним цілочисельним літералом.
Аланкар Місра,

11

Ви також можете спробувати використовувати нерівномірне масштабування. Ви можете забезпечити масштабування X та Y, тому просто встановіть масштабування X або Y більше, ніж інше, і намалюйте коло, і у вас буде еліпс.


9

Я зробив невелику адаптацію цього коду (частково представлений Ендрю Старощиком) для людей, які не хочуть такого загального еліпса і які мають лише більшу піввісь та дані ексцентриситету еліпса (добре для астрономічних іграшок javascript для побудови орбіт , наприклад).

Ось, пам’ятаючи, що можна адаптувати кроки, iщоб мати більшу точність у малюванні:

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

У скрипці є кілька прикладів використання, а також невдала спроба зробити це за допомогою quadraticCurveTo.


Код виглядає приємно, але якщо ви використовуєте це, щоб намалювати коло, передавши однакові значення висоти та ширини, ви отримаєте фігуру, яка нагадує квадрат із дуже закругленими кутами. Чи є спосіб отримати справжнє коло в такому випадку?
Дрю Ноукс,

Я думаю, що найкращим варіантом у такому випадку було б використовувати метод 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

3

Так, це можливо з двома кривими Безьє - ось короткий підручник / приклад: http://www.williammalone.com/briefs/how-to-draw-ellipse-html5-canvas/


1
Зверніть увагу, що в методі, наведеному на цій сторінці, "width" насправді не є шириною результуючого еліпса. Дивіться stackoverflow.com/questions/5694855/…
Тревор Бернхем

3

Chrome і Opera підтримують метод еліпса для 2d-контексту, але IE, Edge, Firefox та Safari його не підтримують.

Ми можемо застосувати метод еліпса за допомогою JS або скористатися стороннім поліфілом.

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

еліпс 1

еліпс 2

еліпс 3

еліпс 4


найкраща відповідь на сьогодні. Оскільки функція рідного затемнення все ще експериментальна, надзвичайно чудово мати резервний варіант. Дякую!
walv

2

Це ще один спосіб створення форми, подібної до еліпса, хоча вона використовує функцію "fillRect ()", яку можна використовувати, змінюючи аргументи у функції 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

За допомогою цього можна навіть намалювати відрізки еліпса:

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

Оскільки ніхто не придумав підходу, використовуючи простіший, quadraticCurveToя додаю рішення для цього. Просто замініть bezierCurveToвиклики в @ Стіва відповідь з цим:

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

Ви також можете видалити closePath . Хоча овал виглядає дещо інакше.


0

Ось я писав функцію, яка використовує ті самі значення, що і дуга еліпса у SVG. X1 & Y1 - останні координати, X2 & Y2 - кінцеві координати, радіус - числове значення, а за годинниковою стрілкою - булеве значення. Також передбачається, що ваш контекст полотна вже визначений.

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

Якщо ви хочете, щоб еліпс повністю помістився всередині прямокутника, це справді так:

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

Переконайтеся, що у вас canvasContext.fillStyle = 'rgba(0,0,0,0)';немає заповнення цим дизайном.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.