JS Canvas如何实现类似Photoshop的不同硬度画笔?
JS Canvas如何实现类似Photoshop的不同硬度画笔?
嘿,我太懂你这种困扰了——想用Canvas做个像PS那样能调硬度的画笔,结果用createRadialGradient做出来的效果总觉得不对味儿,对吧?其实问题出在径向渐变默认的线性过渡和PS画笔的边缘算法不一样,我来给你分享两个靠谱的实现方法~
方法一:改进径向渐变,模拟硬度过渡
PS里的画笔硬度,本质就是控制“实色区域”和“透明过渡区域”的比例:100%硬度时,画笔边缘完全锐利;0%时,从中心到边缘完全平滑过渡。我们可以通过自定义渐变的颜色停止点,来模拟这个逻辑:
function createBrush(size, hardness) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = canvas.height = size; // 把0-100的硬度转成0-1的系数 const hardnessFactor = hardness / 100; const radius = size / 2; // 计算实色区域的半径:硬度越高,实色区域越大 const solidRadius = radius * hardnessFactor; // 过渡带的宽度:剩下的边缘区域 const featherWidth = radius - solidRadius; // 创建从实色区域边缘到画笔外边缘的径向渐变 const gradient = ctx.createRadialGradient( radius, radius, solidRadius, radius, radius, radius ); gradient.addColorStop(0, 'rgba(0,0,0,1)'); // 如果有过渡带,添加过渡的颜色停止点 if (featherWidth > 0) { // 实色区域的边界,透明度保持1 gradient.addColorStop(hardnessFactor, 'rgba(0,0,0,1)'); // 过渡到完全透明 gradient.addColorStop(1, 'rgba(0,0,0,0)'); } else { // 100%硬度,直接从实色切到透明 gradient.addColorStop(1, 'rgba(0,0,0,0)'); } // 用渐变填充整个画笔画布 ctx.fillStyle = gradient; ctx.fillRect(0, 0, size, size); return canvas; }
这个方法的好处是性能好、计算快,适合大部分场景。你要是想改画笔颜色,只要把rgba里的前三个数值换成你要的颜色就行。
方法二:像素级高斯计算,还原PS原生柔边效果
如果你追求和PS柔边画笔完全一致的自然过渡,那得用高斯函数来计算每个像素的透明度——毕竟PS的柔边画笔本质就是高斯模糊的边缘。这种方法会直接操作像素数据,效果更精准:
function createBrushWithGaussian(size, hardness) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = canvas.height = size; const radius = size / 2; const hardnessFactor = hardness / 100; // 高斯函数的标准差,硬度越高,标准差越小(过渡越窄) const sigma = radius * (1 - hardnessFactor) / 3; const imageData = ctx.createImageData(size, size); const data = imageData.data; // 遍历每个像素计算透明度 for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { // 计算当前像素到画笔中心的距离 const dx = x - radius; const dy = y - radius; const distance = Math.sqrt(dx*dx + dy*dy); // 超出画笔半径的像素直接透明 if (distance > radius) { data[(y*size + x)*4 + 3] = 0; continue; } // 计算高斯衰减后的透明度 let alpha = 1; if (sigma > 0) { // 把距离归一化到过渡带的范围 const normalizedDistance = (distance - radius * hardnessFactor) / (radius * (1 - hardnessFactor)); if (normalizedDistance > 0) { // 高斯函数核心计算 alpha = Math.exp(-Math.pow(normalizedDistance * 3, 2) / 2); } } // 设置像素颜色(这里是黑色,可自行修改) data[(y*size + x)*4] = 0; data[(y*size + x)*4 + 1] = 0; data[(y*size + x)*4 + 2] = 0; data[(y*size + x)*4 + 3] = Math.round(alpha * 255); } } ctx.putImageData(imageData, 0, 0); return canvas; }
最后,怎么用这个画笔?
创建好画笔画布后,你只需要在主画布上监听鼠标事件,用drawImage把画笔绘制到对应位置就行,还能配合Canvas的合成模式模拟PS的画笔混合效果:
const mainCanvas = document.getElementById('mainCanvas'); const mainCtx = mainCanvas.getContext('2d'); // 创建一个50px大小、50%硬度的画笔 const brush = createBrush(50, 50); let isDrawing = false; // 监听鼠标事件实现绘制 mainCanvas.addEventListener('mousedown', (e) => { isDrawing = true; const rect = mainCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; drawBrush(x, y); }); mainCanvas.addEventListener('mousemove', (e) => { if (isDrawing) { const rect = mainCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; drawBrush(x, y); } }); mainCanvas.addEventListener('mouseup', () => { isDrawing = false; }); // 绘制画笔的函数 function drawBrush(x, y) { // 可以换成multiply、screen等合成模式,模拟不同画笔效果 mainCtx.globalCompositeOperation = 'source-over'; // 把画笔绘制到鼠标中心位置 mainCtx.drawImage(brush, x - brush.width/2, y - brush.height/2); }
小技巧补充
- 要是想模拟压感,可以在鼠标拖拽时根据压力(或者鼠标移动速度)动态调整画笔的size或hardness;
- 改变画笔颜色时,只要在创建画笔的代码里修改rgba的前三个通道值就行;
- 简单的柔边也可以用
shadowBlur快速实现,但精准控制硬度的话还是上面的方法更可靠。
内容来源于stack exchange
相关产品推荐
相关产品推荐

