技术博客--微信小程序canvas实现图片编辑
阅读原文时间:2022年05月14日阅读:1

技术博客--微信小程序canvas实现图片编辑

我们的这个小程序不仅仅是想给用户提供一个保存和查找的平台,还希望能给用户一个展示自己创意的舞台,因此我们实现了图片的编辑部分。我们对对图片的编辑集成了很多个不同的模块。根据用户的需求,我们主要实现了6个不同的功能,包括添加文字,图片涂鸦,添加滤镜,拼接图片,宣传图片,裁切图片。

虽然这些都是比较基础的图片的编辑能力,但是通过我们的优化,我们能够比较方便直观的修改我们的表情。通过结合多个不同的编辑操作,我们可以比较方便的实现出很多酷炫的效果。我们实现了一个比较有逻辑的平台,用户对于表情的编辑都会累加到上面,因此,用户只需要不断的编辑,效果就会累加,直到用户满意为止。

下面,我们将重点的分享一下几个比较重点的处理方法。

1、前置知识

需要掌握有小程序中canvas的相关的方法使用,例如:

const ctx = wx.createCanvasContext("canvasId");//创建上下文
//scaleX水平缩放,scaleY垂直缩放,skewX水平倾斜,skewY垂直倾斜,translateX水平移动,translateY垂直移动,这个可以做缩放和移动
ctx.transform(scaleX, skewX, skewY, scaleY, translateX, translateY);
ctx.setFillStyle("red");//设置填充颜色为red
ctx.setStrokeStyle("red");//设置线条颜色
ctx.setLineWidth(10);//设置线条为10像素
ctx.fillRect(0,0,100,100);//在(0,0)位置画一个100*100像素的填充矩形
ctx.strokeRect(100,100,100,100);//在(100,100)位置画一个100*100像素的不填充矩形
ctx.moveTo(0, 100);//画线,起点
ctx.lineTo(100, 100);//画线,到哪
ctx.scale(2,2);//放大两倍,控制缩放
ctx.setFontSize(10);//设置字体大小
//在(100,100)的位置写"hello world",最后一个100是文本最大宽度,可以省略
ctx.fillText("hello world",100,100,100);
ctx.stroke();//对当前路径进行描边,说白了就是画线

这只是一些使用的简单例子,更加详细的使用说明可以看官方文档或者自行百度学习。

在设置好之后就可以使用ctx.draw()方法进行绘制,draw()方法可以直接使用,也可以加参数使用,参数情况下有两个参数,第一个是reserve,boolean类型,默认为false,表示是否清空当前画布进行重画,还有一个就是callback回调函数,注意在需要导出图片时最好是在draw内部使用wx.canvastotempfilepath函数作为回调函数来进行指定画布区域的内容的导出,不然很有可能由于画布上还未进行渲染而导出失败。

当然,对图片进行编辑最首要的是先获得图片,我们可以直接通过wx.chooseImage方法来从相册获得想要编辑的图片,参数信息如下:

2、图片旋转

图片旋转的实现较为简单,主要的方式就是使用canvas的旋转的接口rotate。rotate方法具体是以原点为中心旋转canvas的坐标轴,默认情况之下原点是canvas的左上角,因此要是不管不顾的直接使用rotate旋转某个角度就会出现部分图片旋转出了画布的显示范围,因此在使用时要根据我们需要旋转的角度调整好原点的位置和图片画布的大小,才能够完整的显示出整个图片。在我们的小程序中我们固定一次旋转是旋转90度,因此需要使用translate()方法来调整原点的位置到画布的右上角

 cxt.translate(that.data.cWidth,0);  //cWidth是canvas的宽

然后为了保证旋转后的图片完整的出现(长方形图片旋转后无法完整出现在画布中),对图片进行了一定的压缩,代码如下:

cxt.rotate(90 * Math.PI / 180);
      cxt.drawImage(that.data.curImage, 0, 0, cHeight, cWidth); //改变绘制的长宽
      cxt.draw(false,wx.canvasToTempFilePath({
        canvasId: 'edit',
        //destWidth: 3*cWidth,
        //destHeight: 3*cHeight,
        success: function(res) {
          console.log("success!更新curImage")
          console.log(res.tempFilePath);
          that.setData({
            curImage: res.tempFilePath
          })
        }
      }));

3、滤镜实现

