【问题标题】:Optimizing arcs to draw sectors only visible on canvas优化弧线以绘制仅在画布上可见的扇区
【发布时间】:2015-12-20 19:41:49
【问题描述】:

我有一个相当大的弧,其笔划使用rgba 值。它具有 50% 的 alpha 值,因此,它对我的​​浏览器的 cpu 配置文件造成了很大的影响。

所以我想找到一种方法来优化这一点,这样无论何时在画布中绘制弧线,它都只会从一个角度绘制到另一个角度,而另一个角度在屏幕上可见。

我遇到的困难是计算出正确的角度范围。

这是一个视觉示例:

顶部的图像是画布实际执行的操作,即使您没有看到它,底部的图像是我试图做的以节省处理时间。

我创建了一个 JSFiddle,您可以在其中单击并拖动圆圈,但是,这两个角度目前是固定的: https://jsfiddle.net/44tawd81/

这是抽奖代码:

var canvas = document.getElementById('canvas');
var ctx    = canvas.getContext('2d');
    ctx.strokeStyle = 'red';

var radius = 50;
var pos    = {
              'x': canvas.width - 20,
              'y': canvas.height /2
             };


function draw(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.beginPath();

    ctx.arc(pos.x,pos.y,radius,0,2*Math.PI); //need to adjust angle range
    ctx.stroke();

    requestAnimationFrame(draw);
}

draw();

根据画布中的位置和大小找到要绘制的角度范围的最简单方法是什么?

【问题讨论】:

  • "最上面的图像是画布的实际作用,即使你没有看到它" 你确定吗?此外,您将在您身边进行的任何计算都可能比浏览器的计算更占用 CPU:您与 javascript 对话,而浏览器直接与 CPU 对话。而且我认为alpha通道不会被处理到画布区域之外,它应该不会对CPU或GPU产生任何重大影响。
  • 我找不到文档来确定它是否确实如此。但同样,如果圆圈完全超出画布范围,或者太大以至于你在画布中看不到任何圆圈边缘,我可以跳过弧和笔划的函数调用。那肯定会更有效率吗?因为 atm 笔画函数是 3ms,这比我的一些非常大的 +100 行代码函数要长。我需要将帧速率从 30 提高。
  • 您的问题还没有回答吗?
  • @Dave。 “相当大”和 20K 半径之间存在“相当大”的差异。你真的应该在你的问题中提到这一点。 ;-) 出于好奇,你在做什么需要这么大的尺寸?
  • 一个不完全按比例缩放的太阳系,但它相当大! :P

标签: javascript html canvas


【解决方案1】:

剪圆

这是如何将一个圆裁剪为与 x 和 y 轴对齐的矩形区域。

要剪裁圆,我会搜索圆与剪裁区域相交的点列表。从一侧开始,我按顺时针方向添加找到的剪辑点。测试完所有 4 个边后,我会绘制连接找到的点的弧段。

要查找一个点是否与剪切边相交,您需要找到圆心到该边的距离。知道半径和距离,您可以完成直角三角形以找到截距的坐标。

对于左边缘

// define the clip edge and circle
var clipLeftX = 100;
var radius = 200;
var centerX = 200;
var centerY = 200; 

