ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

微信小程序图片处理工具:从画布到像素滤镜

微信小程序图片处理工具:从画布到像素滤镜 简介这是一款微信小程序项目实例——图片处理小工具自制低配版美图秀秀面向小程序初学者与移动开发爱好者可学习图像处理功能的完整实现思路。项目围绕图片美化场景采用小程序原生框架包含滤镜、效果图、动态滤镜三大功能模块用户能选择图片并应用模糊、怀旧、复古、美白等效果也能手动调节饱和度、亮度、对比度并预览动态效果。资源打包为zip格式共29个文件以js逻辑、json配置、wxss样式、wxml页面结构为主辅以png/jpg图片素材整体仅984KB目录区分页面、工具、素材等层次便于按模块拆解。目前已有1934人学习下载适合作为课程设计或自学项目整个项目代码量不大阅读负担低对理解小程序图片选择、参数交互、滤镜渲染等实现有直接参考价值。1. 一张图到像素矩阵微信小程序图片处理的起点低配版美图秀秀的核心不是把滤镜名字堆在界面上而是把一张照片变成可以逐像素修改的数值矩阵。微信小程序没有 DOMcanvas 2D 反而提供了更直接的绘图模型drawImage负责几何变换getImageData负责读取像素canvasToTempFilePath负责导出文件。整个工具就围绕这三个接口闭环选图、旋转、裁剪、调色、模糊、保存。它适合刚做完登录和列表页的小程序初学者找一个有实际交互和图像数据的项目实例也适合在封面编辑、社区发帖场景里需要轻量嵌入图片编辑能力的工程师。这套方案按“选图 → 变换 → 像素 → 导出 → 交互”的顺序落地每步都能独立验证。2. 搭建图片处理小工具选图、Canvas 绘制与旋转裁剪2.1 用 chooseMedia 把相册图片请进 canvaswxml 里必须给 canvas 声明type2d否则拿不到 Canvas 2D 接口view classeditor canvas type2d idimageCanvas classeditor-canvas/canvas /view.editor-canvas需要显式设置宽高比如width: 100%; height: 70vh;。初始化时通过wx.createSelectorQuery拿节点const query wx.createSelectorQuery() query.select(#imageCanvas) .fields({ node: true, size: true }) .exec(res { if (!res || !res[0]) return const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getWindowInfo().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) this.canvas canvas this.ctx ctx })fields({ node: true, size: true })返回的res[0].node是 Canvas 对象res[0].width/height是 CSS 布局尺寸。把canvas.width设成 CSS 尺寸乘以dpr是为了让像素缓冲对齐屏幕物理像素导出时也能得到更清晰的图随后ctx.scale(dpr, dpr)让所有绘制坐标继续用 CSS 坐标不用在每行代码里乘系数。选图统一走wx.chooseMedia它同时覆盖相册和相机返回值比旧wx.chooseImage更干净wx.chooseMedia({ count: 1, mediaType: [image], sourceType: [album, camera], success: res { const tempFilePath res.tempFiles[0].tempFilePath const img this.canvas.createImage() img.onload () { this.currentImage img this.renderImage() } img.src tempFilePath } })注意图片加载是异步的必须在onload回调里执行drawImage。这里的canvas.createImage()是 Canvas 2D 实例上的方法不要误写成全局的wx.createImage。count: 1保证单选后续做多图合成时再放开 count。2.2 微信小程序图片旋转与裁剪transform 与 drawImage 的配合旋转最简单的做法是把坐标原点平移到画布中心再旋转坐标系最后把图片画在中心function renderRotated(image, angle) { const dpr wx.getWindowInfo().pixelRatio const cw this.canvas.width / dpr const ch this.canvas.height / dpr const ctx this.ctx ctx.clearRect(0, 0, cw, ch) ctx.save() ctx.translate(cw / 2, ch / 2) ctx.rotate(angle * Math.PI / 180) ctx.drawImage(image, -image.width / 2, -image.height / 2, image.width, image.height) ctx.restore() }angle取 0、90、180、270 时不需要调整画布尺寸如果允许任意角度旋转后四个角会超出画布必须动态调整canvas.width/height并按对角线长度扩展。image.width在这里是图片的原始像素宽如果你在 2.1 里做过 CSS 坐标缩放需要先计算显示宽度与原始宽度的比例否则旋转后图片大小会漂移。裁剪是更常用的操作。用九参数drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)前四个是源图片上的矩形后四个是画布上的目标矩形function renderCrop(image, cropRect) { // cropRect: { sx, sy, sw, sh } 源图坐标按比例换算 const dpr wx.getWindowInfo().pixelRatio const cw this.canvas.width / dpr const ch this.canvas.height / dpr this.ctx.clearRect(0, 0, cw, ch) this.ctx.drawImage(image, cropRect.sx, cropRect.sy, cropRect.sw, cropRect.sh, 0, 0, cw, ch) }常见坑是相册大图 4000x3000直接 drawImage 会在部分安卓机上卡顿甚至崩溃。我一般会在图片 onload 后先做一个尺寸换算把长边限制在 2048 以内裁剪和滤镜都用降采样后的图最后导出时再按目标尺寸放大。性能和画质在这个低配工具里优先保性能。下表是旋转裁剪的常用参数组合操作方法关键参数90 度旋转translate rotateangle: 0/90/180/270任意角度旋转translate rotateangle: -180~180需要扩大画布固定比例裁剪drawImage 9 参数sx,sy,sw,sh 决定源矩形自由裁剪框计算裁剪框相对图片比例sw 显示宽 / 缩放系数2.3 顶部导航栏高度与页面布局适配自定义导航栏的小程序页面底部工具条好处理顶部如果被胶囊遮住旋转按钮和导出按钮基本没法点。计算顶部导航栏高度不要写死用接口拿const windowInfo wx.getWindowInfo() const menu wx.getMenuButtonBoundingClientRect() const navHeight (menu.top - windowInfo.statusBarHeight) * 2 menu.height this.setData({ navHeight, statusBarHeight: windowInfo.statusBarHeight })statusBarHeight是状态栏高度menu.top是胶囊按钮上边界到屏幕顶部的距离两者差值就是胶囊上方的留白乘 2 补上胶囊下方的留白再加menu.height就得到导航栏完整高度。把这个值绑定到.editor的padding-top页面内容就不会和胶囊重叠。这个适配在 iOS 和安卓上都依赖运行时接口比固定 64px 可靠也是微信小程序页面设计里最容易扣分的一个点。3. 像素滤镜实现亮度、对比度、饱和度与模糊的数学3.1 getImageData 与像素循环滤镜不能直接作用在屏幕 canvas 上。屏幕 canvas 通常只有 375 宽处理后再导出到 1080 会糊。常见做法是先在离屏 canvas 上按目标尺寸画一遍原图再对这块缓冲区做像素操作function createOffscreen(width, height) { const canvas wx.createOffscreenCanvas({ type: 2d, width, height }) const ctx canvas.getContext(2d) ctx.drawImage(this.currentImage, 0, 0, width, height) return { canvas, ctx } }wx.createOffscreenCanvas支持传入配置对象初始化宽高不需要挂到页面上适合做中间缓冲。拿到绘图上下文后读取像素矩阵const imageData ctx.getImageData(0, 0, width, height) const data imageData.datadata是Uint8ClampedArray每 4 个字节对应一个像素的 R、G、B、A取值范围 0 到 255。像素循环的骨架是所有滤镜通用的for (let i 0; i data.length; i 4) { // data[i], data[i1], data[i2] 分别表示 RGB }修改完data后必须写回ctx.putImageData(imageData, 0, 0)。这一步会把整个画布覆盖所以滤镜操作要放在绘制流程的最后阶段或者在离屏 canvas 上完成后再 drawImage 到屏幕。3.2 滤镜参数表与滑动条调节低配版美图秀秀需要的滤镜不多四个就够亮度、对比度、饱和度、模糊。参数范围按滑动条常用习惯设定滤镜参数变量范围公式要点亮度brightness-100 ~ 100每个通道直接加偏移对比度contrast-100 ~ 100factor * (v - 128) 128饱和度saturation-100 ~ 100原始 RGB 与灰度插值模糊blurRadius1 ~ 5盒式模糊迭代亮度与对比度可以放进同一个像素循环function applyBrightnessContrast(imageData, brightness, contrast) { const data imageData.data const factor (259 * (contrast 255)) / (255 * (259 - contrast)) for (let i 0; i data.length; i 4) { data[i] factor * (data[i] - 128) 128 brightness data[i 1] factor * (data[i 1] - 128) 128 brightness data[i 2] factor * (data[i 2] - 128) 128 brightness } }factor是常用对比度公式的近似contrast-100时 factor 很小图会变成灰色contrast100时 factor 很大RGB 接近 0 或 255出现明显二值化。brightness直接加到结果上所以它会在对比度调整之后生效这也是大多数修图工具的处理顺序。饱和度不需要像素周围的信息用灰度值做插值function applySaturation(imageData, saturation) { const data imageData.data const sat saturation / 100 for (let i 0; i data.length; i 4) { const r data[i], g data[i 1], b data[i 2] const gray 0.299 * r 0.587 * g 0.114 * b data[i] gray (r - gray) * (1 sat) data[i 1] gray (g - gray) * (1 sat) data[i 2] gray (b - gray) * (1 sat) } }饱和度系数的映射是0 表示原图-1 表示完全灰度1 表示颜色饱和度翻倍。这里1 sat在 sat100 时等于 2效果明显但不会溢出到怪异。3.3 盒式模糊迭代与离屏 canvas模糊和前面三个滤镜不同它需要访问相邻像素。精确高斯模糊要做二维卷积低配版用盒式模糊迭代近似对每个像素取周围 3x3 区域的平均值连续跑两三次视觉上已经很接近高斯模糊。function boxBlur(ctx, width, height, radius 1) { const imageData ctx.getImageData(0, 0, width, height) const src imageData.data const dst new Uint8ClampedArray(src.length) for (let y 0; y height; y) { for (let x 0; x width; x) { let r 0, g 0, b 0, a 0, count 0 for (let dy -radius; dy radius; dy) { for (let dx -radius; dx radius; dx) { const yy Math.min(height - 1, Math.max(0, y dy)) const xx Math.min(width - 1, Math.max(0, x dx)) const idx (yy * width xx) * 4 r src[idx]; g src[idx 1] b src[idx 2]; a src[idx 3] count } } const o (y * width x) * 4 dst[o] r / count; dst[o 1] g / count dst[o 2] b / count; dst[o 3] a / count } } imageData.data.set(dst) ctx.putImageData(imageData, 0, 0) }边界用截断而不越界处理后的边角不会变黑代价是边缘处采样会重复顶点颜色但低配工具里完全够用。radius每加 1每个像素的采样数按平方增长1080 宽图上 radius1 就已经要跑约一千万次加法。因此我会先把图片降到 540 或 480 宽模糊完再放大回目标尺寸。迭代两次 radius1 比一次 radius2 更自然也更容易控制档位。4. 导出与保存canvasToTempFilePath 与 user_data_path 的配合4.1 导出尺寸与 quality 的选择wx.canvasToTempFilePath负责把 canvas 缓冲区转成图片临时文件。它有两个容易踩的坑第一canvas参数必须传 Canvas 2D 对象不是选择器字符串第二width/height是源缓冲区里的矩形范围destWidth/destHeight才是最终导出尺寸。function exportCanvas(canvas, destWidth 1080) { return new Promise((resolve, reject) { wx.canvasToTempFilePath({ canvas, x: 0, y: 0, width: canvas.width, height: canvas.height, destWidth, destHeight: Math.round(canvas.height * destWidth / canvas.width), fileType: jpg, quality: 0.92, success: res resolve(res.tempFilePath), fail: reject }) }) }destHeight不显式给定的话部分平台会按屏幕比例输出导致导出图被拉伸。用canvas.height * destWidth / canvas.width保持原始比例。quality只对 jpg 生效范围 0 到 1建议 0.9 左右png 没有 quality 概念体积是 jpg 的三倍以上。低配版默认导出 jpg。参数作用建议canvasCanvas 2D 对象从fields({node:true})拿x/y/width/height导出源矩形默认全图destWidth/destHeight导出像素尺寸长边 1080~2048fileTypejpg/png无透明用 jpgqualityjpg 压缩质量0.9~0.954.2 保存到相册与权限失败处理导出得到tempFilePath后本可以直接保存到相册wx.saveImageToPhotosAlbum({ filePath: tempFilePath, success: () wx.showToast({ title: 已保存 }), fail: err handleAuthFail(err) })问题集中在授权。第一次调saveImageToPhotosAlbum会弹出“允许保存相册”用户拒绝后第二次调用直接走 fail。判断权限错误不能只靠 errCode不同基础库返回不同用 errMsg 更稳function handleAuthFail(err) { const msg err.errMsg || if (msg.includes(auth) || msg.includes(deny)) { wx.showModal({ title: 需要相册权限, content: 开启相册权限后才能保存编辑后的图片, confirmText: 去设置, success: res res.confirm wx.openSetting() }) } else { wx.showToast({ title: 保存失败, icon: none }) } }这里有一个细节如果 errMsg 是saveImageToPhotosAlbum:fail cancel说明用户主动点了取消不需要弹设置引导只有包含auth或deny才说明是权限被拒。openSetting会打开小程序设置页用户手动开启后回到页面下一次保存可以正常通过。开发工具里授权状态和真机不同测试一定以真机为准。4.3 保存附件到 wx.env.user_data_path相册权限拿不到或者用户想保留多次编辑的中间结果就用到wx.env.USER_DATA_PATH。这是小程序独立的用户数据目录无需授权适合临时缓存编辑附件。function persistToUserDir(tempFilePath) { const fs wx.getFileSystemManager() const base wx.env.USER_DATA_PATH const fileName edited_${Date.now()}.jpg fs.copyFileSync(tempFilePath, ${base}/${fileName}) const files fs.readdirSync(base).filter(name name.startsWith(edited_)) files.sort() while (files.length 20) { const old files.shift() fs.unlinkSync(${base}/${old}) } return ${base}/${fileName} }copyFileSync要求源文件是本地临时路径或用户目录路径canvasToTempFilePath返回的tempFilePath正合适。文件名用Date.now()保证唯一string 排序天然按时间顺序因此清理时直接从最早的删起。限制 20 个附件是实用做法避免用户目录被塞满如果你做的是多轮编辑还可以用这个路径保存“撤销前”的图片把临时附件当成版本列表。清理完成后返回路径后续可通过fs.unlinkSync再删。5. 长按拖拽与图层命中把工具调成顺手的小产品5.1 长按拖拽滚动catchtouchmove 与 500ms 手势判定页面里滑块一多普通bindtouchmove会带着 scroll-view 一起滚。解决方案是做一个 500ms 长按判定进入拖拽模式后用catchtouchmove截断事件。wxml 中滑块块的触摸事件用catchtouchstart、catchtouchmove、catchtouchend绑定JS 判断拖拽onSliderTouchStart(e) { this.dragTimer setTimeout(() { this.dragging true }, 500) this.startX e.touches[0].clientX this.startValue this.data.sliderValue }, onSliderTouchMove(e) { if (!this.dragging) return const dx e.touches[0].clientX - this.startX const value Math.max(-100, Math.min(100, this.startValue dx * 0.5)) this.setData({ sliderValue: Math.round(value) }) }, onSliderTouchEnd() { clearTimeout(this.dragTimer) this.dragging false }位移基于起点计算所以startX在 move 中不需要更新调完一次后重新赋值startValue。catchtouchmove比preventDefault可靠小程序里只有它能阻止页面滚动。5.2 多图层的 hitTest 与 z 序加文字、贴纸时要维护图层数组绘制时从前往后画触摸时从后往前检测。简单的矩形命中测试function hitTestLayers(layers, x, y) { for (let i layers.length - 1; i 0; i--) { const l layers[i] if (x l.x - l.w / 2 x l.x l.w / 2 y l.y - l.h / 2 y l.y l.h / 2) return i } return -1 }图层旋转后触点要先逆旋转到图层本地坐标再判断否则 45 度角会有误触低配版可以只在绘制时旋转选中仍用包围盒。5.3 性能调优与真机验证像素循环耗时用console.time埋点最直接console.time(filter-cost) applyBrightnessContrast(imageData, brightness, contrast) console.timeEnd(filter-cost)在开发者工具 Performance 面板里看主线程耗时超过 200ms 就说明处理分辨率太高降到 540 宽再跑。真机验证导出图时务必用 iPhone 和安卓各测一次因为相册图片方向、Canvas 尺寸上限和授权弹窗行为在两端差异很大。getImageData在 iOS 上对超过 4096 的 Canvas 会直接报错导出尺寸定在 2048 内更安全。本文还有配套的精品资源点击获取
返回列表