javascript - 自定义 Canvas arc() 方法实现

标签 javascript math html5-canvas geometry

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

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

我知道圆将通过 lineTo() 方法绘制许多短线,所以我需要设置精度(线数。可能由本地常量)。 有一些自己的例子,只适用于某些圈子。但该方法必须与原生 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);
}

另外,请注意,中心点的 Y 轴是倒置的。 Canvas 原点移动了。为了更好地了解情况,我已将其部署到临时 link .该示例现在使用 native arc(),我想替换它。

最佳答案

与 Arc 几乎完美匹配。

我假设实现应该尽可能匹配现有功能。

为了绘制圆,我们围绕圆逐步画出线段。我们不想使用太小的步长,因为那只是不必要的工作,我们也不希望步长太大,否则圆看起来会参差不齐。对于此示例,高质量的线条长度约为 6 像素。

步数是周长除以线步长。周长是 = (PI * 2 * radius)因此编号steps = (PI * 2 * radius) / 6 .但是我们可以稍微作弊。将线的长度设为两个馅饼的长度会使步数等于整个圆的半径。

Arc 的标准行为。

现在是一些标准行为。如果 radius < 0,则 Arc 抛出,如果 radius 为 0,则 arc 的行为类似于 lineTo 函数。可选方向以顺时针 (CW) == false 和 CCW(如果为真)画线。

如果从起点到终点在渲染方向上的 Angular 大于或等于一个完整的圆,则圆弧将绘制一个完整的圆(PI * 2)

两个 Angular ,startend可以在 > -Infinity 中的任何位置至 < Infinity .我们需要将 Angular (如果不绘制完整的圆圈)归一化到 0 到 PI * 2 的范围

迭代步长和 Angular 步长。

一旦我们有了正确的开始和结束 Angular ,我们就可以找到弧的步数 steps = (end - start) / PI * radius根据步数,我们可以计算出步 Angular step = (end - start) / steps

现在只需画出线段即可。 Arc 不使用 moveTo所以所有线段都标有ctx.lineTo .

圆上的一点是

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

步数将有小数部分,因此最后一条线段将比其余线段短。在主迭代之后,我们添加最后一条线段以在结束 Angular 处结束。

要完成该功能,需要使用 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
        );
    }
}

例子

只是因为答案应该有一个可运行的例子。使用新的 arc 函数绘制随机圆。红色圆圈是 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 radius 的小圆圈。我把它留给你来解决。

  • 提示,一个简单的小半径测试,你可以增加steps的数量。 .如果steps将行的长度加倍

关于javascript - 自定义 Canvas arc() 方法实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46287423/

相关文章:

c# - 根据点对划分一个矩形

javascript - 如何计算多个数组的总和?

javascript - 使用 bezierCurveTo 绘制圆弧,圆弧点的相对坐标

javascript - 更改查询字符串而不重新加载/刷新

使用let时的Javascript问题

java - 使用纬度经度计算两点之间的距离?

javascript - 更新 firefox 55 后,内联图像 (img) 未通过 SVG 在 HTML CANVAS 中呈现

javascript - HTML5 Canvas 事件监听器和 JavaScript - addEventListener ("click")

javascript - 如何使用 Jquery 动态克隆一个选项卡

javascript - 谷歌搜索控制台 API : How do you implement multiple OR filters?