Skip to content

图像操作

Canvas 提供了强大的图像处理能力。通过 drawImage() 方法可以将图像绘制到画布上,支持多种重载形式,实现图像缩放、裁剪、平铺等操作。此外,createPattern() 方法可以创建图案填充,用于背景纹理等场景。本节将详细介绍 Canvas 的图像操作 API。

前置知识

阅读本节前,建议先了解:文本渲染

基础概念

drawImage() 是 Canvas 中最灵活的绘图方法之一,它可以从多种图像源获取数据并绘制到画布上。支持的三种图像源:

图像源说明示例
HTMLImageElement通过 <img>new Image() 创建const img = new Image()
HTMLVideoElement<video> 元素document.querySelector('video')
HTMLCanvasElement另一个 <canvas> 元素document.createElement('canvas')
ImageBitmap高性能位图对象createImageBitmap(blob)
OffscreenCanvas离屏画布new OffscreenCanvas(w, h)
SVGImageElementSVG 图像元素document.querySelector('svg image')

drawImage() 的三种重载

重载 1:drawImage(image, dx, dy)

将图像以原始尺寸绘制到指定位置。

javascript
const img = new Image();
img.onload = () => {
  ctx.drawImage(img, 50, 50); // 在 (50, 50) 处绘制原始尺寸的图像
};
img.src = 'photo.jpg';

重载 2:drawImage(image, dx, dy, dWidth, dHeight)

将图像缩放到指定尺寸后绘制。

javascript
img.onload = () => {
  // 在 (50, 50) 处绘制 200x150 的图像
  ctx.drawImage(img, 50, 50, 200, 150);
};

重载 3:drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

从源图像中裁剪指定区域,缩放后绘制到目标位置。这是最强大的重载。

javascript
img.onload = () => {
  // 从源图像的 (100, 50) 处裁剪 200x150 区域
  // 绘制到画布的 (50, 50) 处,缩放为 150x100
  ctx.drawImage(
    img,
    100, 50, 200, 150,   // 源裁剪区域
    50, 50, 150, 100      // 目标绘制区域
  );
};

参数对照表

参数说明含义
sx, sy源起点从源图像哪个位置开始裁剪
sWidth, sHeight源尺寸裁剪区域的宽高
dx, dy目标起点绘制到画布的哪个位置
dWidth, dHeight目标尺寸绘制区域的宽高

图像裁剪

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>图像裁剪</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
    img { display: none; }
  </style>
</head>
<body>
  <h1>Canvas 图像裁剪</h1>

  <!-- 使用在线图片作为演示 -->
  <img id="sourceImage" crossorigin="anonymous"
       src="https://picsum.photos/400/300" alt="示例图片">

  <canvas id="cropCanvas" width="600" height="400"></canvas>

  <script>
    const img = document.getElementById('sourceImage');
    const canvas = document.getElementById('cropCanvas');
    const ctx = canvas.getContext('2d');

    img.onload = () => {
      // 绘制原始图像(缩小版)
      ctx.drawImage(img, 20, 20, 150, 112);
      ctx.fillStyle = '#333';
      ctx.font = '12px sans-serif';
      ctx.fillText('原始图像', 60, 150);

      // 裁剪左上角区域
      ctx.drawImage(img,
        0, 0, 200, 150,        // 源:左上 200x150
        200, 20, 120, 90        // 目标
      );
      ctx.fillText('左上裁剪', 220, 130);

      // 裁剪中心区域
      ctx.drawImage(img,
        100, 75, 200, 150,      // 源:中心 200x150
        350, 20, 120, 90         // 目标
      );
      ctx.fillText('中心裁剪', 370, 130);

      // 裁剪右下角区域
      ctx.drawImage(img,
        200, 150, 200, 150,     // 源:右下 200x150
        200, 150, 120, 90        // 目标
      );
      ctx.fillText('右下裁剪', 220, 260);

      // 反向缩放(放大裁剪区域)
      ctx.drawImage(img,
        0, 0, 100, 100,          // 源:左上 100x100
        380, 170, 180, 180       // 目标:放大到 180x180
      );
      ctx.fillText('放大裁剪', 420, 370);
    };

    img.onerror = () => {
      // 图片加载失败时绘制占位符
      ctx.fillStyle = '#eee';
      ctx.fillRect(20, 20, 150, 112);
      ctx.fillStyle = '#999';
      ctx.font = '14px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('图片加载失败', 95, 85);
      ctx.textAlign = 'start';
    };
  </script>
