绘制图像
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 | 源图像 | 你要画的图片、视频或画布对象。 |
| dx | Destination X | 图片左上角在画布上的 x 坐标。 |
| dy | Destination 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 | 源图像 | 不变。 |
| dx | Destination X | 图片左上角在画布上的 x 坐标。 |
| dy | Destination Y | 图片左上角在画布上的 y 坐标。 |
| dWidth | Destination Width | 控制绘制到画布的最终宽度。决定缩放。 |
| dHeight | Destination 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个参数就是控制这两步的细节。
- 从原图上切一块(裁切、取局部)
- 贴到画布上(定位、缩放)
语法:
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
| 参数 | 全称 | 作用 |
|---|---|---|
| image | 源图像 | 不变。 |
| sx | Source X | 从原图的哪个位置开始切的 x 坐标。 |
| sy | Source Y | 从原图的哪个位置开始切的 y 坐标。 |
| sWidth | Source Width | 从原图上切一块的宽度。 |
| sHeight | Source Height | 从原图上切一块的高度。 |
| dx | Destination X | 裁切好的图片,贴在画布的 x 坐标。 |
| dy | Destination Y | 裁切好的图片,贴在画布的 y 坐标。 |
| dWidth | Destination Width | 最终在画布上显示的宽度(缩放)。 |
| dHeight | Destination Height | 最终在画布上显示的高度(缩放)。 |
💡 原理解析: 这是一个“两步走”过程:
- 第一步(裁切):用
sx/sy/sWidth/sHeight在原图上框选一个区域,像用剪刀剪下来一块。 - 第二步(缩放+定位):把剪下来的这块,拉伸/缩放到
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、矩形、线条、文字)只能在裁剪区域内显示,超出部分直接被裁掉、不渲染
相当于:给画布盖了一个「镂空蒙版」,只漏镂空区域能画画。
这种方法更灵活,尤其适用于需要非矩形裁剪或复杂变换组合的场景。它通过改变画布的坐标系和设置裁剪路径来实现。
核心步骤:
- 保存状态 (
save): 在执行任何变换前,使用ctx.save()保存当前画布状态,以便后续恢复。 - 位移与缩放 (
translate&scale): 使用ctx.translate()移动坐标系原点,实现位移。使用ctx.scale()缩放整个坐标系,实现缩放。 - 创建裁剪路径 (
clip): 定义一个路径(如圆形、多边形),然后调用ctx.clip()。此后所有的绘制都将被限制在这个路径内。 - 绘制图像 (
drawImage): 使用基础的drawImage(img, dx, dy)形式绘制图像。由于坐标系已被变换,图像会自动被缩放和位移;由于设置了裁剪路径,图像会被裁剪。 - 恢复状态 (
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
❌ 后果:
- 画面强制拉伸/挤压,几何变形(人物压扁、物体变瘦变胖)
- 不光模糊,构图比例彻底失真,这是「形态失真」,比模糊更严重
4.一句话归纳
-
等比例缩小(800×400 → 400×200) 不变形、清晰度好,几乎不失真。
-
等比例放大(800×400 → 1600×800) 不变形,但必模糊、细节丢失、插值失真。
-
不按原比例乱缩放 既变形拉伸,还可能附带模糊,双重失真。
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;
imageSmoothingEnabled控制的是:缩放时是否开启「双线性插值」平滑- 双线性插值 = 让缩放边缘更柔和、不锯齿
- 但它只管「清晰度/平滑度」,不管「形状变形」
- 非等比例缩放 → 一定会拉伸变形,插值救不了形状!
Canvas 是怎么实现缩放的?
当你用 drawImage 把图片放大/缩小时,Canvas 本质就做一件事:
重新计算每一个像素的颜色
- 原图有一堆像素点
- 缩放后,目标尺寸变了
- 浏览器要计算新像素应该是什么颜色
这个计算过程,就由
imageSmoothingEnabled 控制:
= false 关闭平滑(最近邻采样)
- 不计算,直接复制最近的像素
- 放大 → 马赛克、方块、锐利
- 像素风游戏、像素图标用这个
= true 开启平滑(双线性插值)
- 对周围像素加权平均、混合颜色
- 放大 → 柔和、模糊、不锯齿
- 照片、图片用这个
双线性插值 =
拿周围 4 个像素的颜色,按距离加权混合,算出新像素颜色。
原图像素网格:
A ●────● B
│ ? │ ← 目标像素位置落在原图 4 个像素中间
C ●────● D
? 位置的颜色 = A、B、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
计算:
-
横向: 50 和 60 中间 → 55 90 和 100 中间 →95
-
纵向: 55 和 95 中间 → 75
最终结果
2×2 图 (1,1) 像素值 = 75
一句话总结计算逻辑
- 找到目标点在原图上的浮点数坐标
- 取周围最近的 4 个真实像素
- 先横向算中间值
- 再纵向算中间值
- 得到最终颜色
非等比例缩放,双线性插值有用吗?
有用,但只改善画质,不修复形状!
- 等比缩放:插值让图平滑、不锯齿
- 非等比缩放(拉伸/压扁):
- 形状一定会变形
- 插值只能让变形后的图边缘更柔和
- 不能把椭圆变回圆形
终极总结(超清晰)
- 双线性插值 = 用周围4个点加权算新点
- 放大:生成过渡色,变柔和
- 缩小:平均多个点,防锯齿
- 只管画质平滑,不管比例变形
6.高质量缩放
imageSmoothingQuality 和双线性插值(Bilinear Interpolation)并不是对立的概念,它们处于不同的层级。简单来说,双线性插值是“基础保底”的算法,而 imageSmoothingQuality: 'high' 是要求浏览器使用“更高级”的算法(如双三次插值)。
以下是它们在原理、画质和性能上的详细区别:
1. 核心定义的区别
-
双线性插值 (Bilinear)
- 地位:这是 Canvas 的默认标准。当你设置
ctx.imageSmoothingEnabled = true时,绝大多数浏览器默认就是使用双线性插值。 - 原理:计算新像素颜色时,只参考周围最近的 4个像素(2x2网格)。它通过简单的加权平均来计算颜色。
- 特点:速度极快,能消除锯齿,但会让图像变得略微模糊(因为它本质上是一种平滑/模糊处理)。
- 地位:这是 Canvas 的默认标准。当你设置
-
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>