【问题标题】:Draw shapes and fill with color in javascript/typescript在 javascript/typescript 中绘制形状并填充颜色
【发布时间】:2017-11-25 02:11:34
【问题描述】:

我正在使用 typescript/javascript 在画布中绘制图像。 我得到的坐标来自线和弧,我从数据库中读取这些数据(线和弧的顺序是随机的)。 图片的绘制没问题,但我需要用颜色填充绘制的形状。使用 moveTo 和 lineTo 执行此操作,然后调用 fill 不起作用。其中之一是因为线条和弧线的随机顺序。二是因为弧线也可以向内,因此它会为“房间”的外部着色。

示例图片:

我的绘图代码片段:

    let canvas = <HTMLCanvasElement>document.getElementById("canvas-view");
    canvas.setAttribute('width', '500');
    canvas.setAttribute('height', '500');
    let ctx = canvas.getContext("2d");
    if (ctx != null)
    {
        for (let count = 0; count < image.lines.length; count++)
        {
            ctx.beginPath();
            ctx.moveTo(image.lines[count].startPoint.x, image.lines[count].startPoint.y);
            ctx.lineTo(image.lines[count].endPoint.x, image.lines[count].endPoint.y);
            ctx.stroke();
        }

        for (let count = 0; count < image.arcs.length; count++)
        {
            ctx.beginPath();
            ctx.arc(image.arcs[count].center.x, image.arcs[count].center.y, image.arcs[count].radius, this.DegreesToRadians(image.arcs[count].startAngle), this.DegreesToRadians(image.arcs[count].endAngle));
            ctx.stroke();
        }
    }

有没有办法给这个形状的内部着色?

【问题讨论】:

    标签: javascript typescript canvas graphics drawing


    【解决方案1】:

    对随机线和弧进行排序

    如何从线段和弧段的随机列表中创建连续路径,其中每条线段和弧段的方向是随机的。

    步骤 1 圆弧端点

    要填充形状,您需要等到所有线条和弧线都完成。然后你必须对它们进行排序,使它们形成一个连续的轮廓。

    为此,您将需要起点和终点,它们是直线的起点和终点,但不是弧的坐标,因此您需要计算它们。

    此函数将起点和终点添加到圆弧中。

    function arcEnds(arc){
        const d2r = d => d * Math.PI / 180; // deg to rad
        arc.startPoint = {
           x : Math.cos(d2r(arc.startAngle)) * arc.radius + arc.center.x,
           y : Math.sin(d2r(arc.startAngle)) * arc.radius + arc.center.y
        }
        arc.endPoint = {
           x : Math.cos(d2r(arc.endAngle)) * arc.radius + arc.center.x,
           y : Math.sin(d2r(arc.endAngle)) * arc.radius + arc.center.y
        }
        return arc;
    }
    

    步骤 2 创建单个数组

    要优化线的构造,您需要创建一个同时包含线和弧的数组。当您这样做时,您还可以计算圆弧端点。

    function createSegmentArray(lines, arcs) {
         return [...lines, ...arcs.map(arcEnds)]; // add lines and end point calculated arcs
    }
    

    步骤 3 创建函数以匹配点

    您可以预期在端点计算中会出现一些浮点错误,因此您需要有一个容差,以便确定 2 个点是否相等。

    有两种方法可以查看两个点是否在同一位置。我将使用距离的平方,因为它更整洁一些。

    // 1 pixel tolerance
    const isSame = (p1,p2) => (((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) < 1; 
    

    步骤 4 创建未排序和排序的数组

    所以现在你创建数组 pf 段

     const segs = createSegmentArray(image.lines, image.arcs);
    

    创建第二个数组来保存有序段。

     const orderedSegs = [];
    

    Step 5 按匹配的终点或起点排序

    然后从第一个段开始,将其从 segs 数组中删除,并找到匹配结束或开始的段(我们还不知道方向)。如果未找到匹配的端点,则形状未关闭且无法填充,因此抛出错误以指示这一点。您将不得不添加一个 catch 处理程序。或者您可能更喜欢跳出循环并标记一个错误

    您还需要反转方向错误的段。对于一条线,您只需交换端点,对于圆,您需要交换端点和端点角度,并添加一个标志以指示方向已反转。

     var current = segs.shift();
     while(seg.length > 0){
         let reverse; // if true the segment needs to be reversed
         const index = segs.findIndex(seg => { // find segment with matching end or start
             if(isSame(current.endPoint, seg.startPoint)){
                reverse = false;
                return true
             }
             if(isSame(current.endPoint, seg.endPoint)){
                reverse = true;
                return true
             }
             return false;
          })
          if(index === -1){ throw new Error("The shape is not closed and can not be filled") }
          orderedSegs.push(current);
          current = segs.splice(index,1)[0]; // get the connected seg
          if(reverse){
               if(current.center){ // is a circle
                    const t = current.endPoint;
                    current.endPoint = current.startPoint;
                    current.startPoint = t;
                    const t1 = current.endAngle;
                    current.endAngle= current.startAngle;
                    current.startAngle = t1;
                    current.reversed = true;
               }else{
                    const t = current.endPoint;
                    current.endPoint = current.startPoint;
                    current.startPoint = t;
               }
          }
          // loop until no more segments
      }
      // push the last seg onto the array
      orderedSegs.push(current);
    

    步骤 6 渲染结果。

    现在所有点都按它们连接的顺序排列,你可以按该顺序渲染它们,但你必须反转弧线的方向

      const d2r = d => d * Math.PI / 180; // deg to rad
      var i;
      ctx.beginPath();
     // arcs have start points now so dont have to check type for first point
      ctx.moveTo(orderedSegs[0].startPoint.x, orderedSegs[0].startPoint.y);
      for(i = 0; i < orderedSegs.length; i++){
           var seg = orderedSegs[i];
           if(seg.center){ // is a arc
               ctx.arc(
                   seg.center.x, seg.center.y, seg.radius,
                   d2r(seg.startAngle), d2r(seg.endAngle), seg.reverse
               );
           }else{
               ctx.lineTo(seg.endPoint.x, seg.endPoint.y);
           }
      }
      ctx.fill();
      ctx.stroke();
    

    这就是过程。

    警告

    1. 如果形状上有孔,这将不起作用。对于每个段,必须有一个段具有匹配的结束点或起点。

    2. 如果第一个和最后一个之间没有段,它将起作用。它假设它们之间有一条线段,不会表示路径没有闭合。您将需要测试orderedSegs 数组中第一段的起点与orderedSegs 数组中最后一段的终点相同的isSame

    3. 如果有两个以上段连接的点,这将不起作用。如果是这样,创建的路径将沿着可能不是正确的段的第一个连接段移动。它不会完成排序并且会抛出错误,即使它可能已经找到了正确的封闭路径。

    4. 这将忽略圆弧方向并假定圆弧在方向上是统一的(全部为 CW 或全部为 CCW)。如果不是这样,那么您必须在渲染路径时进行适当的更正。

    5. 上面创建的弧的起点和终点可能与您为该线所拥有的终点不同。它们只是带有 x、y 的对象。如果您希望它们是同一类型的对象,则需要在 arcEnds 函数中执行此操作。

    6. 由于我没有数据集,我无法测试上面的代码,因此很可能存在任何数量的拼写错误。该代码仅作为流程逻辑的指南。您应该使用它来创建自己的版本,而不仅仅是复制和粘贴,因为它可能会由于拼写错误而引发错误。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-01
      相关资源
      最近更新 更多