微信小程序canvas拖拽、截图组件功能


Posted in Javascript onSeptember 04, 2018

先看下微信小程序canvas拖拽功能

组件地址

github.com/jasondu/wx-… readme近期补上

实现效果

微信小程序canvas拖拽、截图组件功能

如何实现

  1. 使用canvas
  2. 使用movable-view标签

由于movable-view无法实现旋转,所以选择使用canvas

需要解决的问题

  • 如何将多个元素渲染到canvas上
  • 如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层
  • 如何实现拖拽元素
  • 如何缩放、旋转、删除元素

看起来挺简单的嘛,就把上面这几个问题解决了,就可以实现功能了;接下来我们一一解决。

如何将多个元素渲染到canvas上

定义一个DragGraph类,传入元素的各种属性(坐标、尺寸…)实例化后推入一个 渲染数组 里,然后再循环这个数组调用实例中的渲染方法,这样就可以把多个元素渲染到canvas上了。

如何知道手指在元素上、如果多个元素重叠如何知道哪个元素在最上层

在DragGraph类中定义了判断点击位置的方法,我们在canvas上绑定touchstart事件,将手指的坐标传入上面的方法,我们就可以知道手指是点击到元素本身,还是删除图标或者变换大小的图标上了,这个方法具体怎么判断后面会讲解。

通过循环 渲染数组 判断是非点击到哪个元素到,如果点击中了多个元素,也就是多个元素重叠,那第一个元素就是最上层的元素啦。

###如何实现拖拽元素

通过上面我们可以判断手指是否在元素上,当touchstart事件触发时我们记录当前的手指坐标,当touchmove事件触发时,我们也知道这时的坐标,两个坐标取差值,就可以得出元素位移的距离啦,修改这个元素实例的x和y,再重新循环渲染 渲染数组 就可以实现拖拽的功能。

如何缩放、旋转、删除元素

这一步相对比较难一点,我会通过示意图跟大家讲解。

微信小程序canvas拖拽、截图组件功能

我们先讲缩放和旋转

通过touchstart和touchmove我们可以获得旋转前的旋转后的坐标,图中的线A为元素的中点和旋转前点的连线;线B为元素中点和旋转后点的连线;我们只需要求A和B两条线的夹角就可以知道元素旋转的角度。缩放尺寸为A和B两条线长度之差。

计算旋转角度的代码如下:

const centerX = (this.x + this.w) / 2; // 中点坐标
const centerY = (this.y + this.h) / 2; // 中点坐标
const diffXBefore = px - centerX;  // 旋转前坐标
const diffYBefore = py - centerY;  // 旋转前坐标
const diffXAfter = x - centerX;   // 旋转后坐标
const diffYAfter = y - centerY;   // 旋转后坐标
const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180;
const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180;
// 旋转的角度
this.rotate = currentGraph.rotate + angleAfter - angleBefore;

计算缩放尺寸的代码如下:

// 放大 或 缩小
this.x = currentGraph.x - (x - px);
this.y = currentGraph.y - (x - px);

下面介绍下小程序canvas截图组件

最近做一个小程序的过程中,需要用到截图功能,网上搜了一下,发现没有符合要求的,就自己搞了个组件,方便复用。

目前功能很简单,传入宽高和图片路径即可,宽高是为了计算截图的比例,只支持缩放和移动。

实现思路是:

1.模拟一个截取框;

2.移动图片位置,缩放图片;

3.获取图片在其中的位置(left,top,width,height);

4.使用canvas绘制图片,然后截取就ok了。

其中第二步的缩放图片比较麻烦,缩放中心点以及平滑缩放

以下是我的实现方式

wxml:

