Реализация пользовательского метода canvas arc ()

1

Имейте задачу для реализации моего собственного метода arc() с использованием холста HTML5. Он должен иметь ту же подпись, что и внутренняя дуга (код машинописного текста):

arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void;

Я понимаю, что круг будет рисоваться со многими короткими строками методом lineTo(), поэтому мне нужна возможность установить точность (количество строк. Может быть, локальная константа). Имейте некоторые собственные примеры, которые работают только с определенными кругами. Но метод должен быть таким же универсальным, как метод native arc(). Любая помощь/ссылки будут оценены.

Текущий код (рисунок полного круга).

private customCircleDraw(center: Point, radius: number, start: number = 0, end: number = 2) {
const p1 = new Point(center.x + radius * Math.cos(start * Math.PI), -center.y + radius * Math.sin(start * Math.PI));
const p2 = new Point(center.x + radius * Math.cos(end * Math.PI), -center.y + radius * Math.sin(end * Math.PI));

const points = 50;
let angle1;
let angle2;
for(let i = 0; i < points; ++i) {
  angle1 =  i * 2 * Math.PI / points;
  angle2 = (i+1) * 2 * Math.PI / points;
  let firstPoint = new Point(center.x + radius * Math.cos(angle1), center.y + radius * Math.sin(angle1));
  let secondPoint = new Point(center.x + radius * Math.cos(angle2), center.y + radius * Math.sin(angle2));
  // if (some advanced condition) 
    this.drawLine(firstPoint,secondPoint);
}

Также обратите внимание, что точка CENTER имеет инвертированную ось Y. И происхождение холста переместилось. Чтобы лучше понять ситуацию, я развернул ее во временную ссылку. Пример теперь работает с native arc(), и я хочу его заменить.

  • 1
    Вы можете начать с чтения этого -> en.wikipedia.org/wiki/Midpoint_circle_algorithm
  • 0
    Интересная статья, но без примера, как разрезать дугу на 2 балла. Я уверен, что есть хороший и хорошо написанный (но сложный для поиска) пример пользовательской дуги
Показать ещё 5 комментариев
Теги:
math
html5-canvas
geometry

2 ответа

1
Лучший ответ

Вот пример с использованием Javascript, вы должны иметь возможность изменять, чтобы ввести свой машинописный текст.

Другой вариант, например anti-clockwize, вы сможете обрабатывать.

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

function degtorad(degrees) {
  return degrees * Math.PI / 180;
};

function customCircleDraw(center, radius, start, end) {
  var step_size = (end - start) / 50;
  var angle = start;
  var first = true;
  while (angle <= end) {
    let px = (Math.sin(angle) * radius) + center.x,
        py = (-Math.cos(angle) * radius) + center.y;
    if (first) {
      ctx.moveTo(px,py);
      first = false;
    } else {
      ctx.lineTo(px,py);
    }
    angle = angle + step_size;
  }
}

customCircleDraw({x:100, y:100}, 50, degtorad(0), degtorad(90));
customCircleDraw({x:100, y:100}, 50, degtorad(180), degtorad(180+45));
ctx.stroke();
<canvas id="myCanvas" width="200" height="200" style="border:1px solid red;">
</canvas>
  • 0
    Большой! Спасибо.
  • 0
    Но я обнаружил интересную проблему изменения начального и конечного углов. Чтобы заставить его работать так же, как родная дуга (), мне пришлось вычесть Math.PI / 2 из начального и конечного углов.
2

Почти идеальное соответствие для Arc.

Я предполагаю, что реализация должна максимально соответствовать существующей функции.

Чтобы нарисовать круг, пошаговые сегменты по кругу. Мы не хотим использовать слишком маленький шаг, так как это будет просто ненужная работа, и мы не хотим слишком больших шагов, или круг будет выглядеть неровным. Для этого примера длина линии хорошего качества составляет около 6 пикселей.

Количество шагов - это окружность, деленная на длину шага линии. Окружность = (PI * 2 * radius) таким образом, число steps = (PI * 2 * radius)/6. Но мы можем немного обмануть. Создание длины линии длиной два пирога делает число шагов равным радиусу для полного круга.

Стандартное поведение дуги.

Теперь некоторое стандартное поведение. Дуга выдает, если радиус <0, если радиус равен 0, то дуга действует как функция LineTo. Необязательное направление рисует линию в часах (CW) == false и CCW, если true.

Дуга будет рисовать полный круг, если угол от начала до конца в направлении рендеринга больше или равен полному кругу (PI * 2)

Два угла, start и end могут находиться в любой позиции от > -Infinity до < Infinity. Нам нужно нормализовать углы (если не вытянуть полный круг) до диапазона от 0 до PI * 2

Этапы итерации и шаг угла.

Как только мы получим правильные начальные и конечные углы, мы можем найти количество шагов для шага дуги steps = (end - start)/PI * radius и с количеством шагов, которые мы можем вычислить шаг шага угла step = (end - start)/steps

Теперь это просто вопрос построения линейных сегментов. Arc не использует moveTo поэтому все сегменты линии отмечены ctx.lineTo.

Точкой на круге является

x = Math.cos(angle) * radius + centerX
y = Math.sin(angle) * radius + centerY

Количество шагов будет иметь дробную часть, поэтому последний сегмент линии будет короче остальных. После основной итерации мы добавляем последний сегмент линии, чтобы получить конец в конечном угле.

Для завершения функции необходимо использовать CanvasRenderingContext2D, поэтому мы перезапишем существующую функцию дуги нашей новой. Как CanvasRenderingContext2D.prototype.arc =//the function

Функция

CanvasRenderingContext2D.prototype.arc = function (x, y, radius, start, end, direction) {
    const PI = Math.PI;  // use PI and PI * 2 a lot so make them constants for easy reading
    const PI2 = PI * 2;
    // check radius is in range 
    if (radius < 0) { throw new Error('Failed to execute 'arc' on 'CanvasRenderingContext2D': The radius provided (${radius}) is negative.') }
    if (radius == 0) { ctx.lineTo(x,y) } // if zero radius just do a lineTo
    else {
        const angleDist = end - start; // get the angular distance from start to end;
        let step, i;
        let steps = radius;  // number of 6.28 pixel steps is radius
        // check for full CW or CCW circle depending on directio
        if((direction !== true && angleDist >= PI2)){ // full circle
            step = PI2 / steps;
        } else if((direction === true && angleDist <= -PI2)){ // full circle
            step = -PI2 / steps;
        }else{
            // normalise start and end angles to the range 0- 2 PI
            start = ((start % PI2) + PI2) % PI2;
            end = ((end % PI2) + PI2) % PI2;
            if(end < start) { end += PI2 }           // move end to be infront (CW) of start
            if(direction === true){ end -= PI2 }     // if CCW move end behind start
            steps *= (end - start) / PI2;            // get number of 2 pixel steps
            step = (end - start) / steps;            // convert steps to a step in radians
            if(direction === true) { step = -step; } // correct sign of step if CCW
            steps = Math.abs(steps);                 // ensure that the iteration is positive
        }
        // iterate circle 
        for (i = 0 ; i < steps; i += 1){
            this.lineTo( 
                Math.cos(start + step * i) * radius + x,
                Math.sin(start + step * i) * radius + y
            );           
        }
        this.lineTo( // do the last segment
            Math.cos(start + step * steps) * radius + x,
            Math.sin(start + step * steps) * radius + y
        );
    }
}

пример

Просто потому, что ответы должны иметь примерный пример. Рисует случайные круги, используя новую функцию дуги. Красный круг - это CCW и синий CW. внешний зеленый круг является исходной функцией дуги для сравнения.

    CanvasRenderingContext2D.prototype.arcOld = CanvasRenderingContext2D.prototype.arc;
    CanvasRenderingContext2D.prototype.arc = function (x, y, radius, start, end, direction) {
        const PI = Math.PI;  // use PI and PI * 2 a lot so make them constants for easy reading
        const PI2 = PI * 2;
        // check radius is in range 
        if (radius < 0) { throw new Error('Failed to execute 'arc' on 'CanvasRenderingContext2D': The radius provided (${radius}) is negative.') }
        if (radius == 0) { ctx.lineTo(x,y) } // if zero radius just do a lineTo
        else {
            const angleDist = end - start; // get the angular distance from start to end;
            let step, i;
            let steps = radius;  // number of 6.28 pixel steps is radius
            // check for full CW or CCW circle depending on directio
            if((direction !== true && angleDist >= PI2)){ // full circle
                step = PI2 / steps;
            } else if((direction === true && angleDist <= -PI2)){ // full circle
                step = -PI2 / steps;
            }else{
                // normalise start and end angles to the range 0- 2 PI
                start = ((start % PI2) + PI2) % PI2;
                end = ((end % PI2) + PI2) % PI2;
                if(end < start) { end += PI2 }           // move end to be infront (CW) of start
                if(direction === true){ end -= PI2 }     // if CCW move end behind start
                steps *= (end - start) / PI2;            // get number of 2 pixel steps
                step = (end - start) / steps;            // convert steps to a step in radians
                if(direction === true) { step = -step; } // correct sign of step if CCW
                steps = Math.abs(steps);                 // ensure that the iteration is positive
            }
            // iterate circle 
            for (i = 0 ; i < steps; i += 1){
                this.lineTo( 
                    Math.cos(start + step * i) * radius + x,
                    Math.sin(start + step * i) * radius + y
                );           
            }
            this.lineTo( // do the last segment
                Math.cos(start + step * steps) * radius + x,
                Math.sin(start + step * steps) * radius + y
            );
        }
    }




const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;


// test code
const ctx = canvas.getContext("2d");
canvas.width = innerWidth - 20;
canvas.height = innerHeight - 20;
var count = 0;
(function randomCircle(){
    count += 1;
    if(count > 50){
        ctx.clearRect(0,0,canvas.width,canvas.height);
        count = 0;
    }
    var x = rand(canvas.width);
    var y = rand(canvas.height);
    var start = rand(-1000,1000);
    var end = rand(-1000,1000);
    var radius = rand(10,200);
    var dir = rand(1) < 0.5;
    ctx.strokeStyle = dir ? "red" : "blue";
    ctx.beginPath()
    ctx.arc(x,y,radius,start,end,dir)
    ctx.stroke();
    ctx.strokeStyle = "green";
    ctx.beginPath()
    ctx.arcOld(x,y,radius + 4,start,end,dir)
    ctx.stroke();
    setTimeout(randomCircle,250);
})();
canvas { position : absolute; top : 0px; left : 0px; }
Red circles CCW, blue CW.
<canvas id="canvas"></canvas>

К лучшему...

Так почти идеально, кроме одной мелочи. Я использовал размер сегмента линии, длина которого составляет около 6 пикселей. Это не будет работать для малых радиусов <~ 8px. Я оставляю это для исправления.

  • Подсказка - простой тест для малого радиуса, и вы можете увеличить количество steps. Если steps удваивают половинки длины линии
  • 0
    Большое спасибо за такой подробный ответ!

Ещё вопросы

Сообщество Overcoder
Наверх
Меню