</body>
</html>

createPattern() 图案填充

createPattern(image, repetition) 创建一个可重复使用的图案对象,可以用 fillStyle 来填充区域。

repetition 参数

说明
'repeat'在水平和垂直方向都重复(默认)
'repeat-x'只在水平方向重复
'repeat-y'只在垂直方向重复
'no-repeat'不重复

基本用法

javascript
const patternImg = new Image();
patternImg.onload = () => {
  // 创建平铺图案
  const pattern = ctx.createPattern(patternImg, 'repeat');

  ctx.fillStyle = pattern;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
};
patternImg.src = 'tile.png';

用 Canvas 创建图案

javascript
// 动态创建图案 tile
const tileCanvas = document.createElement('canvas');
tileCanvas.width = 20;
tileCanvas.height = 20;
const tileCtx = tileCanvas.getContext('2d');

// 绘制棋盘格 tile
tileCtx.fillStyle = '#ffffff';
tileCtx.fillRect(0, 0, 20, 20);
tileCtx.fillStyle = '#e0e0e0';
tileCtx.fillRect(0, 0, 10, 10);
tileCtx.fillRect(10, 10, 10, 10);

// 创建图案
const pattern = ctx.createPattern(tileCanvas, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 400, 300);

实战示例

示例 1:图像滤镜效果

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>图像滤镜</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
    img { display: none; }
  </style>