<!--component/picPro/picPro.wxml-->
<scroll-view class='body' hidden="{{hidden}}">
<view class='flex-column flex-between full-height full-width' bindtouchstart="touchstart" bindtouchmove="touchmove" bindtouchend="touchend">
<view class='bg_dark out_item'></view>
<view class='flex-row main flex-between' style='height:{{(windowWidth - margin.left - margin.right)/ratio + "px"}}'>
<view class='bg_dark main_item full-height' style='width:{{margin.left + "px"}}'></view>
<view class='inner relative full-width' id='showArea'>
<image class='absolute img' src='{{src}}' style="width:{{img.width}}px;height:{{img.height}}px;left:{{img.left}}px;top:{{img.top}}px;"></image>
<canvas canvas-id='imgCanvas' class='absolute img_canvas full-height full-width' />
<view class='absolute inner_item left_top'></view>
<view class='absolute inner_item right_top'></view>
<view class='absolute inner_item right_bottom'></view>
<view class='absolute inner_item left_bottom'></view>
</view>
<view class='bg_dark main_item full-height' style='width:{{margin.right + "px"}}'></view>
</view>
<view class='bg_dark out_item flex-column flex-end'>
 <view class='flex-around text_white text_bg'>
<view catchtap='outputImg' data-type='1'><text>重新上传</text></view>
<view catchtap='getImg'><text>选择图片</text></view>
</view> 
</view>
<!-- -->
<view class='absolute full-width full-height bg_black'></view>
</view>
</scroll-view>

wxss:(其中引入了一个公共样式,关于flex布局的,看样式名也能猜到)

/* component/picPro/picPro.wxss */
@import '../../resource/style/flex.wxss';
.body{
 position: fixed;
 top: 0;
 right: 0;
 bottom: 0;
 left: 0;
}
.text_white{
 color: white;
}
.main{
}
.out_item{
 width: 100%;
 height: 100%;
 flex: 1;
}
.bg_dark{
 background-color: rgba(0, 0, 0, 0.85)
}
.main_item{
 width: 15px;
}
.inner{
 outline: 3rpx solid white;
 background-color: rgba(0, 0, 0, 0.12);
 box-shadow: 0 0 4px rgba(0, 0, 0, 0.5) inset;
}
.inner_item{
 width: 8px;
 height: 8px;
}
.inner_item.left_top{
 border-left: 3px solid white;
 border-top: 3px solid white;
 left: -3px;
 top: -3px;
}
.inner_item.right_top{
 border-right: 3px solid white;
 border-top: 3px solid white;
 right: -3px;
 top: -3px;
}
.inner_item.right_bottom{
 border-right: 3px solid white;
 border-bottom: 3px solid white;
 right: -3px;
 bottom: -3px;
}
.inner_item.left_bottom{
 border-left: 3px solid white;
 border-bottom: 3px solid white;
 left: -3px;
 bottom: -3px;
}
.img{
 z-index: -1;
}
.bg_black{
 background-color:black;
 z-index: -2; 
}
.text_bg{
 padding-bottom: 2em;
 font-size: 0.9em;
}
.img_canvas{
 opacity: 0.5;
}
.newImg{
 z-index: 2
}

js:

