Skip to content

像素操作

Canvas 提供了直接访问和操作像素数据的 API。通过 getImageData()putImageData()createImageData(),你可以读取画布上的每个像素,进行修改后再写回,实现图像滤镜、颜色调整、碰撞检测等底层操作。本节将详细介绍 Canvas 的像素操作机制和实际应用。

前置知识

阅读本节前,建议先了解:合成与裁剪

基础概念

Canvas 的像素数据以 ImageData 对象形式存在,包含一个 Uint8ClampedArray 类型的 data 属性。每个像素由 4 个字节表示:R(红)、G(绿)、B(蓝)、A(透明度),取值范围 0~255。

ImageData 数据结构

像素排列(从左到右,从上到下):
[R0, G0, B0, A0, R1, G1, B1, A1, R2, G2, B2, A2, ...]
 ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑
 像素 0          像素 1

获取指定像素

javascript
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;

// 获取像素 (x, y) 的颜色值
const index = (y * width + x) * 4;
const r = data[index];     // 红色分量
const g = data[index + 1]; // 绿色分量
const b = data[index + 2]; // 蓝色分量
const a = data[index + 3]; // 透明度分量

// 设置像素 (x, y) 的颜色
data[index] = 255;     // R
data[index + 1] = 0;   // G
data[index + 2] = 0;   // B
data[index + 3] = 255; // A

ImageData API

getImageData()

getImageData(sx, sy, sw, sh) 从画布中读取指定区域的像素数据。

javascript
/**
 * getImageData(sx, sy, sw, sh)
 * @param {number} sx - 起始 X
 * @param {number} sy - 起始 Y
 * @param {number} sw - 宽度
 * @param {number} sh - 高度
 * @returns {ImageData}
 */
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

putImageData()

putImageData(imageData, dx, dy) 将像素数据绘制到画布上。

javascript
/**
 * putImageData(imageData, dx, dy [, dirtyX, dirtyY, dirtyWidth, dirtyHeight])
 * @param {ImageData} imageData - 要绘制的像素数据
 * @param {number} dx - 目标 X
 * @param {number} dy - 目标 Y
 */
ctx.putImageData(imageData, 0, 0);

// 部分更新(只更新脏矩形区域)
ctx.putImageData(imageData, 0, 0, 50, 50, 100, 100);

createImageData()

createImageData(width, height) 创建一个空的 ImageData 对象。

javascript
// 方式 1:直接创建
const imageData = ctx.createImageData(200, 100);

// 方式 2:使用另一个 ImageData 的尺寸
const imageData2 = ctx.createImageData(existingImageData);

// 所有像素初始化为透明黑色 (0, 0, 0, 0)
const data = imageData.data;
console.log(data[0]); // 0 (R)
console.log(data[3]); // 0 (A)

ImageData 属性

属性类型说明
widthnumber图像宽度
heightnumber图像高度
dataUint8ClampedArray像素数据数组
colorSpacestring颜色空间(srgb 默认)

像素操作实战

示例 1:灰度滤镜

javascript
/**
 * 将 ImageData 转换为灰度
 */
function toGrayscale(imageData) {
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    // 加权平均法(人眼对绿色更敏感)
    const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
    data[i] = gray;       // R
    data[i + 1] = gray;   // G
    data[i + 2] = gray;   // B
    // A 保持不变
  }

  return imageData;
}

// 使用
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
toGrayscale(imageData);
ctx.putImageData(imageData, 0, 0);

示例 2:亮度与对比度调整

javascript
/**
 * 调整亮度和对比度
 * @param {ImageData} imageData
 * @param {number} brightness - 亮度偏移(-255 ~ 255)
 * @param {number} contrast - 对比度因子(0 ~ 2,1 为原始)
 */
function adjustBrightnessContrast(imageData, brightness, contrast) {
  const data = imageData.data;
  const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));

  for (let i = 0; i < data.length; i += 4) {
    for (let c = 0; c < 3; c++) {
      // 先调整对比度,再调整亮度
      let value = factor * (data[i + c] - 128) + 128 + brightness;
      data[i + c] = Math.max(0, Math.min(255, value)); // 钳位到 0~255
    }
  }

  return imageData;
}

示例 3:模糊滤镜(盒模糊)

javascript
/**
 * 盒模糊(Box Blur)
 * @param {ImageData} imageData
 * @param {number} radius - 模糊半径
 */
