截图美化与编辑:让画面更有质感
做 WebRTC 截图,说白了只是第一步。你想想看,用户拿到一张原始截图,灰蒙蒙的,啥也没有,这体验能好吗?我个人的习惯是,截图之后一定要做「二次加工」——加滤镜、裁剪、打水印、调大小。这一套下来,画面才真正能用。
今天我们就来聊聊,怎么在浏览器里把这些事干漂亮。
1. 滤镜效果:灰度、反转、模糊
滤镜这东西,听起来高大上,其实底层就是像素级的数学运算。Canvas 提供了 getImageData 和 putImageData,我们可以直接操作每个像素的 RGBA 值。
灰度滤镜
灰度化,就是把彩色变成黑白。公式有很多种,我常用的是加权平均法:
function applyGrayscale(imageData) {
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 gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = gray;
data[i + 1] = gray;
data[i + 2] = gray;
}
return imageData;
}
嗯,这里要注意:权重系数不是随便写的。0.299、0.587、0.114 是 ITU-R BT.601 标准,人眼对绿色最敏感,所以绿色权重最高。我在项目中遇到过有人直接用 (r+g+b)/3,结果画面发灰,细节全丢了。
反转滤镜
反转就是取补色,255 减去当前值:
function applyInvert(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
return imageData;
}
这个简单,但效果很酷。我有时候拿它做「负片」效果,配合灰度一起用,能做出复古胶片感。
模糊滤镜
模糊稍微复杂一点,需要用到卷积核。我推荐用 3x3 的均值模糊:
function applyBlur(imageData, width, height) {
const kernel = [
1/9, 1/9, 1/9,
1/9, 1/9, 1/9,
1/9, 1/9, 1/9
];
// 这里省略了卷积循环,实际需要遍历每个像素及其邻域
// 注意边界处理,我习惯用 clamp 方式
}
为什么用 3x3?因为计算量小,效果也够用。我曾经试过 5x5,在移动端直接卡成 PPT,后来就学乖了。
2. 图片裁剪功能
裁剪,说白了就是「从大图中抠一块出来」。Canvas 的 drawImage 方法天然支持裁剪:
function cropImage(sourceCanvas, x, y, w, h) {
const cropCanvas = document.createElement('canvas');
cropCanvas.width = w;
cropCanvas.height = h;
const ctx = cropCanvas.getContext('2d');
ctx.drawImage(sourceCanvas, x, y, w, h, 0, 0, w, h);
return cropCanvas;
}
这里有个坑:x 和 y 不能超出源图边界。我曾经在项目里没做边界检查,用户拖拽裁剪框时直接崩了。后来我加了一段 clamp 逻辑:
x = Math.max(0, Math.min(x, sourceCanvas.width - w));
y = Math.max(0, Math.min(y, sourceCanvas.height - h));
嗯,稳了。
3. 添加水印
水印有两种:文字水印和图片水印。我一般用文字水印,因为灵活,而且不需要额外加载资源。
function addWatermark(canvas, text) {
const ctx = canvas.getContext('2d');
ctx.font = '24px Arial';
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(text, canvas.width - 20, canvas.height - 20);
}
透明度我习惯用 0.5,太淡看不清,太浓又影响画面。位置嘛,右下角最安全,不容易被主体遮挡。
4. 图片缩放与旋转
缩放
缩放用 drawImage 的宽高参数就能搞定:
function scaleImage(sourceCanvas, scaleFactor) {
const w = sourceCanvas.width * scaleFactor;
const h = sourceCanvas.height * scaleFactor;
const scaledCanvas = document.createElement('canvas');
scaledCanvas.width = w;
scaledCanvas.height = h;
const ctx = scaledCanvas.getContext('2d');
ctx.drawImage(sourceCanvas, 0, 0, w, h);
return scaledCanvas;
}
缩放因子小于 1 是缩小,大于 1 是放大。放大时画质会下降,这是物理限制,没办法。我建议最大放大到 2 倍,再大就糊了。
旋转
旋转需要用到 translate 和 rotate:
function rotateImage(sourceCanvas, angleDeg) {
const angleRad = angleDeg * Math.PI / 180;
const rotatedCanvas = document.createElement('canvas');
// 计算旋转后的宽高
const cos = Math.abs(Math.cos(angleRad));
const sin = Math.abs(Math.sin(angleRad));
rotatedCanvas.width = sourceCanvas.width * cos + sourceCanvas.height * sin;
rotatedCanvas.height = sourceCanvas.width * sin + sourceCanvas.height * cos;
const ctx = rotatedCanvas.getContext('2d');
ctx.translate(rotatedCanvas.width / 2, rotatedCanvas.height / 2);
ctx.rotate(angleRad);
ctx.drawImage(sourceCanvas, -sourceCanvas.width / 2, -sourceCanvas.height / 2);
return rotatedCanvas;
}
这里有个细节:旋转后画布尺寸会变,必须重新计算。我刚开始做的时候直接用了原图尺寸,结果旋转 45 度后四个角全被裁掉了。后来才明白,得用三角函数算新尺寸。
知识体系图
这张图把今天的内容串起来了。你看,所有操作都围绕 Canvas 2D API 展开,核心就是 getImageData、drawImage、translate、rotate 这几个方法。掌握了它们,你就能自由组合出各种效果。
我个人觉得,截图美化这块最考验的是「边界意识」——像素不能越界,坐标不能超限,尺寸不能为负。把这些细节处理好,代码就稳了。