</head>
<body>
  <h1>Canvas 图像滤镜</h1>
  <img id="filterImg" crossorigin="anonymous" src="https://picsum.photos/300/200" alt="filter">
  <canvas id="filterCanvas" width="700" height="500"></canvas>

  <script>
    const img = document.getElementById('filterImg');
    const canvas = document.getElementById('filterCanvas');
    const ctx = canvas.getContext('2d');

    img.onload = () => {
      const w = 140, h = 93;

      // 1. 原始图像
      ctx.drawImage(img, 20, 20, w, h);
      ctx.fillStyle = '#333';
      ctx.font = '12px sans-serif';
      ctx.fillText('原始图像', 50, 130);

      // 2. 灰度效果
      ctx.drawImage(img, 190, 20, w, h);
      const grayData = ctx.getImageData(190, 20, w, h);
      const grayPixels = grayData.data;
      for (let i = 0; i < grayPixels.length; i += 4) {
        const avg = (grayPixels[i] + grayPixels[i + 1] + grayPixels[i + 2]) / 3;
        grayPixels[i] = grayPixels[i + 1] = grayPixels[i + 2] = avg;
      }
      ctx.putImageData(grayData, 190, 20);
      ctx.fillText('灰度效果', 220, 130);

      // 3. 反色效果
      ctx.drawImage(img, 360, 20, w, h);
      const invertData = ctx.getImageData(360, 20, w, h);
      const invertPixels = invertData.data;
      for (let i = 0; i < invertPixels.length; i += 4) {
        invertPixels[i] = 255 - invertPixels[i];
        invertPixels[i + 1] = 255 - invertPixels[i + 1];
        invertPixels[i + 2] = 255 - invertPixels[i + 2];
      }
      ctx.putImageData(invertData, 360, 20);
      ctx.fillText('反色效果', 390, 130);

      // 4. 棕褐色(怀旧)效果
      ctx.drawImage(img, 530, 20, w, h);
      const sepiaData = ctx.getImageData(530, 20, w, h);
      const sepiaPixels = sepiaData.data;
      for (let i = 0; i < sepiaPixels.length; i += 4) {
        const r = sepiaPixels[i];
        const g = sepiaPixels[i + 1];
        const b = sepiaPixels[i + 2];
        sepiaPixels[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
        sepiaPixels[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
        sepiaPixels[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
      }
      ctx.putImageData(sepiaData, 530, 20);
      ctx.fillText('怀旧效果', 560, 130);

      // 5. 亮度增强
      ctx.drawImage(img, 105, 160, w, h);
      const brightData = ctx.getImageData(105, 160, w, h);
      const brightPixels = brightData.data;
      for (let i = 0; i < brightPixels.length; i += 4) {
        brightPixels[i] = Math.min(255, brightPixels[i] + 50);
        brightPixels[i + 1] = Math.min(255, brightPixels[i + 1] + 50);
        brightPixels[i + 2] = Math.min(255, brightPixels[i + 2] + 50);
      }
      ctx.putImageData(brightData, 105, 160);
      ctx.fillText('亮度增强', 135, 270);

      // 6. 对比度增强
      ctx.drawImage(img, 275, 160, w, h);
      const contrastData = ctx.getImageData(275, 160, w, h);
      const contrastPixels = contrastData.data;
      const factor = 1.5; // 对比度因子
      for (let i = 0; i < contrastPixels.length; i += 4) {
        contrastPixels[i] = Math.min(255, Math.max(0, factor * (contrastPixels[i] - 128) + 128));
        contrastPixels[i + 1] = Math.min(255, Math.max(0, factor * (contrastPixels[i + 1] - 128) + 128));
        contrastPixels[i + 2] = Math.min(255, Math.max(0, factor * (contrastPixels[i + 2] - 128) + 128));
      }
      ctx.putImageData(contrastData, 275, 160);
      ctx.fillText('对比度增强', 305, 270);
    };
  </script>
</body>
</html>

示例 2:图片拼贴画

javascript
/**
 * 将图片绘制为马赛克拼贴效果
 */
function drawMosaic(ctx, img, x, y, mosaicSize) {
  const tileCanvas = document.createElement('canvas');
  tileCanvas.width = mosaicSize;
  tileCanvas.height = mosaicSize;
  const tileCtx = tileCanvas.getContext('2d');

  // 在每个 tile 位置绘制缩小的图片
  const cols = Math.ceil(canvas.width / mosaicSize);
  const rows = Math.ceil(canvas.height / mosaicSize);

  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      ctx.drawImage(img,
        0, 0, img.width, img.height,
        col * mosaicSize, row * mosaicSize,
        mosaicSize, mosaicSize
      );
    }
  }
}

示例 3:图案填充综合

javascript
// 创建多种图案

// 1. 棋盘格图案
function createCheckerPattern(ctx, size, color1, color2) {
  const tile = document.createElement('canvas');
  tile.width = size * 2;
  tile.height = size * 2;
  const tCtx = tile.getContext('2d');

  tCtx.fillStyle = color1;
  tCtx.fillRect(0, 0, size * 2, size * 2);
  tCtx.fillStyle = color2;
  tCtx.fillRect(0, 0, size, size);
  tCtx.fillRect(size, size, size, size);

  return ctx.createPattern(tile, 'repeat');
}

// 2. 条纹图案
function createStripePattern(ctx, width, color1, color2, angle) {
  const tile = document.createElement('canvas');
  tile.width = width * 2;
  tile.height = width * 2;
  const tCtx = tile.getContext('2d');

  tCtx.fillStyle = color1;
  tCtx.fillRect(0, 0, width * 2, width * 2);
  tCtx.fillStyle = color2;
  tCtx.save();
  tCtx.translate(width, width);
  tCtx.rotate(angle || 0);
  tCtx.fillRect(-width, -width, width * 2, width);
  tCtx.restore();

  return ctx.createPattern(tile, 'repeat');
}

// 3. 圆点图案
function createDotPattern(ctx, spacing, dotSize, bgColor, dotColor) {
  const tile = document.createElement('canvas');
  tile.width = spacing;
  tile.height = spacing;
  const tCtx = tile.getContext('2d');

  tCtx.fillStyle = bgColor;
  tCtx.fillRect(0, 0, spacing, spacing);
  tCtx.fillStyle = dotColor;
  tCtx.beginPath();
  tCtx.arc(spacing / 2, spacing / 2, dotSize, 0, Math.PI * 2);
  tCtx.fill();

  return ctx.createPattern(tile, 'repeat');
}

// 使用
const checker = createCheckerPattern(ctx, 10, '#ffffff', '#e0e0e0');
const stripe = createStripePattern(ctx, 8, '#4285f4', '#34a853', Math.PI / 4);
const dots = createDotPattern(ctx, 20, 3, '#f5f5f5', '#ea4335');

// 填充不同区域
ctx.fillStyle = checker;
ctx.fillRect(0, 0, 200, 200);

ctx.fillStyle = stripe;
ctx.fillRect(210, 0, 200, 200);

ctx.fillStyle = dots;
ctx.fillRect(420, 0, 200, 200);

注意事项

1. 跨域图像问题

drawImage() 使用跨域图像时,getImageData() 会报安全错误。需要设置 crossorigin 属性:

javascript
const img = new Image();
img.crossOrigin = 'anonymous'; // 关键!
img.src = 'https://example.com/photo.jpg';

img.onload = () => {
  ctx.drawImage(img, 0, 0);
  // 现在 getImageData() 可以正常工作
  const data = ctx.getImageData(0, 0, 100, 100);
};

2. 图像加载时机

图像是异步加载的,必须在 onload 回调中进行绘制:

javascript
// 错误:图像可能还没加载完
const img = new Image();
img.src = 'photo.jpg';
ctx.drawImage(img, 0, 0); // 可能绘制空白

// 正确:等待加载完成
img.onload = () => {
  ctx.drawImage(img, 0, 0);
};

3. Video 元素作为图像源

drawImage() 可以从 <video> 元素捕获帧:

javascript
const video = document.querySelector('video');
const canvas = document.getElementById('captureCanvas');
const ctx = canvas.getContext('2d');

function captureFrame() {
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}

// 每 100ms 捕获一帧
setInterval(captureFrame, 100);

最佳实践

1. 图像预加载

javascript
/**
 * 批量加载图像
 */
class ImageLoader {
  constructor() {
    this.images = new Map();
  }

  /**
   * 加载单张图片
   */
  load(name, src) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.crossOrigin = 'anonymous';
      img.onload = () => {
        this.images.set(name, img);
        resolve(img);
      };
      img.onerror = reject;
      img.src = src;
    });
  }

  /**
   * 批量加载
   */
  loadAll(imageMap) {
    const promises = [];
    for (const [name, src] of Object.entries(imageMap)) {
      promises.push(this.load(name, src));
    }
    return Promise.all(promises);
  }

  get(name) {
    return this.images.get(name);
  }
}

