绘制图像-drawImage方法

1 阅读17分钟

绘制图像

ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) 是 Canvas 中功能最强大的图像绘制 API,完全可以同时实现 裁切 + 位移 + 缩放 三个核心功能,甚至能组合出更多效果。

我会用最通俗的语言拆解每个参数、实现原理,再给你直观示例。


一、参数详细说明

参数含义单位所属坐标系
image要绘制的图像源(HTMLImageElement / HTMLCanvasElement / HTMLVideoElement / ImageBitmap)--
sx源图像中裁切区域的左上角 X 坐标像素源图像自身的坐标系
sy源图像中裁切区域的左上角 Y 坐标像素源图像自身的坐标系
sWidth源图像中裁切区域的宽度(如果为负值,表现因浏览器而异,通常应使用正值)像素源图像自身的坐标系
sHeight源图像中裁切区域的高度像素源图像自身的坐标系
dx目标画布上绘制区域的左上角 X 坐标(位移的目标位置)像素目标 Canvas 坐标系
dy目标画布上绘制区域的左上角 Y 坐标像素目标 Canvas 坐标系
dWidth绘制到目标画布上的宽度(缩放后的宽度)像素目标 Canvas 坐标系
dHeight绘制到目标画布上的高度(缩放后的高度)像素目标 Canvas 坐标系
直观理解图示
源图像 (image)                         目标 Canvas
+-----------------+                    +-------------------------+
| (0,0)           |                    | (0,0)                   |
|                 |                    |                         |
|   +-(sx,sy)     |                    |     +-(dx,dy)           |
|   |裁切区域      |                    |     |绘制区域            |
|   | (sw,sh)     |                    |     | (dw,dh)           |
|   +-------------+                    |     +-----------------+   |
|                 |                    |                         |
+-----------------+                    +-------------------------+

二、先记住核心原理:两步“抠图+贴图”

drawImage 的本质就是:先从原图拿东西,再贴到目标位置

这个 API 按参数个数分成三档,层层递进地拆解。

第一档:3个参数版 👉 只做“定位” (位移)

这是最基础的用法,不裁切、不缩放,把原图原封不动挪到别处。

语法:

ctx.drawImage(image, dx, dy);
参数全称作用
image源图像你要画的图片、视频或画布对象。
dxDestination X图片左上角在画布上的 x 坐标。
dyDestination Y图片左上角在画布上的 y 坐标。

💡 原理解析: 相当于你有一张大贴纸,不剪也不拉伸,直接把它贴在桌子(画布)的 (dx, dy) 这个角落位置。

🚗 代码举例:

// 假设图片原始大小是 100px * 100px
const img = new Image();
img.src = 'car.png';

img.onload = () => {
  // 1. 不裁切,保持原图大小
  // 2. 把图片的左上角,放在画布的 (30, 30) 这个位置
  ctx.drawImage(img, 30, 30); 
};

效果: 图片完整出现在画布左上角向右下偏移 30px 的位置,尺寸没变。


第二档:5个参数版 👉 做“定位 + 缩放”

比上一档多了两个参数,可以在定位的同时,把图片放大或缩小。

语法:

ctx.drawImage(image, dx, dy, dWidth, dHeight);
参数作用与上一档的区别
image源图像不变。
dxDestination X图片左上角在画布上的 x 坐标。
dyDestination Y图片左上角在画布上的 y 坐标。
dWidthDestination Width控制绘制到画布的最终宽度。决定缩放。
dHeightDestination Height控制绘制到画布的最终高度。决定缩放。

💡 原理解析: 还是那张贴纸,但你可以指定贴上去时要把它拉伸到多大。如果 dWidth/dHeight 大于原图尺寸,就是放大;反之则是缩小。

🚗 代码举例:

// 假设图片原始大小是 100px * 100px
const img = new Image();
img.src = 'car.png';

img.onload = () => {
  // 1. 定位在 (30, 30)
  // 2. 强制绘制大小为 200px * 200px (放大一倍)
  ctx.drawImage(img, 30, 30, 200, 200); 
};

效果: 图片出现在 (30,30),但画面变得模糊且巨大,尺寸是原图的两倍。


第三档:9个参数版 👉 做“裁切 + 定位 + 缩放” (终极版)

整个过程一步完成,9个参数就是控制这两步的细节。

  1. 从原图上切一块(裁切、取局部)
  2. 贴到画布上(定位、缩放)

