微信小程序内实现图层的移动、缩放、旋转等其他编辑操作

一、标签的形式

图层有3种

1、背景图层不可操作,只能替换,不可改变层级(最底层)

2、图片图层可移动,缩放(支持双指缩放),旋转,可改变层级

3、文字图层可移动,缩放(会改变文字大小,支持双指缩放),旋转,文字编辑区域拉伸长度和宽度(不会改变文字大小),可编辑更改文字属性(内容,字体大小,字体颜色,描边大小,描边颜色,编辑框背景色),可改变层级

关于不同设备的编辑区域尺寸的转化:

我们以720*1280的设计图为准,先获取设备上编辑区域的尺寸(实际宽高的像素),然后用编辑区域的宽高和设计图的宽高,就可以得出它们之间的dpi,之后就可以用这个dpi进行转换。

图层对象是一个json对象,里面保存很多对象属性 :

{
    show: true, //是否显示图层

    align: "center", // 文字对齐方式
    content: "abcdefg", // 文字内容或图片链接
    fontBgColor: 'red', // 编辑框背景色
    fontColor: "999999", // 文字颜色
    fontFamily: "宋体", // 字体
    fontSize: 5.5, // 字体大小
    strokeColor: "000000", // 描边颜色
    strokeShow: true, // 是否显示描边
    strokeSize: 12, // 描边大小

    left: 27.5, // 左上角x坐标
    top: 27.5, // 左上角y坐标
    type: "bt", // 图层类型
    width: 35.75, // 宽
    height: 66, // 高
    x: 45.375, // 中心点x坐标
    y: 60.5, // 中心点y坐标
    zIndex: 2 // 层级

}

    
    

      

        
        
          
        
        

            
            

            
            {{ item.content }}

          
          
            
          

          
          
          

          
          

          
          

          
          
            
          

          
          
          

          
          
          

        

      
    
    .img-canvas {
      position: relative;
      background: white;
      display: block;
      margin: 0 auto;
      width: 360rpx;
      height: 640rpx;
      overflow: hidden;

      .img-wrap {
        position: absolute;
        top: 20rpx;
        left: 20rpx;
        transform-origin: center;
        padding: 10rpx;
        box-sizing: border-box;

        &.touch-active {
          &::after {
            position: absolute;
            top: 0;
            left: 0;
            content: '';
            width: 100%;
            height: 100%;
            border: 6rpx solid #0054D1;
            box-sizing: border-box;
            pointer-events: none;
          }
        }

        .img {
          display: block;
        }

        .txt {
          display: block;
          /* 通过属性选择器结合伪元素before 实现文字外描边效果 */
          &[data-content]::before {
            /* attr()是用来获取被选中元素的某属性值,并且在样式文件中使用 */
            content: attr(data-content);
            white-space: pre; // 支持换行
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 100%;
            height: 100%;
            /* 实现元素外描边的关键 */
            -webkit-text-stroke: 0;
            /* 文本颜色 */
            color: attr(data-color);
            text-align: attr(data-align);
          }

          &.slh {
            max-width: 370rpx;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }
          // white-space: nowrap;
          // word-wrap: break-word;
        }

        .x {
          z-index: 2;
          width: 50rpx;
          height: 50rpx;
          position: absolute;
          top: 0;
          left: 0;
          transform: translate(-50%, -50%);
          background: white;
          border-radius: 100%;
          display: flex;
          align-items: center;
          justify-content: center;
          box-shadow:0 5rpx 5rpx 5rpx rgba(0, 0, 0, 0.2);

          image {
            width: 100%;
            height: 100%;
          }
        }

        .o {
          width: 50rpx;
          height: 50rpx;
          position: absolute;
          bottom: -20rpx;
          left: 50%;
          transform: translate(-50%, 100%);
          background: white;
          border-radius: 100%;
          display: flex;
          align-items: center;
          justify-content: center;
          box-shadow:0 5rpx 5rpx 5rpx rgba(0, 0, 0, 0.2);

          image {
            width: 100%;
            height: 100%;          
          }
        }

        .s {
          width: 30rpx;
          height: 30rpx;
          position: absolute;
          top: 0;
          right: 0;
          transform: translate(50%, -50%);
          background: #0054D1;
          border-radius: 100%;
          display: flex;
          align-items: center;
          justify-content: center;
          box-shadow:0 5rpx 5rpx 5rpx rgba(0, 0, 0, 0.2);

          &.s2 {
            top: auto;
            bottom: 0;
            right: 0;
            transform: translate(50%, 50%);
          }

          &.s3 {
            top: auto;
            bottom: 0;
            left: 0;
            right: auto;
            transform: translate(-50%, 50%);
          }
        }

        .lw {
          z-index: 2;
          position: absolute;
          top: 50%;
          right: 0;
          transform: translate(50%, -50%);
          width: 15rpx;
          height: 40rpx;
          background: white;
          border-radius: 5rpx;
        }

        .lh {
          z-index: 2;
          position: absolute;
          left: 50%;
          bottom: 0;
          transform: translate(-50%, 50%);
          width: 40rpx;
          height: 15rpx;
          background: white;
          border-radius: 5rpx;
        }

      }
    }