function boxBlur(imageData, radius) {
  const { width, height, data } = imageData;
  const copy = new Uint8ClampedArray(data); // 复制原始数据

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let r = 0, g = 0, b = 0, count = 0;

      // 遍历周围像素
      for (let dy = -radius; dy <= radius; dy++) {
        for (let dx = -radius; dx <= radius; dx++) {
          const nx = Math.min(width - 1, Math.max(0, x + dx));
          const ny = Math.min(height - 1, Math.max(0, y + dy));
          const idx = (ny * width + nx) * 4;

          r += copy[idx];
          g += copy[idx + 1];
          b += copy[idx + 2];
          count++;
        }
      }

      const idx = (y * width + x) * 4;
      data[idx] = r / count;
      data[idx + 1] = g / count;
      data[idx + 2] = b / count;
    }
  }

  return imageData;
}

示例 4:像素级碰撞检测

javascript
/**
 * 检测两个矩形区域是否有非透明像素重叠
 */
function checkPixelCollision(ctx, rect1, rect2) {
  // 计算重叠区域
  const overlapX = Math.max(rect1.x, rect2.x);
  const overlapY = Math.max(rect1.y, rect2.y);
  const overlapW = Math.min(rect1.x + rect1.w, rect2.x + rect2.w) - overlapX;
  const overlapH = Math.min(rect1.y + rect1.h, rect2.y + rect2.h) - overlapY;

  if (overlapW <= 0 || overlapH <= 0) return false;

  const imageData = ctx.getImageData(overlapX, overlapY, overlapW, overlapH);
  const data = imageData.data;

  // 检查是否有任何非透明像素
  for (let i = 3; i < data.length; i += 4) {
    if (data[i] > 0) return true;
  }

  return false;
}

完整示例:图像处理工具

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>像素操作</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
    .controls { margin: 10px 0; }
    button { padding: 5px 12px; margin: 3px; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Canvas 像素操作</h1>

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

  <div class="controls">
    <button onclick="applyFilter('grayscale')">灰度</button>
    <button onclick="applyFilter('invert')">反色</button>
    <button onclick="applyFilter('sepia')">怀旧</button>
    <button onclick="applyFilter('brightness')">增亮</button>
    <button onclick="applyFilter('blur')">模糊</button>
    <button onclick="applyFilter('noise')">噪点</button>
    <button onclick="resetCanvas()">重置</button>
  </div>

  <script>
    const canvas = document.getElementById('pixelCanvas');
    const ctx = canvas.getContext('2d');
    let originalData = null;

    // 绘制初始图案
    function drawInitialPattern() {
      // 背景渐变
      const bgGrad = ctx.createLinearGradient(0, 0, 400, 300);
      bgGrad.addColorStop(0, '#4285f4');
      bgGrad.addColorStop(1, '#34a853');
      ctx.fillStyle = bgGrad;
      ctx.fillRect(0, 0, 400, 300);

      // 绘制圆形
      const radGrad = ctx.createRadialGradient(200, 150, 20, 200, 150, 100);
      radGrad.addColorStop(0, '#fbbc05');
      radGrad.addColorStop(1, '#ea4335');
      ctx.fillStyle = radGrad;
      ctx.beginPath();
      ctx.arc(200, 150, 80, 0, Math.PI * 2);
      ctx.fill();

      // 绘制矩形
      ctx.fillStyle = '#9c27b0';
      ctx.fillRect(50, 50, 80, 80);

      ctx.fillStyle = '#00bcd4';
      ctx.fillRect(270, 50, 80, 80);

      // 保存原始数据
      originalData = ctx.getImageData(0, 0, 400, 300);
    }

    drawInitialPattern();

    // 滤镜函数
    const filters = {
      grayscale(imageData) {
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
          const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
          data[i] = data[i + 1] = data[i + 2] = avg;
        }
      },

      invert(imageData) {
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
          data[i] = 255 - data[i];
          data[i + 1] = 255 - data[i + 1];
          data[i + 2] = 255 - data[i + 2];
        }
      },

      sepia(imageData) {
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
          const r = data[i], g = data[i + 1], b = data[i + 2];
          data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189);
          data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168);
          data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131);
        }
      },

      brightness(imageData) {
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
          data[i] = Math.min(255, data[i] + 40);
          data[i + 1] = Math.min(255, data[i + 1] + 40);
          data[i + 2] = Math.min(255, data[i + 2] + 40);
        }
      },

      blur(imageData) {
        const { width, height, data } = imageData;
        const copy = new Uint8ClampedArray(data);
        const radius = 2;

        for (let y = 0; y < height; y++) {
          for (let x = 0; x < width; x++) {
            let r = 0, g = 0, b = 0, count = 0;
            for (let dy = -radius; dy <= radius; dy++) {
              for (let dx = -radius; dx <= radius; dx++) {
                const nx = Math.max(0, Math.min(width - 1, x + dx));
                const ny = Math.max(0, Math.min(height - 1, y + dy));
                const idx = (ny * width + nx) * 4;
                r += copy[idx]; g += copy[idx + 1]; b += copy[idx + 2];
                count++;
              }
            }
            const idx = (y * width + x) * 4;
            data[idx] = r / count;
            data[idx + 1] = g / count;
            data[idx + 2] = b / count;
          }
        }
      },

      noise(imageData) {
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
          const noise = (Math.random() - 0.5) * 60;
          data[i] = Math.max(0, Math.min(255, data[i] + noise));
          data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
          data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
        }
      }
    };

    function applyFilter(name) {
      if (!originalData) return;
      // 复制原始数据
      const imageData = new ImageData(
        new Uint8ClampedArray(originalData.data),
        originalData.width,
        originalData.height
      );
      filters[name](imageData);
      ctx.putImageData(imageData, 0, 0);
    }

    function resetCanvas() {
      if (originalData) {
        ctx.putImageData(originalData, 0, 0);
      }
    }
  </script>