语法:

ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
参数全称作用
image源图像不变。
sxSource X从原图的哪个位置开始切的 x 坐标。
sySource Y从原图的哪个位置开始切的 y 坐标。
sWidthSource Width从原图上切一块的宽度。
sHeightSource Height从原图上切一块的高度。
dxDestination X裁切好的图片,贴在画布的 x 坐标。
dyDestination Y裁切好的图片,贴在画布的 y 坐标。
dWidthDestination Width最终在画布上显示的宽度(缩放)。
dHeightDestination Height最终在画布上显示的高度(缩放)。

💡 原理解析: 这是一个“两步走”过程:

  1. 第一步(裁切):用 sx/sy/sWidth/sHeight 在原图上框选一个区域,像用剪刀剪下来一块。
  2. 第二步(缩放+定位):把剪下来的这块,拉伸/缩放到 dWidth/dHeight 大小,然后贴到画布的 (dx, dy) 位置。

🚗 代码举例:

// 假设图片原始大小是 400px * 400px
const img = new Image();
img.src = 'car_sprite.png'; // 假设这是一张包含多帧动画的大图

img.onload = () => {
  // 1. 裁切:从原图 (100,100) 位置,切出一块 100px * 100px 的区域
  // 2. 定位:把这块贴在画布 (50, 50) 的位置
  // 3. 缩放:强制显示成 200px * 200px (放大)
  ctx.drawImage(img, 100, 100, 100, 100, 50, 50, 200, 200); 
};

效果: 从大图里截了一个小方块,放大后贴到了画布上。这就是游戏里角色动画帧的核心实现方式。

📊 终极总结:一张表看懂区别

版本核心功能关键参数差异典型场景
3参数仅位移只有 dx, dy,无尺寸控制简单背景图展示
5参数位移 + 缩放多了 dWidth, dHeight压缩图片、制作图标
9参数裁切 + 位移 + 缩放多了 sx, sy, sWidth, sHeight头像裁剪、精灵图动画、纹理贴图

三、底层实现原理

关键点:
  • 坐标映射:通过线性变换将目标 Canvas 上的每个点映射回源矩形中的对应点。这正是仿射变换(缩放+平移)的体现。
  • 插值:当缩放比例不是 1:1 时,需要计算非整数坐标的颜色。默认使用双线性插值(平滑),可以通过 ctx.imageSmoothingEnabled = false 关闭插值,得到像素风格。
  • 性能:该方法是经过高度优化的底层图元操作,通常比使用 translate + scale + clip 的组合更快(因为只需一次遍历像素,且避免了额外的变换矩阵开销)。
使用 clip() + 图形变换

clip() 原理:

  • 先用路径画一个形状区域(圆、多边形、自定义路径)
  • 调用 ctx.clip() 把画布绘制范围锁死在这个路径内部
  • 之后所有绘图(drawImage、矩形、线条、文字)只能在裁剪区域内显示,超出部分直接被裁掉、不渲染

相当于:给画布盖了一个「镂空蒙版」,只漏镂空区域能画画。

这种方法更灵活,尤其适用于需要非矩形裁剪或复杂变换组合的场景。它通过改变画布的坐标系和设置裁剪路径来实现。

核心步骤:

  1. 保存状态 (save): 在执行任何变换前,使用 ctx.save() 保存当前画布状态,以便后续恢复。
  2. 位移与缩放 (translate & scale): 使用 ctx.translate() 移动坐标系原点,实现位移。使用 ctx.scale() 缩放整个坐标系,实现缩放
  3. 创建裁剪路径 (clip): 定义一个路径(如圆形、多边形),然后调用 ctx.clip()。此后所有的绘制都将被限制在这个路径内。
  4. 绘制图像 (drawImage): 使用基础的 drawImage(img, dx, dy) 形式绘制图像。由于坐标系已被变换,图像会自动被缩放和位移;由于设置了裁剪路径,图像会被裁剪。
  5. 恢复状态 (restore): 使用 ctx.restore() 将画布恢复到 save() 时的状态,避免影响后续的绘制。

代码示例:

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();

img.src = 'your-image-path.jpg'; // 替换为你的图片路径
img.onload = () => {
  // 1. 保存当前画布状态
  ctx.save();

  // 2. 位移:将画布原点移动到 (100, 100)
  ctx.translate(100, 100);
  
  // 3. 缩放:将整个画布放大1.5倍
  ctx.scale(1.5, 1.5);

  // 4. 裁切:创建一个圆形裁剪路径
  ctx.beginPath();
  ctx.arc(0, 0, 75, 0, Math.PI * 2); // 在新原点(0,0)处画一个半径为75的圆
  ctx.clip(); // 应用裁剪

  // 5. 绘图:在 (-75, -75) 处绘制图片,使其中心对齐新原点
  // 注意:这里的坐标是基于变换后的坐标系
  ctx.drawImage(img, -75, -75);

  // 6. 恢复画布状态
  ctx.restore();
};

