Skip to content

canvas/图形绘制 #15

@canvascat

Description

@canvascat

以下实现均在仓库 👉 electron-screenshotdemo

图形绘制

箭头绘制

基本上就是三角函数计算,如图所示:

image

/** PI/6 */
const ARROW_ANGLE = Math.PI / 6;

export function drawArrow(
  ctx: CanvasRenderingContext2D,
  startPoint: Point,
  endPoint: Point,
  width: number,
  fillStyle: CanvasFillStrokeStyles['fillStyle']
) {
  const [x1, y1] = startPoint;
  const [x2, y2] = endPoint;
  const alpha = Math.atan((y1 - y2) / (x1 - x2));
  /** EA点重叠时BD长度 */
  const minArrowHeight = Math.abs(
    (x2 - x1) / (Math.cos(alpha) * Math.cos(ARROW_ANGLE))
  );
  /** BD实际长度 */
  const arrowHeight = Math.min(minArrowHeight, 6 + width * 2);
  const d = x2 < x1 ? -1 : 1;
  const [x3, y3] = [
    x2 - Math.cos(alpha - ARROW_ANGLE) * arrowHeight * d,
    y2 - Math.sin(alpha - ARROW_ANGLE) * arrowHeight * d,
  ];
  const [x4, y4] = [
    x2 - Math.cos(alpha + ARROW_ANGLE) * arrowHeight * d,
    y2 - Math.sin(alpha + ARROW_ANGLE) * arrowHeight * d,
  ];
  const [xa, ya] = [(x4 - x3) / 3, (y4 - y3) / 3];
  const [x5, y5] = [x3 + xa, y3 + ya];
  const [x6, y6] = [x4 - xa, y4 - ya];
  const paths: Array<Point> = [
    [x1, y1],
    [x5, y5],
    [x3, y3],
    [x2, y2],
    [x4, y4],
    [x6, y6],
  ];
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  paths.slice(1).forEach((point) => ctx.lineTo(...point));
  ctx.closePath();
  ctx.fillStyle = fillStyle;
  ctx.fill();
}

笔刷效果

如果只是多个点以直线连接,当采集点不够密集,则会出现线条不平滑的现象,如下图所示:

image

用过 Photoshop 的我们都知道贝塞尔曲线的厉害,这里我们采用 quadraticCurveTo 来绘制贝塞尔曲线以达到实现平滑线条的目的。

绘制一条二次贝塞尔曲线,需要 起始点、控制点和终点。当只有两个点时直接连接,三个点以上则每次取三个点,第二个点为 控制点,第二个点和第三个点的中点为终点绘制;然后以这一步的控制点和中点以及下一个点来重复这个步骤,直到只剩最后一点,直接连接。

image

export function drawCurve(
  ctx: CanvasRenderingContext2D,
  path: Array<Point>,
  lineWidth: number,
  strokeStyle: CanvasFillStrokeStyles['strokeStyle']
) {
  if (path.length < 2) return;
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = strokeStyle;
  ctx.lineCap = 'round';
  ctx.beginPath();
  let startPoint = path[0];
  ctx.moveTo(...startPoint);
  for (let i = 1; i < path.length - 1; i++) {
    /** controlPoint, nextPoint */
    const [[cx, cy], [nx, ny]] = path.slice(i, i + 2);
    /** endPoint */
    const [ex, ey] = [cx + (nx - cx) / 2, cy + (ny - cy) / 2];
    ctx.quadraticCurveTo(cx, cy, ex, ey);
    startPoint = [ex, ey];
  }
  ctx.lineTo(...path.slice(-1)[0]);
  ctx.stroke();
  ctx.closePath();
}

绘制椭圆

这儿我们绘制一个矩形的内切椭圆,矩形由两对角的点(startPoint/endPoint)控制。由于 canvas 只有绘制正圆的 arc() 方法,所以得先对 canvas 进行缩放。

export function drawEllipse(
  ctx: CanvasRenderingContext2D,
  startPoint: Point,
  endPoint: Point,
  lineWidth: number,
  strokeStyle: CanvasFillStrokeStyles['strokeStyle']
) {
  const [[x1, y1], [x2, y2]] = [startPoint, endPoint];
  const [r1, r2] = [x1 - x2, y1 - y2].map((n) => Math.abs(n / 2));
  const [x0, y0] = [(x1 + x2) / 2, (y1 + y2) / 2];
  const r = Math.max(r1, r2);
  const [rx, ry] = [r1 / r, r2 / r];
  ctx.save();
  ctx.scale(rx, ry);
  ctx.beginPath();
  ctx.arc(x0 / rx, y0 / ry, r, 0, 2 * Math.PI);
  ctx.closePath();
  ctx.restore();
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = strokeStyle;
  ctx.stroke();
}

矩形和直线的绘制比较简单,这里就不做赘述了。马赛克功能的实现就 To Be Continued...

参考资料:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions