pin_drop当前位置:知识文库 ❯ 图文
多媒体:Canvas图像处理 - 完整教程与代码示例
一、教程简介
Canvas 的 drawImage() 方法是图像处理的核心,它可以将图像、Canvas 或视频帧绘制到画布上,并支持缩放、裁剪等变换操作。通过 drawImage() 结合其他 Canvas API,可以实现图像滤镜、水印、截图等功能。本教程将详细介绍 drawImage() 的三种调用形式、图像缩放与裁剪,以及常见的图像处理技巧。
二、核心概念
drawImage 的三种调用形式
图像源类型
drawImage() 接受以下类型的图像源:
裁剪与缩放的关系
代码示例
源图像 (sx, sy, sWidth, sHeight)
╭──────────────────────╮
│ ╭────────╮ │
│ │ 裁剪区 │ │
│ ╰────────╯ │
╰──────────────────────╯
↓
目标画布 (dx, dy, dWidth, dHeight)
╭──────────────╮
│ 缩放绘制 │
╰──────────────╯三、语法与用法
参数说明
基本绘制参数
缩放绘制参数
裁剪绘制参数
图像加载方法
代码示例
// 方法一:new Image()
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
img.src = 'image.png';
// 方法二:已有 <img> 元素
const img = document.getElementById('myImage');
if (img.complete) {
ctx.drawImage(img, 0, 0);
} else {
img.onload = () => ctx.drawImage(img, 0, 0);
}四、代码示例
drawImage 三种调用形式
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas drawImage 三种形式</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0d1117;
font-family: "Microsoft YaHei", sans-serif;
color: #c9d1d9;
}
h1 { text-align: center; color: #f0883e; }
canvas {
display: block;
margin: 20px auto;
border: 1px solid #30363d;
border-radius: 8px;
}
.info { text-align: center; color: #8b949e; }
</style>
</head>
<body>
<h1>Canvas drawImage 三种调用形式</h1>
<canvas id="drawCanvas" width="750" height="400"></canvas>
<p class="info">使用程序化生成的图像源演示 drawImage</p>
<script>
const canvas = document.getElementById('drawCanvas');
const ctx = canvas.getContext('2d');
// 创建程序化图像源(离屏 Canvas)
function createImageSource() {
const offscreen = document.createElement('canvas');
offscreen.width = 200;
offscreen.height = 150;
const octx = offscreen.getContext('2d');
// 渐变背景
const grad = octx.createLinearGradient(0, 0, 200, 150);
grad.addColorStop(0, '#e94560');
grad.addColorStop(0.5, '#0f3460');
grad.addColorStop(1, '#533483');
octx.fillStyle = grad;
octx.fillRect(0, 0, 200, 150);
// 网格
octx.strokeStyle = 'rgba(255,255,255,0.15)';
octx.lineWidth = 1;
for (let x = 0; x <= 200; x += 25) {
octx.beginPath();
octx.moveTo(x, 0);
octx.lineTo(x, 150);
octx.stroke();
}
for (let y = 0; y <= 150; y += 25) {
octx.beginPath();
octx.moveTo(0, y);
octx.lineTo(200, y);
octx.stroke();
}
// 中心圆
octx.beginPath();
octx.arc(100, 75, 30, 0, Math.PI * 2);
octx.fillStyle = 'rgba(255,255,255,0.3)';
octx.fill();
octx.strokeStyle = '#fff';
octx.lineWidth = 2;
octx.stroke();
// 文字
octx.fillStyle = '#fff';
octx.font = 'bold 14px Arial';
octx.textAlign = 'center';
octx.textBaseline = 'middle';
octx.fillText('200x150', 100, 75);
return offscreen;
}
const imgSource = createImageSource();
// 背景
ctx.fillStyle = '#161b22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// === 形式一:基本绘制 ===
ctx.fillStyle = '#8b949e';
ctx.font = 'bold 14px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('形式一:drawImage(img, dx, dy)', 130, 15);
ctx.drawImage(imgSource, 30, 40);
// 边框标注
ctx.strokeStyle = '#f0883e';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.strokeRect(30, 40, 200, 150);
ctx.setLineDash([]);
// === 形式二:缩放绘制 ===
ctx.fillStyle = '#8b949e';
ctx.fillText('形式二:drawImage(img, dx, dy, dw, dh)', 475, 15);
// 缩小
ctx.drawImage(imgSource, 280, 40, 100, 75);
ctx.strokeStyle = '#57ab5a';
ctx.setLineDash([4, 4]);
ctx.strokeRect(280, 40, 100, 75);
ctx.setLineDash([]);
ctx.fillStyle = '#666';
ctx.font = '11px Arial';
ctx.fillText('100x75 (缩小)', 330, 125);
// 放大
ctx.drawImage(imgSource, 410, 40, 300, 225);
ctx.strokeStyle = '#f47067';
ctx.setLineDash([4, 4]);
ctx.strokeRect(410, 40, 300, 225);
ctx.setLineDash([]);
ctx.fillStyle = '#666';
ctx.fillText('300x225 (放大)', 560, 275);
// === 形式三:裁剪绘制 ===
ctx.fillStyle = '#8b949e';
ctx.font = 'bold 14px "Microsoft YaHei"';
ctx.fillText('形式三:drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)', 375, 310);
// 在源图像上标注裁剪区域
ctx.strokeStyle = '#ffd93d';
ctx.lineWidth = 2;
ctx.setLineDash([4, 2]);
ctx.strokeRect(30 + 50, 40 + 25, 100, 100);
ctx.setLineDash([]);
// 裁剪并绘制
ctx.drawImage(imgSource, 50, 25, 100, 100, 30, 340, 200, 200);
ctx.strokeStyle = '#ffd93d';
ctx.lineWidth = 1;
ctx.strokeRect(30, 340, 200, 200);
ctx.fillStyle = '#666';
ctx.font = '11px Arial';
ctx.fillText('裁剪区域 (50,25,100,100)', 130, 370);
ctx.fillText('绘制到 (30,340,200,200)', 130, 385);
</script>
</body>
</html>图像处理实战 - 滤镜与水印
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas 图像处理实战</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
font-family: "Microsoft YaHei", sans-serif;
color: #e0e0e0;
}
h1 { text-align: center; color: #e94560; }
canvas {
display: block;
margin: 20px auto;
border: 1px solid #333;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>Canvas 图像处理实战 - 滤镜与水印</h1>
<canvas id="filterCanvas" width="750" height="350"></canvas>
<script>
const canvas = document.getElementById('filterCanvas');
const ctx = canvas.getContext('2d');
// 创建测试图像
function createTestImage() {
const offscreen = document.createElement('canvas');
offscreen.width = 200;
offscreen.height = 150;
const octx = offscreen.getContext('2d');
// 渐变背景
const grad = octx.createLinearGradient(0, 0, 200, 150);
grad.addColorStop(0, '#4CAF50');
grad.addColorStop(1, '#2196F3');
octx.fillStyle = grad;
octx.fillRect(0, 0, 200, 150);
// 圆形
octx.beginPath();
octx.arc(100, 75, 40, 0, Math.PI * 2);
octx.fillStyle = '#FFC107';
octx.fill();
// 文字
octx.fillStyle = '#fff';
octx.font = 'bold 18px Arial';
octx.textAlign = 'center';
octx.textBaseline = 'middle';
octx.fillText('SAMPLE', 100, 75);
return offscreen;
}
const img = createTestImage();
ctx.fillStyle = '#16213e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// === 原图 ===
ctx.drawImage(img, 20, 20, 160, 120);
ctx.fillStyle = '#8b949e';
ctx.font = '12px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.fillText('原图', 100, 155);
// === 灰度滤镜 ===
const grayCanvas = document.createElement('canvas');
grayCanvas.width = 200;
grayCanvas.height = 150;
const grayCtx = grayCanvas.getContext('2d');
grayCtx.drawImage(img, 0, 0);
const grayData = grayCtx.getImageData(0, 0, 200, 150);
for (let i = 0; i < grayData.data.length; i += 4) {
const avg = grayData.data[i] * 0.299 + grayData.data[i+1] * 0.587 + grayData.data[i+2] * 0.114;
grayData.data[i] = avg;
grayData.data[i+1] = avg;
grayData.data[i+2] = avg;
}
grayCtx.putImageData(grayData, 0, 0);
ctx.drawImage(grayCanvas, 200, 20, 160, 120);
ctx.fillStyle = '#8b949e';
ctx.fillText('灰度滤镜', 280, 155);
// === 反色滤镜 ===
const invertCanvas = document.createElement('canvas');
invertCanvas.width = 200;
invertCanvas.height = 150;
const invertCtx = invertCanvas.getContext('2d');
invertCtx.drawImage(img, 0, 0);
const invertData = invertCtx.getImageData(0, 0, 200, 150);
for (let i = 0; i < invertData.data.length; i += 4) {
invertData.data[i] = 255 - invertData.data[i];
invertData.data[i+1] = 255 - invertData.data[i+1];
invertData.data[i+2] = 255 - invertData.data[i+2];
}
invertCtx.putImageData(invertData, 0, 0);
ctx.drawImage(invertCanvas, 380, 20, 160, 120);
ctx.fillStyle = '#8b949e';
ctx.fillText('反色滤镜', 460, 155);
// === 模糊滤镜(使用 CSS filter) ===
ctx.save();
ctx.filter = 'blur(3px)';
ctx.drawImage(img, 560, 20, 160, 120);
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.fillText('模糊滤镜', 640, 155);
// === 亮度调节 ===
ctx.save();
ctx.filter = 'brightness(1.5)';
ctx.drawImage(img, 20, 180, 160, 120);
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.fillText('亮度 +50%', 100, 315);
// === 对比度调节 ===
ctx.save();
ctx.filter = 'contrast(2)';
ctx.drawImage(img, 200, 180, 160, 120);
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.fillText('对比度 x2', 280, 315);
// === 棕褐色滤镜 ===
ctx.save();
ctx.filter = 'sepia(1)';
ctx.drawImage(img, 380, 180, 160, 120);
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.fillText('棕褐色', 460, 315);
// === 水印效果 ===
ctx.drawImage(img, 560, 180, 160, 120);
// 添加水印
ctx.save();
ctx.globalAlpha = 0.3;
ctx.translate(640, 240);
ctx.rotate(-Math.PI / 6);
ctx.fillStyle = '#fff';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('WATERMARK', 0, 0);
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.font = '12px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.fillText('水印效果', 640, 315);
</script>
</body>
</html>五、浏览器兼容性
ctx.filter 兼容性
六、注意事项与最佳实践
1. 图像必须加载完成
代码示例
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
img.onerror = function() {
console.error('图像加载失败');
};
img.src = 'image.png';2. 跨域图像与 Canvas 污染
加载跨域图像后,Canvas 会被"污染",无法调用 getImageData() 或 toDataURL():
代码示例
// 设置 crossOrigin 以允许跨域
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://example.com/image.png';3. 图像缩放质量
缩放图像时可能会出现锯齿,可以使用 imageSmoothingEnabled 控制:
代码示例
// 启用平滑(默认)
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; // 'low' | 'medium' | 'high'
// 禁用平滑(像素风格)
ctx.imageSmoothingEnabled = false;4. 保持宽高比
缩放图像时应保持宽高比,避免变形:
代码示例
function drawImageProportional(ctx, img, dx, dy, maxWidth, maxHeight) {
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height);
const width = img.width * ratio;
const height = img.height * ratio;
const x = dx + (maxWidth - width) / 2;
const y = dy + (maxHeight - height) / 2;
ctx.drawImage(img, x, y, width, height);
}5. 使用离屏 Canvas 缓存
对于频繁绘制的图像,使用离屏 Canvas 缓存可以提高性能:
代码示例
const cache = document.createElement('canvas');
cache.width = img.width;
cache.height = img.height;
cache.getContext('2d').drawImage(img, 0, 0);
// 后续绘制使用缓存
ctx.drawImage(cache, x, y);6. 视频帧捕获
可以从视频元素捕获当前帧:
代码示例
const video = document.getElementById('myVideo');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);七、代码规范示例
代码示例
/**
* 图像绘制工具类
* 封装 drawImage 的常用操作
*/
class ImageRenderer {
constructor(ctx) {
this.ctx = ctx;
}
/**
* 加载图像
* @param {string} src - 图像 URL
* @returns {Promise<HTMLImageElement>}
*/
static loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`图像加载失败: ${src}`));
img.src = src;
});
}
/**
* 等比缩放绘制
* @param {HTMLImageElement} img - 图像
* @param {number} dx - 目标 X
* @param {number} dy - 目标 Y
* @param {number} maxWidth - 最大宽度
* @param {number} maxHeight - 最大高度
* @param {string} [align='center'] - 对齐方式
*/
drawProportional(img, dx, dy, maxWidth, maxHeight, align = 'center') {
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height);
const w = img.width * ratio;
const h = img.height * ratio;
let x = dx, y = dy;
if (align === 'center') {
x = dx + (maxWidth - w) / 2;
y = dy + (maxHeight - h) / 2;
} else if (align === 'right') {
x = dx + maxWidth - w;
}
this.ctx.drawImage(img, x, y, w, h);
}
/**
* 裁剪绘制(九宫格拉伸)
* @param {HTMLImageElement} img - 图像
* @param {Object} slice - 裁剪区域
* @param {Object} dest - 目标区域
*/
drawNineSlice(img, slice, dest) {
const { sx, sy, sw, sh } = slice;
const { dx, dy, dw, dh } = dest;
this.ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
}
/**
* 添加水印
* @param {string} text - 水印文字
* @param {Object} [options={}]
*/
addWatermark(text, options = {}) {
const {
color = 'rgba(255, 255, 255, 0.3)',
font = 'bold 24px Arial',
angle = -Math.PI / 6,
x = this.ctx.canvas.width / 2,
y = this.ctx.canvas.height / 2
} = options;
const ctx = this.ctx;
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillStyle = color;
ctx.font = font;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 0, 0);
ctx.restore();
}
/**
* 平铺图像
* @param {HTMLImageElement} img - 图像
* @param {number} x - 起始 X
* @param {number} y - 起始 Y
* @param {number} width - 区域宽度
* @param {number} height - 区域高度
*/
tileImage(img, x, y, width, height) {
const pattern = this.ctx.createPattern(img, 'repeat');
this.ctx.save();
this.ctx.beginPath();
this.ctx.rect(x, y, width, height);
this.ctx.clip();
this.ctx.fillStyle = pattern;
this.ctx.fillRect(x, y, width, height);
this.ctx.restore();
}
}八、常见问题与解决方案
常见问题
drawImage 不绘制怎么办?
原因:图像未加载完成。解决方案:确保在 onload 回调中调用 drawImage,并通过 onerror 捕获加载失败情况。
Canvas 被污染无法导出怎么办?
原因:加载了跨域图像且未设置 crossOrigin。解决方案:设置 img.crossOrigin = 'anonymous',并确保服务器支持 CORS。
图像缩放后模糊怎么处理?
原因:图像分辨率不够或缩放比例过大。解决方案:使用高分辨率源图,或调整 imageSmoothingQuality 为 'high'。
drawImage 参数顺序如何记忆?
记住口诀"先源后目标":裁剪绘制参数顺序为 ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh),先写源图像的裁剪区域,再写目标画布的绘制区域。
大量图像绘制性能差怎么优化?
解决方案:使用离屏 Canvas 缓存静态图像、按需加载、使用 createImageBitmap() 创建高性能位图,以及合理使用 requestAnimationFrame 进行批量绘制。
九、总结
Canvas 图像处理是构建丰富视觉应用的关键能力:
-
三种调用形式:基本绘制、缩放绘制、裁剪绘制
-
图像源:支持 Image、Canvas、Video、ImageBitmap
-
加载时机:必须等待图像加载完成后再绘制
-
跨域问题:设置
crossOrigin避免 Canvas 污染 -
缩放质量:通过
imageSmoothingEnabled和imageSmoothingQuality控制 -
等比缩放:保持宽高比避免图像变形
-
图像滤镜:通过像素操作或
ctx.filter实现各种效果 -
水印:使用
globalAlpha和rotate添加水印 -
性能优化:使用离屏 Canvas 缓存静态图像
本文涉及AI创作
内容由AI创作,请仔细甄别