图像操作
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) |
SVGImageElement | SVG 图像元素 | 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 的 imageSmoothingEnabled 和 imageSmoothingQuality 控制缩放质量:
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();
};下一节
继续学习:变换