截图美化与编辑:让画面更有质感

做 WebRTC 截图,说白了只是第一步。你想想看,用户拿到一张原始截图,灰蒙蒙的,啥也没有,这体验能好吗?我个人的习惯是,截图之后一定要做「二次加工」——加滤镜、裁剪、打水印、调大小。这一套下来,画面才真正能用。

今天我们就来聊聊,怎么在浏览器里把这些事干漂亮。

1. 滤镜效果:灰度、反转、模糊

滤镜这东西,听起来高大上,其实底层就是像素级的数学运算。Canvas 提供了 getImageDataputImageData,我们可以直接操作每个像素的 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,后来就学乖了。

小提示: 滤镜操作很耗性能。建议把截图缩放到合适尺寸后再处理,别直接怼 4K 原图。

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;
}

这里有个坑:xy 不能超出源图边界。我曾经在项目里没做边界检查,用户拖拽裁剪框时直接崩了。后来我加了一段 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 倍,再大就糊了。

旋转

旋转需要用到 translaterotate

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,核心就是操作像素和坐标变换。掌握了这些,你就能做出一个功能完整的截图编辑器。

知识体系图

截图美化与编辑 滤镜效果 灰度 · 反转 · 模糊 图片裁剪 drawImage 区域截取 添加水印 文字水印 · 图片水印 图片缩放 drawImage 宽高控制 图片旋转 translate + rotate Canvas 2D API

这张图把今天的内容串起来了。你看,所有操作都围绕 Canvas 2D API 展开,核心就是 getImageDatadrawImagetranslaterotate 这几个方法。掌握了它们,你就能自由组合出各种效果。

我个人觉得,截图美化这块最考验的是「边界意识」——像素不能越界,坐标不能超限,尺寸不能为负。把这些细节处理好,代码就稳了。

公众号:蓝海资料掘金营,微信deep3321