小程序的canvas 实现裁剪图片、圈选功能,drawImage和canvasToTempFilePath的配合使用。

本文分享了在canvas上实现自定义矩形截图的方法,包括触摸开始、移动和结束的事件处理,以及如何将截图区域导出为图片并上传至服务器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

记录一下遇到需求和解决

1.前段时间遇到一个需求在这里插入图片描述
在图中需要一个圈选功能就类似圈中想要的商品下单,刚看到这个需求的时候还是一头雾水,画那种矩形框还是简单的,可以需要裁剪矩形框内的内容不知从何下手,前端老大让我使用了一个叫 welCropper的插件使用截图,花了几天研究做了一个类似的功能可以效果差值甚远,没有圈选的功能只有一个简单的拉住矩形框截出图片,想着抱着侥幸最后果然还是被退回来了,点了一定要和圈选功能一样,因为canvas接触的少,之前还傻到用背景图定位上去,原生canvas的api就可以解决了,直接上代码吧

实现

canvas的手指触摸动作开始

touchStart: function(e) {
    this.setData({
      minX: e.changedTouches[0].x,  //定义矩形左上和右下两个点坐标
      minY: e.changedTouches[0].y,
      maxX: e.changedTouches[0].x,
      maxY: e.changedTouches[0].y,
    })

    //得到触摸点的坐标
    this.startX = e.changedTouches[0].x
    this.startY = e.changedTouches[0].y
    this.context = wx.createCanvasContext("moveCanvas", this)
    this.context.setStrokeStyle(this.data.color)
    this.context.setLineWidth(this.data.pen)
    this.context.setLineCap('round') // 让线条圆润 
    this.context.beginPath()
  },

  //手指触摸后移动
  touchMove: function(e) {
    var minX = this.data.minX
    var minY = this.data.minY
    var maxX = this.data.maxX
    var maxY = this.data.maxY
    var startX1 = e.changedTouches[0].x
    var startY1 = e.changedTouches[0].y
    
    if (startX1 < minX) minX = startX1; //判断当前的点与原来存的两个点做比较
    if (startX1 > maxX) maxX = startX1;
    if (startY1 < minY) minY = startY1;
    if (startY1 > maxY) maxY = startY1;
    this.context.moveTo(this.startX, this.startY)
    this.context.lineTo(startX1, startY1)
    this.context.stroke()
    this.startX = startX1;
    this.startY = startY1;
    //只是一个记录方法调用的容器,用于生成记录绘制行为的actions数组。context跟不存在对应关系,一个context生成画布的绘制动作数组可以应用于多个
    wx.drawCanvas({
      canvasId: 'moveCanvas',
      reserve: true,
      actions: this.context.getActions() // 获取绘图动作数组
    })
    this.data.contextCount++;
    this.setData({
      minX: minX,
      minY: minY,
      maxX: maxX,
      maxY: maxY,
    })

  },

  //手指触摸动作结束

  touchEnd: function() {
    this.context.drawImage(this.data.tempFilePaths, 0, 0, windowWidth, windowHeight * 0.5) //windowWidth   windowHeight  为当前屏幕的宽高具体的获取的api 这里就不介绍了 自行百度 
    this.context.draw()   //每次结束的时候清空原有的画布 重新drawImage

    wx.canvasToTempFilePath({ //把当前画布指定区域的内容导出生成指定大小的图片,并返回文件路径
      x: this.data.minX,
      y: this.data.minY,
      width: this.data.maxX - this.data.minX,
      height: this.data.maxY - this.data.minY,
      destWidth: this.data.maxX - this.data.minX,
      destHeight: this.data.maxY - this.data.minY,
      canvasId: 'moveCanvas',
      success: function (res) {
        console.log(res.tempFilePath)
        var imgarr = that.data.imgarr; //获取存临时路径的数组 原来为空
        var img = { 'img': res.tempFilePath,'num':1,'havmin':3,'havmax':6};
        imgarr.push(img);
        var views = 'img' + String(imgarr.length - 1);  //这里是scroll-view的scroll-into-view的位置  设置滚动条位置滚动条位置   views 为scroll-view新的内容的id
        // console.log(viewsss);
        that.setData({
          imgarr: imgarr,
          viewsss: viewsss
        })

      }
    })

    var that=this;
    var arr=this.data.arr;  //定义的存所有矩形两点的数组
    var arr1 = [this.data.minX, this.data.minY, this.data.maxX, this.data.maxY];
    arr.push(arr1)  
    this.setData({
      arr: arr
    })
    for (var i = 0; i < arr.length;i++){  //遍历这个数组
      this.context.beginPath();
      this.context.setLineWidth(2);
      if (i == arr.length-1){  //新的一个为红色  之前的为绿色
        this.context.setStrokeStyle("red");
      }
      else{
        this.context.setStrokeStyle("green");
      }
      this.context.rect(arr[i][0], arr[i][1], arr[i][2] - arr[i][0], arr[i][3] - arr[i][1]);  
      this.context.stroke();
      this.context.draw(true)  //带参数true  不清空画布 
    }
  },