</body>
</html>

注意事项

1. 跨域安全限制

javascript
// 跨域图像无法使用 getImageData
const img = new Image();
img.src = 'https://example.com/photo.jpg';
img.onload = () => {
  ctx.drawImage(img, 0, 0);
  try {
    ctx.getImageData(0, 0, 100, 100); // 报错!
  } catch (e) {
    console.error('安全错误:跨域图像无法读取像素');
  }
};

// 解决方案:设置 crossOrigin
img.crossOrigin = 'anonymous';

2. Uint8ClampedArray 的自动钳位

javascript
const data = imageData.data;
data[0] = 300;  // 自动钳位为 255
data[0] = -10;  // 自动钳位为 0
console.log(data[0]); // 255 或 0

3. 大图像的性能

getImageData()putImageData() 是昂贵的操作。对于大画布:

javascript
// 不好的做法:每帧读取全部像素
function animate() {
  const data = ctx.getImageData(0, 0, 1920, 1080); // 很慢!
  requestAnimationFrame(animate);
}

// 好的做法:只读取需要修改的区域
const smallData = ctx.getImageData(100, 100, 200, 200); // 只读取局部

最佳实践

1. 像素操作工具库

javascript
const PixelUtils = {
  /**
   * 创建指定颜色的 ImageData
   */
  create(width, height, r, g, b, a) {
    const imageData = new ImageData(width, height);
    const data = imageData.data;
    for (let i = 0; i < data.length; i += 4) {
      data[i] = r;
      data[i + 1] = g;
      data[i + 2] = b;
      data[i + 3] = a;
    }
    return imageData;
  },

  /**
   * 获取指定坐标的像素
   */
  getPixel(imageData, x, y) {
    const { width, data } = imageData;
    const index = (y * width + x) * 4;
    return {
      r: data[index],
      g: data[index + 1],
      b: data[index + 2],
      a: data[index + 3]
    };
  },

  /**
   * 设置指定坐标的像素
   */
  setPixel(imageData, x, y, r, g, b, a) {
    const { width, data } = imageData;
    const index = (y * width + x) * 4;
    data[index] = r;
    data[index + 1] = g;
    data[index + 2] = b;
    data[index + 3] = a;
  },

  /**
   * 遍历所有像素
   */
  forEach(imageData, callback) {
    const { width, height, data } = imageData;
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;
        callback({ r: data[index], g: data[index + 1], b: data[index + 2], a: data[index + 3] }, x, y);
      }
    }
  }
};

2. ImageData 性能优化技巧

技巧说明
局部读取只读取需要处理的区域
复制后修改使用 new Uint8ClampedArray(data) 创建副本
避免频繁 putImageData批量修改后一次性写回
使用 TypedArray 操作利用 set()subarray() 等高效方法
Web Worker将计算密集的像素操作放到 Worker 中

3. 使用 Web Worker 处理像素

javascript
// 主线程
const worker = new Worker('pixel-worker.js');

worker.postMessage({
  imageData: ctx.getImageData(0, 0, 400, 300),
  filter: 'grayscale'
});

worker.onmessage = (e) => {
  ctx.putImageData(e.data.imageData, 0, 0);
};

// pixel-worker.js
self.onmessage = (e) => {
  const { imageData, filter } = e.data;
  const data = imageData.data;

  if (filter === 'grayscale') {
    for (let i = 0; i < data.length; i += 4) {
      const avg = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
      data[i] = data[i + 1] = data[i + 2] = avg;
    }
  }

  self.postMessage({ imageData });
};

下一节

继续学习:动画基础

参考链接