自定义canvas arc()方法实现

时间:2017-09-18 20:09:13

标签: javascript math html5-canvas geometry

有一项任务是使用HTML5 Canvas实现我自己的arc()方法。 它应该与原生弧(打字稿代码)具有相同的签名:

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

我知道圆圈将通过lineTo()方法用很多短线绘制,所以我需要设置精度(行数。可能是本地const)。 有一些自己的例子只适用于某些圈子。但是该方法必须与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轴。画布原点移动了。为了更好地了解情况,我已将其部署到临时link。该示例现在使用native arc(),我想替换它。

2 个答案:

答案 0 :(得分:2)

以下是使用Javascript的示例,您应该可以修改以打印您的Typescript。

另一个选项,如反时钟,你应该能够处理。

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>

答案 1 :(得分:2)

几乎完美匹配Arc。

我假设实现应尽可能匹配现有函数。

要绘制圆圈,我们会围绕圆圈逐步划线。我们不想使用太小的步骤,因为这只是不需要的工作,我们也不想让步骤太大或圆圈看起来很锯齿。对于此示例,高质量的线长约为6个像素。

步数是圆周除以线步长度。周长为= (PI * 2 * radius),因此数字为steps = (PI * 2 * radius) / 6。但我们可以作弊。使线长度为两个饼,使得数字步长等于整圆的半径。

Arc的标准行为。

现在有些标准行为。如果半径<1,则抛出弧。 0,如果radius为0,那么arc就像一个lineTo函数。可选方向以时钟方式(CW)== false和CCW(如果为真)绘制线条。

如果在渲染方向上从开始到结束的角度大于或等于整圆(PI * 2)

,则圆弧将绘制一个完整的圆。

startend这两个角度可以位于> -Infinity< Infinity的任意位置。我们需要将角度标准化(如果没有绘制整圆)到0到PI * 2的范围

Itereation步骤和角度步骤。

一旦我们得到了正确的起点和终点角度,我们就可以找到弧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,因此我们将使用新的函数覆盖现有的arc函数。作为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。外部绿色圆圈是要比较的原始圆弧函数。

&#13;
&#13;
    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);
})();
&#13;
canvas { position : absolute; top : 0px; left : 0px; }
&#13;
Red circles CCW, blue CW.
<canvas id="canvas"></canvas>
&#13;
&#13;
&#13;

为了更好......

除了一件小事之外,几乎完美无缺。我使用了大约6像素长的线段大小。这不适用于小圈子&lt; ~8px半径。我把它留给你修理。

  • 提示,对于小半径的简单测试,您可以增加steps的数量。如果steps将行长度增加一半