let toucheWrap = {} // 图层容器
let index = 0, // 当前点击图片的index
    items: any[] = []

let doubleTouchesArr: any[] = []; // 双指操作的数组

// 模版尺寸 720*1280
// 不同编辑区域尺寸可以根据这个dpi进行比例转换
let sDpi = 1;


Page({

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

    // 获取画板容器数据
    wx.createSelectorQuery()
    .select('#img-canvas') // 在 WXML 中填入的 id
    .boundingClientRect()
    .exec((res) => {
      console.log('容器', res)
      toucheWrap = res[0];
      sDpi = 720/res[0].width;
    })

  },


  // 计算坐标点到圆心的距离
  getDistancs (cx, cy, pointer_x, pointer_y) {
    var ox = pointer_x - cx;
    var oy = pointer_y - cy;
    return Math.sqrt(
      ox * ox + oy * oy
    );
  },

  /*
   * 参数cx和cy为图片圆心坐标
   * 参数pointer_x和pointer_y为手点击的坐标
   * 返回值为手点击的坐标到圆心的角度
   */
  countDeg (cx, cy, pointer_x, pointer_y) {
    var ox = pointer_x - cx;
    var oy = pointer_y - cy;
    var to = Math.abs(ox / oy);
    var angle = Math.atan(to) / (2 * Math.PI) * 360;

    // 相对在左上角,第四象限,js中坐标系是从左上角开始的,这里的象限是正常坐标系 
    if (ox < 0 && oy < 0) {
      angle = -angle;
    } 
    // 左下角,3象限 
    else if (ox <= 0 && oy >= 0) {
      angle = -(180 - angle)
    } 
    // 右上角,1象限 
    else if (ox > 0 && oy < 0) {
      angle = angle;
    } 
    // 右下角,2象限 
    else if (ox > 0 && oy > 0) {
      angle = 180 - angle;
    }

    return angle;
  },

})
// 判断key是否存在,排除ts报错
export const isValidKey = (key:string,object:object):key is keyof typeof object =>{
  return key in object
}

