# [实战] 纯前端实现 Canvas 图片涂抹与橡皮擦效果(附小程序落地案例)

0 阅读2分钟

前言

最近在做一个基于 Generative AI 的图像修复小程序(香蕉一键去水印)。在开发过程中,遇到一个核心交互难点:如何在小程序端实现流畅的“手指涂抹”生成遮罩(Mask),并将其传输给后端 AI 模型?

本文将剥离业务逻辑,单纯分享一下前端 Canvas 交互的实现细节,以及如何处理坐标点的平滑插值。

一、 交互需求拆解

我们需要实现的效果是:

  1. 用户上传图片到底层。
  2. 手指在屏幕滑动,生成半透明的红色线条。
  3. 线条需要有“笔触感”(两头圆润)。
  4. 导出 Canvas 内容为 Base64 图片。

二、 核心代码实现

为了保证移动端的性能,我没有使用任何第三方 Canvas 库,而是直接操作原生 Context。

1. 贝塞尔曲线平滑处理

如果直接连接 touchmove 的坐标点,线条会产生明显的折线感。我们需要使用二次贝塞尔曲线进行插值。

// 获取 Canvas 上下文
const ctx = canvas.getContext('2d');
ctx.lineCap = 'round'; // 圆润笔触
ctx.lineJoin = 'round'; // 圆润连接点
ctx.lineWidth = 20;

// 绘制平滑曲线的核心函数
function drawSmoothLine(points) {
    if (points.length < 3) return;
    
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);
    
    // 使用控制点进行插值
    for (let i = 1; i < points.length - 2; i++) {
        const xc = (points[i].x + points[i + 1].x) / 2;
        const yc = (points[i].y + points[i + 1].y) / 2;
        ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
    }
    
    // 绘制最后一段
    ctx.stroke();
}
### 2. 生成 AI 需要的 Mask

AI 模型通常需要一张黑底白色的遮罩图。我们在导出时需要利用 `globalCompositeOperation` 进行图层混合。

JavaScript

function exportMask() { // 创建离屏 Canvas const maskCanvas = document.createElement('canvas'); const mCtx = maskCanvas.getContext('2d');

// 填充黑色背景
mCtx.fillStyle = '#000000';
mCtx.fillRect(0, 0, width, height);

// 将涂抹轨迹绘制为白色
mCtx.strokeStyle = '#FFFFFF';
mCtx.drawImage(drawingLayer, 0, 0);

return maskCanvas.toDataURL('image/png');

}


## 三、 落地效果与性能优化

在实际的小程序(**香蕉一键去水印**)中,这套方案经过了数十次迭代。

为了防止 Canvas 内存泄漏,我在 `onUnload` 生命周期中加入了显式的资源释放逻辑。实测在 iOS 设备上,连续涂抹 50 次也不会出现掉帧。

### 遇到的坑:

-   **坐标系偏差**:小程序的 `Canvas` 坐标系与 DOM 略有不同,需要根据 `pixelRatio` 进行换算。
-   **层级穿透**:原生组件 `Canvas` 容易遮挡底部的按钮,建议使用 `view` 模拟或同层渲染方案。

## 四、 总结

Canvas 交互是端侧 AI 应用的重要一环。希望这段代码能给正在做类似功能的开发者一点参考。

目前这个项目已经开源了部分非核心逻辑,也欢迎大家体验成品的实际交互效果。

* * *

### 🔗 相关资源

-   **GitHub 代码仓库**:[imcopilot-home](https://github.com/ahlihongwei-pixel/imcopilot-home)
-   **体验案例**:微信搜索小程序 **“香蕉一键去水印”**

> *技术交流欢迎在评论区留言,看到会回复。*