前言
公众号:【可乐前端】,期待关注交换,分享一些有意思的前端常识
大家好,我是练习时长一坤年的前端练习生,之前公布了一篇打造图像编辑器(一)——基础架构与图像滤镜介绍了图像编辑器的基础架构和根底的图像滤镜,咱们明天接着来介绍一些更有意思的图像滤镜。
体验地址
Canvas
操作像素
明天咱们要实现的滤镜成果,都是通过操作每一个像素点,利用一些非凡的变换去实现的。首先咱们先来看看在 canvas
中如何操作像素点。
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
console.log("data", data);
通过 getImageData
这个 API
能够拿到画布对应的像素数据,data
打印进去长成上面的这个样子。
data
是一个一维数组,蕴含了每个像素的色彩信息。数组中的每四个元素示意一个像素的红、绿、蓝和透明度值(RGBA
格局),取值范畴是 0
到255
。因而,数组的长度是 width * height * 4
。也就是说咱们在操作像素点的时候个别会写出上面这样的代码:
for (let i = 0; i < data.length; i += 4) {const red = data[i];
const green = data[i+1];
const blue = data[i+2];
const alpha = data[i+3];
}
下面用一个 for
循环去遍历 data
这个一维数组,步进长度是 4
,其中红绿蓝透明度就别离对应区间内的第0
项、第 1
项、第 2
项和第 3
项。拿到一个个像素点的信息之后,咱们就能够开始对每一个像素点进行变换操作:
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 255;
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
操作完每一个像素点之后,通过 putImageData
把变换完的像素操作作用在画布上。
滤镜汇合
介绍完了如何操作像素点之后,上面来介绍每一个滤镜的具体实现,明天实现的滤镜包含如下几个:
- 黑白滤镜
- 灰度滤镜
- 念旧滤镜
- 连环画滤镜
- 冷 / 暖色调滤镜
- 油画滤镜
- 素描滤镜
- 水彩画滤镜
- 马赛克滤镜
- 含糊滤镜(高斯含糊)
黑白滤镜
要实现黑白滤镜,咱们能够依照以下的步骤去操作:
- 获取画布的图像区域并提取图像的像素信息
- 设置阈值,用于判断像素是否应该被转换为彩色或红色
- 计算以后像素的灰度值,将红、绿、蓝通道的平均值作为灰度值
- 依据灰度值与阈值的比拟,将像素设置为彩色(
0
)或红色(255
) - 把变更作用到画布上
const blackWhite = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const threshold = 128;
for (let i = 0; i < data.length; i += 4) {const gray = (data[i] + data[i + 1] + data[i + 2]) / 3;
const binaryValue = gray < threshold ? 0 : 255;
data[i] = binaryValue;
data[i + 1] = binaryValue;
data[i + 2] = binaryValue;
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
灰度滤镜
灰度值是示意色彩亮度的繁多值,通常用于将彩色图像转换为黑白或灰度图像。在彩色图像中,每个像素由红(
R
)、绿(G
)、蓝(B
)三个通道的色彩值组成。灰度值的计算方法能够通过对这三个色彩通道的值进行加权均匀失去。在灰度图像中,每个像素仅有一个繁多的灰度值,示意该像素的亮度或强度。这个灰度值通常在
0
(彩色)到255
(红色)的范畴内,示意从最暗到最亮的不同亮度级别。灰度成果是指通过将彩色图像中的色彩信息转换为灰度值,从而呈现出黑白或灰度的视觉效果。这种转换有助于简化图像,突出亮度和暗度的变动,使人们更关注图像的亮度和构造,而不受到色彩的烦扰。
实现灰度滤镜,能够依照以下的步骤去实现:
- 遍历每个像素的
RGBA
值,加权均匀算出灰度值 - 将灰度值作用在红绿蓝通道上
const gray = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {const red = data[i];
const green = data[i + 1];
const blue = data[i + 2];
const gray = 0.299 * red + 0.587 * green + 0.114 * blue;
data[i] = gray;
data[i + 1] = gray;
data[i + 2] = gray;
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
念旧滤镜
实现念旧滤镜的形式也大差不差,外围的片段是:
const newRed = 0.393 * r + 0.769 * g + 0.189 * b;
const newGreen = 0.349 * r + 0.686 * g + 0.168 * b;
const newBlue = 0.272 * r + 0.534 * g + 0.131 * b;
依据旧的 rgb
值通过一个色彩矩阵变换,实现念旧滤镜的成果。这个色彩矩阵产生一种较为暖色调的成果,通过应用这样的矩阵,能够削弱原始图像的娇艳度,加深暖色调,从而使图像看起来更像是以前应用的胶片拍摄的照片。
const old = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const newRed = 0.393 * r + 0.769 * g + 0.189 * b;
const newGreen = 0.349 * r + 0.686 * g + 0.168 * b;
const newBlue = 0.272 * r + 0.534 * g + 0.131 * b;
data[i] = newRed;
data[i + 1] = newGreen;
data[i + 2] = newBlue;
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
连环画
实现连环画的外围片段是:
const newRed = (Math.abs(g - b + g + r) * r) / 256;
const newGreen = (Math.abs(b - g + b + r) * r) / 256;
const newBlue = (Math.abs(b - g + b + r) * g) / 256;
这里应用了对红、绿、蓝通道的差别计算,通过加减运算和取绝对值,加强了色彩通道之间的差别。总体上,这样的变换加强了图像中色彩的对比度,突出了图像的边缘,产生了一种具备连环画格调的成果,这些数学变换是通过试验和调整取得的。
const comics = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const newRed = (Math.abs(g - b + g + r) * r) / 256;
const newGreen = (Math.abs(b - g + b + r) * r) / 256;
const newBlue = (Math.abs(b - g + b + r) * g) / 256;
data[i] = newRed;
data[i + 1] = newGreen;
data[i + 2] = newBlue;
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
冷 / 暖色调
冷色调通常与较低的温度相关联,呈现出相似蓝色、绿色和紫色的色调;暖色调通常与较高的温度相关联,呈现出相似红色、橙色和黄色的色调。在 RGB
色彩模型中,调整冷暖色调个别是调整红蓝通道:
- 若要加强冷色调,能够减少蓝色通道的强度,减小红色通道的强度。
- 若要加强暖色调,能够减小蓝色通道的强度,减少红色通道的强度。
const color = ({ctx, width, height, value}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {const red = data[i];
const green = data[i + 1];
const blue = data[i + 2];
data[i] = Math.max(0, red + value);
data[i + 1] = green;
data[i + 2] = Math.min(255, blue - value);
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
冷色调:
暖色调:
油画
对于油画滤镜的实现,能够分为以下的步骤:
- 计算像素四周肯定半径内的像素色彩平均值,失去一个更加含糊、平滑的色彩,这样能够模仿油画中的画笔过滤成果。
- 将色彩映射到一个无限的离散范畴,模仿油画中显著的色彩变动和档次,模仿手绘油画的成果
通过色彩均匀和离散化的解决,使图像呈现出一种油画般的柔和和层次感,模仿油画的绘图感觉。
const oilPaint = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const radius = 2; // 滤波半径
const levels = 10; // 离散档次数
for (let y = 0; y < height; y += 1) {for (let x = 0; x < width; x += 1) {const i = (y * width + x) * 4;
// 计算滤波半径内的色彩平均值
let totalRed = 0;
let totalGreen = 0;
let totalBlue = 0;
let count = 0;
for (let dy = -radius; dy <= radius; dy += 1) {for (let dx = -radius; dx <= radius; dx += 1) {const nx = Math.min(width - 1, Math.max(0, x + dx));
const ny = Math.min(height - 1, Math.max(0, y + dy));
const ni = (ny * width + nx) * 4;
totalRed += data[ni];
totalGreen += data[ni + 1];
totalBlue += data[ni + 2];
count += 1;
}
}
// 计算平均值并利用到以后像素
const avgRed = totalRed / count;
const avgGreen = totalGreen / count;
const avgBlue = totalBlue / count;
// 将色彩离散化到指定的档次数
const discreteRed =
(Math.round((avgRed / 255) * (levels - 1)) / (levels - 1)) * 255;
const discreteGreen =
(Math.round((avgGreen / 255) * (levels - 1)) / (levels - 1)) * 255;
const discreteBlue =
(Math.round((avgBlue / 255) * (levels - 1)) / (levels - 1)) * 255;
data[i] = discreteRed;
data[i + 1] = discreteGreen;
data[i + 2] = discreteBlue;
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
素描滤镜
通过调整图像中每个像素的色彩以强调其轮廓和明暗关系,能够模仿铅笔素描的视觉效果。具体能够通过以下形式实现,跟灰度滤镜差不多:
- 计算每个像素的红、绿、蓝通道的平均值,失去一个灰度值。
- 计算每个像素色彩值与均匀灰度值的梯度。梯度示意色彩绝对于均匀灰度的变动水平,即色彩绝对于四周区域的明暗水平。
- 将梯度乘以一个权重值,而后将后果加回到均匀灰度值上。
const sketch = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
const gradient = data[i] - avg;
// 梯度权重
const weight = 1;
data[i] = avg + gradient * weight;
data[i + 1] = avg + gradient * weight;
data[i + 2] = avg + gradient * weight;
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
水彩画滤镜
- 通过在半径范畴内随机选取一个像素的色彩来替换以后像素的色彩,模仿水彩画中色彩的扩散和混合成果。
- 在部分区域内实现色彩的混合,营造出水彩画般的柔和和天然的成果。
const waterColor = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const radius = 5; // 水彩画成果的半径
for (let y = 0; y < height; y++) {for (let x = 0; x < width; x++) {
let randomX =
x + Math.floor(Math.random() * radius) - Math.floor(radius / 2);
let randomY =
y + Math.floor(Math.random() * radius) - Math.floor(radius / 2);
// 边界查看
randomX = Math.max(0, Math.min(width - 1, randomX));
randomY = Math.max(0, Math.min(height - 1, randomY));
const i = (y * width + x) * 4;
const randomI = (randomY * width + randomX) * 4;
data[i] = data[randomI];
data[i + 1] = data[randomI + 1];
data[i + 2] = data[randomI + 2];
data[i + 3] = data[randomI + 3];
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
马赛克滤镜
马赛克滤镜的核心思想是将图像分成块状区域,每个块内的像素色彩都设置为该块内的第一个像素的色彩。这样的操作能够让图像中相邻像素的色彩变得雷同,产生了一种像素化马赛克的成果。
const mosaic = ({ctx, width, height}) => {const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const blockSize = 10;
for (let y = 0; y < height; y += blockSize) {for (let x = 0; x < width; x += blockSize) {const index = (y * width + x) * 4;
const red = data[index];
const green = data[index + 1];
const blue = data[index + 2];
// 将以后块内的所有像素色彩设置为第一个像素的色彩
for (let i = 0; i < blockSize; i++) {for (let j = 0; j < blockSize; j++) {const blockIndex = ((y + i) * width + (x + j)) * 4;
data[blockIndex] = red;
data[blockIndex + 1] = green;
data[blockIndex + 2] = blue;
}
}
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
含糊滤镜
- 这里应用的含糊算法是高斯含糊,通过定义一个含糊半径生成一个高斯核
- 计算每个像素在半径范畴内的加权平均值实现高斯含糊
const blur = ({ctx, width, height}) => {
const radius = 10;
const generateGaussianKernel = (radius) => {
const size = radius * 2 + 1;
const kernel = [];
const sigma = radius / 3;
const sigmaSquared = 2 * sigma * sigma;
let sum = 0;
for (let i = -radius; i <= radius; i++) {const row = [];
for (let j = -radius; j <= radius; j++) {
const distanceSquared = i * i + j * j;
const weight =
Math.exp(-distanceSquared / sigmaSquared) / (Math.PI * sigmaSquared);
row.push(weight);
sum += weight;
}
kernel.push(row);
}
// 归一化
for (let i = 0; i < size; i++) {for (let j = 0; j < size; j++) {kernel[i][j] /= sum;
}
}
return kernel;
};
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// 创立一个长期数组来保留原始图像数据
const originalData = new Uint8ClampedArray(data);
// 计算高斯外围权重
const kernel = generateGaussianKernel(radius);
for (let y = 0; y < height; y++) {for (let x = 0; x < width; x++) {
let sumRed = 0;
let sumGreen = 0;
let sumBlue = 0;
for (let i = -radius; i <= radius; i++) {for (let j = -radius; j <= radius; j++) {
let currentX = x + j;
let currentY = y + i;
// 边缘检测
currentX = Math.min(width - 1, Math.max(0, currentX));
currentY = Math.min(height - 1, Math.max(0, currentY));
let index = (currentY * width + currentX) * 4;
let weight = kernel[i + radius][j + radius];
sumRed += originalData[index] * weight;
sumGreen += originalData[index + 1] * weight;
sumBlue += originalData[index + 2] * weight;
}
}
let currentIndex = (y * width + x) * 4;
data[currentIndex] = sumRed;
data[currentIndex + 1] = sumGreen;
data[currentIndex + 2] = sumBlue;
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(imageData, 0, 0);
};
最初
以上就是本文实现的 10
种canvas
图像滤镜,如果你感觉有意思的话,点点关注点点赞吧~如果你有其余想法,欢送评论区或私信交换。