// 生成唯一id
export function algorithm(){
	let abc = ['a','b','c','d','e','f','g','h','i','g','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
	let [max,min]=[Math.floor(Math.random()*(10-7+1)+1),Math.floor(Math.random()*(17-10+1)+17)];
	abc = abc.sort(()=>0.4-Math.random()).slice(max,min).slice(0,8).join("");
	let a=new Date().getTime()+abc;
	return a
}

1、移动

  // 点击图层
  wraptouchStart (e: any) {

    // 循环图片数组获取点击的图片信息
    for (let i = 0; i < items.length; i++) {
      items[i].active = false;
      if (e.currentTarget.dataset.id == items[i].id) {
        index = i;
        items[index].active = true;
      }
    }

 
    if(items[index].type == 'bg') {
      return
    }

    // 双指操作
    if(e.touches.length > 1) {
  
      doubleTouchesArr = [...e.touches]

    }  else {
   
      // 获取点击的坐标值
      items[index].lx = e.touches[0].clientX;
      items[index].ly = e.touches[0].clientY;

    }

  }
  // 拖动图层
  wraptouchMove (e: any) {

    // 双指缩放
    if(e.touches.length > 1) {

      let _mw = Math.sqrt(Math.pow(doubleTouchesArr[0].pageX - doubleTouchesArr[1].pageX, 2) + Math.pow(doubleTouchesArr[0].pageY - doubleTouchesArr[1].pageY, 2));

      let _w = Math.sqrt(Math.pow(e.touches[0].pageX - e.touches[1].pageX, 2) + Math.pow(e.touches[0].pageY - e.touches[1].pageY, 2));

      let _s = _w / _mw

      // 使用缩放
      // let _oSWidth = items[index].scale > 0 ? items[index].width * items[index].scale : items[index].width;
      // let _nSWidth = _oSWidth * _s;
      
      // items[index].scale = _nSWidth / items[index].width;

      // 不使用缩放
      items[index].width = _s * items[index].width
      items[index].height = _s * items[index].height
      items[index].top = items[index].y - items[index].height/2
      items[index].left = items[index].x - items[index].width/2

      if(items[index].type == 'bt') {
        items[index].fontSize = items[index].fontSize * _s
      }

      // 双指操作的坐标
      doubleTouchesArr = [...e.touches]

      
    } else {

      items[index]._lx = e.touches[0].clientX;
      items[index]._ly = e.touches[0].clientY;
  
      items[index].left += items[index]._lx - items[index].lx;
      items[index].top += items[index]._ly - items[index].ly;
      items[index].x += items[index]._lx - items[index].lx;
      items[index].y += items[index]._ly - items[index].ly;
  
      items[index].lx = e.touches[0].clientX;
      items[index].ly = e.touches[0].clientY;


    }
    
  }

2、旋转

  // 点击旋转图标
  oScaleStart (e: any) {
    // 找到点击的那个图片对象,并记录
    for (let i = 0; i < items.length; i++) {
      items[i].active = false;
      if (e.currentTarget.dataset.id == items[i].id) {
        index = i;
        items[index].active = true;
      }
    }

    // 获取作为移动前角度的坐标
    items[index].tx = e.touches[0].pageX - toucheWrap.left;
    items[index].ty = e.touches[0].pageY - toucheWrap.top;

    // 移动前的角度
    items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty);
    
  }
  // 移动旋转图标
  oScaleMove (e: any) {
    
    // 记录移动后的位置
    items[index]._tx = e.touches[0].pageX - toucheWrap.left;
    items[index]._ty = e.touches[0].pageY - toucheWrap.top;

    // 移动的点到圆心的距离
    items[index].disPtoO = this.getDistancs(items[index].x, items[index].y, items[index]._tx, items[index]._ty - 10)
 
    // 移动后位置的角度
    items[index].angleNext = this.countDeg(items[index].x, items[index].y, items[index]._tx, items[index]._ty)

    // 角度差
    items[index].new_rotate = items[index].angleNext - items[index].anglePre;
 
    //叠加的角度差
    items[index].rotate += items[index].new_rotate;
    items[index].angle = items[index].type == 'tt' ? items[index].rotate : 0; //赋值
 
    //用过移动后的坐标赋值为移动前坐标
    items[index].tx = e.touches[0].pageX - toucheWrap.left;
    items[index].ty = e.touches[0].pageY - toucheWrap.top;

    // 下次移动前的角度
    items[index].anglePre = this.countDeg(items[index].x, items[index].y, items[index].tx, items[index].ty)


  }