// 使用
const loader = new ImageLoader();
await loader.loadAll({
  hero: 'images/hero.jpg',
  logo: 'images/logo.png',
  bg: 'images/background.jpg'
});

ctx.drawImage(loader.get('hero'), 0, 0);

2. 图像缩放算法

Canvas 的 imageSmoothingEnabledimageSmoothingQuality 控制缩放质量:

javascript
// 控制图像平滑
ctx.imageSmoothingEnabled = true;  // 启用平滑(默认)
ctx.imageSmoothingEnabled = false; // 禁用平滑(像素风格)

// 平滑质量
ctx.imageSmoothingQuality = 'low';     // 低质量(快)
ctx.imageSmoothingQuality = 'medium';  // 中等
ctx.imageSmoothingQuality = 'high';    // 高质量(慢)

// 应用:绘制像素风图像
ctx.imageSmoothingEnabled = false;
ctx.drawImage(pixelArtImage, 0, 0, 400, 400);

3. ImageBitmap 高性能绘制

javascript
// ImageBitmap 比 Image 元素绘制更快
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'large-image.jpg';
img.onload = async () => {
  const bitmap = await createImageBitmap(img, {
    resizeWidth: 200,
    resizeHeight: 150,
    resizeQuality: 'high'
  });

  // 使用 ImageBitmap 绘制(性能更好)
  ctx.drawImage(bitmap, 0, 0);

  // 用完后释放
  bitmap.close();
};

下一节

继续学习:变换

参考链接