微信小程序:将图片裁剪成九宫格图片(九张图片发朋友圈)

1.先看小程序界面效果

91.jpg
92.jpg
93.jpg

2.用户故事

1)用户选择本地照片或拍照上传
2)用户拖动九宫格,选择截取的区域,九宫格可缩放
3)用户点击裁剪按钮,按九宫格裁剪照片
4)小程序按九宫格形式显示裁剪出的九张图片
5)用户满意则可以保存九张图片到本地相册,不满意可以返回上一步
6)用户朋友圈发九宫格图片

朋友圈效果

94.jpg

3.小程序实现的主要技术说明

page.wxml

页面主要使用了movable-area、image、movable-view、canvas标签
1)movable-area标签内包含了image和movable-view
2)image显示用户上传的照片,movable-area大小和image照片大小一致
3)movable-view设计成九宫格,可在movable-area区域内移动和缩放
4)canvas用于画用户上传的照片,支持从canvas中截取一块区域保存为图片,canvas隐藏对用户不可见

page.js

js代码中主要使用了
1)wx.chooseMedia 选择照片或拍照
2)wx.getImageInfo 获取照片的宽度和高度
3)wx.createSelectorQuery 获取和操作canvas
4)wx.getSystemInfoSync().pixelRatio 获取设备像素比
5)wx.getSystemInfoSync().windowWidth 获取屏幕宽度,单位px
6)canvas.createImage() 创建图片对象
7)ctx.drawImage 将图片对象写入canvas
8)wx.canvasToTempFilePath 将canvas指定区域的内容导出为图片
9)wx.previewImage 图片预览
10)wx.saveImageToPhotosAlbum 将图片保存到相册

4.完整代码

page.wxml

<view class="page">

  <view class="top"></view>

  <view class="body" wx:if="{{step!=3}}">
    <movable-area style="width:{{imageScreenWidth}}rpx;height:{{imageScreenHeight}}rpx;">
      <image src="{{sourceImageUrl}}" style="width:{{imageScreenWidth}}rpx;height:{{imageScreenHeight}}rpx;" wx:if="{{sourceImageUrl}}"></image>
      <movable-view 
        direction="all" 
        bindchange="onChange" 
        scale scale-min="0.2" scale-max="1" bindscale="onScale" 
        style="width:{{gridWidth}}rpx;height:{{gridHeight}}rpx;">
          <block wx:for="{{9}}" wx:for-item="cell" wx:for-index="cellIdx" wx:key="cellIdx">
            <view class="cell light bg-green" style="width:{{gridWidth/3-4}}rpx;height:{{gridHeight/3-3}}rpx;">{{cellIdx+1}}</view>      
          </block>
      </movable-view>
    </movable-area>
    <view class="btnLarge bg-green" wx:if="{{step==1}}" bindtap="onChooseImage">上传照片/拍照</view>
    <view class="btnWrap" wx:if="{{step==2}}">
      <view class="btnSmall bg-green" bindtap="onChooseImage">更换照片</view>
      <view class="btnLarge bg-blue" bindtap="onCutImage">按网格裁剪图片</view>
    </view>
    <view class="tips text-red" wx:if="{{step==2}}">图片上的网格可移动、可缩放</view>
  </view>
  <view class="body" wx:if="{{step==3}}">
    <view class="imageWrap">
      <block wx:for="{{cutImageList}}" wx:for-item="image" wx:for-index="imageIdx" wx:key="imageIdx">
        <image 
          src="{{image}}" 
          style="width:{{imageMaxWidth/3-4}}rpx;height:{{imageMaxWidth/3-3}}rpx;" 
          bindtap="onPreviewImage" 
          data-idx="{{imageIdx}}"></image>
      </block>
    </view>
    <view class="btnWrap" wx:if="{{step==3}}">
      <view class="btnSmall bg-blue" bindtap="onBackStep2">返回上一步</view>
      <view class="btnLarge bg-orange" bindtap="onSaveImages">保存所有图片</view>
    </view>
  </view>

  <view class="bottom"></view>

</view>

<!--通过css(position:fixed; left:100%;)隐藏canvas-->
<!--A4是2480*3508象素 210*297毫米-->
<canvas
  type="2d"
  id="canvas"
  canvas-id="canvas"
  style="width:{{imageScreenWidth2Px}}px; height:{{imageScreenHeight2Px}}px; position:fixed; left:100%;"
></canvas>

page.js

