带圆角的html5画布三角形 [英] html5 canvas triangle with rounded corners

查看:12
本文介绍了带圆角的html5画布三角形的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

我试过了

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>

你能帮我吗?

推荐答案

圆角

我经常使用的一个无价函数是圆角多边形.它需要一组 2D 点来描述多边形的顶点并添加圆弧来使角变圆.

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.

// 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} 这将设置该点的最大半径.零半径将不会四舍五入.

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.

下面的插图显示了两种可能性中的一种,要拟合的角度小于 90 度,另一种情况(大于 90 度)只是有一些细微的计算差异(参见代码).

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).

角由红色ABC 中的三个点定义.圆的半径是r,我们需要找到圆心FDE的绿点将定义弧的起点和终点.

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.

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

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.

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

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)

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

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

为了方便而不是计算 F 是对 BD (评论为第 3 部分) 的求解,因为我将沿线BC移动那个距离(评论为第4部分)然后转90度并向上移动到F (作为第 5 部分评论) 这在过程中给出了点 D 并沿着 BA 线移动到E

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

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

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.

代码部分(特殊部分 A)使用 BABC 两行的长度,并将它们与 的距离进行比较>BD 如果该距离大于线长的一半,我们就知道弧不适合.然后,如果线 BDBABC<最短线长度的一半,我解决三角形以找到半径 DF/strong>

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

该代码段是上述函数的一个简单示例.单击以向画布添加点(创建多边形需要至少 3 个点).您可以拖动点并查看角半径如何适应尖角或短线.代码段运行时的更多信息.要重新启动,请重新运行代码段.(有很多多余的代码可以忽略)

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)

圆角半径设置为 30.

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>

这篇关于带圆角的html5画布三角形的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