Reputation: 1728
Have a task to implement my own an arc() method using HTML5 Canvas. It should have the same signature as the native arc (typescript code):
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void;
I understand that the circle will be drawn with many short lines by lineTo() method, so I need a possibility to set the precision (the number of lines. Maybe by a local const). Have some own examples that work with only certain circles. But the method has to be as universal as native arc() method. Any help/links will be appreciated.
Current code (drawing a full circle).
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);
}
Also, please note, that point CENTER has inverted Y axis. And the canvas origin moved. To understand the situation better, I have deployed it to a temporary link . The example is now working with native arc() and I want to replace it.
Upvotes: 1
Views: 1533
Reputation: 54026
I assume that the implementation should match as closely as possible the existing function.
To draw the circle we step line segments around the circle. We dont want to use too small a step as that will just be unneeded work, nor do we want to have the steps too large or the circle will look jaggy. A good quality line length is around 6 pixels long for this example.
The number of steps is the circumference divided by the line step length. Circumference is = (PI * 2 * radius)
thus number steps = (PI * 2 * radius) / 6
. But we can cheat a little. Making the line length two pie long makes the number steps equal to the radius for the full circle.
Now some standard behaviour. Arc throws if radius < 0, if radius is 0 then arc acts like a lineTo function. The optional direction draws the line in a clock wise (CW) == false and CCW if true.
The arc will draw a full circle if the angle from the start to the end in the direction of rendering is greater or equal to a full circle (PI * 2)
The two angles, start
and end
can be at any position from > -Infinity
to < Infinity
. We need to normalise the angles (if not drawing a full circle) to the range 0 to PI * 2
Once we have the correct start and end angles we can then find the number of steps for the arc steps = (end - start) / PI * radius
and with the number of steps we can calculate the step angle step = (end - start) / steps
Now it's just a matter of plotting out the line segments. Arc does not use a moveTo
so all line segments are marked with ctx.lineTo
.
A point on the circle is
x = Math.cos(angle) * radius + centerX
y = Math.sin(angle) * radius + centerY
The number of steps will have a fractional part, so the last line segment will be shorter than the rest. After the main iteration, we add the last line segment to get the end at the end angle.
To finish of the function needs to use the CanvasRenderingContext2D so we will overwrite the existing arc function with our new one. As 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
);
}
}
Just because answers should have a runnable example. Draws random circles using the new arc function. Red circle are CCW and blue CW. the outer green circle is the original arc function to compare.
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>
So almost perfect apart from one little thing. I have used a line segment size that is about 6 pixels long. This will not work for small circles < ~ 8px radius . I leave that to you to fix.
steps
. If steps
doubles the line length halves Upvotes: 3
Reputation: 24181
Here is an example just using Javascript, you should be able to modify to put your Typescript typing on.
The other option like anti-clockwize, you should be able to handle.
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>
Upvotes: 2