3、缩放

  // 点击伸缩图标
  oTouchStart (e: any) {
    // 找到点击的那个图片对象,并记录
    for (let i = 0; i < items.length; i++) {
      items[i].active = false;
      if (e.currentTarget.dataset.id == items[i].id) {
        index = i;
        items[index].active = true;
      }
    }

    // 获取作为移动前的坐标
    items[index].tx = e.touches[0].pageX - toucheWrap.left;
    items[index].ty = e.touches[0].pageY - toucheWrap.top;

    // 获取图片半径
    items[index].r = this.getDistancs(items[index].x, items[index].y, items[index].tx, items[index].ty);
    
  }
  // 移动伸缩图标
  oTouchMove (e: any) {
    
    // 记录移动后的位置
    items[index]._tx = e.touches[0].pageX - toucheWrap.left;
    items[index]._ty = e.touches[0].pageY - toucheWrap.top;

    // 移动的点到圆心的距离
    items[index].disPtoO = this.getDistancs(items[index].x, items[index].y, items[index]._tx, items[index]._ty - 10)
    let _s = items[index].disPtoO / items[index].r;

    // 使用缩放
    // items[index].scale = items[index].disPtoO / items[index].r;
 
    // 不使用缩放
    items[index].width = _s * items[index].width
    items[index].height = _s * items[index].height
    items[index].top = items[index].y - items[index].height/2
    items[index].left = items[index].x - items[index].width/2
    
    if(items[index].type == 'bt' || items[index].type == 'zm') {
      items[index].fontSize = items[index].fontSize * _s
    }

    // 获取图片半径
    items[index].r = items[index].disPtoO;

  }

4、拉伸文字编辑框

  // 点击文字拉宽高图标
  oLwhStart (e: any) {
    // 找到点击的那个图片对象,并记录
    for (let i = 0; i < items.length; i++) {
      items[i].active = false;
      if (e.currentTarget.dataset.id == items[i].id) {
        index = i;
        items[index].active = true;
      }
    }

    // 获取作为移动前的坐标
    items[index].tx = e.touches[0].pageX - toucheWrap.left;
    items[index].ty = e.touches[0].pageY - toucheWrap.top;

    // 获取触摸点到圆心距离
    items[index].r = this.getDistancs(items[index].x, items[index].y, items[index].tx, items[index].ty);
    
  }
  // 移动文字拉宽高图标
  oLwhMove (e: any) {

    let _type = e.currentTarget.dataset.type
    
    // 记录移动后的位置
    items[index]._tx = e.touches[0].pageX - toucheWrap.left;
    items[index]._ty = e.touches[0].pageY - toucheWrap.top;

    // 移动的点到圆心的距离
    items[index].disPtoO = this.getDistancs(items[index].x, items[index].y, items[index]._tx, items[index]._ty - 10)
    let _s = items[index].disPtoO / items[index].r;
 
    // 不使用缩放
    if(_type == 'w') {
      items[index].width = _s * items[index].width
      items[index].left = items[index].x - items[index].width/2
    
    } else {
      items[index].height = _s * items[index].height 
      items[index].top = items[index].y - items[index].height/2 
    }
    

    // 获取触摸点到圆心距离
    items[index].r = items[index].disPtoO;

  
  }

 5、删除图层

  // 删除图层对象
  deleteItem (e: any) {
    let newList = [];
    for (let i = 0; i < items.length; i++) {
      // 更新层级
      if(items[i].zIndex > items[index].zIndex) {
        items[i].zIndex -= 1
      }
      if (e.currentTarget.dataset.id != items[i].id) {
        newList.push(items[i])
      }

    }

    if (newList.length > 0) {
      newList[newList.length - 1].active = true; // 剩下图片组最后一个选中
      index = newList.length - 1
    } else {
      index = 0
    }
    
    items = newList;

    
  },

