pin_drop当前位置:知识文库 ❯ 图文

多媒体:绘制线条 - 完整教程与代码示例

一、教程简介

线条是 Canvas 绘图中最基本的元素之一。通过 moveTolineTo 可以定义线段的路径,而 lineWidthlineCaplineJoin 等属性则控制线条的视觉表现。本教程将详细讲解线条的绘制方法和样式设置,帮助你掌握从简单直线到复杂线框图形的绘制技巧。


二、核心概念

线条绘制流程

  1. beginPath() - 开始新路径

  2. moveTo(x, y) - 将画笔移动到起点(不画线)

  3. lineTo(x, y) - 从当前点画线到指定点

  4. 设置线条样式(lineWidthlineCaplineJoin 等)

  5. stroke() - 渲染线条

线条样式属性

属性 说明 默认值
lineWidth 线条宽度 1
lineCap 线条端点样式 butt
lineJoin 线条连接处样式 miter
miterLimit 斜接长度限制 10
setLineDash() 设置虚线样式 []
lineDashOffset 虚线偏移量 0

三、语法与用法

moveTo - 移动画笔

代码示例

ctx.moveTo(x, y);

将画笔移动到坐标 (x, y),不绘制任何线条。相当于提起画笔再放下。

lineTo - 画线到指定点

代码示例

ctx.lineTo(x, y);

从当前点绘制一条直线到 (x, y),并将当前点移动到 (x, y)

lineWidth - 线条宽度

代码示例

ctx.lineWidth = 5;

设置线条宽度,单位为像素。线条以路径为中心向两侧各延伸 lineWidth/2

lineCap - 线条端点样式

代码示例

ctx.lineCap = 'butt';    // 默认,平直端点,不超过端点
ctx.lineCap = 'round';   // 圆形端点,超出端点半个线宽
ctx.lineCap = 'square';  // 方形端点,超出端点半个线宽

lineJoin - 线条连接样式

代码示例

ctx.lineJoin = 'miter';  // 默认,尖角
ctx.lineJoin = 'round';  // 圆角
ctx.lineJoin = 'bevel';  // 斜角

setLineDash - 虚线设置

代码示例

ctx.setLineDash([5, 10]);       // 5px实线,10px空白
ctx.setLineDash([5, 5, 15, 5]); // 复杂虚线模式
ctx.setLineDash([]);            // 实线

lineDashOffset - 虚线偏移

代码示例

ctx.lineDashOffset = 5;  // 虚线偏移5px

四、代码示例

示例1:线条样式全面演示