// component/picPro/picPro.js
const state = {
 // 可用区域body
 window: { width: 0, height: 0 },
 // 原始图片信息
 originImg: { width: 0, height: 0 },
 // 第一次图片缩放信息
 firstScaleImg: { width: 0, height: 0 },
 // 截取区域信息
 interArea: { width: 0, height: 0 },
 // 单手触摸位置
 touchLast: { x: 0, y: 0 },
 // 滑动距离
 touchMove: { x: 0, y: 0 },
 // 滑动离开时图片状态
 moveImgState: {
  width: 0,
  height: 0,
  top: 0,
  left: 0,
 },
 // 双手触摸位置
 touchList: [{ x: 0, y: 0 }, { x: 0, y: 0 }],
 // 图片缩放比例
 scale: 1,
}
Component({
 /**
  * 组件的属性列表
  */
 properties: {
  //宽(非实际值)
  width: {
   type: Number,
   value: 600
  },
  //高
  height: {
   type: Number,
   value: 300
  },
  //图片路径
  src: {
   type: String,
   value: ""
  },
  //显示隐藏
  hidden: {
   type: Boolean,
   value: false
  },
  //截取框的信息
  margin: {
   type: Object,
   value: {
    left: 15,
    right: 15,
    top: 200,
    bottom: 200,
   }
  }
 },

 ready() {
  this.initialize();
  // const canvas = wx.createCanvasContext('imgCanvas', this);
  // canvas.draw(false, () => { console.log('ccc') }, this);
 },

 /**
  * 组件的初始数据
  */
 data: {
  touchRange: 8,
  img: {
   width: 0,
   height: 0,
   top: 0,
   left: 0,
  },
  canvas: {},
  ratio: 0,
  originImg: {
   width: 0,
   height: 0
  }
 },

 /**
  * 组件的方法列表
  */
 methods: {
  touchstart(e) {
   // console.log("touchstart", e);

  },
  touchmove(e) {
   if (e.touches.length === 1) { this.singleSlip(e.touches[0]) } else {
    this.doubleSlip(e.touches)
   }
  },
  touchend(e) {
   // console.log("touchend", e);
   const x = 0, y = 0;
   state.touchLast = { x, y };
   state.touchMove = { x, y };
   state.touchList = [{ x, y }, { x, y }];
   state.moveImgState = this.data.img;
   // console.log(this.data.img);
  },
  // 单手滑动操作
  singleSlip(e) {
   const { clientX: x, clientY: y } = e;
   const that = this;
   if (state.touchLast.x && state.touchLast.y) {
    state.touchMove = { x: x - state.touchLast.x, y: y - state.touchLast.y };
    state.touchLast = { x, y };
    const move = (_x = false, _y = false) => {
     const bottom = that.data.img.height + that.data.img.top;
     const right = that.data.img.width + that.data.img.left;
     const h = state.interArea.height;
     const w = state.interArea.width;
     const param = {};
     if (_x) {
      if (right > w && that.data.img.left < 0) {
       param.left = that.data.img.left + state.touchMove.x * 0.1
      } else if (right <= w && state.touchMove.x > 0) {
       param.left = that.data.img.left + state.touchMove.x * 0.1
      } else if (that.data.img.left >= 0 && state.touchMove.x < 0) {
       param.left = that.data.img.left + state.touchMove.x * 0.1
      }
     };
     if (_y) {
      if (bottom > h && that.data.img.top < 0) {
       param.top = that.data.img.top + state.touchMove.y * 0.1
      } else if (bottom <= h && state.touchMove.y > 0) {
       param.top = that.data.img.top + state.touchMove.y * 0.1
      } else if (that.data.img.top >= 0 && state.touchMove.y < 0) {
       param.top = that.data.img.top + state.touchMove.y * 0.1
      }
     };
     // console.log(param);
     that.setImgPos(param)
    };
    if (state.scale == 1) {
     if (that.data.img.width == state.interArea.width) {
      move(false, true)
     } else {
      move(true, false)
     }
    } else {
     move(true, true)
    }
   } else {
    state.touchLast = { x, y }
   }
  },
  // 双手缩放操作
  doubleSlip(e) {
   const that = this;
   const { clientX: x0, clientY: y0 } = e[0];
   const { clientX: x1, clientY: y1 } = e[1];
   if (state.touchList[0].x && state.touchList[0].y) {
    let changeScale = (Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)) - Math.sqrt((state.touchList[1].x - state.touchList[0].x) * (state.touchList[1].x - state.touchList[0].x) + (state.touchList[1].y - state.touchList[0].y) * (state.touchList[1].y - state.touchList[0].y))) * 0.0005;
    changeScale = changeScale >= 1.5 ? 1.5 : (changeScale <= -1 ? -1 : changeScale);
    state.scale = that.data.img.width / state.firstScaleImg.width < 1 ? 1 : (state.scale > 2.5 ? 2.5 : 1 + changeScale);
    let width = state.firstScaleImg.width * (state.scale - 1) + state.moveImgState.width;
    width = width < state.firstScaleImg.width ? state.firstScaleImg.width : width;
    let height = state.firstScaleImg.height * (state.scale - 1) + state.moveImgState.height;
    height = height < state.firstScaleImg.height ? state.firstScaleImg.height : height;
    let left = width * (1 - state.scale) / 4 + state.moveImgState.left;
    left = left * (-1) > width - state.interArea.width ? state.interArea.width - width: left > 0 ? 0 : left;
    let top = height * (1 - state.scale) / 4 + state.moveImgState.top;
    top = top * (-1) > height - state.interArea.height ?state.interArea.height - height : top > 0 ? 0 : top;
    const setImgObj = { width, height, left, top };
    that.setImgPos(setImgObj)
   } else {
    state.touchList = [{ x: x0, y: y0 }, { x: x1, y: y1 }]
   }
  },
  // 获取可用区域宽高
  getScreenInfo() {
   const that = this;
   return new Promise((resolve, reject) => {
    wx.getSystemInfo({
     success: function (res) {
      const { windowHeight, windowWidth } = res;
      state.window = { windowHeight, windowWidth };
      that.setData({ windowHeight, windowWidth })
      // console.log(state.window);
      resolve(res);
     },
    })
   })
  },
  setShowArea() {
   const that = this;
   const w = state.window.windowWidth - that.data.margin.left - that.data.margin.right;
   const h = (that.data.height / that.data.width) * w;
  },
  outputImg() {
   this.setData({
    hidden: true,
   })
  },
  getImgInfo(path) {
   return new Promise((resolve, reject) => {
    wx.getImageInfo({
     src: path,
     success(res) {
      console.log(res);
      resolve(res);
     },
     fail(err) {
      reject(err)
     }
    })
   })
  },
  // 设置图片
  setImgPos({ width, height, top, left }) {
   width = width || this.data.img.width;
   height = height || this.data.img.height;
   top = top || this.data.img.top;
   left = left || this.data.img.left
   this.setData({
    img: { width, height, top, left }
   })
  },
  // 初始化图片位置大小
  initialize() {
   const that = this;
   const ratio = that.data.width / that.data.height;
   this.getScreenInfo().then(res => {
    console.log(res);
    state.interArea = { width: res.windowWidth - that.data.margin.left - that.data.margin.right + 2, height: (res.windowWidth - that.data.margin.left - that.data.margin.right) / ratio };
    console.log("interArea", state.interArea)
    that.getImgInfo(that.data.src).then(imgInfo => {
     const { width, height } = imgInfo;
     const imgRatio = width / height;
     state.originImg = { width, height };
     that.setData({
      ratio: ratio
     });
     if (imgRatio > ratio) {
      that.setImgPos({
       height: state.interArea.height,
       width: state.interArea.height * imgRatio
      })
     } else {
      that.setImgPos({
       height: state.interArea.width / imgRatio,
       width: state.interArea.width,
      })
     };
     state.firstScaleImg = { width: that.data.img.width, height: that.data.img.height }
    });
   });
  },
  // 截图
  getImg(){
   const that = this;
   // console.log('dudu', that.data.img);
   const canvas = wx.createCanvasContext('imgCanvas', this);
   const {width,height,left,top} = that.data.img;
   const saveImg = ()=>{
    console.log('开始截取图片');
    wx.canvasToTempFilePath({
     canvasId:"imgCanvas",
     success(res){
      // console.log(res);
      that.setData({
       hidden:true,
       // src:""
      });
      that.triggerEvent("putimg", { imgUrl: res.tempFilePath},{});
     },
     fail(err){
      console.log(err)
     }
    },that)
   };
   canvas.drawImage(that.data.src, left, top, width, height);
   canvas.draw(false, () => { saveImg() }, that)
  }
 }
})