6、更改层级

  // 改变图层层级
  changeZIndex(e: any) {
    console.log(e)
    let isAdd = e.detail.add

    let _zIndex = items[index].zIndex
    isAdd ? _zIndex += 1 : _zIndex -= 1

    // 循环图片数组获取点击的图片信息
    for (let i = 0; i < items.length; i++) {  
      if (_zIndex == items[i].zIndex) {
        isAdd ? items[i].zIndex -= 1 : items[i].zIndex += 1
        break;
      }
    }

    // 上移一层 | 下移一层
    isAdd ? items[index].zIndex += 1 : items[index].zIndex -= 1

    if(items[index].zIndex > items.length-1) {
      items[index].zIndex = items.length-1;
      wx.showToast({
        title: '已是最顶层',
        icon: 'none',
        duration: 2000
      })
    }

    if(items[index].zIndex < 1) {
      items[index].zIndex = 1;
      wx.showToast({
        title: '已是最底层',
        icon: 'none',
        duration: 2000
      })
    }

  },

7、设置图层对象

// 设置图层对象的信息
  setDropItem(obj: any, call?: any) {
    console.log('setDropItem', obj)

    return new Promise((resolve, reject)=> {
      let data = {}; // 存储拖拽对象信息

      let type = obj.type;
      let content = obj.content;
      

      // 背景、贴图
      if(type == 'bg' || type == 'tt') {
        // 获取图片信息
        wx.getImageInfo({
          src: content,
          success: res => {
            console.log(res)
            // 初始化数据
            let maxWidth = 150, maxHeight = 150; // 设置最大宽高
            if(type == 'bg') {
              maxWidth = 200
              maxHeight = 600
            }

            if (res.width > maxWidth || res.height > maxHeight) { // 原图宽或高大于最大值就执行
              if (res.width / res.height > maxWidth / maxHeight) { // 判断比例使用最大值的宽或高作为基数计算
                data.width = maxWidth;
                data.height = Math.round(maxWidth * (res.height / res.width));
              } else {
                data.height = maxHeight;
                data.width = Math.round(maxHeight * (res.width / res.height));
              }
            } else {
              data.width = res.width;
              data.height = res.height;
            }
          
            data.iobsKey = '';
            data.show = true; // 显示
            data.type = type; // 对象类型 type
            data.content = content; // 显示地址
            data.id = algorithm(); // id
            data.top = 0; // top定位
            data.left = 0; // left定位
            // 圆心坐标
            data.x = data.left + data.width / 2;
            data.y = data.top + data.height / 2;
            data.scale = 1; // scale缩放
            data.rotate = 0; // 旋转角度
            data.active = false; // 选中状态
            
            if(type == 'bg') {
              data.zIndex = 0; // 层级
            } else if(items.find(it => it.type == 'bg')) {
              data.zIndex = items.length; // 层级
            } else {
              data.zIndex = items.length+1; // 层级
            }

            // 覆盖原数据
            data = {
              ...data,
              ...obj
            }

            items.push(data)
                       

            resolve();
            call && call()

            
          },
          fail: err => {
            console.log(err)
          }
        })
      } 
      // 标题
      else if (type == 'bt' || type == 'zm') {
        // 初始化数据
        data.width = 0;
        data.height = 0;

        data.show = true; // 显示
        data.fontFamily = '黑体'; // 字体
        data.strokeShow = true; // 显示描边
        data.align = 'left'; // 文本对齐方式
        data.fontColor = '#000'; // 字体颜色
        data.fontSize = 12; // 字号大小
        data.fontBgColor = ''; // 文本背景颜色
        data.strokeSize = 0; // 描边粗细
        data.strokeColor = '#000'; // 描边颜色
        data.type = type; // 对象类型 type - [bg, ai, tt, zm]
        data.content = content; // 显示内容
        data.id = algorithm(); // id
        
        data.scale = 1; // scale缩放
        data.rotate = 0; // 旋转角度
        data.active = false; // 选中状态

        if(type == 'bg') {
          data.zIndex = 0; // 层级
        } else if(items.find(it => it.type == 'bg')) {
          data.zIndex = items.length; // 层级
        } else {
          data.zIndex = items.length+1; // 层级
        }

        data.top = 0; // top定位
        data.left = 0; // left定位

        // 圆心坐标
        data.x = data.left + data.width / 2;
        data.y = data.top + data.height / 2;

        // 字幕
        if(type == 'zm') {

          // 圆心坐标
          data.x = toucheWrap.width/2;
          data.y = toucheWrap.height - 20;
          
          data.ys = obj.ys
          data.yl = obj.yl
          data.yd = obj.yd
          data.ysu = obj.ysu
    
        }

        items.push(data);


        let _i = items.length - 1;
        
        this.setData({
      
          itemList: items,
        
        }, () => {
          // 获取画板容器数据
          wx.createSelectorQuery()
          .select('#txt' + items[_i].id) // 在 WXML 中填入的 id
          .boundingClientRect()
          .exec((res) => {
            // console.log('容器', res)
            items[_i].width = res[0].width;
            items[_i].height = res[0].height;

            if(type == 'zm') {
              items[_i].left = items[_i].x - items[_i].width / 2;
              items[_i].top = items[_i].y - items[_i].height / 2
            } else {
              items[_i].x = items[_i].left + items[_i].width / 2;
              items[_i].y = items[_i].top + items[_i].height / 2;
            }

            // 覆盖原数据
            items[_i] = {
              ...items[_i],
              ...obj
            }
    
            

            resolve()
            call && call()
          })
        })
      } 
    })
  
  },

