html5画布圆角三角形

vawmfj5a  于 2022-12-09  发布在  HTML5
关注(0)|答案(3)|浏览(133)

I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried

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

but none of them are working.
Here's my code:

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>

Could you help me?

m1m5dgzv

m1m5dgzv1#

Rounding corners

An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.

Note the code has comments that refer to the Maths section below if you want to know what is going on.

roundedPoly(ctx, points, radius)

// 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();
}

You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.

The maths

The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).

The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B(Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD(Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F(Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC

Example use.

The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 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;
}
<canvas id="canvas"></canvas>
niknxzdl

niknxzdl2#

我从@Blindman67的答案开始,它对基本的静态形状非常有效。
我遇到了一个问题,当使用圆弧方法时,有两个相邻的点和只有一个点是非常不同的。如果有两个相邻的点,它将不会是圆形的,即使这是你的眼睛所期望的。如果你正在动画多边形点,这是额外的不和谐。
我用贝塞尔曲线修正了这个问题。我觉得这在概念上也更清晰一些。我只是用一个quadratic curve来做每个角,控制点就是原来的角。这样,在同一个点上有两个点实际上和只有一个点是一样的。
我还没有比较性能,但似乎画布是相当不错的绘制贝塞尔曲线。
与@Blindman67的答案一样,这实际上并没有画出任何东西,所以你需要在前面调用ctx.beginPath(),在后面调用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()
}
zhte4eai

zhte4eai3#

ctx.lineJoin="round"等线条连接样式适用于路径上的描边操作,即考虑路径的宽度、颜色、图案、虚线/点线以及类似的线条样式属性。
线条样式 * 不 * 适用于填充路径的内部。
因此,要影响线条样式,需要一个stroke操作。在下面的代码改编中,我已经转换了canvas输出,以便在不裁剪的情况下查看结果,并绘制了三角形的路径,但没有绘制它下面的矩形:
第一个

相关问题