Skip to main content

Canvas 图像绘制

一、图像绘制

drawImage() 用于图像绘制,有三种调用方式:

  • drawImage(image, dx, dy)
  • drawImage(image, dx, dy, dWidth, dHeight)
  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

参数:

  • image:图像源,例如:HTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmapOffscreenCanvasVideoFrame
  • sx(可选):开始剪切的 x 坐标位置;
  • sy 可选:开始剪切的 y 坐标位置;
  • sWidth:可选,被剪切图像的宽度;
  • sHeight:可选,被剪切图像的高度;
  • dx:在画布上放置图像的 X 轴坐标;
  • dy:在画布上放置图像的 Y 轴坐标;
  • dWidth:所绘制图像的宽度(伸展或缩小图像)
  • dHeight:所绘制图像的高度(伸展或缩小图像)

示例:

// HTML
<canvas id="canvas" width="200" height="150" style="border: 1px dashed gray;"></canvas>
<img id="image" src="./image.png" alt="image" />

// JS
const cvs = document.getElementById('canvas')
const ctx = cvs.getContext('2d')
const image = document.getElementById("image");

image.addEventListener('load', (e) => {
// 将图像绘制到画布的 (50, 0) 坐标处
ctx.beginPath();
ctx.drawImage(image, 50, 0);

// 将图像绘制到画布的 (50, 60) 坐标处
// 并将其缩放为宽 100、高 30 的图像
ctx.beginPath();
ctx.drawImage(image, 50, 60, 100, 30);

// 原图像从坐标 (10,20) 处
// 截取一个宽为 36 高为 37 的图像
// 并将其绘制到画布的 (50, 100) 坐标处
// 再将其缩放为宽 51、高 52 的图像
ctx.beginPath();
ctx.drawImage(image, 10, 20, 36, 37, 50, 100, 51, 52);
});

效果如下:

二、图像平铺

ctx.createPattern(image, repetition) 用于在指定的方向内重复图像。

参数:

  • image:图片、画布或视频元素;
  • repetition:指定如何重复图像,可选值如下:
    • repeat(默认):在水平和垂直方向重复;
    • repeat-x:只在水平方向重复;
    • repeat-y:只在垂直方向重复;
    • no-repeat:只显示一次(不重复)

示例:

image.addEventListener('load', (e) => {
const pattern = ctx.createPattern(image, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 200, 150);
});

效果如下:

三、像素级图像应用

1、getImageData 方法

使用 CanvasRenderingContext2D 提供的 getImageData 来获取获取 Canvas 画布的设定区域的 ImageData 对象,从而拿到图片像素数据。

ctx.getImageData(x, y, width, height);

参数:

  • x:将要被提取的图像数据矩形区域的左上角 x 坐标。
  • y:将要被提取的图像数据矩形区域的左上角 y 坐标。
  • width:将要被提取的图像数据矩形区域的宽度。
  • height:将要被提取的图像数据矩形区域的高度。

返回值:一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据。

1-1、基本用法

// HTML
<canvas id="canvas" width="200" height="150" style="border: 1px dashed gray;"></canvas>
<img id="image" src="./image.png" alt="image" />

// JS
const cvs = document.getElementById('canvas')
const ctx = cvs.getContext('2d')
const image = document.getElementById("image");

image.addEventListener('load', (e) => {
ctx.drawImage(image, 50, 0);
console.log(ctx.getImageData(0, 0, 200, 150))
});

运行控制台可能会报错:

这是由于 getImageData() 需要以服务器形式启动才能使用(跨域限制),以文件形式打开会报错,点击查看解决 canvas 图片 getImageData、toDataURL 跨域问题

如果是用 VSCode,可以安装 Live Server 插件,然后右键运行 Open with Live Server 即可。

运行正常后控制台输出:

其中,

  • width、height:表示所选区域的宽高;
  • data:保存了区域的像素数据数组,数组取值如 [r1, g1, b1, a1, r2, g2, b2, a2],每四个数存储着 1 个像素的 rgba 颜色值,data.length 表示图片像素总量。

1-2、实现简易取色器

// HTML
<canvas id="canvas" width="200" height="150" style="border: 1px dashed gray;"></canvas>
<img id="image" src="./image.png" alt="image" />

// JS
const cvs = document.getElementById('canvas')
const ctx = cvs.getContext('2d')
const image = document.getElementById("image");

const getPixelColor = (e) => {
const pixel = ctx.getImageData(e.offsetX, e.offsetY, 1, 1).data;
const rgba = `rgba(${pixel[0]}, ${pixel[1]}, ${pixel[2]}, ${pixel[3] / 255})`;
console.log(rgba);
}

image.addEventListener('load', (e) => {
ctx.drawImage(image, 50, 0);
cvs.addEventListener("mousemove", getPixelColor);
});

鼠标悬浮画布时控制台会打印指针指向位置的颜色值。

2、putImageData 方法

在获取到图片的像素数据后,就可以对获取的像素数据进行处理,比如进行灰度化或反色处理。完成处理后,可以使用 CanvasRenderingContext2D 提供的另一个 API —— putImageData 在页面上显示处理效果。

ctx.putImageData(imagedata, dx, dy);
ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);

参数:

  • imageData:包含图片像素信息的 ImageData 数组对象,用于替换当前 Canvas 画布上的 ImageData。
  • dx:目标 Canvas 中被图像数据替换的起点横坐标。
  • dy:目标 Canvas 中被图像数据替换的起点纵坐标。
  • dirtyX(可选):图像数据渲染区域的左上角横坐标,默认值为 0。
  • dirtyY(可选):图像数据渲染区域的左上角纵坐标,默认值为 0。
  • dirtyWidth(可选):图像数据渲染区域的宽度,默认为 imagedata 图像的宽度。
  • dirtyHeight(可选):图像数据渲染区域的高度,默认为 imagedata 图像的高度。

2-1、实现图片灰度化

点击查看利用 getImageData 和 putImageData 实现图片灰度化

四、图像保存

可通过 toDataURL 实现图像压缩及保存,点击查看详情

五、Canvas 图像优化

  • 避免浮点数的坐标点,用整数取而代之:
// ❌
ctx.drawImage(myImage, 0.3, 0.5);
// ✅
ctx.drawIamge(myImage, Math.floor(0.3), Math.floor(0.5));
  • 不要在用 drawImage 时缩放图像,可以缓存图片的不同尺寸;
  • 使用多层画布去画一个复杂的场景,比如静态背景和频繁发生移动的对象;
  • 用 CSS 设置大的背景图,避免在每一帧在画布上反复地绘制大图;
  • 用 CSS transform 特性缩放画布以开启硬件加速。当然最好是不直接缩放画布,或者让小画布按比例放大,而非大画布按比例缩小;
  • 关闭透明度:
const ctx = canvas.getContext("2d", { alpha: false });
  • 将画布的函数调用集合到一起(例如,画一条折线,而不要画多条分开的直线)
  • 避免不必要的画布状态改变;
  • 渲染画布中的不同点,而非整个新状态;
  • 尽可能避免 shadowBlur 特性;
  • 尽可能避免 text rendering
  • 不同的场景可以选择不同的清除画布的方法:clearRect()fillRect()、调整 canvas 大小;
  • 使用 window.requestAnimationFrame() 而非 window.setInterval()
  • 谨慎使用大型物理库。