思 路
- 创建2个canvas,一个当作背景,一个当作拼图。
- 传入3个参数,x:canvas中裁剪区域的横坐标y:canvas中裁剪区域的纵坐标l:拼图的边长
- 背景图使用fill()的方法裁剪出一个洞
- 拼图使用wx.canvasToTempFilePath的方法裁剪成一个拼图图片。
- 小程序中的触控事件bindtouchmove,bindtouchend分别记录移动的距离和松手时的距离
- 判断移动距离和x的差距,如果两者小于一定阈值,则验证成功,反之失败。

开始操作
## 创建两个canvas
在微信小程序中,由于不能操作DOM,所以要想更改样式需要在组件中用style表明,在data中修改,通过{{}}渲染出来。比如我们需要控制canvas距离顶部的高度,除了在wxss中定义以外,还可以使用<canvas style='top:{{this.data.top}}rpx'>这种方式实现。小程序中操作创建canvas的方法如下:

  1. const canvas = wx.createCanvasContext('canvas1'),
  2. block = wx.createCanvasContext('block');

这样就创建了两个canvas画布

定义所需参数

  1. let l = 50, //拼图的边长
  2. x =150+Math.random()*(canvas_width-l-150), //裁剪的x坐标
  3. y = 10+Math.random()*(canvas_height-l-10);//裁剪的y坐标
  4. that.setData({
  5. block_w:l,
  6. y:y,
  7. x:x
  8. })

背景图的制作
使用block.drawImage(img, 0, 0, canvas_width, canvas_height);的方法使图片绘制到canvas上。
使用globalCompositeOperation = 'xor'的方法,使裁剪的那一块变得透明。

  1. block.beginPath()
  2. block.moveTo(x,y)
  3. block.lineTo(x,y+l)
  4. block.lineTo(x+l,y+l)
  5. block.lineTo(x+l,y)
  6. block.globalCompositeOperation = 'xor'
  7. block.fill()
  8. block.drawImage(img, 0, 0, canvas_width, canvas_height);
  9. block.draw( )

有可能这里无法显示图片,把代码放到 onReady 下就可以了,用本地图片

拼图的制作
使用wx.canvasToTempFilePath方法,从另一张canvas画布上截取一块。

  1. canvas.drawImage(img, 0, 0, canvas_width, canvas_height);
  2. canvas.draw(false, setTimeout(() => {
  3. wx.canvasToTempFilePath({
  4. x: x,
  5. y: y,
  6. width:l,
  7. height: l,
  8. canvasId: 'canvas1',
  9. fileType: 'png',
  10. success(res) {
  11. console.log(res.tempFilePath)
  12. that.setData({
  13. pic: res.tempFilePath
  14. })
  15. },
  16. fail: err => {
  17. console.log(err)
  18. }
  19. }, this)
  20. }, 500))

这样res.temFilePath就是我们截取出来的拼图了。
注意!如果图片是空白的,需要添加一个定时器 setTimeout() 清除 canvas 缓存
## 在滑动块上添加触控事件
滑动的过程需要两个事件来完成bindtouchmove='move' bindtouchend='end'
js中创建这两个事件。

  1. move:function(res){
  2. let left = res.touches[0].pageX;
  3. // 由于我这里是和page没有距离,也没有加外层盒子的,所以,pageX就是位移距离。
  4. if (left>0){
  5. this.setData({
  6. left: left
  7. })
  8. }
  9. else{
  10. this.setData({
  11. left:0
  12. })
  13. }
  14. },
  15. end:function(res){
  16. let end = this.data.left,
  17. moves = this.data.x;
  18. console.log(end)
  19. })
  20. }
  21. }

判断,出结果

  1. end:function(res){
  2. ...
  3. ...
  4. if (Math.abs(end-moves)<2){ //当小于2px的可接受阈值时,验证成功
  5. console.log('bingo')
  6. wx.showToast({
  7. title: '验证成功',
  8. icon:'success',
  9. duration:2000
  10. })
  11. setTimeout(function(){
  12. wx.redirectTo({
  13. url: 'verification',
  14. })
  15. },2000)
  16. }
  17. else{
  18. this.setData({
  19. left:0
  20. })
  21. }
  22. }

参考github/pages/index)

  1. <canvas canvas-id='canvas1' class='canvas' ></canvas>
  2. <canvas canvas-id='block' class='block' ></canvas>
  3. <cover-image wx:if='{{pic}}' src="{{pic}}" class='three' style='top:{{y}}px;left:{{left+20}}px'></cover-image>
  4. <view class='container'>
  5. <view class="area">
  6. <movable-area>
  7. <movable-view disable-scroll="true" direction="horizontal" bindtouchmove='move' bindtouchend='end' class='view' style='left:{{left}}px'>| | |</movable-view>
  8. </movable-area>
  9. </view>
  10. </view>
  11. <view class="title">
  12. 拖动上方滑块完成拼图
  13. <image src="../../styles/img/refresh.png" bindtap="refresh" class="refresh"></image>
  14. </view>
  15. <view class="prompt" wx:if="{{prompt}}">请控制切图块对齐缺口</view>

