【问题标题】:html5 canvas triangle with rounded corners带圆角的html5画布三角形
【发布时间】:2017-12-04 23:09:17
【问题描述】:

我是 HTML5 Canvas 的新手,我正在尝试绘制一个带圆角的三角形。

我试过了

ctx.lineJoin = "round";
ctx.lineWidth = 20;

但它们都不起作用。

这是我的代码:

var ctx = document.querySelector("canvas").getContext('2d');

ctx.scale(5, 5);
    
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;

// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
    
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>

你能帮帮我吗?

【问题讨论】:

    标签: javascript html canvas


    【解决方案1】:

    圆角

    我经常使用的一个非常宝贵的功能是圆角多边形。它采用一组 2D 点来描述多边形的顶点并添加圆弧来圆角。

    圆角并保持在多边形区域的约束范围内的问题是,您不能始终拟合具有特定半径的圆角。

    在这些情况下,您可以忽略拐角并将其保留为尖角,或者您可以减小圆角半径以尽可能适合拐角。

    如果拐角太尖并且拐角的线不够长而无法获得所需的半径,则以下函数将调整拐角圆角半径以适应拐角。

    注意如果您想知道发生了什么,代码中的 cmets 可以参考下面的数学部分。

    roundedPoly(ctx, 点, 半径)

    // ctx is the context to add the path to
    // points is a array of points [{x :?, y: ?},...
    // radius is the max rounding radius 
    // this creates a closed polygon.
    // To draw you must call between 
    //    ctx.beginPath();
    //    roundedPoly(ctx, points, radius);
    //    ctx.stroke();
    //    ctx.fill();
    // as it only adds a path and does not render. 
    function roundedPoly(ctx, points, radiusAll) {
      var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
      // convert 2 points into vector form, polar form, and normalised 
      var asVec = function(p, pp, v) {
        v.x = pp.x - p.x;
        v.y = pp.y - p.y;
        v.len = Math.sqrt(v.x * v.x + v.y * v.y);
        v.nx = v.x / v.len;
        v.ny = v.y / v.len;
        v.ang = Math.atan2(v.ny, v.nx);
      }
      radius = radiusAll;
      v1 = {};
      v2 = {};
      len = points.length;
      p1 = points[len - 1];
      // for each point
      for (i = 0; i < len; i++) {
        p2 = points[(i) % len];
        p3 = points[(i + 1) % len];
        //-----------------------------------------
        // Part 1
        asVec(p2, p1, v1);
        asVec(p2, p3, v2);
        sinA = v1.nx * v2.ny - v1.ny * v2.nx;
        sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
        angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
        //-----------------------------------------
        radDirection = 1;
        drawDirection = false;
        if (sinA90 < 0) {
          if (angle < 0) {
            angle = Math.PI + angle;
          } else {
            angle = Math.PI - angle;
            radDirection = -1;
            drawDirection = true;
          }
        } else {
          if (angle > 0) {
            radDirection = -1;
            drawDirection = true;
          }
        }
        if(p2.radius !== undefined){
            radius = p2.radius;
        }else{
            radius = radiusAll;
        }
        //-----------------------------------------
        // Part 2
        halfAngle = angle / 2;
        //-----------------------------------------
    
        //-----------------------------------------
        // Part 3
        lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
        //-----------------------------------------
    
        //-----------------------------------------
        // Special part A
        if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
          lenOut = Math.min(v1.len / 2, v2.len / 2);
          cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
        } else {
          cRadius = radius;
        }
        //-----------------------------------------
        // Part 4
        x = p2.x + v2.nx * lenOut;
        y = p2.y + v2.ny * lenOut;
        //-----------------------------------------
        // Part 5
        x += -v2.ny * cRadius * radDirection;
        y += v2.nx * cRadius * radDirection;
        //-----------------------------------------
        // Part 6
        ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
        //-----------------------------------------
        p1 = p2;
        p2 = p3;
      }
      ctx.closePath();
    }
    

    您可能希望为每个点添加一个半径,例如{x :10,y:10,radius:20},这将设置该点的最大半径。半径为零将不进行舍入。

    数学

    下面的插图显示了两种可能性之一,适合的角度小于 90 度,另一种情况(大于 90 度)只是有一些小的计算差异(见代码)。

    角由红色ABC三个点定义。圆的半径为r,我们需要找到圆心的绿色点FDE将定义圆弧的开始和结束角度。

    首先我们找到 B,AB,C 的线之间的角度,这是通过对两条线的向量进行归一化并获得叉积来完成的。 (Commented as Part 1) 我们还发现 BC 线与 BA 成 90 度的线的夹角为这将有助于确定将圆圈放在直线的哪一侧。

    现在我们有了线之间的角度,我们知道这个角度的一半定义了圆心所在的线F,但我们不知道那个点离多远>B (评论为第 2 部分)

    有两个相同的直角三角形BDFBEF。我们在 B 有角度,我们知道边 DFEF 等于圆的半径 r 因此我们可以求解三角形以得到从 B

    F 的距离

    为了方便而不是计算到 F 是解决 BD (Commented as Part 3) as I将沿着 BC 线移动该距离 (评论为第 4 部分) 然后转 90 度并向上移动到 F (评论为第 5 部分) 这在过程中给出了点 D 并沿着线 BA 移动到E

    我们使用点 DE 以及圆心 F(以它们的抽象形式)来计算起点和终点的角度弧。 (在 arc 函数第 6 部分完成)

    代码的其余部分涉及沿线移动和远离线的方向以及扫过弧线的方向。

    代码部分(特殊部分 A)使用 BABC 两行的长度,并将它们与 的距离进行比较>BD 如果该距离大于线长的一半,我们就知道弧线不适合。如果线 BDBABCDF /strong>

    使用示例。

    sn-p 是使用上述函数的一个简单示例。单击以将点添加到画布(需要至少 3 个点来创建多边形)。您可以拖动点并查看圆角半径如何适应尖角或短线。 sn-p 运行时的更多信息。要重新启动重新运行 sn -p。 (有很多额外的代码可以忽略)

    圆角半径设置为 30。

    const ctx = canvas.getContext("2d");
    const mouse = {
      x: 0,
      y: 0,
      button: false,
      drag: false,
      dragStart: false,
      dragEnd: false,
      dragStartX: 0,
      dragStartY: 0
    }
    
    function mouseEvents(e) {
      mouse.x = e.pageX;
      mouse.y = e.pageY;
      const lb = mouse.button;
      mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
      if (lb !== mouse.button) {
        if (mouse.button) {
          mouse.drag = true;
          mouse.dragStart = true;
          mouse.dragStartX = mouse.x;
          mouse.dragStartY = mouse.y;
        } else {
          mouse.drag = false;
          mouse.dragEnd = true;
        }
      }
    }
    ["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
    
    const pointOnLine = {x:0,y:0};
    function distFromLines(x,y,minDist){   
      var index = -1;
      const v1 = {};
      const v2 = {};
      const v3 = {};
      const point = P2(x,y);
      eachOf(polygon,(p,i)=>{
        const p1 = polygon[(i + 1) % polygon.length];
        v1.x = p1.x - p.x;
        v1.y = p1.y - p.y;
        v2.x = point.x - p.x;
        v2.y = point.y - p.y;
        const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
        if(u >= 0 && u <= 1){
          v3.x = p.x + v1.x * u;
          v3.y = p.y + v1.y * u;
          dist = Math.hypot(v3.y - point.y, v3.x - point.x);
          if(dist < minDist){
            minDist = dist;
            index = i;
            pointOnLine.x = v3.x;
            pointOnLine.y = v3.y;
          }
        }
      })
      return index;
      
    }
    
    
    
    function roundedPoly(ctx, points, radius) {
      var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
      var asVec = function(p, pp, v) {
        v.x = pp.x - p.x;
        v.y = pp.y - p.y;
        v.len = Math.sqrt(v.x * v.x + v.y * v.y);
        v.nx = v.x / v.len;
        v.ny = v.y / v.len;
        v.ang = Math.atan2(v.ny, v.nx);
      }
      v1 = {};
      v2 = {};
      len = points.length;
      p1 = points[len - 1];
      for (i = 0; i < len; i++) {
        p2 = points[(i) % len];
        p3 = points[(i + 1) % len];
        asVec(p2, p1, v1);
        asVec(p2, p3, v2);
        sinA = v1.nx * v2.ny - v1.ny * v2.nx;
        sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
        angle = Math.asin(sinA);
        radDirection = 1;
        drawDirection = false;
        if (sinA90 < 0) {
          if (angle < 0) {
            angle = Math.PI + angle;
          } else {
            angle = Math.PI - angle;
            radDirection = -1;
            drawDirection = true;
          }
        } else {
          if (angle > 0) {
            radDirection = -1;
            drawDirection = true;
          }
        }
        halfAngle = angle / 2;
        lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
        if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
          lenOut = Math.min(v1.len / 2, v2.len / 2);
          cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
        } else {
          cRadius = radius;
        }
        x = p2.x + v2.nx * lenOut;
        y = p2.y + v2.ny * lenOut;
        x += -v2.ny * cRadius * radDirection;
        y += v2.nx * cRadius * radDirection;
        ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
        p1 = p2;
        p2 = p3;
      }
      ctx.closePath();
    }
    const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
    const P2 = (x = 0, y = 0) => ({x, y});
    const polygon = [];
    
    function findClosestPointIndex(x, y, minDist) {
      var index = -1;
      eachOf(polygon, (p, i) => {
        const dist = Math.hypot(x - p.x, y - p.y);
        if (dist < minDist) {
          minDist = dist;
          index = i;
        }
      });
      return index;
    }
    
    
    // short cut vars 
    var w = canvas.width;
    var h = canvas.height;
    var cw = w / 2; // center 
    var ch = h / 2;
    var dragPoint;
    var globalTime;
    var closestIndex = -1;
    var closestLineIndex = -1;
    var cursor = "default";
    const lineDist = 10;
    const pointDist = 20;
    var toolTip = "";
    // main update function
    function update(timer) {
      globalTime = timer;
      cursor = "crosshair";
      toolTip = "";
      ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
      ctx.globalAlpha = 1; // reset alpha
      if (w !== innerWidth - 4 || h !== innerHeight - 4) {
        cw = (w = canvas.width = innerWidth - 4) / 2;
        ch = (h = canvas.height = innerHeight - 4) / 2;
      } else {
        ctx.clearRect(0, 0, w, h);
      }
      if (mouse.drag) {
        if (mouse.dragStart) {
          mouse.dragStart = false;
          closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
          if(closestIndex === -1){        
            closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
            if(closestLineIndex === -1){
              polygon.push(dragPoint = P2(mouse.x, mouse.y));
            }else{
              polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
            }
            
          }else{
            dragPoint = polygon[closestIndex];
          }
        }
        dragPoint.x = mouse.x;
        dragPoint.y = mouse.y
        cursor = "none";
      }else{
        closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
        if(closestIndex === -1){
          closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
          if(closestLineIndex > -1){
            toolTip = "Click to cut line and/or drag to move.";
          }
        }else{
          toolTip = "Click drag to move point.";
          closestLineIndex = -1;
        }
      }
      ctx.lineWidth = 4;
      ctx.fillStyle = "#09F";
      ctx.strokeStyle = "#000";
      ctx.beginPath();
      roundedPoly(ctx, polygon, 30);
      ctx.stroke();
      ctx.fill();
      ctx.beginPath();
      ctx.strokeStyle = "red";
      ctx.lineWidth = 0.5;
      eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
      ctx.closePath();
      ctx.stroke();
      ctx.strokeStyle = "orange";
      ctx.lineWidth = 1;
      eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
      if(closestIndex > -1){
         ctx.strokeStyle = "red";
         ctx.lineWidth = 4;
         dragPoint = polygon[closestIndex];
         ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
         cursor = "move";
      }else if(closestLineIndex > -1){
         ctx.strokeStyle = "red";
         ctx.lineWidth = 4;
         var p = polygon[closestLineIndex];
         var p1 = polygon[(closestLineIndex + 1) % polygon.length];
         ctx.beginPath();
         ctx.lineTo(p.x,p.y);
         ctx.lineTo(p1.x,p1.y);
         ctx.stroke();
         ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
         cursor = "pointer";     
      
      
      }
    
      if(toolTip === "" && polygon.length < 3){
        toolTip = "Click to add a corners of a polygon.";
      }
      canvas.title = toolTip;
      canvas.style.cursor = cursor;
      requestAnimationFrame(update);
    }
    requestAnimationFrame(update);
    canvas {
      border: 2px solid black;
      position: absolute;
      top: 0px;
      left: 0px;
    }
    &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

    【讨论】:

      【解决方案2】:

      我首先使用 @Blindman67 的答案,它非常适合基本的静态形状。

      我遇到的问题是,在使用弧线方法时,两个点彼此相邻与只有一个点有很大不同。两个点彼此相邻,即使这是您的眼睛所期望的,它也不会是圆形的。如果您正在为多边形点设置动画,这会更加不和谐。

      我改用贝塞尔曲线解决了这个问题。 IMO 这在概念上也更干净一些。我只是用quadratic curve 制作每个角落,其中控制点是原始角落所在的位置。这样,在同一个点上有两个点实际上与只有一个点是一样的。

      我没有比较性能,但似乎画布很擅长绘制贝塞尔曲线。

      与@Blindman67 的回答一样,这实际上并没有绘制任何内容,因此您需要先调用ctx.startPath(),然后再调用ctx.stroke()

      /**
       * Draws a polygon with rounded corners 
       * @param {CanvasRenderingContext2D} ctx The canvas context
       * @param {Array} points A list of `{x, y}` points
       * @radius {number} how much to round the corners
       */
      function myRoundPolly(ctx, points, radius) {
          const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
      
          const lerp = (a, b, x) => a + (b - a) * x
      
          const lerp2D = (p1, p2, t) => ({
              x: lerp(p1.x, p2.x, t),
              y: lerp(p1.y, p2.y, t)
          })
      
          const numPoints = points.length
      
          let corners = []
          for (let i = 0; i < numPoints; i++) {
              let lastPoint = points[i]
              let thisPoint = points[(i + 1) % numPoints]
              let nextPoint = points[(i + 2) % numPoints]
      
              let lastEdgeLength = distance(lastPoint, thisPoint)
              let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
              let start = lerp2D(
                  thisPoint,
                  lastPoint,
                  lastOffsetDistance / lastEdgeLength
              )
      
              let nextEdgeLength = distance(nextPoint, thisPoint)
              let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
              let end = lerp2D(
                  thisPoint,
                  nextPoint,
                  nextOffsetDistance / nextEdgeLength
              )
      
              corners.push([start, thisPoint, end])
          }
      
          ctx.moveTo(corners[0][0].x, corners[0][0].y)
          for (let [start, ctrl, end] of corners) {
              ctx.lineTo(start.x, start.y)
              ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
          }
      
          ctx.closePath()
      }
      
      

      【讨论】:

        【解决方案3】:

        ctx.lineJoin="round" 等线条的连接样式适用于路径上的笔划操作 - 即考虑到它们的宽度、颜色、图案、点划线和类似的线条样式属性。

        线条样式适用于填充路径的内部。

        所以要影响线条样式,需要stroke 操作。在以下对已发布代码的改编中,我翻译了画布输出以查看未裁剪的结果,并抚摸了三角形的路径而不是其下方的矩形:

        var ctx = document.querySelector("canvas").getContext('2d');
        
        ctx.scale(5, 5);
        ctx.translate( 18, 12);
            
        var x = 18 / 2;
        var y = 0;
        var triangleWidth = 48;
        var triangleHeight = 8;
        
        // how to round this triangle??
        
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
        ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
        ctx.closePath();
        ctx.fillStyle = "#009688";
        ctx.fill();
        
        // stroke the triangle path.
        
        ctx.lineWidth = 3;
        ctx.lineJoin = "round";
        ctx.strokeStyle = "orange";
        ctx.stroke();
            
        ctx.fillStyle = "#8BC34A";
        ctx.fillRect(0, triangleHeight, 9, 126);
        ctx.fillStyle = "#CDDC39";
        ctx.fillRect(9, triangleHeight, 9, 126);
        &lt;canvas width="800" height="600"&gt;&lt;/canvas&gt;

        【讨论】:

          猜你喜欢
          • 2013-02-07
          • 2015-08-12
          • 1970-01-01
          • 2014-05-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-03-03
          • 1970-01-01
          相关资源
          最近更新 更多