前言
大家好,我是练习时长一坤年的前端练习生,之前发布了一篇打造图像编辑器(一)——基础架构与图像滤镜介绍了图像编辑器的基础架构和基础的图像滤镜,我们今天接着来介绍一些更有意思的图像滤镜。
体验地址
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
图像滤镜,如果你觉得有意思的话,点点关注点点赞吧~如果你有其他想法,欢迎评论区或私信交流。