划重点 movable-area
这里因为小程序有左滑关闭,所以滑块区域和起始值加大一点

  1. /* pages/index/vfblock.wxss */
  2. .container{
  3. position: relative;
  4. overflow: hidden;
  5. margin-top:180px;
  6. height: 140rpx;
  7. }
  8. canvas{
  9. top: 0
  10. }
  11. .canvas{
  12. left: -9999px;
  13. position: absolute;
  14. z-index: -1;
  15. }
  16. .block{
  17. width: 100vw;
  18. height: 30vh;
  19. position: absolute;
  20. }
  21. .three{
  22. position: absolute;
  23. width: 50px;
  24. height: 50px;
  25. z-index: 999;
  26. border: 2px dashed rgb(252, 252, 252);
  27. box-sizing: border-box;
  28. }
  29. .view {
  30. text-align: center;
  31. height: 70rpx;
  32. width: 120rpx;
  33. line-height: 70rpx;
  34. background: #3a82fa;
  35. color: #fff;
  36. border-radius: 40rpx;
  37. overflow: hidden;
  38. box-shadow: 2px 2px 2px 2px rgba(44, 117, 244,0.3)
  39. }
  40. .area {
  41. height: 40rpx;
  42. width: 95%;
  43. background-color: #e5e5e5;
  44. /* overflow: hidden; */
  45. border-radius: 20rpx;
  46. margin: 50rpx auto;
  47. }
  48. movable-area{
  49. position: absolute;
  50. top:35rpx;
  51. width: 120rpx;
  52. height: 70rpx;
  53. }
  54. .title{
  55. text-align: center;
  56. font-weight: 500;
  57. font-size: 36rpx;
  58. width: 100%;
  59. height: 75rpx;
  60. line-height: 75rpx;
  61. position: relative;
  62. }
  63. .refresh{
  64. width: 60rpx;
  65. height: 60rpx;
  66. position: absolute;
  67. right: 40rpx;
  68. top: 50%;
  69. transform: translate(0,-50%);
  70. }
  71. .prompt{
  72. width: 95%;
  73. height: 44rpx;
  74. margin: 20rpx auto;
  75. color: #fa3a3a;
  76. font-size: 30rpx;
  77. font-weight: 500;
  78. }
  1. Page({
  2. /**
  3. * 页面的初始数据
  4. */
  5. data: {
  6. img:'',
  7. width:'',
  8. height:'',
  9. pic:'',
  10. y:'',
  11. x:'',
  12. left:0,
  13. prompt:false
  14. },
  15. /**
  16. * 生命周期函数--监听页面加载
  17. */
  18. onLoad: function (options) {
  19. },
  20. /**
  21. * 生命周期函数--监听页面初次渲染完成
  22. */
  23. onReady: function () {
  24. this.verification()
  25. },
  26. move:function(res){
  27. let left = res.touches[0].pageX;
  28. if (left>0){
  29. this.setData({
  30. left: left,
  31. prompt:false
  32. })
  33. }
  34. else{
  35. this.setData({
  36. left:0
  37. })
  38. }
  39. },
  40. end:function(res){
  41. var that = this
  42. let end = this.data.left,
  43. moves = this.data.x-20;
  44. if (Math.abs(end-moves)<5){
  45. console.log('bingo')
  46. }
  47. else{
  48. this.setData({
  49. left:0,
  50. prompt:true
  51. })
  52. //这里加了个动画提示效果
  53. this.animate('.prompt', [
  54. { translateX: -5 },
  55. { translateX: 5 },
  56. { translateX: -5 },
  57. { translateX: 5 },
  58. { translateX: -5 },
  59. { translateX: 5 },
  60. ], 200, function () {
  61. this.clearAnimation('.prompt', { translateX: true }, function () {
  62. })
  63. setTimeout(()=>{
  64. this.setData({
  65. prompt: false
  66. })
  67. },500)
  68. }.bind(this))
  69. }
  70. },
  71. //刷新
  72. refresh:function(){
  73. this.verification()
  74. this.setData({
  75. left:0
  76. })
  77. },
  78. verification:function()
  79. {
  80. let that = this;
  81. wx.getSystemInfo({
  82. success: function (res) {
  83. let width = res.windowWidth;
  84. let height = res.windowHeight;
  85. that.setData({
  86. width: width,
  87. height: height
  88. })
  89. },
  90. })
  91. const canvas = wx.createCanvasContext('canvas1');
  92. const block = wx.createCanvasContext('block'),
  93. three = wx.createCameraContext('three');
  94. const img = that.data.img,
  95. canvas_width = that.data.width,
  96. canvas_height = that.data.height * 0.3;
  97. let l = 50,
  98. x = 150 + Math.random() * (canvas_width - l - 150),
  99. y = 10 + Math.random() * (canvas_height - l - 10);
  100. that.setData({
  101. block_w: l,
  102. y: y,
  103. x: x
  104. })
  105. canvas.drawImage(img, 0, 0, canvas_width, canvas_height);
  106. canvas.draw(false, setTimeout(() => {
  107. wx.canvasToTempFilePath({
  108. x: x,
  109. y: y,
  110. width: l,
  111. height: l,
  112. canvasId: 'canvas1',
  113. fileType: 'png',
  114. success(res) {
  115. console.log(res.tempFilePath)
  116. that.setData({
  117. pic: res.tempFilePath
  118. })
  119. },
  120. fail: err => {
  121. console.log(err)
  122. }
  123. }, this)
  124. }, 500))
  125. block.beginPath()
  126. block.moveTo(x, y)
  127. block.lineTo(x, y + l)
  128. block.lineTo(x + l, y + l)
  129. block.lineTo(x + l, y)
  130. block.globalCompositeOperation = 'xor'
  131. block.fill()
  132. block.drawImage(img, 0, 0, canvas_width, canvas_height);
  133. block.draw()
  134. }
  135. })