以下几个滤镜的实现主要用到的方法是wx.canvasGetImageData(OBJECT, this)和wx.canvasPutImageData(OBJECT, this),使用这两个方法来获取图片的数据信息并对图片的像素点进行修改从而实现图片的滤镜效果(利用修改图片每个像素的颜色RGBA来实现滤镜)。

  • 灰度滤镜

    效果图如下:

    wx.canvasGetImageData({
    canvasId: 'canvas',
    x: 0,
    y: 0,
    width: 300,
    height: 300,
    success(result) {
    let data = result.data;
    for (let i = 0; i < result.width * result.height;i++){ //只有这里有区别* let R = data[i * 4 + 0]; let G = data[i * 4 + 1]; let B = data[i * 4 + 2]; let grey = R * 0.3 + G * 0.59 + B * 0.11; data[i * 4 + 0] = grey; data[i * 4 + 1] = grey; data[i * 4 + 2] = grey; //只有这里有区别¨C11C¨C12C*
    }
    wx.canvasPutImageData({
    canvasId: 'canvasNew',
    x: 0,
    y: 0,
    width: 300,
    data: data,
    success(res) {
    console.log(res)
    }
    })
    }
    })
    })

  • 黑白滤镜

    效果图如下:

    wx.canvasGetImageData({
            canvasId: 'canvas',
            x: 0,
            y: 0,
            width: 300,
            height: 300,
            success(result) {
              let data = result.data;
              for (let i = 0; i < result.width * result.height;i++){
              //********************只有这里有区别****************************
                let R = data[i * 4 + 0];
                let G = data[i * 4 + 1];
                let B = data[i * 4 + 2];
                let grey = R * 0.3 + G * 0.59 + B * 0.11;
                if (grey > 125){
                  grey=255;
                } else {
                  grey = 0;
                }
                data[i * 4 + 0] = grey;
                data[i * 4 + 1] = grey;
                data[i * 4 + 2] = grey;
              //********************只有这里有区别****************************
              }
              wx.canvasPutImageData({
                canvasId: 'canvasNew',
                x: 0,
                y: 0,
                width: 300,
                data: data,
                success(res) {
                  console.log(res)
                }
              })
            }
          })
        })
  • 反相滤镜

    效果图如下:

    wx.canvasGetImageData({
            canvasId: 'canvas',
            x: 0,
            y: 0,
            width: 300,
            height: 300,
            success(result) {
              let data = result.data;
              for (let i = 0; i < result.width * result.height;i++){
              //********************只有这里有区别****************************
                let R = data[i * 4 + 0];
                let G = data[i * 4 + 1];
                let B = data[i * 4 + 2];
                data[i * 4 + 0] = 255-R;
                data[i * 4 + 1] = 255-G;
                data[i * 4 + 2] = 255-B;
              //********************只有这里有区别****************************
              }
              wx.canvasPutImageData({
                canvasId: 'canvasNew',
                x: 0,
                y: 0,
                width: 300,
                data: data,
                success(res) {
                  console.log(res)
                }
              })
            }
          })
        })
  • 马赛克滤镜

    马赛克滤镜的实现方法稍有不同,主要思路是将整个图片分为多个小的像素块(例如10*10的像素块),取小像素块中的某个像素的颜色作为整个像素块的颜色填充,从而实现模糊马赛克的效果。

    效果图如下:

    wx.canvasGetImageData({
            canvasId: 'canvas',
            x: 0,
            y: 0,
            width: 300,
            height: 300,
            success(result) {
              let data = result.data;
              const size = 10;
              const totalnum = size*size;
              for(let i=0;i<result.height;i+=size){
                for(let j=0;j<result.width;j+=size){
                  var totalR=0,totalG=0,totalB=0;
                  for(let dx=0;dx<size;dx++){
                    for(let dy=0;dy<size;dy++){
                      var x = i+dx;
                      var y = j+dy;
                  var p = x * result.width + y;
                  totalR += data[p * 4 + 0];
                  totalG += data[p * 4 + 1];
                  totalB += data[p * 4 + 2];
                }
              }
    
              var p = i * result.width + j;
              var resR = totalR / totalnum;
              var resG = totalG / totalnum;
              var resB = totalB / totalnum;
              for (let dx = 0; dx &lt; size; dx++){
                for (let dy = 0; dy &lt; size; dy++) {
                  var x = i + dx;
                  var y = j + dy;
    
                  var p = x * result.width + y;
                  data[p * 4 + 0] = resR;
                  data[p * 4 + 1] = resG;
                  data[p * 4 + 2] = resB;
                }
              }
            }
          }
          wx.canvasPutImageData({
            canvasId: 'canvasNew',
            x: 0,
            y: 0,
            width: 300,
            data: data,
            success(res) {
              console.log(res)
            }
          })
        }
      })
    })</code></pre></li>

4、图片涂鸦

画一条线的方法需要在canvas上绑定两个函数:touchstart和touchmove

<canvas canvas-id="myCanvas" disable-scroll="true" bindtouchstart="touchStart"
  bindtouchmove="touchMove" wx:if="{{hasChoosedImg}}"
  style="height: {{cHeight}}px; width: {{cWidth}}px;" />