引用的时候除了宽高路径以外,需要wx:if;如果不卸载组件,会出现只能截一次的bug

因为小程序里面没有类似vue中catch的观测数据变化的东西,也不想为了个组件专门去搞一个,就用这种方式代替了,嘻嘻,好敷衍。。

总结

以上所述是小编给大家介绍的微信小程序canvas拖拽、截图组件功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
js判断上传文件类型判断FileUpload文件类型代码
May 20 Javascript
node.js中的path.sep方法使用说明
Dec 08 Javascript
JS在Chrome浏览器中showModalDialog函数返回值为undefined的解决方法
Aug 03 Javascript
让浏览器崩溃的12行JS代码(DoS攻击分析及防御)
Oct 10 Javascript
AngularJS的ng Http Request与response格式转换方法
Nov 07 Javascript
微信小程序实现流程进度的图样式功能
Jan 16 Javascript
webpack4.0打包优化策略整理小结
Mar 30 Javascript
微信小程序网络封装(简单高效)
Aug 06 Javascript
回顾Javascript React基础
Jun 15 Javascript
vue服务端渲染操作简单入门实例分析
Aug 28 Javascript
jQuery实现简单QQ聊天框
Aug 27 jQuery
Vue中添加滚动事件设置的方法详解
Sep 14 Javascript
mpvue写一个CPASS小程序的示例
Sep 04 #Javascript
Vue表单及表单绑定方法
Sep 04 #Javascript
elementUI Vue 单个按钮显示和隐藏的变换功能(两种方法)
Sep 04 #Javascript
为jquery的ajax请求添加超时timeout时间的操作方法
Sep 04 #jQuery
vue做移动端适配最佳解决方案(亲测有效)
Sep 04 #Javascript
基于element-ui的rules中正则表达式
Sep 04 #Javascript
微信小程序的部署方法步骤
Sep 04 #Javascript
You might like
关于UEditor编辑器远程图片上传失败的解决办法
2012/08/31 PHP
php函数间的参数传递(值传递/引用传递)
2013/09/23 PHP
2014年10个最佳的PHP图像操作库
2014/07/14 PHP
新浪SAE搭建PHP项目教程
2015/01/28 PHP
php打造智能化的柱状图程序,用于报表等
2015/06/19 PHP
iis 7下安装laravel 5.4环境的方法教程
2017/06/14 PHP
Domino中运用jQuery读取视图内容的方法
2009/10/21 Javascript
Javascript 去除数组的重复元素
2010/05/04 Javascript
js弹出层包含flash 不能关闭隐藏的2种处理方法
2013/06/17 Javascript
使用apply方法实现javascript中的对象继承
2013/12/16 Javascript
jQuery获取和设置表单元素的方法
2014/02/14 Javascript
多种方法实现load加载完成后把图片一次性显示出来
2014/02/19 Javascript
Javascript中数组sort和reverse用法分析
2014/12/30 Javascript
原创jQuery弹出层插件分享
2015/04/02 Javascript
JavaScript ES5标准中新增的Array方法
2016/06/28 Javascript
AngularJS指令与指令之间的交互功能示例
2016/12/14 Javascript
js 数据存储和DOM编程
2017/02/09 Javascript
浅谈regExp的test方法取得的值变化的原因及处理方法
2017/03/01 Javascript
微信小程序 中wx.chooseAddress(OBJECT)实例详解
2017/03/31 Javascript
解决webpack -p压缩打包react报语法错误的方法
2017/07/03 Javascript
JS中call和apply函数用法实例分析
2018/06/20 Javascript
如何提升vue.js中大型数据的性能
2019/06/21 Javascript
解决layer.confirm选择完之后消息框不消失的问题
2019/09/16 Javascript
浅谈vue中$bus的使用和涉及到的问题
2020/07/28 Javascript
Python基于OpenCV实现视频的人脸检测
2018/01/23 Python
Python模块/包/库安装的六种方法及区别
2020/02/24 Python
使用python从三个角度解决josephus问题的方法
2020/03/27 Python
pytorch 查看cuda 版本方式
2020/06/23 Python
经贸日语专业个人求职信
2013/12/13 职场文书
《自然之道》教学反思
2014/02/11 职场文书
事业单位绩效考核实施方案
2014/03/27 职场文书
《陈涉世家》教学反思
2014/04/12 职场文书
世博会口号
2014/06/20 职场文书
教育项目合作协议书格式
2014/10/17 职场文书
学校学习型党组织建设心得体会
2019/06/21 职场文书
《自然之道》读后感3篇
2019/12/17 职场文书