8、实现文字外描边的关键代码

{{ item.content }}
    .txt {
          display: block;

          /* 通过属性选择器结合伪元素before 实现文字外描边效果 */
          &[data-content]::before {
            /* attr()是用来获取被选中元素的某属性值,并且在样式文件中使用 */
            content: attr(data-content);
            white-space: pre; // 支持换行
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 100%;
            height: 100%;
            /* 实现元素外描边的关键 */
            -webkit-text-stroke: 0;
            /* 文本颜色 */
            color: attr(data-color);
            text-align: attr(data-align);
          }
    }

二、canvas的形式

更完善的案例请参考:vue3使用canvas实现图层的移动、缩放、旋转等其他编辑操作_不怕麻烦的鹿丸的博客-CSDN博客

使用标签的方式好处是可以很方便就判断是否点击到某个图层上的操作按钮,而采用canvas的话,就需要我们自己去判断点击的区域是否是某个图层上的某个操作按钮,坐标点的转换比较复杂。

1、求旋转角度

可以考虑用反弦函数来求的弧度值。

2、射线法判断点是否在多边形内部

  /**
   * 射线法判断点是否在多边形内部
   * 
   * @param p 
   * @param ptPolygon Array
   * 
   * */ 
  isInPolygon (p, ptPolygon) {
    let ncross = 0;
    for (let i = 0; i < ptPolygon.ncount; i++) {
      let p1 = ptPolygon[i];
      let p2 = ptPolygon[(i + 1) % ptPolygon.ncount]; // 相邻两条边p1,p2
      if (p1.y == p2.y) {
        continue;
      }        
      if (p.y < Math.min(p1.y, p2.y)) {
        continue;
      }
      if (p.y >= Math.max(p1.y, p2.y)) {
        continue;
      }
      let x = (p.y - p1.y)*(p2.x - p1.x) / (p2.y - p1.y) + p1.x;
      if (x > p.x) {
        ncross++; // 只统计单边交点
      }
    }

    return(ncross % 2 == 1);
  }

3、判断旋转是顺时针或逆时针

  // 判断旋转是顺时针或逆时针
  rotateDirection(p1, p2, p3) {

    // 向量叉乘方向判断
    let r = (p2.x - p1.x) * (p3.y - p2.y) - (p2.y - p1.y) * (p3.x - p2.x)
    
    if(r > 0) {
      // 逆时针
      console.log('逆时针')
      return 1
    } else if(r < 0) {
      // 顺时针
      console.log('顺时针')
      return -1
    } else {
      // 不变
      console.log('不变')
      return 0
    }
  }

4、绘制图层和改变层级

可以新建一个数组,然后把图层对象存到这个数组里,然后依次遍历数组里的对象,依次绘制图层。

改变层级的话,就是改变对象在数组里位置即可。

判断点击到哪个图层,也是依次遍历数组里的对象,最先判断到的对象为点击到的对象。

你可能感兴趣的:(微信小程序,小程序)