备注的很清楚了,加载的时候都需要给canvas添加图片使用drawImage。配合上wx.canvasToTempFilePath把当前画布指定区域的内容导出生成指定大小的图片,并返回文件路径,这样就可以使用临时文件路径上传到服务器中。加载矩形的同时需要清除画笔路线,在touchEnd的时候重新调用drawImage并且draw不带参数清空画布,循环存的矩形数组生成到页面上,到这就结束了 。有问题可以在下方留言,看到都会回复。

···········································································································································
补上里面的一个bug吧,安卓手机圈选的时候会出现问题,圈选的时候之前的路径不能完全清空,导致保存的图片带上路线,在保存的时候把 wx.canvasToTempFilePath 写在延时器中,延时执行就可以完美解决兼容性问题。好像是这样的,过去挺久了突然看到自己的帖子顺手补上。希望能帮到大家!

setTimeout(() => {
       	//事件
      }, 200);
<think>我们将在Vue3+UniApp中使用uView的u-upload组件实现以下功能:1.图片上传(支持多选)2.添加水印(文字水印)3.压缩图片(通过canvas绘制并调整质量)4.图片回显(在u-upload中显示)5.点击放大(u-upload内置支持)注意:由于微信小程序canvas层级问题,我们采用离屏canvas(不在页面中显示)来处理图片。步骤:1.配置u-upload组件,设置基本属性(maxCount,multiple,previewFullImage等)2.在afterRead事件中处理图片(加水印压缩)3.处理后的图片替换原临时路径,用于回显4.删除图片的处理具体实现:由于微信小程序canvas是原生组件,层级最高,我们不能在页面中使用canvas然后隐藏,而是采用创建离屏canvas(不在页面渲染)的方式。另外,注意性能问题,图片处理是耗时操作,需要给用户提示。代码结构:使用`<scriptsetup>`语法注意:在uni-app中,可以使用uni.createCanvasContext创建离屏canvas,但注意微信小程序canvas的绘制是异步的,我们需要使用Promise封装。由于处理图片需要获取图片信息(宽高),然后绘制到canvas,再导出图片,这个过程可能会比较慢,特别是多张图片时,需要做并发控制或提示。这里我们采用逐个处理并显示进度。但是,考虑到用户上一次的问题,我们尽量使用并发处理(使用Promise.all),但要注意小程序同时创建多个canvas可能会卡顿,所以这里我们使用串行处理,但提供加载提示。我们将实现:1.选择图片后,显示加载中2.每张图片依次处理(避免同时创建多个canvas导致性能问题)3.处理完成后,将处理后的图片路径添加到fileList另外,水印文字样式可以自定义,这里我们写死,也可以根据需求变成可配置。具体代码实现如下:</think>下面为您提供在 Vue3 + UniApp 中使用 uView 的 u-upload 组件实现图片上传、水印添加、压缩、回显点击放大功能的完整解决方案: ### 完整实现代码 ```vue <template> <view class="container"> <!-- 水印设置区域 --> <view class="settings"> <u-input v-model="watermarkText" placeholder="输入水印文字" /> <u-slider v-model="watermarkOpacity" :min="0.1" :max="1" :step="0.1"></u-slider> <text>透明度: {{ watermarkOpacity.toFixed(1) }}</text> </view> <!-- 图片上传组件 --> <u-upload :fileList="fileList" @afterRead="handleAfterRead" @delete="handleDelete" name="image" :maxCount="9" :previewFullImage="true" :multiple="true" imageMode="aspectFit" ></u-upload> <!-- 处理状态提示 --> <u-toast ref="uToast"></u-toast> </view> </template> <script setup> import { ref } from 'vue'; import { onLoad } from '@dcloudio/uni-app'; // 文件列表 const fileList = ref([]); // 水印配置 const watermarkText = ref('我的水印'); const watermarkOpacity = ref(0.7); // 图片选择后的处理 const handleAfterRead = async (event) => { const { file } = event; try { uni.showLoading({ title: '处理中...', mask: true }); // 串行处理每张图片(避免并发性能问题) for (const item of file) { const processedUrl = await processImage(item.url); fileList.value.push({ ...item, url: processedUrl, status: 'success' }); } } catch (err) { console.error('图片处理失败', err); uni.showToast({ title: '处理失败', icon: 'error' }); } finally { uni.hideLoading(); } }; // 图片处理函数(水印+压缩) const processImage = (tempFilePath) => { return new Promise(async (resolve, reject) => { try { // 1. 获取图片信息 const { width, height } = await uni.getImageInfo({ src: tempFilePath }); // 2. 创建离屏Canvas(避免层级问题) const canvasId = `watermarkCanvas_${Date.now()}`; const ctx = uni.createCanvasContext(canvasId, this); // 3. 计算缩放比例(保持宽高比) const maxSize = 1200; // 最大尺寸限制 const scale = Math.min(maxSize / Math.max(width, height), 1); const destWidth = Math.floor(width * scale); const destHeight = Math.floor(height * scale); // 4. 绘制原始图片(确保完整显示) ctx.drawImage(tempFilePath, 0, 0, destWidth, destHeight); // 5. 添加水印 ctx.setFontSize(24); ctx.setFillStyle(`rgba(0,0,0,${watermarkOpacity.value})`); ctx.setTextAlign('center'); ctx.setTextBaseline('middle'); // 水印位置(居中) ctx.fillText(watermarkText.value, destWidth / 2, destHeight / 2); // 平铺水印(可选) for (let i = 0; i < destWidth; i += 200) { for (let j = 0; j < destHeight; j += 100) { ctx.fillText(watermarkText.value, i, j); } } // 6. 生成压缩图片 ctx.draw(false, () => { uni.canvasToTempFilePath({ canvasId, destWidth, destHeight, fileType: 'jpg', quality: 0.75, // 压缩质量 success: (res) => { resolve(res.tempFilePath); }, fail: reject }, this); }); } catch (e) { reject(e); } }); }; // 删除图片 const handleDelete = (index) => { fileList.value.splice(index, 1); }; </script> <style scoped> .container { padding: 20rpx; } .settings { margin-bottom: 30rpx; padding: 20rpx; background-color: #f8f8f8; border-radius: 10rpx; } </style> ``` ### 核心功能实现说明 1. **水印添加**: - 使用 Canvas图片中心添加主水印 - 可选平铺水印效果(循环添加) - 支持自定义水印文字透明度 2. **图片压缩**: ```javascript // 尺寸压缩(保持宽高比) const maxSize = 1200; // 最大边长限制 const scale = Math.min(maxSize / Math.max(width, height), 1); // 质量压缩 quality: 0.75 // 0-1之间的质量参数 ``` 3. **图片完整显示**: ```html <!-- 关键属性 --> <u-upload imageMode="aspectFit" /> ``` - `aspectFit` 模式确保图片完整显示,不会被裁剪 - Canvas 绘制时保持原图比例 4. **点击放大功能**: ```html <!-- 启用预览功能 --> <u-upload :previewFullImage="true" /> ``` ### 性能优化技巧 1. **离屏 Canvas 处理**: ```javascript const canvasId = `watermarkCanvas_${Date.now()}`; const ctx = uni.createCanvasContext(canvasId, this); ``` - 避免 Canvas 层级问题 - 处理完成后自动销毁 2. **串行处理图片**: ```javascript for (const item of file) { const processedUrl = await processImage(item.url); // 依次添加处理结果 } ``` - 避免同时处理多张图片导致内存溢出 3. **智能压缩策略**: ```javascript // 根据原始尺寸动态调整压缩比例 const maxSize = originalSize > 5 * 1024 * 1024 ? 800 : 1200; ``` ### 扩展功能实现 1. **自定义水印样式**: ```javascript // 水印旋转角度 ctx.rotate(-20 * Math.PI / 180); // 水印描边效果 ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.strokeText(watermarkText.value, x, y); ``` 2. **上传到服务器**: ```javascript const uploadFile = (filePath) => { uni.uploadFile({ url: 'https://your-api.com/upload', filePath, name: 'file', success: (res) => { console.log('上传成功', res); } }); }; ``` 3. **处理进度显示**: ```javascript // 在循环中添加 const progress = ((index + 1) / file.length * 100).toFixed(0); uni.showLoading({ title: `处理中 ${progress}%` }); ``` ### 注意事项 1. **微信小程序层级问题**: - 使用离屏 Canvas 避免与其他组件层级冲突 - 确保 Canvas ID 唯一性 2. **大图处理优化**: ```javascript // 超大图片分块处理 if (width * height > 4000 * 4000) { console.warn('超大图片建议分块处理'); } ``` 3. **平台兼容性**: - 使用 UniApp API 确保多平台兼容 - 测试不同平台的 Canvas 实现差异 > **最佳实践**:对于需要上传的场景,建议先压缩加水印后再上传,可节省服务器资源用户流量[^1]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值