前置知识: CSS

CSS Canvas 绘图

00:00
6 min Intermediate 2026/5/3

Canvas绘图API与动画实战

1. Canvas 概述 | Canvas Overview

Canvas 是 HTML5 提供的一个绘图 API,通过 JavaScript 可以在网页上绘制各种图形、动画和交互效果。Canvas 元素提供了一个矩形区域,我们可以使用各种绘图命令在这个区域内绘制内容。

1.1 Canvas 的特点

  • 像素级控制:可以精确控制每个像素的颜色和位置
  • 丰富的绘图 API:支持绘制路径、形状、文本、图像等
  • 动画支持:可以通过 JavaScript 实现复杂的动画效果
  • 交互性:可以响应鼠标和键盘事件,实现交互效果
  • 性能优势:对于复杂的图形和动画,Canvas 通常比 DOM 操作更高效

1.2 Canvas 与 SVG 的区别

特性CanvasSVG
绘制方式基于像素基于矢量
缩放效果放大后可能失真放大后不失真
事件处理不支持元素级事件支持元素级事件
性能适合绘制大量图形和动画适合绘制少量复杂图形
存储方式存储为像素数据存储为 XML 结构
## 2. Canvas 基础Canvas Basics

2.1 创建 Canvas 元素

<canvas id="myCanvas" width="400" height="300"></canvas>

2.2 获取 Canvas 上下文

要在 Canvas 上绘图,首先需要获取 Canvas 的 2D 上下文:

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

2.3 基本绘图操作

2.3.1 绘制矩形

// 填充矩形
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);
// 描边矩形
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.strokeRect(120, 10, 100, 50);
// 清除矩形
ctx.clearRect(230, 10, 100, 50);

2.3.2 绘制路径

// 开始路径
ctx.beginPath();
// 移动到起始点
ctx.moveTo(50, 100);
// 绘制线条
ctx.lineTo(150, 100);
ctx.lineTo(100, 150);
// 闭合路径
ctx.closePath();
// 填充路径
ctx.fillStyle = 'green';
ctx.fill();
// 描边路径
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();

2.3.3 绘制圆形

ctx.beginPath();
ctx.arc(200, 125, 50, 0, Math.PI * 2);
ctx.fillStyle = 'yellow';
ctx.fill();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();

2.3.4 绘制文本

ctx.font = '24px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.fillText('Hello Canvas!', 200, 250);
// 描边文本
ctx.strokeStyle = 'red';
ctx.lineWidth = 1;
ctx.strokeText('Hello Canvas!', 200, 280);

3. Canvas 进阶 | Canvas Advanced

3.1 渐变效果

3.1.1 线性渐变

// 创建线性渐变
const linearGradient = ctx.createLinearGradient(0, 0, 400, 0);
linearGradient.addColorStop(0, 'red');
linearGradient.addColorStop(0.5, 'yellow');
linearGradient.addColorStop(1, 'green');
// 使用渐变
ctx.fillStyle = linearGradient;
ctx.fillRect(0, 0, 400, 300);

3.1.2 径向渐变

// 创建径向渐变
const radialGradient = ctx.createRadialGradient(200, 150, 0, 200, 150, 150);
radialGradient.addColorStop(0, 'white');
radialGradient.addColorStop(1, 'blue');
// 使用渐变
ctx.fillStyle = radialGradient;
ctx.fillRect(0, 0, 400, 300);

3.2 图案填充

// 创建图案
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 20;
patternCanvas.height = 20;
const patternCtx = patternCanvas.getContext('2d');
patternCtx.fillStyle = 'red';
patternCtx.fillRect(0, 0, 10, 10);
patternCtx.fillRect(10, 10, 10, 10);
// 创建重复图案
const pattern = ctx.createPattern(patternCanvas, 'repeat');
// 使用图案
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 400, 300);

3.3 图像处理

3.3.1 绘制图像

const img = new Image();
img.src = 'image.jpg';
img.onload = function () {
  // 绘制完整图像
  ctx.drawImage(img, 0, 0);
  // 绘制缩放后的图像
  ctx.drawImage(img, 0, 150, 200, 100);
  // 绘制图像的一部分
  ctx.drawImage(img, 100, 100, 200, 100, 200, 150, 200, 100);
};