以下示例演示了 lineCap、lineJoin、lineWidth 和虚线样式的视觉效果:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>线条样式全面演示</title>
  <style>
    body { margin: 0; padding: 20px; background: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', sans-serif; display: flex; flex-direction: column; align-items: center; }
    canvas { border: 1px solid #30363d; border-radius: 8px; background: #161b22; }
  </style>
</head>
<body>
  <h1>线条样式全面演示</h1>
  <canvas id="myCanvas" width="750" height="550"></canvas>
  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    // === lineCap 演示 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 16px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('lineCap 端点样式', 375, 30);

    const caps = ['butt', 'round', 'square'];
    const capColors = ['#e94560', '#3fb950', '#58a6ff'];

    caps.forEach((cap, i) => {
      const x = 130 + i * 250;
      const y = 80;

      // 参考线
      ctx.strokeStyle = '#30363d';
      ctx.lineWidth = 1;
      ctx.setLineDash([4, 4]);
      ctx.beginPath();
      ctx.moveTo(x, y - 20);
      ctx.lineTo(x, y + 60);
      ctx.moveTo(x + 150, y - 20);
      ctx.lineTo(x + 150, y + 60);
      ctx.stroke();
      ctx.setLineDash([]);

      // 示例线条
      ctx.lineCap = cap;
      ctx.strokeStyle = capColors[i];
      ctx.lineWidth = 20;
      ctx.beginPath();
      ctx.moveTo(x, y + 20);
      ctx.lineTo(x + 150, y + 20);
      ctx.stroke();

      // 标签
      ctx.fillStyle = capColors[i];
      ctx.font = '14px sans-serif';
      ctx.fillText(cap, x + 75, y + 60);
    });

    // === lineJoin 演示 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 16px sans-serif';
    ctx.fillText('lineJoin 连接样式', 375, 180);

    const joins = ['miter', 'round', 'bevel'];
    const joinColors = ['#f8b500', '#d2a8ff', '#f78166'];

    joins.forEach((join, i) => {
      const x = 100 + i * 250;
      const y = 220;
      ctx.lineJoin = join;
      ctx.lineCap = 'butt';
      ctx.strokeStyle = joinColors[i];
      ctx.lineWidth = 20;
      ctx.beginPath();
      ctx.moveTo(x, y + 60);
      ctx.lineTo(x + 60, y);
      ctx.lineTo(x + 120, y + 60);
      ctx.stroke();
      ctx.fillStyle = joinColors[i];
      ctx.font = '14px sans-serif';
      ctx.fillText(join, x + 60, y + 90);
    });

    // === lineWidth 演示 ===
    ctx.fillStyle = '#c9d1d9';
    ctx.font = 'bold 16px sans-serif';
    ctx.fillText('lineWidth 线条宽度', 375, 350);

    ctx.lineCap = 'round';
    ctx.strokeStyle = '#58a6ff';
    for (let i = 1; i <= 8; i++) {
      ctx.lineWidth = i;
      ctx.beginPath();
      ctx.moveTo(80, 370 + i * 20);
      ctx.lineTo(670, 370 + i * 20);
      ctx.stroke();
      ctx.fillStyle = '#8b949e';
      ctx.font = '12px sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText(i + 'px', 680, 374 + i * 20);
    }
    ctx.textAlign = 'center';
  </script>
</body>
</html>

示例2:虚线与蚂蚁线动画

以下示例展示了多种虚线模式以及蚂蚁线(选区)动画效果:

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>虚线与蚂蚁线动画</title>
  <style>
    body { margin: 0; padding: 20px; background: #1a1a2e; color: #eee; font-family: 'Segoe UI', sans-serif; display: flex; flex-direction: column; align-items: center; }
    canvas { border: 1px solid #333; border-radius: 8px; background: #16213e; }
  </style>
</head>
<body>
  <h1>虚线与蚂蚁线动画</h1>
  <canvas id="myCanvas" width="700" height="450"></canvas>
  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    let dashOffset = 0;

    // 静态虚线展示
    const dashPatterns = [
      { pattern: [5, 5], label: '[5, 5]' },
      { pattern: [10, 5], label: '[10, 5]' },
      { pattern: [15, 5, 5, 5], label: '[15, 5, 5, 5]' },
      { pattern: [20, 5, 5, 5, 5, 5], label: '[20, 5, 5, 5, 5, 5]' },
      { pattern: [2, 5], label: '[2, 5] 点线' },
      { pattern: [1, 3, 6, 3], label: '[1, 3, 6, 3] 点划线' }
    ];

    dashPatterns.forEach((item, i) => {
      const y = 30 + i * 30;
      ctx.setLineDash(item.pattern);
      ctx.strokeStyle = '#58a6ff';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(30, y);
      ctx.lineTo(300, y);
      ctx.stroke();
      ctx.fillStyle = '#8b949e';
      ctx.font = '12px sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText(item.label, 310, y + 4);
    });
    ctx.setLineDash([]);

    // 蚂蚁线动画(选区效果)
    function drawMarchingAnts() {
      ctx.clearRect(350, 0, 350, 450);
      ctx.fillStyle = '#16213e';
      ctx.fillRect(350, 0, 350, 450);
      ctx.fillStyle = '#eee';
      ctx.font = 'bold 14px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('蚂蚁线动画(选区效果)', 525, 25);

      ctx.setLineDash([6, 4]);
      ctx.lineDashOffset = -dashOffset;
      ctx.strokeStyle = '#ffffff';
      ctx.lineWidth = 1.5;
      ctx.strokeRect(400, 50, 250, 150);

      ctx.beginPath();
      ctx.arc(525, 310, 80, 0, Math.PI * 2);
      ctx.strokeStyle = '#3fb950';
      ctx.lineWidth = 2;
      ctx.stroke();

      ctx.beginPath();
      ctx.moveTo(525, 390);
      ctx.lineTo(580, 420);
      ctx.lineTo(560, 440);
      ctx.lineTo(490, 440);
      ctx.lineTo(470, 420);
      ctx.closePath();
      ctx.strokeStyle = '#f78166';
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.setLineDash([]);

      dashOffset++;
      if (dashOffset > 10) dashOffset = 0;
      requestAnimationFrame(drawMarchingAnts);
    }
    drawMarchingAnts();
  </script>
</body>
</html>

五、浏览器兼容性

属性/方法 Chrome Firefox Safari Edge IE
moveTo 1+ 1.5+ 2+ 12+ 9+
lineTo 1+ 1.5+ 2+ 12+ 9+
lineWidth 1+ 1.5+ 2+ 12+ 9+
lineCap 1+ 1.5+ 2+ 12+ 9+
lineJoin 1+ 1.5+ 2+ 12+ 9+
setLineDash 23+ 27+ 6.1+ 12+ 11+
lineDashOffset 23+ 27+ 7+ 12+ 11+

六、注意事项与最佳实践

1. 像素对齐问题

线条宽度为奇数时,以像素边界为中心绘制,会产生抗锯齿模糊:

代码示例

// 问题:1px 线条模糊
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.stroke();

// 解决方案1:偏移 0.5 像素
ctx.moveTo(10, 10.5);
ctx.lineTo(100, 10.5);

// 解决方案2:使用偶数线宽
ctx.lineWidth = 2;

2. lineWidth 的居中特性

线条以路径为中心向两侧延伸,实际占用宽度为 lineWidth

代码示例

ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.stroke();
// 线条实际覆盖 y=45 到 y=55 的区域

3. miterLimit 的作用

lineJoin = 'miter' 时,如果两条线夹角很小,尖角会非常长。miterLimit 限制斜接长度与线宽的比值:

代码示例

ctx.lineJoin = 'miter';
ctx.miterLimit = 10; // 默认值
// 如果斜接长度超过 lineWidth * miterLimit / 2,自动退化为 bevel

4. 虚线重置

代码示例

// 设置虚线
ctx.setLineDash([5, 5]);

// 重置为实线
ctx.setLineDash([]);
// 或者
ctx.lineDashOffset = 0;

5. 线条绘制性能

大量线条绘制时,合并为一条路径可以提升性能:

代码示例

// 推荐:合并路径
ctx.beginPath();
for (let i = 0; i < 100; i++) {
  ctx.moveTo(Math.random() * 800, Math.random() * 600);
  ctx.lineTo(Math.random() * 800, Math.random() * 600);
}
ctx.stroke(); // 一次绘制

// 不推荐:每条线单独绘制
for (let i = 0; i < 100; i++) {
  ctx.beginPath();
  ctx.moveTo(Math.random() * 800, Math.random() * 600);
  ctx.lineTo(Math.random() * 800, Math.random() * 600);
  ctx.stroke(); // 100次绘制
}

七、代码规范示例

代码示例

// 推荐:封装线条绘制函数
function drawLine(ctx, x1, y1, x2, y2, options) {
  const {
    color = '#000000',
    width = 1,
    cap = 'butt',
    dash = []
  } = options || {};

  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineCap = cap;
  ctx.setLineDash(dash);
  ctx.stroke();
  ctx.setLineDash([]); // 重置虚线
}

// 推荐:绘制折线
function drawPolyline(ctx, points, options) {
  const {
    color = '#000000',
    width = 1,
    cap = 'round',
    join = 'round',
    dash = [],
    close = false
  } = options || {};

  if (points.length < 2) return;

  ctx.beginPath();
  ctx.moveTo(points[0][0], points[0][1]);
  for (let i = 1; i < points.length; i++) {
    ctx.lineTo(points[i][0], points[i][1]);
  }
  if (close) ctx.closePath();

  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineCap = cap;
  ctx.lineJoin = join;
  ctx.setLineDash(dash);
  ctx.stroke();
  ctx.setLineDash([]);
}

// 使用示例
drawLine(ctx, 10, 10, 200, 10, {
  color: '#58a6ff',
  width: 2,
  cap: 'round'
});

drawPolyline(ctx, [
  [50, 100], [100, 50], [150, 100], [200, 50]
], {
  color: '#3fb950',
  width: 3,
  join: 'round',
  dash: [8, 4]
});

八、常见问题与解决方案

1像素线条为什么显示为2像素且模糊?

原因:线条以路径为中心向两侧延伸 0.5 像素,落在像素边界上产生抗锯齿。

解决方案

代码示例

// 方案1:坐标偏移 0.5
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(10.5, 10.5);
ctx.lineTo(100.5, 10.5);
ctx.stroke();

// 方案2:使用 2 像素线宽
ctx.lineWidth = 2;
如何绘制平滑曲线而不是折线?

解决方案:使用贝塞尔曲线(后续教程详细讲解):

代码示例

// 使用二次贝塞尔曲线连接点
function drawSmoothLine(ctx, points) {
  if (points.length < 2) return;
  ctx.beginPath();
  ctx.moveTo(points[0][0], points[0][1]);

  for (let i = 1; i < points.length - 1; i++) {
    const xc = (points[i][0] + points[i + 1][0]) / 2;
    const yc = (points[i][1] + points[i + 1][1]) / 2;
    ctx.quadraticCurveTo(points[i][0], points[i][1], xc, yc);
  }

  const last = points[points.length - 1];
  ctx.lineTo(last[0], last[1]);
  ctx.stroke();
}
如何实现蚂蚁线(选区虚线动画)?

解决方案

代码示例

let offset = 0;
function animateDash() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.setLineDash([6, 4]);
  ctx.lineDashOffset = -offset;
  ctx.strokeStyle = '#fff';
  ctx.lineWidth = 1;
  ctx.strokeRect(50, 50, 200, 150);

  offset++;
  requestAnimationFrame(animateDash);
}
animateDash();
lineJoin = 'miter' 时尖角太长怎么办?

解决方案

代码示例

// 设置 miterLimit
ctx.lineJoin = 'miter';
ctx.miterLimit = 5; // 超过5倍线宽时退化为 bevel

// 或者直接使用 round/bevel
ctx.lineJoin = 'round';
如何绘制渐变色线条?

解决方案

代码示例

const gradient = ctx.createLinearGradient(50, 50, 300, 50);
gradient.addColorStop(0, '#e94560');
gradient.addColorStop(0.5, '#58a6ff');
gradient.addColorStop(1, '#3fb950');

ctx.strokeStyle = gradient;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(300, 50);
ctx.stroke();

九、总结

线条绘制是 Canvas 图形的基础,关键要点:

  • moveTo/lineTo:定义线段路径,moveTo 提笔移动,lineTo 画线

  • lineWidth:线条宽度,以路径为中心向两侧延伸

  • lineCap:端点样式,butt(平直)、round(圆形)、square(方形)

  • lineJoin:连接样式,miter(尖角)、round(圆角)、bevel(斜角)

  • setLineDash:虚线样式,通过数组定义实线与空白的交替模式

  • 像素对齐:奇数线宽需偏移 0.5 像素以避免模糊

  • 性能优化:合并多条线为一条路径一次绘制

下一教程将介绍如何绘制弧线与圆形。

标签: Canvas线条 moveTo lineTo lineWidth lineCap 虚线

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 多媒体:绘制路径 - 完整教程与代码示例 下一篇: 多媒体:绘制弧线与圆 - 完整教程与代码示例

poll相关推荐