四、缩放会失真吗?

先算三种情况:原图 800×400

原图尺寸:宽800,高400
原始比例 = 2:1


1.情况1:800×400 → 缩放到 400×200

比例还是 2:1,等比例缩小
宽高都缩为原来 0.5倍

✅ 效果:

  • 不变形、不拉伸
  • Canvas 向下采样很干净
  • 基本不失真、清晰度很好

2.情况2:800×400 → 放大到 1600×800

比例依旧 2:1,等比例放大 2倍

⚠️ 效果重点:

  • 不会变形(比例对得上)
  • 但原图就那么多像素,强行放大属于插值补像素
  • 肉眼会:变模糊、发虚、细节糊化、有轻微马赛克感

总结: 等比例放大 = 不变形,但一定会变模糊失真等比例缩小 = 不变形,基本不失真


3.情况3:缩放不按原图比例(非等比缩放)

举例: 原图800×400(2:1) 硬画成比如:

  • 400 × 300
  • 500 × 200

❌ 后果:

  1. 画面强制拉伸/挤压,几何变形(人物压扁、物体变瘦变胖)
  2. 不光模糊,构图比例彻底失真,这是「形态失真」,比模糊更严重

4.一句话归纳

  1. 等比例缩小(800×400 → 400×200) 不变形、清晰度好,几乎不失真。

  2. 等比例放大(800×400 → 1600×800) 不变形,但必模糊、细节丢失、插值失真

  3. 不按原比例乱缩放变形拉伸,还可能附带模糊,双重失真。


5.配套写法示例

// 1. 等比缩小 800*400 → 400*200
ctx.drawImage(img, 0, 0, 400, 200);

// 2. 等比放大 800*400 → 1600*800
ctx.drawImage(img, 0, 0, 1600, 800);

// 3. 非等比(会拉伸变形)
ctx.drawImage(img, 0, 0, 400, 300);

6.补充小知识点

想放大时稍微锐化一点,可以控制图像平滑:

// 放大想锐利一点(像素风、图标可用)
ctx.imageSmoothingEnabled = false;

// 照片缩小想要柔和过渡 canvas默认值是 true
ctx.imageSmoothingEnabled = true;
  1. imageSmoothingEnabled 控制的是:缩放时是否开启「双线性插值」平滑
  2. 双线性插值 = 让缩放边缘更柔和、不锯齿
  3. 但它只管「清晰度/平滑度」,不管「形状变形」
  4. 非等比例缩放 → 一定会拉伸变形,插值救不了形状!

Canvas 是怎么实现缩放的?

当你用 drawImage 把图片放大/缩小时,Canvas 本质就做一件事:

重新计算每一个像素的颜色
  • 原图有一堆像素点
  • 缩放后,目标尺寸变了
  • 浏览器要计算新像素应该是什么颜色

这个计算过程,就由 imageSmoothingEnabled 控制:

= false 关闭平滑(最近邻采样)
  • 不计算,直接复制最近的像素
  • 放大 → 马赛克、方块、锐利
  • 像素风游戏、像素图标用这个
= true 开启平滑(双线性插值)
  • 对周围像素加权平均、混合颜色
  • 放大 → 柔和、模糊、不锯齿
  • 照片、图片用这个

双线性插值 =

拿周围 4 个像素的颜色,按距离加权混合,算出新像素颜色。

原图像素网格:
A ●────● B
  │  ?  │     ← 目标像素位置落在原图 4 个像素中间
C ●────● D

? 位置的颜色 = AB、C、D 的加权平均(按距离)
举例:

4×4 原图 → 放大到 8×8、缩小到 2×2 一步步算给你看!

双线性插值 = 先横向线性插值,再纵向线性插值,用周围4个点算新点。


1.先定义一个 4×4 像素小图(灰度值 0~255)

我们用纯数字表示像素亮度(0黑,255白):

(0,0)  (1,0)  (2,0)  (3,0)
  0      10     20     30

(0,1)  (1,1)  (2,1)  (3,1)
  40     50     60     70

(0,2)  (1,2)  (2,2)  (3,2)
  80     90    100    110

(0,3)  (1,3)  (2,3)  (3,3)
 120    130    140    150

这是4×4 原图,坐标是浮点数(0~3)。


2.场景1:4×4 → 放大到 8×8(2倍放大)

8×8 的坐标是 0~7,对应原图 0~3, 所以 目标坐标 ÷ 2 = 原图坐标