touchstart记录下路线开始的点的坐标,并设置好线的颜色和宽度,然后在touchmove函数中,随着移动事件,记录下来移动过程中的每个点的坐标,这样子就能够得到一条线的路径,并将其存储下来,然后根据每次移动的两个点画出它们的二次贝塞尔曲线(使用ctx.quadraticCurveTo()方法),最后得到一条平滑的涂鸦出来的线条。

想要撤回时就可以简单的通过将存储路径数据结构清空最后一个再重新画图,就可以实现撤销的操作啦。

  touchStart: function (e) {
    // 开始画图,隐藏所有的操作栏
    this.setData({
      color: false,
      width: false,
      canvasHeightLen: 0,
      prevPosition: [e.touches[0].x, e.touches[0].y],
      movePosition: [e.touches[0].x, e.touches[0].y],
    });
    const { r, g, b } = this.data;
    let color = `rgb(${r},${g},${b})`;
    let width = this.data.w;
    startTouch(e, color, width, this.data.masaic);
  },

  touchMove: function (e) {
    const { r, g, b, prevPosition, movePosition, eraser, w, } = this.data;
    // 触摸,绘制中。。
    const ctx = wx.createCanvasContext('myCanvas');
    // 画笔的颜色
    let color = `rgb(${r},${g},${b})`;
    let width = w;
    if (eraser) {
      ctx.clearRect(e.touches[0].x, e.touches[0].y, 30, 30);
      ctx.draw();
      return;
    }

    const [pX, pY, cX, cY] = [...prevPosition, e.touches[0].x, e.touches[0].y];
    const drawPosition = [pX, pY, (cX + pX) / 2, (cY + pY) / 2];
    if (this.data.masaic == true) {
      ctx.setFillStyle('red')
      ctx.fillRect(e.touches[0].x, e.touches[0].y, 10, 10)
      ctx.fillRect(e.touches[0].x + 10, e.touches[0].y + 10, 10, 10)
      ctx.setFillStyle('pink')
      ctx.fillRect(e.touches[0].x + 10, e.touches[0].y, 10, 10)
      ctx.fillRect(e.touches[0].x, e.touches[0].y + 10, 10, 10)
      ctx.draw(true)
    }else {
      ctx.setLineWidth(width);
      ctx.setStrokeStyle(color);

      ctx.setLineCap('round');
      ctx.setLineJoin('round');
      ctx.moveTo(...movePosition);
      ctx.quadraticCurveTo(...drawPosition);
      ctx.stroke();
      ctx.draw(true);
    }

    recordPointsFun(movePosition, drawPosition)
  }

粗细的调整很简单,直接调整绘制时候的线的粗细就可以实现,颜色的调整则也还是通过调整像素点的rgb值来进行颜色的变动,颜色的rgb变动代码如下:

<view class="choose-box" wx:if="{{color}}">
  <view class="color-box" style="background: {{'rgb(' + r + ', ' + g + ', ' + b + ')'}}; height: {{w}}px; border-radius: {{w/2}}px"></view>
  <slider min="0" max="255" step="1" show-value="true" activeColor="red" value="{{r}}" data-color="r" bindchange="changeColor"/>
  <slider min="0" max="255" step="1" show-value="true" activeColor="green" value="{{g}}" data-color="g" bindchange="changeColor"/>
  <slider min="0" max="255" step="1" show-value="true" activeColor="blue" value="{{b}}" data-color="b" bindchange="changeColor"/>
</view>

再根据得到的rgb值,用如下代码就可以合成颜色:

let color = `rgb(${r},${g},${b})`;

效果图如下:

5、图片保存

在对所选择的图片完成了处理之后,我们还需要对处理后的图片进行保存,就可以直接使用wx.canvasToTempFilePath方法来将指定的画布区域的内容进行保存,所得到的新的图片的临时路径可以在其seccess的回调方法中获得,例如:

wx.canvasToTempFilePath({
   canvasId: 'edit',
   success: function(res) {
     that.setData({
       curImage: res.tempFilePath   //res.tempFilePath就是该图片的临时路径
     })
   }
})

注意该方法最好作为draw的回调函数使用才能保证获得图片一定成功。

得到了图片的路径之后,就可以调用wx.saveImageToPhotosAlbum方法来将图片保存到本地啦。

wx.saveImageToPhotosAlbum({
      filePath: path,
      success (res) {
        wx.showToast({
          title: '保存成功',
        })
      }
    })

总结一下,在这个板块的开发中,我们学到了很多图片处理的算法,这对于以前对此一无所知的我们来说是一个非常大的提升。同时,在开发完成这个板块的功能之后,我们感受非常有成就感。因为这个部分的一个一个的操作都是我们通过自己的代码实现。