var dist = centerX - clipLeftX;
if(dist > radius) { // circle inside }
if(dist < -radius) {// circle completely outside}
// we now know the circle is clipped 

现在计算两个剪辑点到圆 y 的距离

// the right triangle with hypotenuse and one side know can be solved with
var clipDist = Math.sqrt(radius * radius - dist * dist);

所以圆与剪切线相交的点

var clipPointY1 = centerY - clipDist;
var clipPointY2 = centerY + clipDist;

这样,您可以通过测试左侧线的顶部和底部的两个点来确定这两个点是在左侧顶部还是底部的内部或外部。

您最终会得到 0,1 或 2 个剪切点。

因为圆弧需要角度来绘制,所以您需要计算从圆心到找到的点的角度。您已经拥有所需的所有信息

// dist is the x distance from the clip
var angle = Math.acos(radius/dist); // for left and right side

最难的部分是确保到剪切点的所有角度都按正确的顺序排列。有点摆弄标志以确保弧的顺序正确。

检查所有四个边后,您最终会得到 0、2、4、6 或 8 个剪切点,分别代表各种剪切圆弧的起点和终点。然后它只是简单地迭代弧段并渲染它们。

// Helper functions are not part of the answer
var canvas;
var ctx;
var mouse;
var resize = function(){
    /** fullScreenCanvas.js begin **/
    canvas = (function(){
        var canvas = document.getElementById("canv");
        if(canvas !== null){
            document.body.removeChild(canvas);
        }
        // creates a blank image with 2d context
        canvas = document.createElement("canvas"); 
        canvas.id = "canv";    
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight; 
        canvas.style.position = "absolute";
        canvas.style.top = "0px";
        canvas.style.left = "0px";
        canvas.style.zIndex = 1000;
        canvas.ctx = canvas.getContext("2d"); 
        document.body.appendChild(canvas);
        return canvas;
    })();
    ctx = canvas.ctx;
    /** fullScreenCanvas.js end **/
    /** MouseFull.js begin **/
    var canvasMouseCallBack = undefined;  // if needed
    mouse = (function(){
        var mouse = {
            x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
            interfaceId : 0, buttonLastRaw : 0,  buttonRaw : 0,
            over : false,  // mouse is over the element
            bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
            getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
            startMouse:undefined,
        };
        function mouseMove(e) {
            var t = e.type, m = mouse;
            m.x = e.offsetX; m.y = e.offsetY;
            if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
            m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
            if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
            } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
            } else if (t === "mouseover") { m.over = true;
            } else if (t === "mousewheel") { m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") { m.w = -e.detail;}
            if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
            e.preventDefault();
        }
        function startMouse(element){
            if(element === undefined){
                element = document;
            }
            "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
            function(n){element.addEventListener(n, mouseMove);});
            element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
        }
        mouse.mouseStart = startMouse;
        return mouse;
    })();
    if(typeof canvas === "undefined"){
        mouse.mouseStart(canvas);
    }else{
        mouse.mouseStart();
    }
}
/** MouseFull.js end **/
resize();
// Answer starts here
var w = canvas.width;
var h = canvas.height;
var d = Math.sqrt(w * w + h * h); // diagnal size
var cirLWidth = d * (1 / 100);
var rectCol = "black";
var rectLWidth = d * (1 / 100);
const PI2 = Math.PI * 2;
const D45_LEN = 0.70710678;
var angles = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // declared outside to stop GC


// create a clipArea
function rectArea(x, y, x1, y1) {
    return {
        left : x,
        top : y,
        width : x1 - x,
        height : y1 - y
    };
}
// create a arc
function arc(x, y, radius, start, end, col) {
    return {
        x : x,
        y : y,
        r : radius,
        s : start,
        e : end,
        c : col
    };
}

// draws an arc
function drawArc(arc, dir) {
    ctx.strokeStyle = arc.c;
    ctx.lineWidth = cirLWidth;
    ctx.beginPath();
    ctx.arc(arc.x, arc.y, arc.r, arc.s, arc.e, dir);
    ctx.stroke();
}

// draws a clip area
function drawRect(r) {
    ctx.strokeStyle = rectCol;
    ctx.lineWidth = rectLWidth;
    ctx.strokeRect(r.left, r.top, r.width, r.height);

}