比如我们要计算 8×8 图里的 (1,0) 这个点:

  • 目标坐标 (1,0)
  • 对应原图坐标 (0.5, 0)

这个点不在原图像素上,在两个像素中间,双线性插值开始计算!

步骤1:找周围 4 个原图像素(Q11, Q12, Q21, Q22) 原图 (0.5, 0) 周围四个点是:

(0,0) = 0     (1,0) = 10
(0,1) = 40    (1,1) = 50

步骤2:先横向插值(x 方向) 在 y=0 这一行: 0 和 10 中间 0.5 的位置:

0 + (10 - 0) × 0.5 = 5

在 y=1 这一行: 40 和 50 中间 0.5 的位置:

40 + (50 - 40) × 0.5 = 45

步骤3:再纵向插值(y 方向) 上面算出 5 和 45,y=0.0 位置:

5 + (45 - 5) × 0 = 5

最终结果 8×8 图 (1,0) 像素值 = 5


3.场景2:4×4 → 缩小到 2×2(0.5倍缩小)

2×2 坐标 01,对应原图 03, 所以 目标坐标 ×2 = 原图坐标

比如计算 2×2 图里的 (0,0)

  • 对应原图 (0,0) → 直接取 0

计算 2×2 图里的 (1,1)

  • 对应原图 (1.5, 1.5)

周围4个像素:

(1,1)=50    (2,1)=60
(1,2)=90   (2,2)=100

计算:

  1. 横向: 50 和 60 中间 → 55 90 和 100 中间 →95

  2. 纵向: 55 和 95 中间 → 75

最终结果

2×2 图 (1,1) 像素值 = 75


一句话总结计算逻辑
  1. 找到目标点在原图上的浮点数坐标
  2. 取周围最近的 4 个真实像素
  3. 先横向算中间值
  4. 再纵向算中间值
  5. 得到最终颜色

非等比例缩放,双线性插值有用吗?

有用,但只改善画质,不修复形状!

  • 等比缩放:插值让图平滑、不锯齿
  • 非等比缩放(拉伸/压扁):
    • 形状一定会变形
    • 插值只能让变形后的图边缘更柔和
    • 不能把椭圆变回圆形

终极总结(超清晰)
  1. 双线性插值 = 用周围4个点加权算新点
  2. 放大:生成过渡色,变柔和
  3. 缩小:平均多个点,防锯齿
  4. 只管画质平滑,不管比例变形

6.高质量缩放

imageSmoothingQuality 和双线性插值(Bilinear Interpolation)并不是对立的概念,它们处于不同的层级。简单来说,双线性插值是“基础保底”的算法,而 imageSmoothingQuality: 'high' 是要求浏览器使用“更高级”的算法(如双三次插值)。

以下是它们在原理、画质和性能上的详细区别:

1. 核心定义的区别
  • 双线性插值 (Bilinear)

    • 地位:这是 Canvas 的默认标准。当你设置 ctx.imageSmoothingEnabled = true 时,绝大多数浏览器默认就是使用双线性插值。
    • 原理:计算新像素颜色时,只参考周围最近的 4个像素(2x2网格)。它通过简单的加权平均来计算颜色。
    • 特点:速度极快,能消除锯齿,但会让图像变得略微模糊(因为它本质上是一种平滑/模糊处理)。
  • imageSmoothingQuality = 'high'

    • 地位:这是一个质量提示。它是 Canvas API 提供的一个属性,用来告诉浏览器:“我不介意多花点时间,请给我最好的画质”。
    • 原理:当设置为 'high' 时,浏览器通常会切换到更复杂的算法,最常见的是双三次插值(Bicubic)。这种算法在计算时会参考周围 16个像素(4x4网格),甚至更多。
    • 特点:计算量大,速度慢,但能更好地保留图像的边缘细节和纹理,减少模糊感。
2. 视觉效果对比
特性双线性插值 (默认)imageSmoothingQuality = 'high' (通常指双三次插值)
采样范围4个像素 (2x2)16个像素 (4x4) 或更多
放大效果图像会变得柔和、模糊,丢失部分锐度。图像更加清晰、锐利,边缘过渡自然。
缩小效果容易产生摩尔纹或细节粘连。细节保留更好,噪点更少。
适用场景实时视频流、游戏背景、快速预览。照片编辑工具、电商商品图展示、打印输出。
3. 代码演示

如果你想强制使用高质量缩放,需要同时开启平滑并设置质量为 'high':

const ctx = canvas.getContext('2d');

// 1. 必须开启平滑,否则下面的设置无效(会变成最近邻插值,全是锯齿)
ctx.imageSmoothingEnabled = true;

