Canvas图像处理
原图
1. 图像滤镜效果
灰度化
function grayscaleImage(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // red
data[i + 1] = avg; // green
data[i + 2] = avg; // blue
}
return imageData;
}
// 应用灰度效果
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const grayImageData = grayscaleImage(imageData);
ctx.putImageData(grayImageData, 0, 0);
灰度化
老照片
function getHue(imageData) {
const data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
const r = data[i + 0]
const g = data[i + 1]
const b = data[i + 2]
data[i + 0] = r * 0.28 + g * 0.72 + b * 0.22
data[i + 1] = r * 0.25 + g * 0.63 + b * 0.13
data[i + 2] = r * 0.17 + g * 0.66 + b * 0.13
}
return imageData;
}
// 应用老照片效果
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const grayImageData = getHue(imageData);
ctx.putImageData(grayImageData, 0, 0);
老照片
模糊化
模糊化通常涉及更复杂的像素操作,可以使用Web Workers进行后台处理,避免阻塞UI线程。这里只提供简单的模糊概念代码片段。
function getBlur(imageData) {
const data = imageData.data;
let width = imageData.width;
let height = imageData.height;
const gaussMatrix = [];
let gaussSum = 0;
let x = 0;
let y = 0;
let i = 0;
let j = 0;
let k = 0;
let len = 0;
const radius = 10;
const sigma = 8;
let r = 0;
let g = 0;
let b = -1 / (2 * sigma * sigma);
let a = 1 / (Math.sqrt(2 * Math.PI) * sigma);
// 生成高斯矩阵
for (i = 0, x = -radius; x <= radius; x++, i++) {
g = a * Math.exp(b * x * x);
gaussMatrix[i] = g;
gaussSum += g;
}
// 归一化, 保证高斯矩阵的值在[0,1]之间
for (i = 0, len = gaussMatrix.length; i < len; i++) {
gaussMatrix[i] /= gaussSum;
}
// x 方向一维高斯运算
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
r = g = b = a = 0;
gaussSum = 0;
for (j = -radius; j <= radius; j++) {
k = x + j;
if (k >= 0 && k < width) {
// 确保 k 没超出 x 的范围
// r,g,b,a 四个一组
i = (y * width + k) * 4;
r += data[i] * gaussMatrix[j + radius];
g += data[i + 1] * gaussMatrix[j + radius];
b += data[i + 2] * gaussMatrix[j + radius];
gaussSum += gaussMatrix[j + radius];
}
}
i = (y * width + x) * 4;
// 除以 gaussSum 是为了消除处于边缘的像素, 高斯运算不足的问题
// console.log(gaussSum)
data[i] = r / gaussSum;
data[i + 1] = g / gaussSum;
data[i + 2] = b / gaussSum;
}
}
// y 方向一维高斯运算
for (x = 0; x < width; x++) {
for (y = 0; y < height; y++) {
r = g = b = a = 0;
gaussSum = 0;
for (j = -radius; j <= radius; j++) {
k = y + j;
if (k >= 0 && k < height) {
// 确保 k 没超出 y 的范围
i = (k * width + x) * 4;
r += data[i] * gaussMatrix[j + radius];
g += data[i + 1] * gaussMatrix[j + radius];
b += data[i + 2] * gaussMatrix[j + radius];
gaussSum += gaussMatrix[j + radius];
}
}
i = (y * width + x) * 4;
data[i] = r / gaussSum;
data[i + 1] = g / gaussSum;
data[i + 2] = b / gaussSum;
}
}
return imageData;
}
// 应用老照片效果
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const grayImageData = getBlur(imageData);
ctx.putImageData(grayImageData, 0, 0);
模糊化
马赛克效果
function getCodeFeel(imageData) {
const data = imageData.data;
let width = imageData.width;
let height = imageData.height;
const blur = 4; // 马赛克块宽度
const blurR = 2 * blur + 1;
const total = blurR * blurR;
for (let i = blur; i <= width; i = i + blurR) {
for (let j = blur; j <= height; j = j + blurR) {
let r = 0;
let g = 0;
let b = 0;
for (let leny = -blur; leny <= blur; leny++) {
for (let lenx = -blur; lenx <= blur; lenx++) {
r += data[4 * ((j + leny) * width + i + lenx) + 0];
g += data[4 * ((j + leny) * width + i + lenx) + 1];
b += data[4 * ((j + leny) * width + i + lenx) + 2];
}
}
let vr = r / total;
let vg = g / total;
let vb = b / total;
for (let leny = -blur; leny <= blur; leny++) {
for (let lenx = -blur; lenx <= blur; lenx++) {
data[4 * ((j + leny) * width + i + lenx) + 0] = vr;
data[4 * ((j + leny) * width + i + lenx) + 1] = vg;
data[4 * ((j + leny) * width + i + lenx) + 2] = vb;
}
}
}
}
return imageData;
}
// 应用马赛克效果
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const grayImageData = getCodeFeel(imageData);
ctx.putImageData(grayImageData, 0, 0);
马赛克
反转
function invertImage(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // red
data[i + 1] = 255 - data[i + 1]; // green
data[i + 2] = 255 - data[i + 2]; // blue
}
return imageData;
}
// 应用反转效果
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const invertedImageData = invertImage(imageData);
ctx.putImageData(invertedImageData, 0, 0);
2. 图像合成
Canvas 2D API 的 CanvasRenderingContext2D.globalCompositeOperation 属性设置要在绘制新形状时应用的合成操作的类型。通过改变globalCompositeOperation属性,可以实现各种图像合成效果。
这是默认设置,并在现有画布上绘制新图形。
仅在新形状和目标画布重叠的地方绘制新形状。其他的都是透明的。
在不与现有画布内容重叠的地方绘制新图形。
只在与现有画布内容重叠的地方绘制新图形。
在现有画布内容的后面绘制新的图形。
仅保留现有画布内容和新形状重叠的部分。其他的都是透明的。
仅保留现有画布内容和新形状不重叠的部分。
仅保留现有画布内容和新形状重叠的部分。新形状是在现有画布内容的后面绘制的。
两个重叠图形的颜色是通过颜色值相加来确定的。
只显示新图形。
形状在重叠处变为透明,并在其他地方正常绘制。
将顶层像素与底层相应像素相乘,结果是一幅更黑暗的图片。
像素被倒转、相乘、再倒转,结果是一幅更明亮的图片(与 ****multiply ****相反)。
multiply ****和 ****screen ****的结合。原本暗的地方更暗,原本亮的地方更亮。
保留两个图层中最暗的像素。
保留两个图层中最亮的像素。
将底层除以顶层的反置。
将反置的底层除以顶层,然后将结果反过来。
类似于 ****overlay , multiply ****和 ****screen ****的结合——但上下图层互换了。
柔和版本的 ****hard-light 。纯黑或纯白不会导致纯黑或纯白。
从顶层减去底层(或反之亦然),始终得到正值。
与 ****difference ****类似,但对比度较低。
保留底层的亮度(luma)和色度(chroma),同时采用顶层的色调(hue)。
保留底层的亮度和色调,同时采用顶层的色度。
保留了底层的亮度,同时采用了顶层的色调和色度。
保持底层的色调和色度,同时采用顶层的亮度。
ctx.globalCompositeOperation = 'multiply'; // 将顶层像素与底层相应像素相乘
ctx.drawImage(img2, 0, 0); // img2
与画布上的现有内容以乘法模式合成。
multiply-合成后
lighter-合成后
overlay-合成后
优化与性能提升
图像资源管理
const imageCache = {}; // 图像缓存对象
function loadImage(src, callback) {
if (imageCache[src]) {
callback(imageCache[src]);
return;
}
const img = new Image();
img.onload = function() {
imageCache[src] = img; // 缓存图像
callback(img);
};
img.src = src;
}
Canvas性能优化技巧
- 尽量减少重绘次数,通过offscreenCanvas进行离屏绘制,再一次性绘制到主Canvas上。
- 合并绘制操作,如使用beginPath和closePath来减少路径绘制的次数。
- 避免在循环中频繁调用绘图方法,而是预先计算好所有需要的值,再一次性绘制。
移动端适配
// 响应式设计
const canvas = document.getElementById('myCanvas');
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
// 触控事件处理
canvas.addEventListener('touchstart', handleTouchStart, false);
canvas.addEventListener('touchmove', handleTouchMove, false);