// pages/tools/cut9.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    step: 1,

    gridX: 0,
    gridY: 0,
    gridScale: 1,

    imageMaxWidth: 690,
    sourceImageWidth: 690,
    sourceImageHeight: 690,
    imageScreenWidth: 690,
    imageScreenHeight: 690,
    imageScreenWidth2Px: Math.floor(690/750*wx.getSystemInfoSync().windowWidth),
    imageScreenHeight2Px: Math.floor(690/750*wx.getSystemInfoSync().windowWidth),
    gridWidth: 690,
    gridHeight: 690,

    cutImageList: [],
  },

  // 选择照片
  onChooseImage: function (e) {
    var that = this
    wx.chooseMedia({
      camera: 'back',
      sourceType: ['album', 'camera'],
      mediaType: ['image'],
      count: 1,
      success(res) {
        // console.log('### choose media success', res)
        // 0: {tempFilePath: "http://tmp/puQIgiWCT7TAcd9be358091eb7161d1af7d57c191eb4.jpg", size: 263247, fileType: "image"}
        let sourceImageUrl = res.tempFiles[0].tempFilePath
        that.setData({
          sourceImageUrl: sourceImageUrl
        })
        that.getSourceImageInfo()
      },
      fail(err) {
        console.error('### choose media failure', err)
      }
    })
  },

  // 获取图片宽高
  getSourceImageInfo() {
    var that = this
    let sourceImageUrl = this.data.sourceImageUrl
    wx.getImageInfo({
      src: sourceImageUrl,
      success (res) {
        // console.log('### get image info success', res)

        let sourceImageWidth = res.width
        let sourceImageHeight = res.height
        console.log('### source image width & height', sourceImageWidth, sourceImageHeight)
      
        let imageMaxWidth = that.data.imageMaxWidth
        let imageScreenWidth = sourceImageWidth
        let imageScreenHeight = sourceImageHeight        
        imageScreenWidth = imageMaxWidth
        imageScreenHeight = Math.floor((sourceImageHeight * imageMaxWidth) / sourceImageWidth)
        console.log('### image screen width & height', imageScreenWidth, imageScreenHeight)

        let imageScreenWidth2Px = Math.floor(imageScreenWidth/750*wx.getSystemInfoSync().windowWidth)
        let imageScreenHeight2Px = Math.floor(imageScreenHeight/750*wx.getSystemInfoSync().windowWidth)
        console.log('### image screen width(px) & height(px)', imageScreenWidth2Px, imageScreenHeight2Px)

        let gridWidth = imageScreenWidth<=imageScreenHeight ? imageScreenWidth : imageScreenHeight
        let gridHeight = gridWidth
        console.log('### grid width & height', gridWidth, gridHeight)

        that.setData({
          sourceImageWidth: sourceImageWidth,
          sourceImageHeight: sourceImageHeight,
          imageScreenWidth: imageScreenWidth,
          imageScreenHeight: imageScreenHeight,
          imageScreenWidth2Px: imageScreenWidth2Px,
          imageScreenHeight2Px: imageScreenHeight2Px,
          gridWidth: gridWidth,
          gridHeight: gridHeight,
          step: 2,
        })
      },
      fail(err) {
        console.error('### get image info failure', err)
      }
    })
  },

  // 移动网格
  onChange: function (e) {
    // console.log(e)
    let x = e.detail.x
    let y = e.detail.y
    // console.log('### onChange() x, y', x, y)
    this.setData({
      gridX: x,
      gridY: y,
    })
  },

  // 缩放网格
  onScale: function (e) {
    // console.log(e)
    let x = e.detail.x
    let y = e.detail.y
    let scale = e.detail.scale
    // console.log('### onScale() x, y, scale', x, y, scale)
    this.setData({
      gridX: x,
      gridY: y,
      gridScale: scale,
    })
  },

  // 按网格切图
  onCutImage: function (e) {
    // 显示提示
    wx.showLoading({
      title: '正在裁剪...',
      mask: true,
    })
    this.onCanvas()
  },
  
  // 调用 canvas
  onCanvas() {
    // 通过 SelectorQuery 获取 Canvas 节点
    console.log('### get canvas node.')
    wx.createSelectorQuery()
      .select('#canvas')
      .fields({
        node: true,
        size: true,
      })
      .exec((res)=>{
        this.initCanvas(res)
      })
  },
  // 初始化 canvas
  initCanvas(res) {
    var that = this
    
    const width = res[0].width
    const height = res[0].height
    console.log('### initial width & height', width, height)

    const canvas = res[0].node
    const ctx = canvas.getContext('2d')

    try {
      const dpr = wx.getSystemInfoSync().pixelRatio
      console.log('### dpr', dpr)

      canvas.width = width * dpr
      canvas.height = height * dpr
      console.log('### by dpr set canvas width & height', canvas.width, canvas.height)

      that.setData({
        canvasWidth: canvas.width,
        canvasHeight: canvas.height,
        dpr: dpr,
      })
      ctx.scale(dpr, dpr)
      // 添加图片到 canvas
      this.addImage2Canvas(canvas, ctx)
    } catch (e) {
      console.log('### init canvas catch error', e)
    }
  },
  // 添加图片到 canvas
  async addImage2Canvas(canvas, ctx) {
    let imageUrl = this.data.sourceImageUrl
    // 创建图片对象
    console.log('### create image object.', imageUrl)
    const image = canvas.createImage()
    image.src = imageUrl
    // 绑定图片 onload 事件
    let imageObject = await new Promise((resolve, reject) => {
      image.onload = () => {
        console.log('### image onload success.')
        resolve(image)
      }
      image.onerror = (e) => {
        console.log('### image onload error.')
        reject(e)
      }
    })    
    // 添加图片
    // let imageWidth = this.data.sourceImageWidth
    // let imageHeight = this.data.sourceImageHeight
    let imageWidth = this.data.imageScreenWidth2Px
    let imageHeight = this.data.imageScreenHeight2Px
    console.log('### imageWidth, imageHeight', imageWidth, imageHeight)
    ctx.drawImage(imageObject, 0, 0, imageWidth, imageHeight)
    ctx.save()
    // 裁剪
    this.canvasCut(canvas)
  },

  // 裁剪
  canvasCut(canvas) {
    // 裁剪出第1张图片
    wx.showLoading({
      title: '正在裁剪...',
    })
    this.cutOneImage(0, canvas)
  },

  // 裁剪1张图片
  cutOneImage(idx, canvas) {
    var that = this
    let gridX = this.data.gridX
    let gridY = this.data.gridY
    console.log('### gridX, gridY', gridX, gridY)

    let baseX = Math.floor(this.data.gridX) //
    let baseY = Math.floor(this.data.gridY) //
    // let baseX = Math.floor(gridX*(this.data.imageScreenWidth/this.data.sourceImageWidth)) //
    // let baseY = Math.floor(gridY*(this.data.imageScreenWidth/this.data.sourceImageWidth)) //
    console.log('### baseX, baseY', baseX, baseY)
    
    let imageScreenWidth2Px = this.data.imageScreenWidth2Px
    let imageScreenHeight2Px = this.data.imageScreenHeight2Px
    let gridScale = this.data.gridScale
    // 纵向图片使用宽,横向图片使用高
    let cellWidth = Math.floor((imageScreenWidth2Px<imageScreenHeight2Px?imageScreenWidth2Px:imageScreenHeight2Px)/3*gridScale)
    let cellHeight = cellWidth
    let cutImageList = this.data.cutImageList

    let x = baseX+((idx%3)*cellWidth)
    let y = baseY+(Math.floor(idx/3)*cellHeight)
    console.log('### x, y, cellWidth, cellHeight', x, y, cellWidth, cellHeight)
    wx.canvasToTempFilePath({
      x: x,
      y: y,
      width: cellWidth,
      height: cellHeight,
      // destWidth: 100,
      // destHeight: 100,
      canvas: canvas,
      success(res) {
        console.log('### Canvas To Temp File Path Success.', idx, res.tempFilePath)
        cutImageList.push(res.tempFilePath)
      },
      fail(err) {
        console.log('### Canvas To Temp File Path ERROR.', err)
      },
      complete() {
        if(idx+1<9) {
          // 继续裁剪
          wx.showLoading({
            title: '正在裁剪第'+(idx+1)+'张',
          })
          that.cutOneImage(idx+1, canvas)
        }else {
          that.setData({
            cutImageList: cutImageList,
            step: 3,
          })
          // 隐藏提示
          wx.hideLoading({
            success: (res) => {},
          })
        }
      }
    })
  },

  // 预览图片
  onPreviewImage: function (e) {
    console.log('### preview image.')
    let idx = e.currentTarget.dataset.idx
    let cutImageList = this.data.cutImageList
    wx.previewImage({
      current: cutImageList[idx],
      urls: cutImageList,
      success(res) {
        console.log('### preview image success.', res)
      },
      fail(err) {
        console.log('### preview image failure.', err)
      }
    })
  },

  // 返回第二步
  onBackStep2: function (e) {
    this.setData({
      cutImageList: [],
      step: 2,
    })
  },

  // 保存所有图片
  onSaveImages: function (e) {
    wx.showLoading({
      title: '开始保存...',
    })
    // 保存第1张
    this.saveImage(0)
  },

  // 保存单张图片到相册
  saveImage(idx) {
    var that = this
    let cutImageList = this.data.cutImageList
    let filePath = cutImageList[idx]
    wx.saveImageToPhotosAlbum({
      filePath: filePath,
      success(res) {
        console.log('### Save Image To Photos Album SUCCESS.', idx)
        wx.showLoading({
          title: '已保存第'+(idx+1)+'张  ',
        })
      },
      fail(err) {
        console.log('### Save Image To Photos Album FAILURE.', err)
      },
      complete() {
        if((idx+1)<9) {
          that.saveImage(idx+1)
        }else {
          wx.hideLoading({
            success: (res) => {
              wx.showToast({
                title: '已保存所有图片',
              })
            },
          })
        }
      }
    })
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {

  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {

  }
})

page.wxss

.page {  
  display: flex;
  flex-direction: column;
  justify-content: start;
  align-items: center;
}



.body {
  width: 100%;

  display: flex;
  flex-direction: column;
  justify-content: start;
  align-items: center;
}
movable-area image {
  border: 1rpx solid #cccccc;
}
movable-view {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;  
}
.cell {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  opacity: 0.8;
  margin: 1rpx;
}


.imageWrap {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;  
}
.imageWrap image {
  margin: 1rpx;
}


.btnWrap {
  width: 100%;

  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
}
.btnSmall {
  width: 280rpx;
}
.btnLarge {
  width: 380rpx;
}
.btnLarge, .btnSmall {
  padding: 25rpx 0;
  border-radius: 15rpx;
  margin: 25rpx 10rpx;
  text-align: center;
}
.tips {
  font-size: small;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容