// 2. 设置为高质量
// 注意:这只是一个提示,具体效果取决于浏览器的实现(Chrome/Safari/Edge 支持较好)
ctx.imageSmoothingQuality = 'high'; 

// 3. 执行绘制
ctx.drawImage(img, 0, 0, 400, 200);
4. 总结建议
  • 如果你只是在做一个简单的图表或者对性能极其敏感(比如每秒刷新几十次的动画),默认的双线性插值(即不设置 imageSmoothingQuality)就足够了,它的性价比最高。
  • 如果你是在做一个图片编辑器,或者展示精美的商品图片(如你之前提到的 800x400 缩略图),强烈建议加上 ctx.imageSmoothingQuality = 'high'。虽然会多消耗几毫秒的渲染时间,但用户看到的图片质感会有明显提升,不会显得“灰蒙蒙”的。

源码案例

drawImage绘制案例
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

    </style>
</head>
<body>
    <canvas id="myCanvas"></canvas>
    <script>
        var canvas = document.getElementById("myCanvas");
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        var ctx = canvas.getContext("2d");
        var img = new Image();
        img.src = "./imgs/月之暗面.png";
        img.onload = function(){
            // 0.原图完整绘制
            // ctx.drawImage(img); 
            // 第一档:3个参数版 👉 只做“定位” (位移)
            // 1. 不裁切,保持原图大小
            // 2. 把图片的左上角,放在画布的 (30, 30) 这个位置
            // ctx.drawImage(img, 30, 30); 
            // 第二档:5个参数版 👉 做“定位 + 缩放”
            // 1. 定位在 (30, 30)
            // 2. 强制绘制大小为 540px * 242.5px (缩小一倍)
            // ctx.imageSmoothingQuality = 'high';
            // ctx.drawImage(img, 30, 30, 540, 242.5); 
            // 第三档:9个参数版 👉 做“裁切 + 定位 + 缩放” (终极版)
            // 1. 裁切:从原图 (300, 20) 位置,切出一块 500px * 460px 的区域
            // 2. 定位:把这块贴在画布 (20, 20) 的位置
            // 3. 缩放:强制显示成 1000px * 920px (放大)
            // 验证平滑相关属性
            // 4. 必须开启平滑,否则下面的设置无效(会变成最近邻插值,全是锯齿)
            // ctx.imageSmoothingEnabled = true;
            // 5. 设置为高质量
            // 注意:这只是一个提示,具体效果取决于浏览器的实现(Chrome/Safari/Edge 支持较好)
            ctx.imageSmoothingQuality = 'high'; 
            ctx.drawImage(img, 300, 20, 500, 460, 20, 20, 1000, 920); 
        }
    </script>
</body>
</html>
离屏 canvas绘制案例
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas"></canvas>
    <script>
        var canvas = document.getElementById("myCanvas");
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        var ctx = canvas.getContext("2d");
        var img = new Image();
        img.src = "./imgs/月之暗面.png";
        img.onload = function(){
            // 第三档:9 个参数版 👉 做"裁切 + 定位 + 缩放" (终极版)
            // 1. 裁切:从原图 (300, 20) 位置,切出一块 500px * 460px 的区域
            // 2. 定位:把这块贴在画布 (20, 20) 的位置
            // 3. 缩放:强制显示成 1000px * 920px (放大 2 倍)
            ctx.imageSmoothingEnabled = true;
            ctx.imageSmoothingQuality = 'high';

            // 9 参数原版(用于对比)
            // ctx.drawImage(img, 300, 20, 500, 460, 20, 20, 1000, 920);

            // ---------------------
            // 用 transform + 离屏 canvas 实现 9 参数 drawImage 效果
            // 步骤 1: 创建离屏 canvas,用于裁切源图
            var offCanvas = document.createElement('canvas');
            var offCtx = offCanvas.getContext('2d');
            offCanvas.width = 500;
            offCanvas.height = 460;

            // 步骤 2: 在离屏上裁切并绘制源图
            offCtx.drawImage(img, 300, 20, 500, 460, 0, 0, 500, 460);

            // 步骤 3: 保存主画布状态并设置变换
            ctx.save();

            // 步骤 4: 位移到目标位置 (20, 20)
            ctx.translate(20, 20);

            // 步骤 5: 缩放到目标大小 (2 倍)
            ctx.scale(2.0, 2.0);

            // 步骤 6: 绘制离屏上的裁切图像
            ctx.drawImage(offCanvas, 0, 0);

            ctx.restore();
        }
    </script>
</body>
</html>