// clip and draw an arc
// arc is the arc to clip
// clip is the clip area
function clipArc(arc, clip){
    var count, distTop, distLeft, distBot, distRight, dist, swap, radSq, bot,right;

   // cir1 is used to draw the clipped circle
   cir1.x = arc.x;
   cir1.y = arc.y;   

   count = 0;  // number of clip points found;

   bot = clip.top + clip.height;  // no point adding these two over and over
   right = clip.left + clip.width;

   // get distance from all edges
   distTop = arc.y - clip.top;
   distBot = bot - arc.y;
   distLeft = arc.x - clip.left;
   distRight = right - arc.x;
   
   radSq = arc.r * arc.r; // get the radius squared
   
   // check if outside
   if(Math.min(distTop, distBot, distRight, distLeft) < -arc.r){
       return; // nothing to see so go home
   }
   // check inside
   if(Math.min(distTop, distBot, distRight, distLeft) > arc.r){
       drawArc(cir1);  
       return;
   }
   swap = true;
   if(distLeft < arc.r){
       // get the distance up and down to clip
       dist = Math.sqrt(radSq - distLeft * distLeft);
       // check the point is in the clip area
       if(dist + arc.y < bot && arc.y + dist > clip.top){
           // get the angel
           angles[count] = Math.acos(distLeft / -arc.r);
           count += 1;
       }
       if(arc.y - dist < bot && arc.y - dist > clip.top){
           angles[count] = PI2 - Math.acos(distLeft / -arc.r); // get the angle
           if(count === 0){  // if first point then set direction swap
               swap = false;
           }
           count += 1;
       }
   }
   if(distTop < arc.r){
       dist = Math.sqrt(radSq - distTop * distTop);
       if(arc.x - dist < right && arc.x - dist > clip.left){
           angles[count] = Math.PI + Math.asin(distTop / arc.r);
           count += 1;
       }
       if(arc.x+dist < right && arc.x+dist > clip.left){
           angles[count] = PI2-Math.asin(distTop/arc.r);
           if(count === 0){
               swap = false;
           }
           count += 1;
       }
   }
   if(distRight < arc.r){
       dist = Math.sqrt(radSq - distRight * distRight);
       if(arc.y - dist < bot && arc.y - dist > clip.top){
           angles[count] = PI2 - Math.acos(distRight / arc.r);
           count += 1;
       }
       if(dist + arc.y < bot && arc.y + dist > clip.top){
           angles[count] = Math.acos(distRight / arc.r);
           if(count === 0){
               swap = false;
           }
           count += 1;
       }
   }
   if(distBot < arc.r){
       dist = Math.sqrt(radSq - distBot * distBot);
       if(arc.x + dist < right && arc.x + dist > clip.left){
           angles[count] = Math.asin(distBot / arc.r);
           count += 1;
       }
       if(arc.x - dist < right && arc.x - dist > clip.left){
           angles[count] =  Math.PI + Math.asin(distBot / -arc.r);
           if(count === 0){
               swap = false;
           }
           count += 1;
       }
   }
   //  now draw all the arc segments
   if(count === 0){
       return;
   }
   if(count === 2){
        cir1.s = angles[0];
        cir1.e = angles[1];
        drawArc(cir1,swap);
   }else
   if(count === 4){
        if(swap){
            cir1.s = angles[1];
            cir1.e = angles[2];
            drawArc(cir1);
            cir1.s = angles[3];
            cir1.e = angles[0];
            drawArc(cir1);
        }else{
            cir1.s = angles[2];
            cir1.e = angles[3];
            drawArc(cir1);
            cir1.s = angles[0];
            cir1.e = angles[1];
            drawArc(cir1);
        }
   }else
   if(count === 6){
        cir1.s = angles[1];
        cir1.e = angles[2];
        drawArc(cir1);
        cir1.s = angles[3];
        cir1.e = angles[4];
        drawArc(cir1);
        cir1.s = angles[5];
        cir1.e = angles[0];
        drawArc(cir1);
        
   }else
   if(count === 8){
        cir1.s = angles[1];
        cir1.e = angles[2];
        drawArc(cir1);
        cir1.s = angles[3];
        cir1.e = angles[4];
        drawArc(cir1);
        cir1.s = angles[5];
        cir1.e = angles[6];
        drawArc(cir1);
        cir1.s = angles[7];
        cir1.e = angles[0];
        drawArc(cir1);
        
   }
   return;
}


