pin_drop当前位置:知识文库 ❯ 图文

多媒体:Canvas图像处理 - 完整教程与代码示例

一、教程简介

Canvas 的 drawImage() 方法是图像处理的核心,它可以将图像、Canvas 或视频帧绘制到画布上,并支持缩放、裁剪等变换操作。通过 drawImage() 结合其他 Canvas API,可以实现图像滤镜、水印、截图等功能。本教程将详细介绍 drawImage() 的三种调用形式、图像缩放与裁剪,以及常见的图像处理技巧。


二、核心概念

drawImage 的三种调用形式

形式 语法 说明
基本绘制 drawImage(image, dx, dy) 原始尺寸绘制到指定位置
缩放绘制 drawImage(image, dx, dy, dWidth, dHeight) 缩放到指定尺寸绘制
裁剪绘制 drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 从源图像裁剪后缩放绘制

图像源类型

drawImage() 接受以下类型的图像源:

类型 说明
HTMLImageElement <img> 元素或 new Image()
HTMLCanvasElement 另一个 Canvas 元素
HTMLVideoElement <video> 元素的当前帧
ImageBitmap 高性能的位图对象

裁剪与缩放的关系

代码示例

源图像 (sx, sy, sWidth, sHeight)
╭──────────────────────╮
│     ╭────────╮       │
│     │ 裁剪区 │       │
│     ╰────────╯       │
╰──────────────────────╯
          ↓
目标画布 (dx, dy, dWidth, dHeight)
╭──────────────╮
│   缩放绘制   │
╰──────────────╯

三、语法与用法

参数说明

基本绘制参数

参数 类型 说明
image ImageSource 图像源
dx number 目标 X 坐标
dy number 目标 Y 坐标

缩放绘制参数

参数 类型 说明
dWidth number 目标宽度
dHeight number 目标高度

裁剪绘制参数

参数 类型 说明
sx number 源裁剪起始 X
sy number 源裁剪起始 Y
sWidth number 源裁剪宽度
sHeight number 源裁剪高度
dx number 目标 X 坐标
dy number 目标 Y 坐标
dWidth number 目标宽度
dHeight number 目标高度

图像加载方法

代码示例

// 方法一: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>

五、浏览器兼容性

浏览器 支持版本 备注
Chrome 1+ 完全支持
Firefox 1.5+ 完全支持
Safari 2+ 完全支持
Edge 12+ 完全支持
IE 9+ 基本支持,不支持 SVG 图像源

ctx.filter 兼容性

浏览器 支持版本
Chrome 52+
Firefox 49+
Safari 不支持
Edge 79+

六、注意事项与最佳实践

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 污染

  • 缩放质量:通过 imageSmoothingEnabledimageSmoothingQuality 控制

  • 等比缩放:保持宽高比避免图像变形

  • 图像滤镜:通过像素操作或 ctx.filter 实现各种效果

  • 水印:使用 globalAlpharotate 添加水印

  • 性能优化:使用离屏 Canvas 缓存静态图像

标签: Canvas图像 drawImage 图像缩放 图像裁剪 图像滤镜 水印 跨域

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 多媒体:Canvas阴影 - 完整教程与代码示例 下一篇: 多媒体:Canvas变换 - 完整教程与代码示例

poll相关推荐