繁体   English   中英

带圆角的html5画布三角形

[英]html5 canvas triangle with rounded corners

我是 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>

你能帮我吗?

圆角

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

圆角和保持在多边形区域的约束范围内的问题是您不能总是适合具有特定半径的圆角。

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

如果拐角太尖锐并且从拐角开始的线条不够长而无法获得所需的半径,则以下函数将调整拐角圆角半径的大小以适合拐角。

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

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 ,我们需要找到圆心的绿色点F以及定义圆弧起点和终点角度的DE。

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

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

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

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

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

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

代码部分(特殊部分 A )使用线BABC的长度,并将它们与到BD的距离进行比较,如果该距离大于我们知道弧线无法容纳的线长度的一半。 如果线BDBABC最短线长度的一半,我然后求解三角形以找到半径DF

示例使用。

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

角半径设置为 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 - px; v1.y = p1.y - py; v2.x = point.x - px; v2.y = point.y - py; 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 = px + v1.x * u; v3.y = py + 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) { vx = pp.x - px; vy = pp.y - py; v.len = Math.sqrt(vx * vx + vy * vy); v.nx = vx / v.len; v.ny = vy / 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 - px, y - py); 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(px,py) ); ctx.closePath(); ctx.stroke(); ctx.strokeStyle = "orange"; ctx.lineWidth = 1; eachOf(polygon, p => ctx.strokeRect(px-2,py-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(px,py); 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>

我开始使用 @Blindman67 的答案,它适用于基本的静态形状。

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

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

我没有比较性能,但似乎 canvas 非常擅长绘制贝塞尔曲线。

正如@ 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()
}

用于连接线条的样式,例如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);
 <canvas width="800" height="600"></canvas>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM