1.先看小程序界面效果
91.jpg 92.jpg 93.jpg2.用户故事
1)用户选择本地照片或拍照上传
2)用户拖动九宫格,选择截取的区域,九宫格可缩放
3)用户点击裁剪按钮,按九宫格裁剪照片
4)小程序按九宫格形式显示裁剪出的九张图片
5)用户满意则可以保存九张图片到本地相册,不满意可以返回上一步
6)用户朋友圈发九宫格图片
朋友圈效果
94.jpg3.小程序实现的主要技术说明
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;
}
网友评论