var rect = rectArea(50, 50, w - 50, h - 50);
var circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
var cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
var counter = 0;
var countStep = 0.03;
function update() {
    var x, y;
    ctx.clearRect(0, 0, w, h);
    circle.x = mouse.x;
    circle.y = mouse.y;
    drawArc(circle, "#888"); // draw unclipped arc
    x = Math.cos(counter * 0.1);
    y = Math.sin(counter * 0.3);
    rect.top = h / 2 - Math.abs(y * (h * 0.4)) - 5;
    rect.left = w / 2 - Math.abs(x * (w * 0.4)) - 5;
    rect.width = Math.abs(x * w * 0.8) + 10;
    rect.height = Math.abs(y * h * 0.8) + 10;
    cir1.col = "RED";  
    clipArc(circle, rect); // draw the clipped arc
    
    drawRect(rect); // draw the clip area. To find out why this method
                    // sucks move this to before drawing the clipped arc.
    requestAnimationFrame(update);
    if(mouse.buttonRaw !== 1){
        counter += countStep;
    }
    ctx.font = Math.floor(w * (1 / 50)) + "px verdana";
    ctx.fillStyle = "white";
    ctx.strokeStyle = "black";
    ctx.lineWidth = Math.ceil(w * (1 / 300));
    ctx.textAlign = "center";
    ctx.lineJoin = "round";
    ctx.strokeText("Left click and hold to pause", w/ 2, w * (1 / 40));
    ctx.fillText("Left click and hold to pause", w/ 2, w * (1 / 40));
}

update();
window.addEventListener("resize",function(){
   resize();
   w = canvas.width;
   h = canvas.height;
   rect = rectArea(50, 50, w - 50, h - 50);
   circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
   cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
});

剪圆的最快方法。

这是我在代码中能做到的最快速度。算法有一些优化空间,但没有那么多。

最好的解决方案当然是使用canvas 2D context API clip()方法。

ctx.save();
ctx.rect(10,10,200,200); // define the clip region
ctx.clip();  // activate the clip.

//draw your circles

ctx.restore(); // remove the clip.

这比我上面展示的方法快得多,除非你真的需要知道剪辑区域内部或外部的剪辑点和弧段,否则应该使用它。

【讨论】:

  • 所以您同意 OP 的观点,即它可以优化性能以在绘图前进行这些检查,而不是让浏览器完成他的工作?
  • @Kaiido 我永远不会使用这种类型的剪辑,除非我对圆形剪辑点做一些特别的事情。我会为简单区域使用标准上下文 2D 剪辑,或者为复杂剪辑使用蒙版。仅对于具有慢速 GPU 硬件的设备,此剪辑才有任何优势,而且充其量只是微不足道的。
  • @Kaiido 你必须意识到我的弧线很大(半径超过 20k)!而当我在这么大的圆圈上加笔画的时候,笔画功能就变成了整个app最重的功能!请参阅此处:i.imgur.com/DpOC77d.png,对我而言,这意味着它是尝试优化的第一行调用。
  • @Blindman67 我很想知道clip 是否值得,我读过它是一个相当昂贵的功能。但是,我没有找到真正确定的 JSPerf。
  • 抱歉又看了一遍。你的问题是保存和恢复。保存应该在剪辑方法之前。您正在做的是每帧都添加到剪辑区域,因为恢复不会删除剪辑
【解决方案2】:

根据圆圈位置、画布位置、圆圈大小和画布大小查找要绘制的角度:

  1. 确定圆与画布的交集
  2. 计算圆上发生相交的点

然后你有一个等腰三角形。

您可以使用余弦公式来计算角度。

c^2=a^2+b^2−2abcos(α) a 和 b 是与角 α 相邻的边,它们是中心 r 的半径。 c 是两点 P1 和 P2 之间的距离。所以我们得到:

|P1−P2|^2=2r^2−2r^2cos(α)

2r^2−|P1−P2|^2/2r2=cos(α)

α=cos−1(2r^2−|P1−P2|^2/2r^2)

【讨论】:

  • 这对圆圈超出整个画布大小因此根本不需要绘制它,或者当它超出画布偏移时是否也有帮助?就像我了解数学一样,但不了解如何将其合并到我的 JavaScript 中。
猜你喜欢
  • 1970-01-01
  • 2011-09-07
  • 2014-11-20
  • 2017-11-02
  • 1970-01-01
  • 1970-01-01
  • 2013-11-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多