油画滤镜需逐像素在邻域内随机采样并取RGB众数,而非模糊或调色;radius控制笔触粗细,sampleCount影响质感,众数统计是实现厚重笔触感的核心。

Canvas 实现油画滤镜,核心是模拟颜料堆积的局部采样与色彩聚合,不能靠 CSS filter 的模糊+饱和度“假装”,必须逐像素操作。
油画效果的关键原理
不是简单模糊或调色,而是对每个像素,在其周围一个方形区域内(比如 5×5 像素)随机采样若干点,统计这些采样点中出现频率最高的 RGB 组合(即“众数”),再把这个颜色赋给当前像素。这样能产生块状、厚重、有笔触感的视觉效果。
- radius(邻域半径)控制笔触粗细:值越大,采样范围越广,画面越概括、越“写意”
- sampleCount(采样点数)影响质感:太少会显得颗粒化,太多则趋于平滑,一般取 8–20 较自然
- 不直接取平均值或中位数,而用众数——这是油画感的根源,它保留了局部色彩集群的“真实感”
基础实现步骤
先将图片绘制到 canvas 上,再读取、处理、写回像素数据:
- 用
ctx.drawImage(img, 0, 0)把图像画入离屏 canvas(推荐,避免污染主画布) - 调用
ctx.getImageData(0, 0, width, height)获取ImageData对象 - 遍历每个像素(步长为 4),对每个像素执行邻域采样逻辑
- 为避免边界越界,循环起始/结束坐标需预留
radius缓冲(如从radius到width - radius) - 处理完所有像素后,用
ctx.putImageData()写回画布
简化版代码示例(含注释)
以下是一个可运行的油画滤镜片段,注意:需确保图片已加载且无跨域问题:
function applyOilPaint(ctx, width, height, radius = 4, sampleCount = 12) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const tempData = new Uint8ClampedArray(data.length); // 避免边写边读干扰
<p>for (let y = radius; y width + x) 4;
const colors = [];</p><pre class="brush:php;toolbar:false;"> // 在 radius×radius 区域内随机采样
for (let i = 0; i {
rFreq[r] = (rFreq[r] || 0) + 1;
gFreq[g] = (gFreq[g] || 0) + 1;
bFreq[b] = (bFreq[b] || 0) + 1;
});
const mostR = Object.entries(rFreq).reduce((a, b) => (b[1] > a[1] ? b : a))[0];
const mostG = Object.entries(gFreq).reduce((a, b) => (b[1] > a[1] ? b : a))[0];
const mostB = Object.entries(bFreq).reduce((a, b) => (b[1] > a[1] ? b : a))[0];
tempData[centerIndex] = +mostR;
tempData[centerIndex + 1] = +mostG;
tempData[centerIndex + 2] = +mostB;
tempData[centerIndex + 3] = 255; // alpha 不变
}
}
// 一次性写回,提升性能 for (let i = 0; i
进阶优化建议
若追求更真实的油画表现,可进一步增强:
- 分区域采样权重:靠近中心的点采样概率更高,模拟画笔压力变化
- 色彩空间转换:在 Lab 或 HSV 空间做众数统计,比 RGB 更符合人眼感知(尤其避免灰阶失真)
- 多层叠加:先做一次大 radius 油画(底色铺陈),再叠加小 radius 局部强化(细节提亮)
-
结合 CSS filter 微调:导出前加
filter: saturate(1.3) contrast(1.1)提升最终观感,但注意该效果不会被toDataURL()保存
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











