Mike Kovetsky
Mike Kovetsky

Reputation: 1728

Custom canvas arc() method implementation

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

Answers (2)

Blindman67
Blindman67

Reputation: 54026

Almost perfect match for Arc.

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.

Arc's standard behaviour.

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

Itereation steps, and angle step.

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

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

Example

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>

For better...

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.

  • Hint , a simple test for small radius and you can increase the number of steps. If steps doubles the line length halves

Upvotes: 3

Keith
Keith

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

Related Questions