3.3.2 图像变换

const img = new Image();
img.src = 'image.jpg';
img.onload = function () {
  // 保存当前状态
  ctx.save();
  // 平移
  ctx.translate(100, 50);
  // 旋转
  ctx.rotate(Math.PI / 4);
  // 缩放
  ctx.scale(0.5, 0.5);
  // 绘制图像
  ctx.drawImage(img, 0, 0);
  // 恢复之前的状态
  ctx.restore();
};

3.4 合成模式

// 绘制第一个矩形
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);
// 设置合成模式
ctx.globalCompositeOperation = 'source-over'; // 默认
// ctx.globalCompositeOperation = 'source-in';
// ctx.globalCompositeOperation = 'source-out';
// ctx.globalCompositeOperation = 'destination-over';
// ctx.globalCompositeOperation = 'destination-in';
// ctx.globalCompositeOperation = 'destination-out';
// ctx.globalCompositeOperation = 'lighter';
// ctx.globalCompositeOperation = 'copy';
// ctx.globalCompositeOperation = 'xor';
// 绘制第二个矩形
ctx.fillStyle = 'blue';
ctx.fillRect(100, 100, 100, 100);

4. Canvas 动画 | Canvas Animation

4.1 基本动画循环

function animate() {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 绘制动画内容
  // ...
  // 请求下一帧
  requestAnimationFrame(animate);
}
// 开始动画
animate();

4.2 移动动画

let x = 0;
let y = 150;
let dx = 2;
let dy = 2;
function animate() {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // 绘制圆形
  ctx.beginPath();
  ctx.arc(x, y, 20, 0, Math.PI * 2);
  ctx.fillStyle = 'red';
  ctx.fill();
  // 更新位置
  x += dx;
  y += dy;
  // 边界检测
  if (x + 20 > canvas.width || x - 20 < 0) {
    dx = -dx;
  }
  if (y + 20 > canvas.height || y - 20 < 0) {
    dy = -dy;
  }
  // 请求下一帧
  requestAnimationFrame(animate);
}
// 开始动画
animate();

4.3 交互动画

let isDrawing = false;
let lastX = 0;
let lastY = 0;
// 鼠标按下事件
canvas.addEventListener('mousedown', (e) => {
  isDrawing = true;
  [lastX, lastY] = [e.offsetX, e.offsetY];
});
// 鼠标移动事件
canvas.addEventListener('mousemove', (e) => {
  if (!isDrawing) return;
  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.strokeStyle = 'black';
  ctx.lineWidth = 2;
  ctx.stroke();
  [lastX, lastY] = [e.offsetX, e.offsetY];
});
// 鼠标释放事件
canvas.addEventListener('mouseup', () => {
  isDrawing = false;
});
// 鼠标离开事件
canvas.addEventListener('mouseout', () => {
  isDrawing = false;
});

5. Canvas 实战示例 | Canvas Practical Examples

5.1 简单的绘图应用

<!DOCTYPE html>
<html>
  <head>
    <title>Canvas Drawing App</title>
    <style>
      canvas {
        border: 1px solid black;
        cursor: crosshair;
      }
      .controls {
        margin-bottom: 10px;
      }
    </style>
  </head>
  <body>
    <div class="controls">
      <label for="color">Color:</label>
      <input type="color" id="color" value="#000000" />
      <label for="size">Size:</label>
      <input type="range" id="size" min="1" max="20" value="2" />
      <button id="clear">Clear</button>
    </div>
    <canvas id="canvas" width="600" height="400"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      const colorInput = document.getElementById('color');
      const sizeInput = document.getElementById('size');
      const clearButton = document.getElementById('clear');
      let isDrawing = false;
      let lastX = 0;
      let lastY = 0;
      // 鼠标按下事件
      canvas.addEventListener('mousedown', (e) => {
        isDrawing = true;
        [lastX, lastY] = [e.offsetX, e.offsetY];
      });
      // 鼠标移动事件
      canvas.addEventListener('mousemove', (e) => {
        if (!isDrawing) return;
        ctx.beginPath();
        ctx.moveTo(lastX, lastY);
        ctx.lineTo(e.offsetX, e.offsetY);
        ctx.strokeStyle = colorInput.value;
        ctx.lineWidth = sizeInput.value;
        ctx.stroke();
        [lastX, lastY] = [e.offsetX, e.offsetY];
      });
      // 鼠标释放事件
      canvas.addEventListener('mouseup', () => {
        isDrawing = false;
      });
      // 鼠标离开事件
      canvas.addEventListener('mouseout', () => {
        isDrawing = false;
      });
      // 清除按钮点击事件
      clearButton.addEventListener('click', () => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
      });
    </script>
  </body>
