Имейте задачу для реализации моего собственного метода 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(), и я хочу его заменить.
Вот пример с использованием 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>
Я предполагаю, что реализация должна максимально соответствовать существующей функции.
Чтобы нарисовать круг, пошаговые сегменты по кругу. Мы не хотим использовать слишком маленький шаг, так как это будет просто ненужная работа, и мы не хотим слишком больших шагов, или круг будет выглядеть неровным. Для этого примера длина линии хорошего качества составляет около 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
удваивают половинки длины линии