绘制图像-clip方法

18 阅读5分钟

1.前言

绘制图像,之前我们介绍过drawImage方法

ctx.drawImage(img, 300, 20, 500, 460, 20, 20, 1000, 920);
参数含义坐标系
300, 20源图裁切起点相对于图片左上角 (0,0)
500, 460源图裁切大小相对于图片
20, 20画布绘制起点相对于画布左上角 (0,0)
1000, 920画布绘制大小相对于画布

9 参数版是"主动去源图取东西":从图片的 (300, 20) 位置开始,取 500x460 的区域,绘制到画布 (20, 20) 位置,缩放到 1000x920


2.Transform + Clip 版本详解

目标效果

transform + clip 实现 9 参数 drawImage 的等价效果:

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

// transform + clip 版本
ctx.save();
ctx.translate(1100, 20);
ctx.scale(2.0, 2.0);
ctx.rect(0, 0, 500, 460);
ctx.clip();
ctx.drawImage(img, -300, -20);
ctx.restore();

Transform + Clip 版本的核心思路

不是"去源图取东西",而是"把图片拉到窗口下面"

  1. clip() 在画布上开了一个"窗口",只能看到窗口下面的内容
  2. 通过 translate/scale 变换坐标系
  3. 负坐标把图片的 (300, 20) 区域"拉"到窗口下面

每一步的坐标系变化

初始状态

画布坐标系:原点在 (0, 0)

第 1 步:translate(1100, 20)

画布坐标系:原点移动到了 (1100, 20)
             现在的 (0, 0) 实际是画布的 (1100, 20)

第 2 步:scale(2.0, 2.0)

画布坐标系:单位长度放大 2 倍
             现在的 1 个单位 = 原来的 2 个像素
             现在的 (1, 1) 实际是画布的 (1100+2, 20+2) = (1102, 24)

第 3 步:rect(0, 0, 500, 460) + clip()

在**当前坐标系**中定义裁切区域:
- 起点:(0, 0) → 实际画布 (1100, 20)
- 大小:500x460 → 实际画布 1000x920(因为缩放了 2 倍)

所以 clip 区域实际是画布的 (1100, 20, 1000, 920)

第 4 步:drawImage(img, -300, -20)

**当前坐标系**中绘制图片:
- 图片的 (0, 0) 点画在 (-300, -20) 位置

换算成实际画布坐标:
- x: 1100 + (-300)*2 = 1100 - 600 = 500
- y: 20 + (-20)*2 = 20 - 40 = -20

关键理解:
- clip 区域在原点 (0, 0) 开始,大小 500x460(当前坐标系)
- 图片从 (-300, -20) 开始画
- 所以 clip 区域内显示的是图片的 (300, 20) 开始的部分

为什么要用负坐标?图解

clip 窗口(固定不动)

┌─────────────────────────┐
│                         │
│   (0,0)                 │  只能看到这个矩形内的东西
│   ┌─────────┐           │
│   │         │           │
│   │  可见   │           │
│   │  区域   │           │
│   │         │           │
│   └─────────┘           │
│                         │
└─────────────────────────┘

情况 1:drawImage(img, 0, 0) — 图片左上角对齐窗口左上角

┌─────────────────────────┐
│ img(0,0) 从这里开始画    │
│ ┌───────────────────────┤
│ │■■■■■可见■■■■■         │  看到的是图片的 (0,0) 区域
│ └───────────────────────┤
─────────────────────────┘

情况 2:drawImage(img, -300, -20) — 图片往左上角"拉"

┌─────────────────────────┐
│            img(0,0)      │  图片起点在窗口外面(左上)
│              ┌───────────┤
│              │■■■■■■■■■  │  窗口现在看到的是图片的
│              │■可见■     │  (300, 20) 区域!
│              │■■■■■■■■■  │
│              └───────────┤
└─────────────────────────┘

数学计算

clip 窗口:从 (0, 0) 开始,大小 500x460

drawImage(img, -300, -20) 的含义:
- 图片的 (0, 0) 点 画在 (-300, -20) 位置

那么 clip 窗口内的 (0, 0) 点,对应图片的哪个位置?
- 窗口 (0, 0) - 图片起点 (-300, -20) = 图片上的 (300, 20)

所以窗口内显示的就是图片的 (300, 20) 开始的区域!

对比总结

版本思路坐标
9 参数版从源图取:从图片 (300, 20) 开始取 500x460+300, +20(源图坐标)
transform 版把图拉过来:把图片往反方向拉,让 (300, 20) 对齐窗口-300, -20(绘制起点偏移)

关键理解

  • 9 参数版的 (300, 20)源图上的裁剪起点
  • transform 版的 (-300, -20)图片在画布上的绘制起点

两者方向相反,但效果等价!


为什么 clip 区域是 500x460,但显示是 1000x920?

ctx.scale(2.0, 2.0);       // 先放大 2 倍
ctx.rect(0, 0, 500, 460);  // 定义 500x460 的裁切区域
ctx.clip();

clip() 是在 scale() 之后调用的,所以:

  • rect(0, 0, 500, 460) 是在缩放后的坐标系中定义的
  • 换算成实际画布像素:500*2 = 1000, 460*2 = 920
  • 所以 clip 区域实际是 1000x920

完整的变换链

原图 (1080x495)
    │
    │ drawImage(img, -300, -20)
    │ → 图片的 (300, 20) 点对齐到 clip 窗口的 (0, 0)
    ▼
clip 窗口 (500x460 @ 缩放坐标系)
    │
    │ scale(2.0, 2.0)
    │ → 放大到 1000x920 @ 画布像素
    ▼
画布显示区域 (1100, 20, 1000, 920)

记忆口诀

9 参数:从源图"取"东西 → 正坐标 (300, 20)
transform:把图片"拉"过来 → 负坐标 (-300, -20)

源代码案例

<!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(){
            console.log('图片尺寸:', img.width, 'x', img.height);

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

            // 用绿框标出
            ctx.strokeStyle = 'lime';
            ctx.lineWidth = 5;
            ctx.strokeRect(20, 20, 1000, 920);

            // ---------------------
            // 二.transform + clip 版本
            ctx.save();

            // 1. 位移到目标位置
            ctx.translate(1100, 20);

            // 2. 缩放 2 倍
            ctx.scale(2.0, 2.0);

            // 3. 裁切 500x460 区域
            ctx.beginPath();
            ctx.rect(0, 0, 500, 460);
            ctx.clip();

            // 4. 绘图:关键!用负坐标让源图的 (300, 20) 对齐到 clip 区域的 (0, 0)
            ctx.drawImage(img, -300, -20);//就是drawImage3个参数版 👉 源图只做“定位” (位移)

            ctx.restore();

            // 红框标出
            ctx.beginPath();
            ctx.strokeStyle = 'red';
            ctx.lineWidth = 5;
            ctx.strokeRect(1100, 20, 1000, 920);
        }
    </script>
</body>
</html>