</html>

5.2 粒子效果

<!DOCTYPE html>
<html>
  <head>
    <title>Canvas Particle Effect</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
      canvas {
        display: block;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      // 设置画布大小
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      // 粒子数组
      const particles = [];
      const particleCount = 100;
      // 创建粒子
      for (let i = 0; i < particleCount; i++) {
        particles.push({
          x: Math.random() * canvas.width,
          y: Math.random() * canvas.height,
          size: Math.random() * 5 + 1,
          speedX: Math.random() * 3 - 1.5,
          speedY: Math.random() * 3 - 1.5,
          color: `hsl(${Math.random() * 360}, 50%, 50%)`,
        });
      }
      // 动画函数
      function animate() {
        // 清除画布
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        // 更新和绘制粒子
        for (let i = 0; i < particles.length; i++) {
          const p = particles[i];
          // 绘制粒子
          ctx.beginPath();
          ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
          ctx.fillStyle = p.color;
          ctx.fill();
          // 更新粒子位置
          p.x += p.speedX;
          p.y += p.speedY;
          // 边界检测
          if (p.x + p.size > canvas.width || p.x - p.size < 0) {
            p.speedX = -p.speedX;
          }
          if (p.y + p.size > canvas.height || p.y - p.size < 0) {
            p.speedY = -p.speedY;
          }
          // 连接粒子
          for (let j = i; j < particles.length; j++) {
            const p2 = particles[j];
            const dx = p.x - p2.x;
            const dy = p.y - p2.y;
            const distance = Math.sqrt(dx * dx + dy * dy);
            if (distance < 100) {
              ctx.beginPath();
              ctx.strokeStyle = p.color;
              ctx.lineWidth = 0.2;
              ctx.moveTo(p.x, p.y);
              ctx.lineTo(p2.x, p2.y);
              ctx.stroke();
            }
          }
        }
        // 请求下一帧
        requestAnimationFrame(animate);
      }
      // 开始动画
      animate();
      // 窗口大小改变时调整画布大小
      window.addEventListener('resize', () => {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
      });
    </script>
  </body>
</html>

5.3 时钟效果

<!DOCTYPE html>
<html>
  <head>
    <title>Canvas Clock</title>
    <style>
      canvas {
        display: block;
        margin: 50px auto;
        border: 1px solid black;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="400" height="400"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      const centerX = canvas.width / 2;
      const centerY = canvas.height / 2;
      const radius = 180;
      // 绘制时钟
      function drawClock() {
        // 清除画布
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        // 获取当前时间
        const now = new Date();
        const hours = now.getHours();
        const minutes = now.getMinutes();
        const seconds = now.getSeconds();
        // 绘制表盘
        ctx.beginPath();
        ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
        ctx.fillStyle = 'white';
        ctx.fill();
        ctx.strokeStyle = 'black';
        ctx.lineWidth = 2;
        ctx.stroke();
        // 绘制刻度
        for (let i = 0; i < 12; i++) {
          const angle = (i / 12) * Math.PI * 2;
          const x1 = centerX + Math.cos(angle) * (radius - 20);
          const y1 = centerY + Math.sin(angle) * (radius - 20);
          const x2 = centerX + Math.cos(angle) * (radius - 10);
          const y2 = centerY + Math.sin(angle) * (radius - 10);
          ctx.beginPath();
          ctx.moveTo(x1, y1);
          ctx.lineTo(x2, y2);
          ctx.strokeStyle = 'black';
          ctx.lineWidth = 2;
          ctx.stroke();
          // 绘制数字
          const text = i === 0 ? '12' : i.toString();
          ctx.font = '20px Arial';
          ctx.fillStyle = 'black';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          const textX = centerX + Math.cos(angle) * (radius - 40);
          const textY = centerY + Math.sin(angle) * (radius - 40);
          ctx.fillText(text, textX, textY);
        }
        // 绘制时针
        const hourAngle = ((hours % 12) / 12) * Math.PI * 2 + (minutes / 60) * ((Math.PI * 2) / 12);
        const hourX = centerX + Math.cos(hourAngle) * (radius - 80);
        const hourY = centerY + Math.sin(hourAngle) * (radius - 80);
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.lineTo(hourX, hourY);
        ctx.strokeStyle = 'black';
        ctx.lineWidth = 4;
        ctx.stroke();
        // 绘制分针
        const minuteAngle = (minutes / 60) * Math.PI * 2 + (seconds / 60) * ((Math.PI * 2) / 60);
        const minuteX = centerX + Math.cos(minuteAngle) * (radius - 60);
        const minuteY = centerY + Math.sin(minuteAngle) * (radius - 60);
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.lineTo(minuteX, minuteY);
        ctx.strokeStyle = 'black';
        ctx.lineWidth = 2;
        ctx.stroke();
        // 绘制秒针
        const secondAngle = (seconds / 60) * Math.PI * 2;
        const secondX = centerX + Math.cos(secondAngle) * (radius - 40);
        const secondY = centerY + Math.sin(secondAngle) * (radius - 40);
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.lineTo(secondX, secondY);
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 1;
        ctx.stroke();
        // 绘制中心点
        ctx.beginPath();
        ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
        ctx.fillStyle = 'black';
        ctx.fill();
      }
      // 绘制时钟并每秒更新
      drawClock();
      setInterval(drawClock, 1000);
    </script>
  </body>
</html>

6. Canvas 性能优化 | Canvas Performance Optimization

6.1 减少绘制操作

  • 批量绘制:将多个绘制操作合并为一个路径
  • 避免频繁清除:只清除需要更新的区域
  • 使用离屏 Canvas:对于复杂的绘制,使用离屏 Canvas 预渲染

6.2 优化图像操作

  • 使用适当的图像格式:根据需要选择 JPEG、PNG 或 WebP
  • 压缩图像:减少图像文件大小
  • 使用 CSS 缩放:在绘制前使用 CSS 缩放图像

6.3 优化动画

  • 使用 requestAnimationFrame:代替 setTimeout 或 setInterval
  • 限制帧率:对于不需要 60fps 的动画,限制帧率
  • 使用 transforms:使用 translate、rotate、scale 等变换代替重新绘制

6.4 内存管理

  • 释放不再使用的资源:及时释放图像、路径等资源
  • 避免内存泄漏:注意事件监听器的移除

7. Canvas 最佳实践 | Canvas Best Practices

7.1 代码组织

  • 模块化设计:将 Canvas 相关代码封装为模块
  • 使用面向对象:使用类和对象组织代码
  • 注释:添加适当的注释,说明代码的功能和逻辑

7.2 兼容性

  • 检测 Canvas 支持:在使用 Canvas 前检测浏览器是否支持
  • 提供替代方案:为不支持 Canvas 的浏览器提供替代内容

7.3 安全性

  • 验证用户输入:对于用户输入的坐标和尺寸,进行验证
  • 防止 XSS于从用户输入生成的 Canvas 内容,进适当的过滤

7.4 可访问性

  • 提供替代文本:为 Canvas 元素添加 alt 属性
  • 使用 ARIA 标签:为 Canvas 元素添加适当的 ARIA 标签
  • 支持键盘导航交互式 Canvas,支持键盘导航

8. 总结 | Summary

Canvas 是 HTML5 提供的强大绘 API,通过 JavaScript 可以在网页上创建各种图形动画交互效果。Canvas 具有像素级控制、丰富的绘 API、动画支持和交互性等特,适用于创建游戏、数据可视化图像处理等应用通过学习 Canvas 的基础操作、进阶特性性能优化技巧,你可以创建各种复杂的图形动画效果。在实际开发中,应据具体需求选择合适的技术方案,并遵循相关的最佳实践,以创建性能、可维护的 Canvas 应用

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式